   #Start Prev Next Contents

   Jak pisac programy w jezyku asembler pod Linuksem?

Czesc 10 - Nie jestesmy sami, czyli jak laczyc asemblera z innymi jezykami

   Jak wiemy, w asemblerze mozna napisac wszystko. Jednak nie zawsze
   wszystko trzeba pisac w tym jezyku. W tej czesci pokaze, jak asemblera
   laczyc z innymi jezykami. Sa na to 2 sposoby:
     * Wstawki asemblerowe wpisywane bezposrednio w kod programu
     * Osobne moduly asemblerowe dolaczane potem do modulow napisanych w
       innych jezykach

   Postaram sie z grubsza omowic te dwa sposoby na przykladzie jezykow
   Pascal, C i Fortran 77. Uprzedzam jednak, ze moja znajomosc jezyka
   Pascal i narzedzi zwiazanych z tym jezykiem jest slaba.
     _________________________________________________________________

Pascal

   (przeskocz Pascala)

   Wstawki asemblerowe realizuje sie uzywajac slowa "asm". Oto przyklad:
        { Linux uzywa skladni AT&T do asemblera - jak zauwazycie,
         argumenty instrukcji sa odwrocone. }

        program pas1;

        begin
         asm movl $4,%eax
         end;
        end.

   Mozna tez stosowac nieco inny sposob - deklarowanie zmiennej
   reprezentujacej rejestry procesora. Ponizszy wycinek kodu prezentuje
   to wlasnie podejscie (wywoluje przerwanie 13h z AH=48h, DL=80h, DS:DX
   wskazujacym na obiekt a):
        uses crt,dos;

        Var
           regs: Registers;

        BEGIN
           clrscr();
           With regs DO
             Begin
               Ah:=$48;
               DL:=$80;
               DS:=seg(a);
               DX:=ofs(a);
             End;

               Intr($13,regs);

   Teraz zajmiemy sie bardziej skomplikowana sprawa - laczenie modulow
   napisanych w Pascalu i asemblerze. Pascal dekoruje nazwy zmiennych i
   procedur, dorabiajac znak podkreslenia z przodu. Jakby tego bylo malo,
   do nazwy procedury dopisywana jest informacja o jej parametrach. Tak
   wiec z kodu
        var
         c:integer;
         d:char;

        procedure aaa(a:integer;b:char);

   otrzymujemy symbole: _C, _D oraz _AAA$INTEGER$CHAR.

   Oprocz tego, zwykle w Pascalu argumenty na stos szly od lewej do
   prawej, ale z tego co widze teraz, to Free Pascal Compiler dziala
   odwrotnie - argumenty ida na stos wspak. W naszym przykladzie najpierw
   na stos pojdzie zmienna typu "char", a potem typu "integer" (obie
   rozszerzone do rozmiaru DWORDa).

   Jedno jest pewne: jezeli Twoja procedura jest uruchamiana z programu
   napisanego w Pascalu, to Ty "sprzatasz po sobie" stos - nalezy przy
   wyjsciu z procedury wykonac   RET liczba, gdzie liczba to rozmiar
   wszystkich parametrow wlozonych na stos (wszystkie parametry sa
   rozmiaru co najmniej DWORD).
   Jesli to Ty uruchamiasz procedury napisane w Pascalu, to nie musisz
   sie martwic o zdejmowanie parametrow ze stosu.

   Samo dolaczanie modulow odbywa sie na linii polecen, najlepiej w tym
   celu uzyc linkera (po uprzednim skompilowaniu innych modulow na pliki
   obiektowe).
     _________________________________________________________________

