Funktionen und Stack: Unterschied zwischen den Versionen

Aus C und Assembler mit Raspberry
KKeine Bearbeitungszusammenfassung
 
(Eine dazwischenliegende Version desselben Benutzers wird nicht angezeigt)
Zeile 1: Zeile 1:
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:
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 ==
== Funktion aufrufen und Rückkehr ==
Zeile 48: Zeile 43:
</syntaxhighlight>
</syntaxhighlight>
== Verwendung des Stacks für lokale Variablen und Funktionsaufrufe ==
== Verwendung des Stacks für lokale Variablen und Funktionsaufrufe ==
In vielen Programmiersprachen einschließlich C, werden Parameter häufig über den Stack übergeben, besonders wenn viele Parameter übergeben werden oder wenn die Parameter größer sind als Register (z.B. Strukturen). Im Fall von ARM64-Assembler wird jedoch oft eine Kombination verwendet: Die ersten paar Parameter werden über Register übergeben und zusätzliche Parameter über den Stack.


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.
Wie dies vereinfacht funktioniert, wird hier ein Beispiel gezeigt, das die Übergabe der Parameter über den Stack zeigt und dann die Berechnung innerhalb der Funktion durchführt:
 
Beispiel:
<syntaxhighlight lang="asm">
<syntaxhighlight lang="asm">
.global _start
.global _start


