Funktionen und Stack
In diesem Kapitel werden wir uns mit der Erstellung und Verwendung von Funktionen im ARM64-Assembler beschäftigen. Insbesondere werden wir uns mit den folgenden Themen befassen:
Funktion aufrufen und Rückkehr Funktionsparameter und Rückgabewerte Verwendung des Stacks für lokale Variablen und Funktionsaufrufe Regeln für das Aufrufen von Funktionen (AAPCS)
Funktion aufrufen und Rückkehr
Eine Funktion im ARM64-Assembler wird durch das Setzen eines Labels deklariert und kann mit dem Befehl bl (Branch with Link) aufgerufen werden. Der bl-Befehl speichert die Rücksprungadresse in das Register lr (Link Register, auch bekannt als x30), wodurch die Funktion nach ihrem Abschluss dorthin zurückkehren kann.
Beispiel:
.global _start
_start:
mov x0, #10 // Setze x0 auf 10
bl my_function // Rufe my_function auf
// Weiterer Code nach der Rückkehr von my_function
mov x8, #93 // exit system call
svc 0 // system call
my_function:
// Funktion beginnt hier
mov x0, #0 // Setze x0 auf 0, als Beispiel
ret // Kehre zur Rücksprungadresse zurück
Funktionsparameter und Rückgabewerte
In ARM64-Assembler werden Funktionsparameter in den Registern x0 bis x7 übergeben. Rückgabewerte werden üblicherweise in x0 zurückgegeben.
Beispiel:
.global _start
_start:
mov x0, #10 // Parameter 1: 10
mov x1, #20 // Parameter 2: 20
bl add_numbers // Rufe add_numbers auf
// Das Ergebnis (30) ist jetzt in x0
mov x8, #93 // exit system call
svc 0 // system call
add_numbers:
add x0, x0, x1 // Addiere x0 (10) und x1 (20), Ergebnis in x0 (30)
ret // Kehre zurück
Verwendung des Stacks für lokale Variablen und Funktionsaufrufe
Der Stack wird verwendet, um lokale Variablen zu speichern und den aktuellen Kontext bei verschachtelten Funktionsaufrufen zu sichern. Dies geschieht mit den Befehlen stp (store pair) und ldp (load pair), die Paare von Registern auf dem Stack speichern und wiederherstellen können.
Beispiel:
.global _start
_start:
mov x0, #10 // Parameter 1: 10
mov x1, #20 // Parameter 2: 20
bl add_numbers_with_stack // Rufe add_numbers_with_stack auf
// Das Ergebnis (30) ist jetzt in x0
mov x8, #93 // exit system call
svc 0 // system call
add_numbers_with_stack:
stp x29, x30, [sp, #-16]! // Speichere x29 (FP) und x30 (LR) auf dem Stack
mov x29, sp // Setze den Frame-Pointer
add x0, x0, x1 // Addiere x0 (10) und x1 (20), Ergebnis in x0 (30)
ldp x29, x30, [sp], #16 // Lade x29 und x30 vom Stack zurück
ret // Kehre zurück
Problem des "LR" bei verschachtelten Funktionsaufrufen
Bei verschachtelten Funktionsaufrufen (eine Funktion ruft eine andere auf) muss das lr-Register gesichert werden, um sicherzustellen, dass die Rücksprungadresse korrekt erhalten bleibt. Dies geschieht typischerweise durch das Speichern von lr auf dem Stack bei jedem Funktionsaufruf.
Beispiel:
.global _start
_start:
mov x0, #10
mov x1, #20
bl outer_function
mov x8, #93
svc 0
outer_function:
stp x29, x30, [sp, #-16]!
mov x29, sp
bl inner_function
ldp x29, x30, [sp], #16
ret
inner_function:
stp x29, x30, [sp, #-16]!
mov x29, sp
add x0, x0, x1
ldp x29, x30, [sp], #16
ret
Regeln für das Aufrufen von Funktionen (AAPCS)
Die ARM Architecture Procedure Call Standard (AAPCS) legt Regeln für das Aufrufen von Funktionen fest, um die Interoperabilität zwischen verschiedenen Code-Modulen sicherzustellen.
Wichtige Regeln des AAPCS
- Parameterübergabe: Die ersten acht Parameter werden in den Registern x0 bis x7 übergeben. Weitere Parameter werden auf dem Stack übergeben.
- Rückgabewerte: Rückgabewerte werden in x0 und x1 zurückgegeben, abhängig von der Größe des Rückgabewerts.
- Callee-saved Register: Register x19 bis x29 müssen von der aufgerufenen Funktion gesichert und wiederhergestellt werden. Register x0 bis x18 und x30 (LR) können von der Funktion frei verwendet werden.
- Stack Alignment: Der Stack muss immer auf eine 16-Byte-Grenze ausgerichtet sein.
Beispiel für eine Funktion, die sich an AAPCS hält:
global _start
_start:
mov x0, #10
mov x1, #20
bl add_numbers // Aufruf der Funktion
mov x8, #93 // exit system call
svc 0 // system call
add_numbers:
stp x29, x30, [sp, #-16]! // Speichere x29 (FP) und x30 (LR) auf dem Stack
mov x29, sp // Setze den Frame-Pointer
stp x19, x20, [sp, #-16]! // Speichere x19 und x20 (callee-saved Register)
mov x19, x0 // Benutze x19 als temporäres Register
mov x20, x1 // Benutze x20 als temporäres Register
add x0, x19, x20 // Addiere x0 und x1 und speichere das Ergebnis in x0
ldp x19, x20, [sp], #16 // Lade x19 und x20 vom Stack zurück
ldp x29, x30, [sp], #16 // Lade x29 und x30 vom Stack zurück
ret // Kehre zur Rücksprungadresse zurück