Kurz: Der Stack (Stapel) ist ein Speicherbereich, der “von oben nach unten” wächst. Das Register rsp zeigt auf das oberste Element. push legt einen Wert ab (und verringert rsp um 8), pop holt ihn zurück (und erhöht rsp).
Teil des Kurses Assembler (x86-64)
Kapitel 5 von 8 im Kurs Assembler (x86-64) (Abschnitt „Funktionen“). Mit Fortschritt, Quiz und Zertifikat auf der Lernseite.
Der Stack
Der Stack (Stapel) ist ein Speicherbereich, der “von oben nach unten” wächst. Das Register rsp zeigt auf das oberste Element. push legt einen Wert ab (und verringert rsp um 8), pop holt ihn zurück (und erhöht rsp). Er funktioniert nach dem Prinzip zuletzt hinein, zuerst heraus (LIFO).
section .bss
puffer resb 32
section .text
global _start
_start:
mov rax, 1
mov rbx, 2
mov rcx, 3
push rax
push rbx
push rcx ; Stack: 3 (oben), 2, 1
pop rax ; rax = 3
call ausgabe_zahl
pop rax ; rax = 2
call ausgabe_zahl
pop rax ; rax = 1
call ausgabe_zahl
mov rax, 10
push rax
mov rax, [rsp] ; oberstes Element lesen, ohne zu entfernen
call ausgabe_zahl
add rsp, 8 ; Element verwerfen (wie pop ohne Ziel)
mov rax, rsp
and rax, 0xF
call ausgabe_zahl ; rsp ist beim Start ausgerichtet
mov rax, 60
xor rdi, rdi
syscall
; --- Hilfsroutine: gibt die Zahl in RAX dezimal aus (mit Zeilenumbruch), alle Register bleiben erhalten ---
ausgabe_zahl:
push rax
push rbx
push rcx
push rdx
push rsi
push rdi
mov rcx, puffer + 20 ; Ende des Puffers
mov byte [rcx], 10 ; Zeilenumbruch ans Ende
mov rbx, 10
.schleife:
xor rdx, rdx
div rbx ; RAX = RAX / 10, RDX = Rest
add dl, '0' ; Ziffer in ASCII umwandeln
dec rcx
mov [rcx], dl
test rax, rax
jnz .schleife
mov rax, 1 ; write
mov rdi, 1
mov rsi, rcx
lea rdx, [puffer + 21]
sub rdx, rcx
syscall
pop rdi
pop rsi
pop rdx
pop rcx
pop rbx
pop rax
retAusgabe:
3
2
1
10
0Funktionen mit call und ret
call ziel legt die Rücksprungadresse auf den Stack und springt; ret holt sie wieder und springt zurück. Argumente übergibt man laut der System-V-Aufrufkonvention (Linux, macOS) so:
| Zweck | Register |
|---|---|
| Argumente 1 bis 6 | rdi, rsi, rdx, rcx, r8, r9 |
| Rückgabewert | rax |
| Von der Funktion zu sichern (“callee-saved”) | rbx, rbp, r12 bis r15 |
| Dürfen überschrieben werden (“caller-saved”) | rax, rcx, rdx, rsi, rdi, r8 bis r11 |
section .bss
puffer resb 32
section .text
global _start
; int addiere(int a, int b)
addiere:
mov rax, rdi
add rax, rsi
ret
; int maximum(int a, int b)
maximum:
mov rax, rdi
cmp rax, rsi
cmovl rax, rsi ; bedingtes mov: wenn kleiner, dann rax = rsi
ret
; long fakultaet(long n) (rekursiv)
fakultaet:
cmp rdi, 1
jg .rekursiv
mov rax, 1
ret
.rekursiv:
push rdi ; n sichern
dec rdi
call fakultaet ; rax = fakultaet(n-1)
pop rdi
imul rax, rdi ; rax = n * fakultaet(n-1)
ret
; long summe(long* array, long anzahl)
summe:
xor rax, rax
xor rcx, rcx
.schleife:
cmp rcx, rsi
jge .ende
add rax, [rdi + rcx*8]
inc rcx
jmp .schleife
.ende:
ret
section .data
liste dq 3, 14, 15, 92, 65
section .text
_start:
mov rdi, 20
mov rsi, 22
call addiere
call ausgabe_zahl
mov rdi, 17
mov rsi, 99
call maximum
call ausgabe_zahl
mov rdi, 10
call fakultaet
call ausgabe_zahl
mov rdi, liste
mov rsi, 5
call summe
call ausgabe_zahl
mov rax, 60
xor rdi, rdi
syscall
; --- Hilfsroutine: gibt die Zahl in RAX dezimal aus (mit Zeilenumbruch), alle Register bleiben erhalten ---
ausgabe_zahl:
push rax
push rbx
push rcx
push rdx
push rsi
push rdi
mov rcx, puffer + 20 ; Ende des Puffers
mov byte [rcx], 10 ; Zeilenumbruch ans Ende
mov rbx, 10
.schleife:
xor rdx, rdx
div rbx ; RAX = RAX / 10, RDX = Rest
add dl, '0' ; Ziffer in ASCII umwandeln
dec rcx
mov [rcx], dl
test rax, rax
jnz .schleife
mov rax, 1 ; write
mov rdi, 1
mov rsi, rcx
lea rdx, [puffer + 21]
sub rdx, rcx
syscall
pop rdi
pop rsi
pop rdx
pop rcx
pop rbx
pop rax
retAusgabe:
42
99
3628800
189Der Einstiegspunkt _start steht hier bewusst hinter den Funktionen, die Ausführung beginnt trotzdem dort (global _start).
Stack-Rahmen und lokale Variablen
Funktionen mit lokalen Variablen legen einen Stack-Rahmen an: rbp zeigt auf den Anfang, lokale Variablen liegen darunter.
section .bss
puffer resb 32
section .text
global _start
; long quadratsumme(long a, long b) → a*a + b*b mit lokalen Variablen
quadratsumme:
push rbp
mov rbp, rsp ; Rahmen anlegen
sub rsp, 16 ; Platz für zwei lokale 8-Byte-Variablen
mov rax, rdi
imul rax, rax
mov [rbp - 8], rax ; lokal1 = a*a
mov rax, rsi
imul rax, rax
mov [rbp - 16], rax ; lokal2 = b*b
mov rax, [rbp - 8]
add rax, [rbp - 16]
mov rsp, rbp ; Rahmen abbauen
pop rbp
ret
_start:
mov rdi, 3
mov rsi, 4
call quadratsumme
call ausgabe_zahl ; 25 (Pythagoras)
mov rdi, 5
mov rsi, 12
call quadratsumme
call ausgabe_zahl ; 169
mov rax, 60
xor rdi, rdi
syscall
; --- Hilfsroutine: gibt die Zahl in RAX dezimal aus (mit Zeilenumbruch), alle Register bleiben erhalten ---
ausgabe_zahl:
push rax
push rbx
push rcx
push rdx
push rsi
push rdi
mov rcx, puffer + 20 ; Ende des Puffers
mov byte [rcx], 10 ; Zeilenumbruch ans Ende
mov rbx, 10
.schleife:
xor rdx, rdx
div rbx ; RAX = RAX / 10, RDX = Rest
add dl, '0' ; Ziffer in ASCII umwandeln
dec rcx
mov [rcx], dl
test rax, rax
jnz .schleife
mov rax, 1 ; write
mov rdi, 1
mov rsi, rcx
lea rdx, [puffer + 21]
sub rdx, rcx
syscall
pop rdi
pop rsi
pop rdx
pop rcx
pop rbx
pop rax
retAusgabe:
25
169Achtung
Wer
pushundpopnicht ausgleicht oderretausführt, währendrspnicht auf die Rücksprungadresse zeigt, springt ins Nirwana. Genau solche Fehler (Stack-Überläufe, überschriebene Rücksprungadressen) sind die Grundlage klassischer Buffer-Overflow-Angriffe.
Merke
- Stack:
push/pop,rsp;callspeichert die Rücksprungadresse,retkehrt zurück - System-V-Konvention: Argumente in
rdi,rsi,rdx,rcx,r8,r9, Ergebnis inrax - Callee-saved:
rbx,rbp,r12-r15; alle anderen dürfen überschrieben werden - Stack-Rahmen:
push rbp/mov rbp, rsp/sub rsp, n…leave/ret - Rekursion funktioniert, weil jeder Aufruf seinen eigenen Stack-Bereich hat
Übungsaufgabe
Schreibe eine rekursive Funktion für die n-te Fibonacci-Zahl und gib fib(15) aus.
Quiz zur Selbstkontrolle
Was legt call auf den Stack?
- Die Rücksprungadresse (richtig)
- Alle Register
- Das erste Argument
- Die Funktionsnamen
In welchem Register steht der Rückgabewert einer Funktion?
- rax (richtig)
- rdi
- rsp
- rdx allein
Wie viele Argumente werden in Registern übergeben (System V)?
- Die ersten sechs ganzzahligen Argumente (richtig)
- Alle
- Nur zwei
- Keines
Weiter im Kurs
Zurück: Speicher, Arrays und Zeichenketten
Weiter: Systemaufrufe, Eingabe und Werkzeuge
Alle Kapitel: Assembler (x86-64) im Überblick