C i C++

   (przeskocz C i C++)

   Wstawki asemblerowe zaczynaja sie slowami "__asm (" a koncza nawiasem
   zamykajacym ")". W Linuksie wygladaja one nieco dziwnie i to nie tylko
   ze wzgledu na "odwrotna" skladnie
   AT&T:
        __asm ("movl $4,%eax\n"
                "movb $0xff,%bl\n");

   Jak widac, po kazdej instrukcji trzeba dac znak przejscia do nowej
   linii (w jednej linii moze byc tylko 1 instrukcja asemblera). Mozna
   dorzucic tez znak tabulacji "\t".

   Wyglad blokow "__asm" jest zlozony. Po szczegoly odsylam do stron
   przeznaczonych temu zagadnieniu. W szczegolnosci, mozecie poczytac
   podrecznik GCC (sekcje: 5.34 i 5.35), strony DJGPP oraz (w jezyku
   polskim) strone pana Danileckiego.

   U siebie tez mam krotkie porownanie tych skladni.

   W C i C++ mozna, podobnie jak w Pascalu, deklarowac zmienne
   reprezentujace rejestry procesora. Plik naglowkowy BIOS.H (niestety
   tylko w Windows) oferuje nam kilka mozliwosci. Oto przyklad:
        #include <bios.h>
        ...

        REGS rejestry;
        ...
                rejestry.x.ax = 0x13;
                rejestry.h.bl = 0xFF;
                int86 (0x10, rejestry, rejestry);

   Laczenie modulow jest prostsze niz w Pascalu. Jezyk zwykle C dekoruje
   nazwy, dodajac znak podkreslenia z przodu, ale nie w Linuksie, gdzie
   po prostu nic nie jest dorabiane.
   W Linuksie deklaracja funkcji zewnetrznej wyglada po prostu tak:
