Podprogramy
Przekazywanie parametrów

Kod który wywołuje podprogram i podprogram muszą uzgodnić

Często takie umowy standaryzuje się jako tzw. konwencje wywołania (calling conventions)

Konwencje wywołania

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ć!

Pełniejsza lista

Wywołania systemowe Linux x86 - int 0x80
Wywołania systemowe Linux AMD64 - syscall
Konwencja cdecl

Konwencja cdecl - funkcja wywołująca

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

Konwencja cdecl - funkcja wywoływana

Podprogram może swobodnie modyfikować rejestry EAX, ECX, EDX pozostałe po wyjściu z podprogramu powinny być niezmienione.

Przykładowy podprogram

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
 

ENTER i LEAVE

Instrukcje ENTER i LEAVE tworzą i usuwają ramkę stosu.

Instrukcja

  enter rozmiar, 0
odpowiada
   push ebp               
   mov  ebp, esp     
   sub  esp, rozmiar
Nie jest ona jednak powszechnie używana gdyż jej wykonanie jest znacznie wolniejsze niż odpowiadającej jej sekwencji prostszych instrukcji.

Instrukcja

  leave
odpowiada
   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
 

Interfejsowanie z C

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);
i możemy już jej normalnie używać.

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

Konsekwencje złamania konwencji

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


Tomasz Kapela, Kraków 2011-2020