_start:
_start:
     mov x0, #10           // Parameter 1: 10
     mov x0, #10         // Parameter 1: 10
     mov x1, #20           // Parameter 2: 20
     mov x1, #20         // Parameter 2: 20
    str x0, [sp, #-8]!  // Speicher Parameter 1 (10) auf dem Stack; Stack-Pointer adjustiert
    str x1, [sp, #-8]!  // Speicher Parameter 2 (20) auf dem Stack; Stack-Pointer adjustiert
     bl add_numbers_with_stack // Rufe add_numbers_with_stack auf
     bl add_numbers_with_stack // Rufe add_numbers_with_stack auf
     // Das Ergebnis (30) ist jetzt in x0
     // Das Ergebnis (30) ist jetzt in x0
     mov x8, #93           // exit system call
    add sp, sp, #16      // Stack-Pointer zurücksetzen
     svc 0                 // system call
     mov x8, #93         // exit system call
     svc 0               // system call


add_numbers_with_stack:
add_numbers_with_stack:
     stp x29, x30, [sp, #-16]! // Speichere x29 (FP) und x30 (LR) auf dem Stack
     stp x29, x30, [sp, #-16]! // Speichere x29 (FP) und x30 (LR) auf dem Stack; Stack-Pointer adjustiert
     mov x29, sp               // Setze den Frame-Pointer
     mov x29, sp               // Setze den Frame-Pointer
     add x0, x0, x1             // Addiere x0 (10) und x1 (20), Ergebnis in x0 (30)
    ldr x0, [x29, #16]        // Lade Parameter 1 (10) vom Stack
     ldp x29, x30, [sp], #16  // Lade x29 und x30 vom Stack zurück
    ldr x1, [x29, #8]        // Lade Parameter 2 (20) vom Stack
     ret                       // Kehre zurück
     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; Stack-Pointer adjustiert
     ret                       // Kehre zurück
</syntaxhighlight>
 
=== Schritt für Schritt Erklärung ===
 
* Parameter vorbereiten und auf den Stack legen:
<syntaxhighlight lang="asm">
mov x0, #10          // Parameter 1: 10 in Register x0
mov x1, #20          // Parameter 2: 20 in Register x1
str x0, [sp, #-8]!  // Speicher x0 (10) auf den Stack und adjustiere den Stack-Pointer
str x1, [sp, #-8]!  // Speicher x1 (20) auf den Stack und adjustiere den Stack-Pointer
</syntaxhighlight>
 
Hier werden die Werte 10 und 20 in die Register x0 und x1 geladen und dann auf den Stack gelegt.
 
* Funktionsaufruf:
<syntaxhighlight lang="asm">
bl add_numbers_with_stack // Rufe add_numbers_with_stack auf
</syntaxhighlight>
 
* Speichern von Frame-Pointer und Link-Register:
<syntaxhighlight lang="asm">
stp x29, x30, [sp, #-16]! // Speichere x29 (FP) und x30 (LR) auf den Stack und adjustiere den Stack-Pointer
mov x29, sp                // Setze den Frame-Pointer (x29)
</syntaxhighlight>
 
stp speichert die Register x29 und x30 auf dem Stack und adjustiert den Stack-Pointer. Danach wird der Frame-Pointer aktualisiert.
 
* Laden der Parameter und Berechnung:
<syntaxhighlight lang="asm">
ldr x0, [x29, #16]        // Lade Parameter 1 (10) vom Stack
ldr x1, [x29, #8]        // Lade Parameter 2 (20) vom Stack
add x0, x0, x1            // Addiere x0 (10) und x1 (20), Ergebnis in x0 (30)
</syntaxhighlight>
 
Die Parameter werden vom Stack in die Register x0 und x1 geladen und die Addition wird durchgeführt. Das Ergebnis wird in x0 gespeichert.
 
* Wiederherstellung des Kontextes und Funktionsrückkehr:
<syntaxhighlight lang="asm">
ldp x29, x30, [sp], #16  // Lade x29 und x30 vom Stack zurück und adjustiere den Stack-Pointer
ret                      // Kehre zurück
</syntaxhighlight>
</syntaxhighlight>
ldp lädt die Register x29 und x30 vom Stack und stellt den Stack-Pointer wieder her. Der ret-Befehl kehrt zur aufrufenden Funktion zurück.
* Stack-Pointer zurücksetzen und Beenden:
<syntaxhighlight lang="asm">
add sp, sp, #16      // Stack-Pointer zurücksetzen, um zuvor gespeicherte Parameter zu entfernen
mov x8, #93          // exit system call
svc 0                // system call
</syntaxhighlight>
Der Stack-Pointer wird auf seinen ursprünglichen Zustand zurückgesetzt, bevor das Programm beendet wird.
=== Wichtige Informationen ===
Parameter Übergabe über den Stack: Beweg die Register auf den Stack, um sie an die Funktion zu übergeben.
* Frame-Pointer (x29): Verwendet zur Verwaltung des Stack-Frames innerhalb der Funktion.
* Link-Register (x30): Hält die Rücksprungadresse für die Rückkehr zur aufrufenden Funktion.
* Stack-Pointer: Wird angepasst, um den Stack für lokale Variablen und Parameter zu nutzen.
Diese Methode stellt sicher, dass bei verschachtelten Funktionsaufrufen und bei der Verwendung vieler Parameter der Kontext korrekt gesichert und wiederhergestellt wird.
=== Problem des "LR" bei verschachtelten Funktionsaufrufen ===
=== Problem des "LR" bei verschachtelten Funktionsaufrufen ===


Zeile 99: Zeile 157:
     ret
     ret
</syntaxhighlight>
</syntaxhighlight>
== Regeln für das Aufrufen von Funktionen (AAPCS) ==
== Regeln für das Aufrufen von Funktionen (AAPCS) ==



Aktuelle Version vom 8. April 2025, 13:21 Uhr

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

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

In vielen Programmiersprachen einschließlich C, werden Parameter häufig über den Stack übergeben, besonders wenn viele Parameter übergeben werden oder wenn die Parameter größer sind als Register (z.B. Strukturen). Im Fall von ARM64-Assembler wird jedoch oft eine Kombination verwendet: Die ersten paar Parameter werden über Register übergeben und zusätzliche Parameter über den Stack.

Wie dies vereinfacht funktioniert, wird hier ein Beispiel gezeigt, das die Übergabe der Parameter über den Stack zeigt und dann die Berechnung innerhalb der Funktion durchführt:

.global _start

_start:
    mov x0, #10          // Parameter 1: 10
    mov x1, #20          // Parameter 2: 20
    str x0, [sp, #-8]!   // Speicher Parameter 1 (10) auf dem Stack; Stack-Pointer adjustiert
    str x1, [sp, #-8]!   // Speicher Parameter 2 (20) auf dem Stack; Stack-Pointer adjustiert
    bl add_numbers_with_stack // Rufe add_numbers_with_stack auf
    // Das Ergebnis (30) ist jetzt in x0
    add sp, sp, #16      // Stack-Pointer zurücksetzen
    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; Stack-Pointer adjustiert
    mov x29, sp               // Setze den Frame-Pointer
    ldr x0, [x29, #16]        // Lade Parameter 1 (10) vom Stack
    ldr x1, [x29, #8]         // Lade Parameter 2 (20) vom Stack
    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; Stack-Pointer adjustiert
    ret                       // Kehre zurück

Schritt für Schritt Erklärung

  • Parameter vorbereiten und auf den Stack legen:
mov x0, #10          // Parameter 1: 10 in Register x0
mov x1, #20          // Parameter 2: 20 in Register x1
str x0, [sp, #-8]!   // Speicher x0 (10) auf den Stack und adjustiere den Stack-Pointer
str x1, [sp, #-8]!   // Speicher x1 (20) auf den Stack und adjustiere den Stack-Pointer

Hier werden die Werte 10 und 20 in die Register x0 und x1 geladen und dann auf den Stack gelegt.

  • Funktionsaufruf:
bl add_numbers_with_stack // Rufe add_numbers_with_stack auf
  • Speichern von Frame-Pointer und Link-Register:
stp x29, x30, [sp, #-16]! // Speichere x29 (FP) und x30 (LR) auf den Stack und adjustiere den Stack-Pointer
mov x29, sp                // Setze den Frame-Pointer (x29)

stp speichert die Register x29 und x30 auf dem Stack und adjustiert den Stack-Pointer. Danach wird der Frame-Pointer aktualisiert.

  • Laden der Parameter und Berechnung:
ldr x0, [x29, #16]        // Lade Parameter 1 (10) vom Stack
ldr x1, [x29, #8]         // Lade Parameter 2 (20) vom Stack
add x0, x0, x1            // Addiere x0 (10) und x1 (20), Ergebnis in x0 (30)

Die Parameter werden vom Stack in die Register x0 und x1 geladen und die Addition wird durchgeführt. Das Ergebnis wird in x0 gespeichert.

  • Wiederherstellung des Kontextes und Funktionsrückkehr:
ldp x29, x30, [sp], #16   // Lade x29 und x30 vom Stack zurück und adjustiere den Stack-Pointer
ret                       // Kehre zurück

ldp lädt die Register x29 und x30 vom Stack und stellt den Stack-Pointer wieder her. Der ret-Befehl kehrt zur aufrufenden Funktion zurück.

  • Stack-Pointer zurücksetzen und Beenden:
add sp, sp, #16      // Stack-Pointer zurücksetzen, um zuvor gespeicherte Parameter zu entfernen
mov x8, #93          // exit system call
svc 0                // system call

Der Stack-Pointer wird auf seinen ursprünglichen Zustand zurückgesetzt, bevor das Programm beendet wird.

Wichtige Informationen

Parameter Übergabe über den Stack: Beweg die Register auf den Stack, um sie an die Funktion zu übergeben.

  • Frame-Pointer (x29): Verwendet zur Verwaltung des Stack-Frames innerhalb der Funktion.
  • Link-Register (x30): Hält die Rücksprungadresse für die Rückkehr zur aufrufenden Funktion.
  • Stack-Pointer: Wird angepasst, um den Stack für lokale Variablen und Parameter zu nutzen.

Diese Methode stellt sicher, dass bei verschachtelten Funktionsaufrufen und bei der Verwendung vieler Parameter der Kontext korrekt gesichert und wiederhergestellt wird.

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