extern void naszafunkcja (int parametr, char* parametr2);

   UWAGA - w jezyku C++ sprawy sa trudniejsze nawet niz w Pascalu.
   Dlatego, jesli chcemy, aby nazwa naszej funkcji byla niezmieniona
   (poza tym, ze ewentualnie dodamy podkreslenie z przodu) i jednoczesnie
   dzialala w C++, zawsze przy deklaracji funkcji w pliku naglowkowym,
   nalezy dodac  extern "C", na przyklad
        #ifdef __cplusplus
        extern "C" {
        #endif

        extern void naszafunkcja (int parametr, char* a);

        #ifdef  __cplusplus
        }
        #endif

   W systemach 32-bitowych parametry przekazywane sa OD PRAWEJ DO LEWEJ,
   czyli pierwszy parametr (u nas powyzej: int) bedzie wlozony na stos
   jako ostatni, czyli bedzie "najplycej", a ostatni (u nas: char*)
   bedzie "najglebiej".

   W systemach 64-bitowych sprawa wyglada trudniej: parametry, w
   zaleznosci od klasy, sa przekazywane (od LEWEJ do PRAWEJ):
     * na stosie, jesli ich rozmiar przekracza 8 bajtow lub zawiera pola
       niewyrownane co do adresu
     * kolejno w rejestrach RDI, RSI, RDX, RCX, R8, R9, jesli jest klasy
       calkowitej (miesci sie w rejestrze ogolnego przeznaczenia)
     * kolejno w rejestrach XMM0 ... XMM7 lub ich gornych czesciach,
       jesli jest klasy SSE lub SSEUP, odpowiednio
     * w obszarze pamieci, jesli jest klasy
       zmiennoprzecinkowej/zespolonej

   Dodatkowo, dla funkcji typu "stdarg" (inaczej: "vararg"), czyli dla
   takich z wielokropkiem w deklaracji, jak np. printf, rejestr AL
   zawiera liczbe rejestrow SSE zuzytych na parametry.

   W C/C++ to funkcja uruchamiajaca zdejmuje wlozone parametry ze stosu,
   a NIE funkcja uruchamiana.

   Na systemach 32-bitowych parametry calkowitoliczbowe do 32 bitow
   zwracane sa w rejestrze EAX (lub jego czesciach: AL, AX, w zaleznosci
   od rozmiaru), 64-bitowe w EDX:EAX, zmiennoprzecinkowe w ST0. Wskazniki
   w 32-bitowych kompilatorach sa 32-bitowe i sa zwracane w EAX (w
   16-bitowych zapewne w AX).
   Struktury sa wkladane na stos od ostatnich pol, a jesli funkcja zwraca
   strukture przez wartosc, na przyklad
   struct xxx f ( struct xxx a )
   to tak naprawde jest traktowana jak taka funkcja:
   void f ( struct xxx *tu_bedzie_wynik, struct xxx a )
   czyli jako ostatni na stos wkladany jest adres struktury, do ktorej ta
   funkcja ma wlozyc strukture wynikowa.

   Na systemach 64-bitowych sprawa ponownie wyglada inaczej. Tu takze
   klasyfikuje sie typ zwracanych danych, ktore sa wtedy przekazywane:
     * w pamieci, ktorej adres przekazano w RDI (tak, jakby byl to
       pierwszy parametr) - tak na przyklad mozna zwracac struktury. Po
       powrocie, RAX bedzie zawieral przekazany adres
     * w kolejnym wolnym rejestrze z grupy RAX, RDX, jesli klasa jest
       calkowita
     * w kolejnym wolnym rejestrze z grupy XMM0, XMM1, jesli klasa to SSE
     * w gornej czesci ostatniego uzywanego rejestru SSE, jesli klasa to
       SSEUP
     * w ST0, jesli klasa jest zmiennoprzecinkowa
     * razem z poprzednia wartoscia w ST0, jesli klasa to X87UP
     * czesc rzeczywista w ST0, a czesc urojona w ST1, jesli klasa jest
       zespolona

   Polecam do przeczytania x64 ABI (na przyklad dokument x64-abi.pdf, do
   znalezienia w Internecie).

   Dolaczanie modulow (te napisane w asemblerze musza byc uprzednio
   skompilowane) odbywa sie na linii polecen, z tym ze tym razem mozemy
   uzyc samego kompilatora (GCC), aby wykonal za nas laczenie (nie musimy
   uruchamiac linkera LD).

   Teraz krotki 32-bitowy przykladzik (uzyje NASMa i GCC):
        ; NASM - casm1l.asm

        ; use32 nie jest potrzebne w Linuksie, ale tez nie zaszkodzi
        section .text use32

        global  suma

        suma:

        ; po wykonaniu push ebp i mov ebp, esp:
        ; w [ebp]    znajduje sie stary EBP
        ; w [ebp+4]  znajduje sie adres powrotny z procedury
        ; w [ebp+8]  znajduje sie pierwszy parametr,
        ; w [ebp+12] znajduje sie drugi parametr
        ; itd.

        %idefine        a       [ebp+8]
        %idefine        b       [ebp+12]

                push    ebp
                mov     ebp, esp

                mov     eax, a
                add     eax, b

        ; LEAVE = mov esp, ebp / pop ebp
                leave
                ret

   I jeszcze plik casml.c:
        #include <stdio.h>

        extern int suma (int a, int b);

        int c=1, d=2;

        int main()
        {
                printf("%d\n", suma(c,d));
                return 0;
        }

   Kompilacja wyglada tak:
        nasm -f elf casm1l.asm
        gcc -o casm casml.c casm1l.o

   Po uruchomieniu programu na ekranie pojawia sie oczekiwana cyfra 3.

   Jesli NASM wyswietla ostrzezenie odnosnie atrybutu use32 (np. Unknown
   section attribute 'use32' ignored on declaration of section `.text'),
   mozna go usunac lub przeniesc na gore pliku:
        use32
        section .text

   Ten sam kod w wersji 64-bitowej (tez dla NASMa):
        ; NASM - casm1l.asm
        use64

        section .text

        global  suma

        suma:

        ; po wykonaniu push rbp i mov rbp, rsp:
        ; w [rbp]    znajduje sie stary RBP
        ; w [rbp+8]  znajduje sie adres powrotny z procedury
        ; w rdi  znajduje sie pierwszy parametr calkowitoliczbowy,
        ; w rsi  znajduje sie drugi parametr calkowitoliczbowy,
        ; w rdx  znajduje sie trzeci parametr calkowitoliczbowy,
        ; w rcx  znajduje sie czwarty parametr calkowitoliczbowy,
        ; w r8   znajduje sie piaty parametr calkowitoliczbowy,
        ; w r9   znajduje sie szosty parametr calkowitoliczbowy,
        ; w [rbp+16]  znajduje sie pierwszy parametr wymagajacy stosu,
        ; w [rbp+24] znajduje sie drugi parametr wymagajacy stosu
        ; itd.

        %idefine        a       rdi
        %idefine        b       rsi

                push    rbp
                mov     rbp, rsp

                mov     rax, a
                add     rax, b

        ; LEAVE = mov rsp, rbp / pop rbp
                leave
                ret

   I jeszcze plik casm1l.c:
        #include <stdio.h>

        extern long int suma (long int a, long int b);

        static long int c=1, d=2;

        int main()
        {
                printf("%ld\n", suma(c,d));
                return 0;
        }

   Kompilacja wyglada tak:
        nasm -f elf64 casm1l.asm
        gcc -o casm casm1l.c casm1l.o

   Po uruchomieniu programu na ekranie pojawia sie oczekiwana cyfra 3.

   Moze sie zdarzyc tez, ze chcemy tylko korzystac z funkcji jezyka C,
   ale glowna czesc programu chcemy napisac w asemblerze. Nic trudnego:
   uzywane funkcje deklarujemy jako zewnetrzne, ale uwaga - swoja funkcje
   glowna musimy nazwac "main". Jest tak dlatego, ze teraz punkt startu
   programu nie jest w naszym kodzie, lecz w samej bibliotece jezyka C.
   Program zaczyna sie miedzy innymi ustawieniem tablic argumentow listy
   polecen i zmiennych srodowiska. Dopiero po tych operacjach biblioteka
   C uruchamia funkcje "main" instrukcja CALL.

   Inna wazna sprawa jest to, ze nasza funkcje glowna powinnismy
   zakonczyc instrukcja RET (zamiast normalnych instrukcji wyjscia z
   programu), ktora pozwoli przekazac kontrole z powrotem do biblioteki
   C, umozliwiajac posprzatanie (na przyklad wyrzucenie buforow z
   wyswietlonymi informacjami w koncu na ekran).
   Krotki (takze 32-bitowy) przykladzik:
        section .text

        global main

        extern printf

        main:

                ; printf("Liczba jeden to: %d\n", 1);
                push    dword 1         ; drugi argument
                push    dword napis     ; pierwszy argument
                call    printf          ; uruchomienie funkcji
                add     esp, 2*4        ; posprzatanie stosu

                ; return 0;
                xor     eax, eax
                ret                     ; wyjscie z programu

        section .data

        napis: db "Liczba jeden to: %d", 10, 0

   Kompilacja powinna odbyc sie tak:
        nasm -o casm2.o -f elf casm2.asm
        gcc -o casm2 casm2.o

   64-bitowy przykladzik (tez NASM):
        use64
        section .text

        global main

        extern printf

        main:
                ; printf("Liczba jeden to: %ld\n", 1);
                xor     al, al          ; liczba argumentow wymagajacych SSE
                                        ; w funkcjach varargs
                mov     rsi, 1          ; drugi argument
                mov     rdi, napis      ; pierwszy argument

                call    printf          ; uruchomienie funkcji
                        ; sprzatanie stosu niepotrzebne
                        ; add   rsp, 2*8

                ; return 0;
                xor     rax, rax
                ret                     ; wyjscie z programu

        section .data

        napis: db "Liczba jeden to: %ld", 10, 0

   Kompilacja powinna odbyc sie tak:
        nasm -o casm2l.o -f elf casm2l.asm
        gcc -o casm2l casm2l.o

   Jedna uwaga: funkcje biblioteki C moga zamazac nam zawartosc
   wszystkich rejestrow (poza EBX, EBP, ESI, EDI w systemach 32-bitowych,
   i RBX, RBP, R12, R13, R14, R15 na systemach 64-bitowych), wiec nie
   wolno nam polegac na zawartosci rejestrow po uruchomieniu
   jakiejkolwiek funkcji C.
     _________________________________________________________________

Fortran 77

   (przeskocz Fortrana 77)

   W tym jezyku nie wiem nic o wstawkach asemblerowych, wiec przejdziemy
   od razu do laczenia modulow.

   Fortran dekoruje nazwy, stawiajac znak podkreslenia PO nazwie funkcji
   lub zmiennej (wyjatkiem jest funkcja glowna - blok PROGRAM - ktora
   nazywa sie MAIN__, z dwoma podkresleniami).

   Nie musimy pisac externow, ale jest kilka regul przekazywania
   parametrow:
     * parametry przekazywane sa od prawej do lewej, czyli tak jak w C.
     * jesli to jest tylko mozliwe, wszystkie parametry przekazywane sa
       przez referencje, czyli przez wskaznik. Gdy to jest niemozliwe,
       przekazywane sa przez wartosc.
     * jesli na liscie parametrow pojawia sie lancuch znakowy (lub inna
       tablica), to na stosie przed innymi parametrami umieszczana jest
       jego dlugosc (lub wymiary podawane wspak - od ostatniego do
       pierwszego - w przypadku tablic wielowymiarowych).
     * wyniki sa zwracane w tych samych miejscach, co w jezyku C.

   Na przyklad, nastepujacy kod:
        REAL FUNCTION aaa (a, b, c, i)

                CHARACTER a*(*)
                CHARACTER b*(*)
                REAL c
                INTEGER i

                aaa = c
        END

[...]
                CHARACTER x*8
                CHARACTER y*5
                REAL z,t
                INTEGER u

                t=aaa (x, y, z, u)
[...]

   bedzie przetlumaczony na asemblera tak (samo uruchomienie funkcji):
        push    5
        push    8
        push    u_      ; adres, czyli offset zmiennej "u"
        push    z_
        push    y_
        push    x_

        call    aaa_

   (to niekoniecznie musi wygladac tak ladnie, gdyz zmienne "x", "y", "u"
   i "z" sa lokalne w funkcji MAIN__, czyli sa na stosie, wiec ich adresy
   moga wygladac jak [ebp-28h] lub podobnie).

   Funkcja uruchamiajaca sprzata stos po uruchomieniu (podobnie jak w C).

   Dolaczac moduly mozna bezposrednio z linii polecen (w kazdym razie
   kompilatorem F77/G77).

   Podam teraz przyklad laczenia Fortrana 77 i asemblera (uzyje NASMa i
   F77):
        ; NASM - asm1fl.asm

        section .text use32

        global  suma_

        suma_:

        ; po wykonaniu push ebp i mov ebp, esp:
        ; w [ebp]    znajduje sie stary EBP
        ; w [ebp+4]  znajduje sie adres powrotny z procedury
        ; w [ebp+8]  znajduje sie pierwszy parametr,
        ; w [ebp+12] znajduje sie drugi parametr
        ; itd.

        %idefine        a       [ebp+8]
        %idefine        b       [ebp+12]

                push    ebp
                mov     ebp, esp

        ; przypominam, ze nasze parametry sa w rzeczywistosci
        ; wskaznikami do prawdziwych parametrow

                mov     edx, a          ; EDX = adres pierwszego parametru
                mov     eax, [edx]      ; EAX = pierwszy parametr
                mov     edx, b
                add     eax, [edx]

        ; LEAVE = mov esp, ebp / pop ebp
                leave
                ret

   I teraz plik asmfl.f:
        PROGRAM funkcja_zewnetrzna

        INTEGER a,b,suma

        a=1
        b=2

        WRITE (*,*) suma(a,b)

        END

   Po skompilowaniu:
        nasm -f elf asm1fl.asm
        g77 -o asmfl asmfl.f asm1fl.o

   i uruchomieniu, na ekranie ponownie pojawia sie cyfra 3.

   I ponownie, jesli NASM wyswietla ostrzezenie odnosnie atrybutu use32,
   mozna go usunac lub przeniesc do osobnej linii na gore pliku:
        use32
        section .text

   W wersji 64-bitowej obowiazuja powyzsze reguly przekazywania
   parametrow, a pozostale reguly sa takie same, jak dla jezyka C.

   Powyzszy program w wersji 64-bitowej wygladalby to tak:
        ; NASM - asm2fl.asm

        use64
        section .text

        global  suma_

        suma_:

        ; po wykonaniu push rbp i mov rbp, rsp:
        ; w [rbp]    znajduje sie stary RBP
        ; w [rbp+8]  znajduje sie adres powrotny z procedury
        ; w rdi  znajduje sie pierwszy parametr calkowitoliczbowy,
        ; w rsi  znajduje sie drugi parametr calkowitoliczbowy,
        ; w rdx  znajduje sie trzeci parametr calkowitoliczbowy,
        ; w rcx  znajduje sie czwarty parametr calkowitoliczbowy,
        ; w r8   znajduje sie piaty parametr calkowitoliczbowy,
        ; w r9   znajduje sie szosty parametr calkowitoliczbowy,
        ; w [rbp+16]  znajduje sie pierwszy parametr wymagajacy stosu,
        ; w [rbp+24] znajduje sie drugi parametr wymagajacy stosu
        ; itd.

        %idefine        a       rdi
        %idefine        b       rsi

                push    rbp
                mov     rbp, rsp

        ; przypominam, ze nasze parametry sa w rzeczywistosci
        ; wskaznikami do prawdziwych parametrow

                mov     rdx, a          ; EDX = adres pierwszego parametru
                mov     rax, [rdx]      ; EAX = pierwszy parametr
                mov     rdx, b
                add     rax, [rdx]

        ; LEAVE = mov rsp, rbp / pop rbp
                leave
                ret

   I teraz plik asm2fl.f:
        PROGRAM funkcja_zewnetrzna

        INTEGER a,b,suma

        a=1
        b=2

        WRITE (*,*) suma(a,b)

        END

   Po skompilowaniu (NASM i GNU Fortran):
        nasm -f elf64 asm2fl.asm
        gfortran -o asm2fl asm2fl.f asm2fl.o

   i uruchomieniu, na ekranie tez pojawia sie cyfra 3.
     _________________________________________________________________

Inne jezyki

   Co do innych jezykow, jesli kompilator posiada taka opcje, mozna
   sprobowac wygenerowac kod asemblerowy i z niego dowiedziec sie, jaka
   jest umowa (konwencja) przekazywania parametrow, np. dla kompilatora
   GNU C:
        gcc -S plik.c

   Mozna tez poszukac takich informacji w Internecie.
     _________________________________________________________________

   Informacji podanych w tym dokumencie NIE nalezy traktowac jako
   "uniwersalnych, jedynie slusznych regul dzialajacych w kazdej
   sytuacji". Aby uzyskac kompletne informacje, nalezy zapoznac sie z
   dokumentacja posiadanego kompilatora.

   Poprzednia czesc kursu (klawisz dostepu 3)
   Kolejna czesc kursu (klawisz dostepu 4)
   Spis tresci off-line (klawisz dostepu 1)
   Spis tresci on-line (klawisz dostepu 2)
   Ulatwienia dla niepelnosprawnych (klawisz dostepu 0)
     _________________________________________________________________

Cwiczenia

    1. Napisz plik asemblera, zawierajacy funkcje obliczania reszty z
       dzielenia dwoch liczb calkowitych. Nastepnie, polacz ten plik z
       programem napisanym w dowolnym innym jezyku (najlepiej w C/C++,
       gdyz jest najpopularniejszy) w taki sposob, by Twoja funkcje mozna
       bylo uruchamiac z tamtego programu. Jesli planujesz laczyc
       asemblera z C, upewnij sie ze Twoja funkcja dziala rowniez z
       programami napisanymi w C++.
