Funktionen & der Stack
Eine Funktion ist ein benannter Block von Anweisungen, den du mehr als einmal verwenden kannst. Du springst zu ihr, sie führt ihre Aufgabe aus und gibt die Kontrolle an die Stelle zurück, von der aus du sie aufgerufen hast. So kannst du ein Stück Logik einmal schreiben und von vielen Stellen aus verwenden, anstatt dieselben Zeilen im ganzen Programm zu wiederholen.
Verwende , um eine Funktion aufzurufen: Es springt zum Label und speichert die Rücksprungadresse in ra. Die Funktion endet mit , das zur Adresse in ra zurückspringt. und sind Pseudoinstruktionen: praktische Namen, die der Assembler in die tatsächlichen Instruktionen und expandiert.
Argumente werden in a0, a1 und den darauffolgenden Registern abgelegt. Der Rückgabewert wird in a0 abgelegt. Diese Anordnung ist die Aufrufkonvention, die gemeinsame Vereinbarung, die es getrennten Codeteilen ermöglicht, zusammenzuarbeiten.
li a0, 6 # Argumentcall square # aufrufen, und ra merkt sich, wohin es zurückkehren sollli a7, 1 # square hinterlässt sein Ergebnis in a0ecallli a7, 10ecallsquare:mul a0, a0, a0 # a0 = a0 * a0ret # springt zurück zu ra
Folge dem Aufruf. legt das Argument 6 in a0 ab. springt zum Label square und speichert die Rücksprungadresse in ra. Innerhalb der Funktion berechnet 6 * 6 = 36 und hinterlässt das Ergebnis in a0, dem Register, das für Rückgabewerte verwendet wird. springt zurück zur Instruktion direkt nach dem Aufruf, wo und 36 ausgeben, und dann beendet sich das Programm.
Der Aufrufer ist dabei, A aufzurufen.
caller führt aus, wodurch der Rücksprungpunkt automatisch im Register ra für caller gespeichert wird. Bevor A ausführen kann, stößt es auf ein Problem: Die Anweisung ist dabei, ra mit einer neuen Rücksprungadresse zu überschreiben und damit diejenige zu zerstören, die A benötigt, um nach Hause zurückzukehren. Um dies zu beheben, schafft A Platz auf dem Stack () und speichert das ursprüngliche ra dort (). Jetzt ist es sicher, auszuführen. B läuft und kehrt mit einem gewöhnlichen zurück, wobei es wieder innerhalb von A landet. Schließlich stellt A das ursprüngliche ra vom Stack wieder her (), bereinigt den Stack-Zeiger (), und sein eigenes verwendet diesen wiederhergestellten Wert, um erfolgreich zurück zu caller zu springen. Hätte A diesen Schritt übersprungen, hätte die Anweisung ra dauerhaft überschrieben, und das abschließende von A würde an die falsche Stelle springen, anstatt nach Hause zurückzukehren.Der Stack
Da nur eine Handvoll Register vorhanden sind, benötigen Funktionen, die andere Funktionen aufrufen, temporären Speicherplatz. Der Stack ist Speicher, den Sie über den Stack-Zeiger sp ausleihen: Subtrahieren Sie, um Platz zu schaffen, speichern Sie, was Sie behalten müssen, und stellen Sie es vor wieder her.
Der Stack ist eine Last-In, First-Out (LIFO)-Struktur: Der zuletzt gespeicherte Wert ist der erste, den du wieder abrufst. In RISC-V wächst der Stack nach unten durch den Speicher. Subtrahieren von sp reserviert neuen Speicherplatz, der sicher verwendet werden kann. Wenn du Werte wieder entnimmst, musst du dies in umgekehrter Reihenfolge zum Speichern tun, sonst wird der Ausführungsfluss des Programms beschädigt.
addi sp, sp, -4 # schafft Platz für ein Wortsw ra, 0(sp) # sichert die Rücksprungadresse# ... hier andere Funktionen aufrufen ...lw ra, 0(sp) # stellt sie wieder heraddi sp, sp, 4 # gibt den Platz wieder freiret
Jedes / überschreibt ra. Wenn deine Funktion also eine andere Funktion aufruft, zerstört der innere Aufruf deine Rücksprungadresse und dein eigenes springt an die falsche Stelle. Um dies zu verhindern, reserviert 4 Bytes auf dem Stack, und speichert ra dort. Nachdem die inneren Aufrufe beendet sind, stellt die Rücksprungadresse wieder her, und gibt den Speicherplatz vor zurück.
ra vor wieder her, und setze sp immer auf den Wert zurück, den es zu Beginn hatte. Ein Stack, der nicht mit seinem Ausgangszustand übereinstimmt, ist die übliche Ursache für einen Absturz in Assemblerprogrammen.