Intel x86 Assembly Language & Microarchitecture
Aufrufkonventionen
Suche…
Bemerkungen
Ressourcen
Übersichten / Vergleiche: Agner Fog's netter Call Guide . X86-ABIs (wikipedia) : Aufrufkonventionen für Funktionen, einschließlich x86-64 Windows und System V (Linux).
SystemV x86-64 ABI (offizieller Standard) . Wird von allen Betriebssystemen außer Windows verwendet. ( Diese github-Wiki-Seite , die von HJ Lu auf dem neuesten Stand gehalten wird, enthält Links zu 32bit, 64bit und x32. Auch Links zum offiziellen Forum für ABI-Betreuer / innen.) Beachten Sie auch, dass clang / gcc sign / zero schmale Argumente ausdehnen 32bit , obwohl das ABI, wie geschrieben, dies nicht erfordert. Von Clang generierter Code hängt davon ab.
SystemV 32bit (i386) ABI (offizieller Standard) , verwendet von Linux und Unix. ( alte Version ).
OS X 32bit x86-Aufrufkonvention mit Links zu den anderen . Die 64-Bit-Aufrufkonvention ist System V. Die Site von Apple verlinkt hierzu lediglich eine FreeBSD-PDF.
Windows x86-64
__fastcallAufrufkonventionWindows
__vectorcall: Dokumentiert die 32bit und 64bit VersionenWindows 32bit
__stdcall:__stdcallverwendet, um Win32-API-Funktionen aufzurufen. Diese Seite ist mit den anderen aufrufenden Konventionsdokumenten (z. B.__cdecl)__cdecl.Warum verwendet Windows64 eine andere Aufrufkonvention als alle anderen Betriebssysteme unter x86-64? : einige interessante geschichte, insb. für das SysV-ABI, wo die Mailinglisten-Archive öffentlich sind, und zurückgehen, bevor AMDs erstes Silizium veröffentlicht.
32-bit cdecl
cdecl ist eine Windows-32-Bit-Funktionsaufrufkonvention, die der auf vielen POSIX-Betriebssystemen verwendeten Aufrufkonvention sehr ähnlich ist (dokumentiert im i386 System V ABI ). Einer der Unterschiede besteht in der Rückgabe kleiner Strukturen.
Parameter
Parameter werden an den Stack übergeben, wobei sich das erste Argument zum Zeitpunkt des Aufrufs an der niedrigsten Adresse auf dem Stack befindet (zuletzt gedrückt, also bei Eingabe der Funktion nur oberhalb der Rücksprungadresse). Der Aufrufer ist dafür verantwortlich, die Parameter nach dem Aufruf wieder aus dem Stack zu entfernen.
Rückgabewert
Bei skalaren Rückgabetypen wird der Rückgabewert in EAX oder EDX: EAX für 64-Bit-Ganzzahlen abgelegt. Fließkommatypen werden in st0 (x87) zurückgegeben. Die Rückgabe größerer Typen wie Strukturen erfolgt durch Verweis, wobei ein Zeiger als impliziter erster Parameter übergeben wird. (Dieser Zeiger wird in EAX zurückgegeben, sodass der Anrufer sich nicht daran erinnern muss, was er übergeben hat.)
Gespeicherte und überholte Register
EBX, EDI, ESI, EBP und ESP (und die Einstellungen für den Rundungsmodus FP / SSE) müssen vom Angerufenen beibehalten werden, sodass der Anrufer darauf vertrauen kann, dass diese Register nicht durch einen Anruf geändert wurden.
Alle anderen Register (EAX-, ECX-, EDX-, FLAGS- (außer DF), x87- und Vektorregister) können vom Betrüger frei modifiziert werden; Wenn ein Aufrufer vor und nach dem Funktionsaufruf einen Wert beibehalten möchte, muss er den Wert an anderer Stelle speichern (z. B. in einem der gespeicherten Register oder auf dem Stack).
64-Bit-System V
Dies ist die Standardaufrufkonvention für 64-Bit-Anwendungen auf vielen POSIX-Betriebssystemen.
Parameter
Die ersten acht Skalarparameter werden (in Reihenfolge) in RDI, RSI, RDX, RCX, R8, R9, R10, R11 übergeben. Parameter, die nach den ersten acht liegen, werden auf dem Stack platziert, wobei sich die früheren Parameter näher am oberen Rand des Stack befinden. Der Aufrufer ist dafür verantwortlich, diese Werte nach dem Aufruf vom Stack zu entfernen, wenn er nicht mehr benötigt wird.
Rückgabewert
Bei skalaren Rückgabetypen wird der Rückgabewert in RAX platziert. Um größere Typen wie Strukturen zurückzugeben, wird die Signatur der Funktion konzeptionell so geändert, dass am Anfang der Parameterliste ein Parameter hinzugefügt wird, der ein Zeiger auf eine Stelle ist, an der der Rückgabewert platziert werden soll.
Gespeicherte und überholte Register
RBP, RBX und R12 – R15 werden vom Aufseher bewahrt. Alle anderen Register können vom Angerufenen modifiziert werden, und der Aufrufer muss den Wert eines Registers selbst (z. B. auf dem Stapel) beibehalten, wenn er diesen Wert später verwenden möchte.
32-Bit-Standardruf
stdcall wird für 32-Bit-Windows-API-Aufrufe verwendet.
Parameter
Parameter werden an den Stapel übergeben, wobei der erste Parameter am oberen Rand des Stapels liegt. Der Betrüger wird diese Werte aus dem Stapel ziehen, bevor er zurückkehrt.
Rückgabewert
Skalare Rückgabewerte werden in EAX platziert.
Gespeicherte und überholte Register
EAX, ECX und EDX können vom Angerufenen frei modifiziert werden und müssen vom Anrufer auf Wunsch gespeichert werden. EBX, ESI, EDI und EBP müssen vom Angerufenen gespeichert werden, wenn sie geändert und bei der Rückkehr auf ihre ursprünglichen Werte zurückgesetzt werden.
32-Bit, cdecl - Umgang mit ganzen Zahlen
Als Parameter (8, 16, 32 Bit)
8, 16, 32-Bit-Ganzzahlen werden auf dem Stapel immer als 32-Bit-Werte mit voller Breite ( 1) übergeben .
Es ist keine Erweiterung (signiert oder nullgestellt) erforderlich.
Der Aufseher verwendet nur den unteren Teil der Werte für die volle Breite.
//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
Als Parameter (64 Bit)
64-Bit-Werte werden durch zwei Push-Vorgänge an den Stack übergeben, wobei die Konvention von Little Endian 2 beachtet wird. Dabei werden zuerst die höheren 32 Bits und dann die niedrigeren Bits verschoben.
//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
Als Rückgabewert
In AL 8-Bit-Ganzzahlen zurückgegeben, die schließlich den ganzen eax .
In AX 16-Bit-Ganzzahlen zurückgegeben, die schließlich den gesamten eax .
32-Bit-Ganzzahlen werden in EAX .
In EDX:EAX 64-Bit-Ganzzahlen zurückgegeben, wobei EAX die unteren 32 Bits und EDX die oberen enthält.
//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 Dadurch bleibt der Stapel auf 4 Byte, der natürlichen Wortgröße, ausgerichtet. Auch eine x86-CPU kann nur 2 oder 4 Bytes übertragen, wenn sie sich nicht im langen Modus befindet.
2 Senken Sie DWORD an der unteren Adresse
32-Bit, cdecl - Umgang mit Floating Point
Als Parameter (float, double)
Floats haben eine Größe von 32 Bit und werden auf dem Stack natürlich weitergegeben.
Doubles haben eine Größe von 64 Bits. Sie werden auf dem Stack unter Beachtung der Little-Endian-Konvention 1 übergeben , wobei zuerst die oberen 32 Bits und dann die unteren Bits gedrückt werden.
//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
Als Parameter (langes Doppel)
Long-Doubles sind 80 Bit 2 breit, während auf dem Stack ein TBYTE mit zwei 32-Bit-Pushs und einem 16-Bit-Push (für 4 + 4 + 2 = 10) gespeichert werden kann, um den Stack auf 4 Bytes ausgerichtet zu halten 12 Bytes, also drei 32-Bit-Push-Vorgänge.
Gemäß der Little-Endian-Konvention werden die Bits 79-64 zuerst 3 , dann die Bits 63-32 gefolgt von den Bits 31-0 geschoben.
//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
Als Rückgabewert
Ein Gleitkommawert, unabhängig von seiner Größe, wird in ST(0) 4 zurückgegeben .
//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 Senken Sie DWORD an der unteren Adresse.
2 Bekannt als TBYTE aus zehn Bytes.
3 Bei einem Push mit voller Breite und beliebiger Erweiterung wird kein höheres WORD verwendet.
4 Was TBYE-Breit ist, beachten Sie, dass FP im Gegensatz zu ganzen Zahlen immer mit einer höheren Genauigkeit zurückgegeben wird, als dies erforderlich ist.
64-Bit-Windows
Parameter
Die ersten 4 Parameter werden (in Reihenfolge) an RCX, RDX, R8 und R9 übergeben. XMM0 bis XMM3 werden zum Übergeben von Gleitkomma-Parametern verwendet.
Alle weiteren Parameter werden an den Stack übergeben.
Parameter, die größer als 64 Bit sind, werden von der Adresse übergeben.
Platz verschütten
Selbst wenn die Funktion weniger als 4 Parameter verwendet, bietet der Aufrufer immer Platz für 4 QWORD-Parameter auf dem Stack. Dem Angerufenen steht es frei, sie für jeden Zweck zu verwenden, es ist üblich, die Parameter dort zu kopieren, wenn sie von einem anderen Anruf verschüttet würden.
Rückgabewert
Bei skalaren Rückgabetypen wird der Rückgabewert in RAX platziert. Wenn der Rückgabetyp größer als 64 Bit ist (z. B. für Strukturen), ist RAX ein Zeiger darauf.
Gespeicherte und überholte Register
Alle bei der Parameterübergabe verwendeten Register (RCX, RDX, R8, R9 und XMM0 bis XMM3), RAX, R10, R11, XMM4 und XMM5 können vom Angreifer übergossen werden. Alle anderen Register müssen vom Aufrufer erhalten bleiben (zB auf dem Stack).
Stapelausrichtung
Der Stapel muss 16-Byte ausgerichtet bleiben. Da die Anweisung "Aufruf" eine 8-Byte-Rücksprungadresse drückt, bedeutet dies, dass jede Nicht-Blatt-Funktion den Stapel um einen Wert der Form 16n + 8 anpassen wird, um die 16-Byte-Ausrichtung wiederherzustellen.
Es ist die Aufgabe des Aufrufers, den Stack nach einem Anruf zu bereinigen.
Quelle: Die Geschichte der Anrufkonventionen, Teil 5: amd64 Raymond Chen
32-Bit, cdecl - Umgang mit Strukturen
Polsterung
Denken Sie daran, dass Mitglieder einer Struktur normalerweise aufgefüllt werden, um sicherzustellen, dass sie an ihrer natürlichen Grenze ausgerichtet sind:
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)
};
Als Parameter (Referenz übergeben)
Bei der Übergabe als Verweis wird ein Zeiger auf die Struktur im Speicher als erstes Argument im Stapel übergeben. Dies entspricht der Übergabe eines ganzzahligen Werts in natürlicher Größe (32 Bit); Weitere Informationen finden Sie unter 32-Bit-cdecl .
Als Parameter (Wert übergeben)
Bei der Übergabe als Wert werden die Strukturen vollständig auf den Stapel kopiert, wobei das ursprüngliche Speicherlayout beachtet wird ( dh das erste Element befindet sich an der unteren Adresse).
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
Als Rückgabewert
Wenn sie nicht trivial 1 sind , werden die Strukturen vor der Rückgabe in einen vom Aufrufer bereitgestellten Puffer kopiert. Dies ist gleichbedeutend mit einem versteckten ersten Parameter struct S *retval (wobei struct S der Typ der struct ist).
Die Funktion muss mit diesem Zeiger auf den Rückgabewert in eax . Der Aufrufer darf sich darauf verlassen, dass eax den Zeiger auf den Rückgabewert hält, den er direkt vor dem call gedrückt hat.
struct S
{
unsigned char a, b, c;
};
struct S foo(); // compiled as struct S* foo(struct S* _out)
Der ausgeblendete Parameter wird nicht zur Parameterauflistung zum Zwecke der Stapelbereinigung hinzugefügt, da er vom Aufseher gehandhabt werden muss.
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
Im obigen Beispiel wird die Struktur oben im Stapel gespeichert.
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 Eine "triviale" Struktur ist eine Struktur, die nur ein Mitglied eines Nicht-Struktur-Nicht-Array-Typs (bis zu 32 Bit) enthält. Für solche Strukturen wird der Wert dieses Members einfach im eax Register zurückgegeben. (Dieses Verhalten wurde bei GCC-Targeting unter Linux beobachtet.)
Die Windows-Version von cdecl unterscheidet sich von der Aufrufkonvention von System V ABI: Eine "triviale" Struktur darf bis zu zwei Member eines Nicht-Struktur-Nicht-Array-Typs (bis zu 32 Bit) enthalten. Diese Werte werden in eax und edx , genau wie bei einer 64-Bit-Ganzzahl. (Dieses Verhalten wurde für MSVC und Clang beobachtet, die auf Win32 abzielen.)