   #Start Prev Next Contents

   Jak pisac programy w jezyku asembler?

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:
        { DOS/Windows }

        program pas1;

        begin
                asm mov eax,4
        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
   wskazujacymi 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 wyrazeniem "asm {" a koncza klamra
   zamykajaca "}" (ale NIE w gcc, o tym pozniej). Przyklad:
        asm {
                mov eax, 1
        }

   Wszystkie nowe kompilatory produkuja programy 32- lub 64-bitowe,
   przypominam wiec, aby we wstawkach NIE uzywac przerwan ( DOS-a i
   BIOS-u w Windows).

   W C i C++ mozna, podobnie jak w Pascalu, deklarowac zmienne
   reprezentujace rejestry procesora. Plik naglowkowy BIOS.H 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. Kompilatory jezyka C dla
   DOS dekoruja nazwy, dodajac znak podkreslenia z przodu.
   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 na stosie 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 (takze od prawej do lewej):
     * 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 lub
       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, aby wykonal za nas laczenie (nie musimy
   uruchamiac linkera).

   No to krotki 32-bitowy przykladzik (uzyje NASMa i Borland C++
   Builder):
        ; NASM casm1.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

                mov     eax, a
                add     eax, b

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

   oraz plik casm.c:
        #include <stdio.h>

        extern int _suma (int a, int b); /* deklaracja funkcji zewnetrznej */

        int suma (int a, int b);         /* prototyp funkcji */

        int c=1, d=2;

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

   Kompilacja odbywa sie tak:
        nasm -o casm1.obj -f obj casm1.asm
        bcc32 casm.c casm1.obj

   Uwaga: w kompilatorach GNU: DJGPP, Dev-C++, MinGW, CygWin format
   wyjsciowy NASMa powinien byc ustawiony na COFF. Mozliwe, ze format
   COFF trzeba bedzie wybrac takze w innych.

   W wyniku otrzymujemy programik, ktory na ekranie elegancko wyswietla
   wynik rowny 3.

   DOS nie uruchomi programow 64-bitowych, ale dla kompletnosci
   przedstawiam powyzszy 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 long int suma (long long int a, long long int b);

        static long long int c=1, d=2;

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

   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 (pamietajac o znaku
   podkreslenia), 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.obj -f obj casm2.asm
        bcc32 casm2.obj

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

        global main

        extern printf

        main:
                ; printf("Liczba jeden to: %lld\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: %lld", 10, 0

   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.

   Kompilator GNU gcc wymaga osobnego wytlumaczenia. Skladnia wstawek
   asemblerowych rozni sie od powyzszej dosc znacznie, a jej opisy
   mozecie znalezc w podreczniku GCC (sekcje: 5.34 i 5.35), na stronach
   DJGPP oraz (w jezyku polskim) na stronie pana Danileckiego.

   Jak zauwazycie, rozni sie nawet sam wyglad instrukcji, gdyz domyslnie
   gcc uzywa skladni AT&T jezyka asembler. U siebie mam krotkie
   porownanie tych skladni.
     _________________________________________________________________

Fortran 77

   (przeskocz Fortrana 77)

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

   Fortran 77 dekoruje nazwy, stawiajac znak podkreslenia PO nazwie
   funkcji lub zmiennej (wyjatkiem jest funkcja glowna - blok PROGRAM -
   ktora nazywa sie MAIN__, z dwoma podkresleniami).
   Dodatkowo, kompilatory jezyka Fortran 77 dla DOS dekoruja nazwy,
   dodajac znak podkreslenia z przodu (podobnie, jak w przypadku jezyka
   C).
   Tak wiec, pod DOS, funkcja glowna nazywa sie _MAIN__.

   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_   ; podkreslenie z przodu dla DOS, z tylu - dla Fortrana

   (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 pod
   Linuksem z kompilatorem F77/G77).

   Podam teraz przyklad laczenia Fortrana 77 i asemblera. W oryginale
   uzylem narzedzi Linuksowych: NASMa i F77, ale po minimalnych
   przerobkach powinno to tez dzialac pod Windows. Oto pliki:
        ; NASM - asm1fl.asm
        section .text use32
        global  _suma_

        _suma_:         ; podkreslenie z przodu dla DOS, z tylu - dla Fortrana

        ; 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 (ewentualnie zmieniajac opcje "-f" u NASMa):
        nasm -f obj -o asm1fl.obj asm1fl.asm
        f77 -o asmfl.exe asmfl.f asm1fl.obj

   i uruchomieniu na ekranie powinna ponownie pojawic sie cyfra 3.

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

   Ponownie, dla kompletnosci, przedstawiam powyzszy program w wersji
   64-bitowej:
        ; 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
     _________________________________________________________________

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++.
