Intel x86 Assembly Language & Microarchitecture
Convenzioni di chiamata
Ricerca…
Osservazioni
risorse
Panoramiche / confronti: guida alle convenzioni di buona pratica di Agner Fog . Inoltre, x86 ABI (wikipedia) : chiama le convenzioni per le funzioni, tra cui x86-64 Windows e System V (Linux).
SystemV x86-64 ABI (standard ufficiale) . Utilizzato da tutti i sistemi operativi ma Windows. ( Questa pagina wiki github , aggiornata da HJ Lu, ha collegamenti a 32bit, 64bit e x32. Inoltre i link al forum ufficiale per i manutentori / contributori di ABI.) Nota anche che clang / gcc sign / zero estendono gli argomenti stretti a 32 bit , anche se l'ABI come scritto non lo richiede. Il codice generato da Clang dipende da questo.
SystemV 32bit (i386) ABI (standard ufficiale) , utilizzato da Linux e Unix. ( vecchia versione ).
Convenzione di chiamata OS X 32 bit x86, con collegamenti agli altri . La convenzione di chiamata a 64 bit è System V. Il sito di Apple si collega semplicemente a un pdf di FreeBSD.
Windows x86-64
__fastcallconvenzione di chiamataWindows
__vectorcall: documenta le versioni a 32 bit e 64 bitWindows
__stdcall32__stdcall: usato per chiamare le funzioni API Win32. Quella pagina si collega agli altri documenti della convenzione di chiamata (es.__cdecl).Perché Windows64 utilizza una convenzione di chiamata diversa da tutti gli altri SO su x86-64? : qualche storia interessante, esp. per l'ABI di SysV in cui gli archivi delle mailing list sono pubblici e risalgono prima del rilascio del primo silicio da parte di AMD.
Cdecl a 32 bit
cdecl è una convenzione di chiamata di una funzione a 32 bit di Windows che è molto simile alla convenzione di chiamata utilizzata su molti sistemi operativi POSIX (documentata nell'Ii6 V System i386 ). Una delle differenze sta nel restituire piccole strutture.
parametri
I parametri vengono passati in pila, con il primo argomento all'indirizzo più basso nello stack al momento della chiamata (premuto per ultimo, quindi è appena sopra l'indirizzo di ritorno all'ingresso della funzione). Il chiamante è responsabile del ritorno dei parametri di popping allo stack dopo la chiamata.
Valore di ritorno
Per i tipi di ritorno scalari, il valore restituito viene inserito in EAX o EDX: EAX per gli interi a 64 bit. I tipi a virgola mobile vengono restituiti in st0 (x87). Il ritorno di tipi più grandi come le strutture avviene per riferimento, con un puntatore passato come primo parametro implicito. (Questo puntatore viene restituito in EAX, quindi il chiamante non deve ricordare cosa ha passato).
Registri salvati e danneggiati
EBX, EDI, ESI, EBP ed ESP (e le impostazioni della modalità di arrotondamento FP / SSE) devono essere conservate dal destinatario, in modo che il chiamante possa fare affidamento su quei registri che non sono stati modificati da una chiamata.
Tutti gli altri registri (EAX, ECX, EDX, FLAGS (diversi da DF), x87 e registri vettoriali) possono essere liberamente modificati dal callee; se un chiamante desidera conservare un valore prima e dopo la chiamata alla funzione, deve salvare il valore altrove (ad esempio in uno dei registri salvati o in pila).
Sistema V a 64 bit
Questa è la convenzione di chiamata predefinita per le applicazioni a 64 bit su molti sistemi operativi POSIX.
parametri
I primi otto parametri scalari vengono passati (in ordine) RDI, RSI, RDX, RCX, R8, R9, R10, R11. I parametri oltre i primi otto sono posizionati sullo stack, con i parametri precedenti più vicini alla cima dello stack. Il chiamante è responsabile di estrarre questi valori dallo stack dopo la chiamata se non sono più necessari.
Valore di ritorno
Per i tipi di ritorno scalari, il valore di ritorno viene inserito in RAX. Il ritorno di tipi più grandi come le strutture avviene cambiando concettualmente la firma della funzione per aggiungere un parametro all'inizio dell'elenco dei parametri che è un puntatore a una posizione in cui posizionare il valore di ritorno.
Registri salvati e danneggiati
RBP, RBX e R12-R15 sono conservati dal destinatario. Tutti gli altri registri possono essere modificati dal callee, e il chiamante deve conservare il valore di un registro stesso (ad esempio nello stack) se desidera utilizzare quel valore in seguito.
Stdcall a 32 bit
stdcall viene utilizzato per chiamate API Windows a 32 bit.
parametri
I parametri vengono passati in pila, con il primo parametro più vicino alla cima dello stack. Il callee espellerà questi valori dalla pila prima di tornare.
Valore di ritorno
I valori di ritorno scalari sono posizionati in EAX.
Registri salvati e danneggiati
EAX, ECX ed EDX possono essere modificati liberamente dal callee e, se lo si desidera, devono essere salvati dal chiamante. EBX, ESI, EDI ed EBP devono essere salvati dal callee se modificati e ripristinati ai loro valori originali al ritorno.
32-bit, cdecl - Gestire i numeri interi
Come parametri (8, 16, 32 bit)
Gli interi 8, 16, 32 bit vengono sempre passati, nello stack, come valori a 32 bit a larghezza intera 1 .
Non è necessaria alcuna estensione, firmata o azzerata.
Il callee utilizzerà solo la parte inferiore dei valori di larghezza completa.
//C prototype of the callee
void __attribute__((cdecl)) foo(char a, short b, int c, long d);
foo(-1, 2, -3, 4);
;Call to foo in assembly
push DWORD 4 ;d, long is 32 bits, nothing special here
push DWORD 0fffffffdh ;c, int is 32 bits, nothing special here
push DWORD 0badb0002h ;b, short is 16 bits, higher WORD can be any value
push DWORD 0badbadffh ;a, char is 8 bits, higher three bytes can be any value
call foo
add esp, 10h ;Clean up the stack
Come parametri (64 bit)
I valori a 64 bit vengono passati sullo stack usando due push, rispettando la convenzione 2 di littel endian, spingendo prima i 32 bit più alti di quelli inferiori.
//C prototype of the callee
void __attribute__((cdecl)) foo(char a, short b, int c, long d);
foo(0x0123456789abcdefLL);
;Call to foo in assembly
push DWORD 89abcdefh ;Higher DWORD of 0123456789abcdef
push DWORD 01234567h ;Lower DWORD of 0123456789abcdef
call foo
add esp, 08h
Come valore di ritorno
Gli interi a 8 bit vengono restituiti in AL , finendo con il clobbering dell'intera eax .
Gli interi a 16 bit vengono restituiti in AX , finendo con il clobbering dell'intera eax .
I numeri interi a 32 bit vengono restituiti in EAX .
Gli interi a 64 bit vengono restituiti in EDX:EAX , dove EAX contiene i 32 bit inferiori e EDX quelli superiori.
//C
char foo() { return -1; }
;Assembly
mov al, 0ffh
ret
//C
unsigned short foo() { return 2; }
;Assembly
mov ax, 2
ret
//C
int foo() { return -3; }
;Assembly
mov eax, 0fffffffdh
ret
//C
int foo() { return 4; }
;Assembly
xor edx, edx ;EDX = 0
mov eax, 4 ;EAX = 4
ret
1 Ciò mantiene la pila allineata su 4 byte, la dimensione naturale della parola. Anche una CPU x86 può solo inviare 2 o 4 byte quando non è in modalità lunga.
2 Abbassare DWORD all'indirizzo inferiore
32-bit, cdecl - Gestire il punto mobile
Come parametri (float, double)
I float hanno una dimensione di 32 bit, sono passati naturalmente in pila.
I doppi hanno una dimensione di 64 bit, sono passati, in pila, rispettando la convenzione di Little Endian 1 , spingendo prima i 32 bit superiori e quelli inferiori.
//C prototype of callee
double foo(double a, float b);
foo(3.1457, 0.241);
;Assembly call
;3.1457 is 0x40092A64C2F837B5ULL
;0.241 is 0x3e76c8b4
push DWORD 3e76c8b4h ;b, is 32 bits, nothing special here
push DWORD 0c2f837b5h ;a, is 64 bits, Higher part of 3.1457
push DWORD 40092a64h ;a, is 64 bits, Lower part of 3.1457
call foo
add esp, 0ch
;Call, using the FPU
;ST(0) = a, ST(1) = b
sub esp, 0ch
fstp QWORD PTR [esp] ;Storing a as a QWORD on the stack
fstp DWORD PTR [esp+08h] ;Storing b as a DWORD on the stack
call foo
add esp, 0ch
Come parametri (lungo doppio)
I doppi lunghi sono larghi 80 bit 2 , mentre nello stack un TBYTE può essere memorizzato con due push da 32 bit e uno push da 16 bit (per 4 + 4 + 2 = 10), per mantenere allineato lo stack su 4 byte, finisce di occupare 12 byte, usando quindi tre push da 32 bit.
Rispettando la convenzione Little Endian, i bit 79-64 vengono premuti per primi 3 , quindi i bit 63-32 seguiti dai bit 31-0.
//C prototype of the callee
void __attribute__((cdecl)) foo(long double a);
foo(3.1457);
;Call to foo in assembly
;3.1457 is 0x4000c9532617c1bda800
push DWORD 4000h ;Bits 79-64, as 32 bits push
push DWORD 0c9532617h ;Bits 63-32
push DWORD 0c1bda800h ;Bits 31-0
call foo
add esp, 0ch
;Call to foo, using the FPU
;ST(0) = a
sub esp, 0ch
fstp TBYTE PTR [esp] ;Store a as ten byte on the stack
call foo
add esp, 0ch
Come valore di ritorno
Un valore a virgola mobile, qualunque sia la sua dimensione, viene restituito in ST(0) 4 .
//C
float one() { return 1; }
;Assembly
fld1 ;ST(0) = 1
ret
//C
double zero() { return 0; }
;Assembly
fldz ;ST(0) = 0
ret
//C
long double pi() { return PI; }
;Assembly
fldpi ;ST(0) = PI
ret
1 Abbassare DWORD all'indirizzo inferiore.
2 Conosciuto come TBYTE, da dieci byte.
3 Usando una spinta a larghezza piena con qualsiasi estensione, non si utilizza WORD più alta.
4 Che è TBYE ampio, si noti che, contrariamente agli interi, FP viene sempre restituito con maggiore precisione che è richiesto.
Windows a 64 bit
parametri
I primi 4 parametri sono passati in (in ordine) RCX, RDX, R8 e R9. Da XMM0 a XMM3 vengono utilizzati per passare i parametri in virgola mobile.
Eventuali ulteriori parametri vengono passati in pila.
I parametri maggiori di 64 bit vengono passati per indirizzo.
Spill Space
Anche se la funzione utilizza meno di 4 parametri, il chiamante offre sempre spazio per 4 parametri QWORD in pila. Il callee è libero di usarli per qualsiasi scopo, è comune copiare i parametri lì se verrebbero versati da un'altra chiamata.
Valore di ritorno
Per i tipi di ritorno scalari, il valore di ritorno viene inserito in RAX. Se il tipo di ritorno è maggiore di 64 bit (ad es. Per le strutture) RAX è un puntatore a questo.
Registri salvati e danneggiati
Tutti i registri utilizzati nel passaggio dei parametri (RCX, RDX, R8, R9 e XMM0 a XMM3), RAX, R10, R11, XMM4 e XMM5 possono essere sversati dal destinatario. Tutti gli altri registri devono essere preservati dal chiamante (ad esempio nello stack).
Allineamento dello stack
Lo stack deve essere mantenuto allineato a 16 byte. Poiché l'istruzione "call" preme un indirizzo di ritorno a 8 byte, ciò significa che ogni funzione non foglia regolerà lo stack di un valore del modulo 16n + 8 per ripristinare l'allineamento a 16 byte.
È il lavoro dei chiamanti per pulire lo stack dopo una chiamata.
Fonte: la storia delle convenzioni di chiamata, parte 5: amd64 Raymond Chen
32-bit, cdecl - Gestire le strutture
Imbottitura
Ricorda, i membri di una struct sono solitamente riempiti per assicurarsi che siano allineati sul loro confine naturale:
struct t
{
int a, b, c, d; // a is at offset 0, b at 4, c at 8, d at 0ch
char e; // e is at 10h
short f; // f is at 12h (naturally aligned)
long g; // g is at 14h
char h; // h is at 18h
long i; // i is at 1ch (naturally aligned)
};
Come parametri (passa per riferimento)
Quando viene passato per riferimento, un puntatore alla struttura in memoria viene passato come primo argomento sullo stack. Questo è equivalente al passaggio di un valore intero di dimensione naturale (32 bit); vedi cdecl a 32 bit per le specifiche.
Come parametri (passa per valore)
Quando vengono passati per valore, le strutture vengono interamente copiate nello stack, rispettando il layout di memoria originale ( ovvero , il primo membro sarà all'indirizzo più basso).
int __attribute__((cdecl)) foo(struct t a);
struct t s = {0, -1, 2, -3, -4, 5, -6, 7, -8};
foo(s);
; Assembly call
push DWORD 0fffffff8h ; i (-8)
push DWORD 0badbad07h ; h (7), pushed as DWORD to naturally align i, upper bytes can be garbage
push DWORD 0fffffffah ; g (-6)
push WORD 5 ; f (5)
push WORD 033fch ; e (-4), pushed as WORD to naturally align f, upper byte can be garbage
push DWORD 0fffffffdh ; d (-3)
push DWORD 2 ; c (2)
push DWORD 0ffffffffh ; b (-1)
push DWORD 0 ; a (0)
call foo
add esp, 20h
Come valore di ritorno
A meno che non siano banali 1 , le strutture vengono copiate in un buffer fornito dal chiamante prima di tornare. Questo equivale ad avere una prima struct S *retval parametri nascosta struct S *retval (dove struct S è il tipo della struct).
La funzione deve restituire con questo puntatore il valore restituito in eax ; Il chiamante può dipendere da eax tenendo il puntatore al valore di ritorno, che è stato premuto subito prima della call .
struct S
{
unsigned char a, b, c;
};
struct S foo(); // compiled as struct S* foo(struct S* _out)
Il parametro nascosto non viene aggiunto al conteggio dei parametri ai fini della pulizia dello stack, poiché deve essere gestito dal destinatario.
sub esp, 04h ; allocate space for the struct
; call to foo
push esp ; pointer to the output buffer
call foo
add esp, 00h ; still as no parameters have been passed
Nell'esempio sopra, la struttura verrà salvata nella parte superiore della pila.
struct S foo()
{
struct S s;
s.a = 1; s.b = -2; s.c = 3;
return s;
}
; Assembly code
push ebx
mov eax, DWORD PTR [esp+08h] ; access hidden parameter, it is a pointer to a buffer
mov ebx, 03fe01h ; struct value, can be held in a register
mov DWORD [eax], ebx ; copy the structure into the output buffer
pop ebx
ret 04h ; remove the hidden parameter from the stack
; EAX = pointer to the output buffer
1 Una struttura "banale" è una che contiene solo un membro di un tipo non-struct, non-array (fino a 32 bit di dimensione). Per tali strutture, il valore di tale membro viene semplicemente restituito nel registro eax . (Questo comportamento è stato osservato con GCC targeting per Linux)
La versione Windows di cdecl è diversa dalla convenzione di chiamata ABI di System V: una struttura "banale" può contenere fino a due membri di un tipo non-struct, non-array (fino a 32 bit di dimensione). Questi valori vengono restituiti in eax ed edx , proprio come sarebbe un intero a 64 bit. (Questo comportamento è stato osservato per MSVC e Clang targeting Win32.)