Kod który wywołuje podprogram i podprogram muszą uzgodnić
Często takie umowy standaryzuje się jako tzw. konwencje wywołania (calling conventions)
Najczęściej spotykane konwencje 32-bitowe dla x86
Kolejność RLO (right to left order) parametrów na stosie pozwala na definiowanie funkcji o zmiennej liczbie parametrów np. printf. W tym wypadku funkcja nie może sprzątać!
ebx, ecx, edx, esi, edi, ebp
RDI, RSI, RDX, R10, R8, R9
Funkcja wywołująca:
; b = f(a, 5) push ecx ; zachowujemy licznik pętli mov eax, 5 ; odkładamy argumenty na stos push eax ; w odwrotnej kolejności mov eax, [a] push eax call f ; wywołujemy funkcję add esp, 8 ; czyścimy stos mov [b], eax ; zapisujemy wynik pop ecx ; przywracamy licznik pętli
Podprogram może swobodnie modyfikować rejestry EAX, ECX, EDX pozostałe po wyjściu z podprogramu powinny być niezmienione.
int f(int a, int b){ int c = a+b; return c; }
; zwraca sumę a+b f: push ebp ; zachowujemy starą wartość ebp mov ebp, esp sub esp, 4 ; przydzielamy pamięć na zmienna lokalna c mov eax, [ebp+8] ; mov eax, [a] add eax, [ebp+12] ; add eax, [b] mov [ebp-4], eax ; mov [c], eax ; wynik jest juz w eax mov esp, ebp ; zwolnienie pamięci na stosie pop ebp ; przywrócenie poprzedniej wartości ebp ret ; powrót
Zawartość stosu
[ebp+12] b [ebp+8] a [ebp+4] adres powrotu z funkcji [ebp] poprzednia wartość ebp [ebp-4] zmienna lokalna c
Przykład cdecl 32 bit (Pokaż/ukryj kod)
section .data a dd 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 section .bss tab resd 10 section .text ; int f(int a, int b){ ; int c = a+b; ; return c; ; } ; zwraca sumę a+b f: ; etykieta = nazwa fukcji push ebp ; zachowujemy starą wartość ebp mov ebp, esp sub esp, 4 ; przydzielamy pamięć na zmienna lokalna c mov eax, [ebp+8] ; mov eax, [a] add eax, [ebp+12] ; add eax, [b] mov [ebp-4], eax ; mov [c], eax ; wynik jest juz w eax mov esp, ebp ; zwolnienie pamięci na stosie pop ebp ; przywrócenie poprzedniej wartości ebp ret ; powrót global _start _start: ; for(c = 10, c > 0, c--) ; tab[c-1] = f(a[c-1], 5); mov ecx, 10 petla: ; tab[ecx] = f(a, 5) push ecx ; zachowujemy licznik pętli mov eax, 5 ; odkładamy argumenty na stos push eax ; w odwrotnej kolejności mov eax, [a+4*ecx-4] push eax call f ; wywołujemy funkcję add esp, 8 ; czyścimy stos pop ecx ; przywracamy licznik pętli mov [tab+4*ecx-4], eax ; zapisujemy wynik loop petla mov eax, 1 int 80h
Przyklad cdecl ze stosem 64 bitowym (Pokaż/ukryj kod)
section .data a dd 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 section .bss tab resd 10 section .text ; int f(int a, int b){ ; int c = a+b; ; return c; ; } ; zwraca sume a+b f: ; etykieta = nazwa fukcji push rbp ; zachowujemy starą wartość ebp mov rbp, rsp sub rsp, 8 ; przydzielamy pamięć na zmienna lokalna c mov eax, [rbp+16] ; mov eax, [a] add eax, [rbp+24] ; add eax, [b] mov [rbp-8], eax ; mov [c], eax ; wynik jest juz w eax mov rsp, rbp ; zwolnienie pamięci na stosie pop rbp ; przywrócenie poprzedniej wartości ebp ret ; powrót global _start _start: ; for(c = 10, c > 0, c--) ; tab[c-1] = f(a[c-1], 5); mov rcx, 10 petla: ; tab[ecx] = f(a, 5) push rcx ; zachowujemy licznik pętli mov rax, 5 ; odkładamy argumenty na stos push rax ; w odwrotnej kolejności mov eax, [a+4*rcx-4] push rax call f ; wywołujemy funkcję add rsp, 16 ; czyścimy stos pop rcx ; przywracamy licznik pętli mov [tab+4*rcx-4], eax ; zapisujemy wynik loop petla mov rax, 1 int 80h
Instrukcje ENTER i LEAVE tworzą i usuwają ramkę stosu.
Instrukcja
enter rozmiar, 0
push ebp mov ebp, esp sub esp, rozmiar
Instrukcja
leave
mov esp, ebp pop ebp
Przy ich użyciu funkcja f ma postać
; zwraca sumę a+b f: enter 4,0 ; utworzenie ramki stosu mov eax, [ebp+8] ; mov eax, [a] add eax, [ebp+12] ; add eax, [b] mov [ebp-4], eax ; mov [c], eax ; wynik jest juz w eax leave ; usunięcie ramki stosu ret ; powrót
Większość kompilatorów C poprzedza nazwy funkcji znakiem podkreślenia np. funkcja suma będzie
w assemblerze miała nazwę _suma. Kompilator GCC jest tu wyjątkiem: nazwy są dokładnie takie same.
W pliku C deklarujemy funkcję jako zewnętrzną
extern int suma(int, int);
W pliku assemblerowym musimy odpowiedni symbol zdefiniowac jako globalny, aby był widoczny w innych jednostach kompilacji
global suma suma: ; tutaj cialo funkcji ret
Następnie kompilujemy każdy plik osobno i linkujemy razem.
Program w C
Moduł w assemblerze
Jeżeli w naszej funkcji asemblerowej nie bedziemy przestrzegać przyjętej konwencji, to może to doprowadzić do trudnego do zrozumienia dziwnego zachowania programu.
Jeżeli zmienimy wartość jednego z rejestrów, które nie powinny być modyfikowane przez funkcje wywołaną to tak jak gdybyśmy zmieniali zmienne lokalne funkcji wywołującej.
Przykład
Program w C
Moduł w assemblerze