EMZETT.
Login

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
    ret

Ausgabe:

3
2
1
10
0

Funktionen 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:

ZweckRegister
Argumente 1 bis 6rdi, rsi, rdx, rcx, r8, r9
Rückgabewertrax
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
    ret

Ausgabe:

42
99
3628800
189

Der 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
    ret

Ausgabe:

25
169

Achtung

Wer push und pop nicht ausgleicht oder ret ausführt, während rsp nicht 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; call speichert die Rücksprungadresse, ret kehrt zurück
  • System-V-Konvention: Argumente in rdi, rsi, rdx, rcx, r8, r9, Ergebnis in rax
  • 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

Weiter im Kurs

Zurück: Speicher, Arrays und Zeichenketten

Weiter: Systemaufrufe, Eingabe und Werkzeuge

Alle Kapitel: Assembler (x86-64) im Überblick