x86-64-Speicheradressierung und Stackframes
Effektive Adressen, Ausrichtung, Stackwachstum und Funktionsrahmen nachvollziehen
x86-64-Speicheradressierung und Stackframes
Diese Blogartikel sind als Lehrmaterial gedacht. Die Beispiele verwenden Intel-Syntax. Das genaue Stacklayout hängt von ABI, Compiler, Optimierung und Debuginformationen ab.
Im Assembler kann player.health als [rcx+12] und values[i] als [rdi+rsi*4] erscheinen. Auch eine lokale Variable ist kein dauerhaftes Stackfach, sondern Speicher relativ zum aktuellen rsp oder einem optionalen Frame Pointer.
Effektive Adressen
[base + index * scale + displacement]
scale ist 1, 2, 4 oder 8; alle anderen Bestandteile können entfallen.
mov eax, [rcx]
mov eax, [rcx + 12]
mov eax, [rcx + rdx*4]
mov eax, [rcx + rdx*4 + 12]
Der Klammerausdruck wird als Adresse berechnet und in der angegebenen Breite gelesen oder geschrieben. lea gibt dagegen nur die berechnete Adresse zurück.
lea rax, [rcx + 8] ; Adresse
mov rax, [rcx + 8] ; dort gespeicherter Wert
Registerbits tragen kein Zeiger-Typetikett. Beim Debuggen sollte deshalb markiert werden, ob ein Wert eine Adresse oder Nutzdaten darstellt.
Arrays und Strukturen
Für ein Array aus 4-Byte-int:
mov eax, dword ptr [rcx + rdx*4]
Die Adresse lautet base + index * sizeof(int). Eine 12-Byte-Struktur braucht zusätzliche Arithmetik, da der Hardware-Scale nur 1, 2, 4 und 8 erlaubt.
lea rax, [rdx + rdx*2]
mov ecx, [rcx + rax*4 + 4]
Ein Feldoffset ist ein Binärvertrag, kein Feldname. Reihenfolge, Packing, Vererbung oder ABI können seine Bedeutung ändern.
static_assert(offsetof(Player, health) == 12);
static_assert(sizeof(Player) == 16);
RIP-relativ und Little Endian
lea rdi, [rip + message]
mov eax, dword ptr [rip + counter]
Die Daten werden relativ zur Befehlsposition gefunden, statt eine vollständige absolute Adresse einzubetten. Dynamisches Linken kann zusätzliche Indirektion über ELF-GOT/PLT oder PE-Importtabellen erzeugen.
0x1122334455667788 liegt im x86-64-Speicher so:
Adresse +0 +1 +2 +3 +4 +5 +6 +7
Byte 88 77 66 55 44 33 22 11
Endianness beschreibt die Bytefolge einer Mehrbyte-Speicherdarstellung, nicht die Reihenfolge der Arithmetik.
Ausrichtung hat mehrere Verträge
Viele x86-64-Loads und -Stores tolerieren unaligned Adressen. Das macht sie nicht immer korrekt oder kostenlos.
- Cacheline- und Seitengrenzen können die Kosten erhöhen.
- Einige SIMD-Formen erfordern Ausrichtung.
- Das ABI verlangt Stackausrichtung an Aufrufgrenzen.
- Ein unterausgerichteter typisierter Zeiger kann in C/C++ undefiniertes Verhalten verursachen.
- Atomarität hängt von Befehl, Breite, Ausrichtung und Grenze ab.
Der Stack wächst zu kleineren Adressen
push rbx ; rsp -= 8; [rsp] = rbx
pop rbx ; rbx = [rsp]; rsp += 8
call speichert die Rücksprungadresse und überträgt die Kontrolle; ret holt sie zurück.
vor call RSP = S
am Callee-Eintritt RSP = S - 8
nach ret RSP = S
Auch wenn moderne ABIs viele Argumente in Registern übergeben, liegt die Rücksprungadresse auf dem Stack.
Stackframe
push rbp
mov rbp, rsp
sub rsp, 32
; body
add rsp, 32
pop rbp
ret
Der Code sichert rbp, erzeugt einen stabilen Bezugspunkt, reserviert 32 Byte und macht alles rückgängig. Ein Frame Pointer ist optional; optimierter Code kann rbp allgemein verwenden oder ganz ohne Frame auskommen.
Ein Frame kann lokale Arrays, Register-Spills, gesicherte nonvolatile Register, Stackargumente, Windows-Shadow-Space, Padding, Canary und dynamischen Speicher enthalten. [rsp+40] ist daher nicht automatisch „die fünfte lokale Variable“.
Leaf-Funktionen und ABI-Unterschiede
add_three:
lea eax, [rdi + rsi]
add eax, edx
ret
Eine Leaf-Funktion kann den Stack vollständig vermeiden. Unter Windows x64 reserviert der Caller 32 Byte Shadow Space. System V kennt diesen Bereich nicht, erlaubt geeigneten User-Space-Leaf-Funktionen aber eine 128-Byte-Red-Zone unter rsp. Windows besitzt diese Red Zone nicht.
Pufferschleife als Vertrag lesen
xor eax, eax
xor r8d, r8d
loop_begin:
cmp r8, rdx
jae loop_end
movsxd r9, dword ptr [rcx + r8*4]
add rax, r9
inc r8
jmp loop_begin
loop_end:
ret
[rcx+r8*4] ist die mögliche Adresse von values[r8], movsxd erweitert ein signed 32-Bit-Element. Bei count > 0 muss der Caller mindestens count*4 lesbare Bytes bereitstellen. ASM kann die Allokationsgröße nicht aus einem Zeiger ermitteln.
Debug-Arbeitsblatt
| Zeitpunkt | rsp | Bedeutung von [rsp] | Sicherungen |
|---|---|---|---|
| Eintritt | Basis | Rücksprungadresse | noch keine |
| nach Prolog | Basis - N | Local/Save-Bereich | auflisten |
vor nested call | ABI-Ausrichtung prüfen | Outgoing-Bereich | Livewerte schützen |
| nach Epilog | Eintrittswert | Rücksprungadresse | vollständig restauriert |
Stackbeschädigung zeigt sich oft als ret zu einer fremden Adresse, veränderte nonvolatile Caller-Register, nur bei Exceptions kaputtes Stack Walking oder Faults in ausgerichteten SIMD-Befehlen.
Zusammenfassung
x86-64 bildet effektive Adressen als [base + index*scale + displacement]. Scale verrät oft die Elementgröße, Displacement den Feldoffset. call legt die Rücksprungadresse auf den Stack; Funktionen reservieren lokale, Spill-, Sicherungs- und Aufrufbereiche nur bei Bedarf. Statt eines universellen Framebilds muss jede Änderung von rsp verfolgt werden.