Eigentlich können Übersetzer machen, was sie wollen, Speicher und Register beliebig
verwenden, Hauptsache das resultierende Programm funktioniert im Anschluss.
Das kann allerdings dann zu einem Problem werden, wenn wir ein Unterprogramm verwenden
wollen, welches von einem anderen Übersetzer erstellt wurde, vielleicht wegen einer anderen
Programmiersprache. Um Interoperabilität zu gewährleisten, müssen sich die Übersetzer
an eine Aufrufkonvention halten. Zum Beispiel, wer welche Register bei einem Funktionsaufruf
sichern muss. Der Aufrufende muss sich um die flüchtigen Register kümmern und die aufgerufene
Funktion um die nicht flüchtigen, sofern sie diese dann überhaupt verwendet. Aber
welche Register sind nun was? Wir sind im Long Mode auf der 64-bit x86-Architektur.
Für uns spielen weder Segmentregister noch Kontroll- oder Debugregister eine Rolle beim
Funktionsaufruf. Und da wir uns im Betriebssystem bewegen, ignorieren wir auch die SSE-Register,
sondern konzentrieren uns primär auf die Allzweckregister und berücksichtigen Statusregister
und Stapel sowie Instruktionszeiger. Nach der System-5-ABI für 64-bit sind diese
10 blau eingefärbten Register flüchtig definiert. Auch Scratch oder ColorSafe genannt, müssen
also von der aufrufenden Funktion zuerst gesichert werden. Die anderen Register sind
nicht flüchtig, natürlich mit Ausnahme der Sonderfälle Stapel und Instruktionszeiger.
Wenn ich nun die flüchtigen Register eh bei Verwendung sichern und später wieder herstellen
muss, dann würden sich diese durch eigenen Datenanschluss an die aufzurufende Funktion
zu übermitteln. Und das wird auch gemacht. RDI wird beispielsweise für den ersten und
RSI für den zweiten Funktionsparameter verwendet, während der Rückgabewert in rax geschrieben
wird. Wie sieht das nun in der Praxis aus, bei einem Funktionsaufruf mit eben zwei Parametern?
Schauen wir uns dazu den Aufruf von Funk mit Parameter 23 und 42 an. Der resultierende
Assemblercode für den Aufruf könnte in etwa wie folgt aussehen.
Dabei verwende ich hier die Intel-Syndex, wie sie auch von unserem NetWide Assembler verwendet
wird. Bei dieser wird zuerst das Ziel, dann die Quelle genannt. Im Gegensatz zu AT&T-Syndex,
die genau anders herum ist. Aber aufgrund zusätzlicher Zeichen leicht identifiziert
werden kann, wie dem Prozentzeichen vor Registernamen. Letzteres wird übrigens standardmäßig beim
InlineAssembly und in ObjectDump verwendet. Als erstes werden, so vernötig, flüchtige
Register gesichert, beispielsweise R9. Anschließend werden die Parameter in die entsprechenden
Register geschrieben. 0x2a entspricht 42, dem zweiten Parameter, und wird somit in RSI
geschrieben. 0x17 für 23 als ersten Parameter in RDI.
Der Compiler generiert, wie hier zu sehen, für kleinere Zahlen innerhalb der 32-Bit-Grenze
ein Move nach ESI statt RSI. Das ist valit, da dabei automatisch die oberen 32-Bit von
RSI auf 0 gesetzt werden. Der Aufruf hat also die gleiche Wirkung, es werden sich durch
diese Variante jedoch ein paar Bytes im Maschinencode gespart.
Das gilt aber übrigens nicht für ein 16-Bit Move nach SI. Da bleiben die oberen Bits dann
unverändert, werden also nicht genullt. Wieso? Nun, historisch gewachsen. Ja, x86
ist nicht einfach. Dann der Funktionsaufruf, welcher implizit die Rücksprungadresse, also
die Adresse der nächsten Instruktion nach dem Call, auf den Stack schreibt. Der Einfachheit
halber nehmen wir das Label L1 stellvertretend für eine explizite Adresse.
Wir erinnern uns, der Stack wächst auf dem x86er nach unten in Richtung der kleineren
Adressen. Dabei wird bei einem Push zuerst der Wert des Stack-Zeigers RSP um die Adressbreite
bei 64-Bit eben 8-Byte verringert und danach an die neue Adresse der Wert geschrieben.
Somit zeigt RSP immer auf das letzte hinzugefügte und noch nicht gepoppte Datum.
Sowohl Wachstumsrichtung als auch wohin der Stack-Pointer zeigt, ist natürlich kein Naturgesetz,
sondern hier eine Designentscheidung von Intel, die auf anderen Architekturen auch anders
sein kann.
Außerdem muss die aktuelle Adresse des Stack-Zeigers bei einem Funktionsaufruf an einer 16-Byte-Grenze
ausgerichtet sein, also die Adresse des Stack-Zeigers Modulo 16 muss direkt vor dem Call Null sein.
Dies schreibt uns die System 5 API vor, die 16-Byte Ausrichtung werden von den SSE Instruktionen
Presenters
Zugänglich über
Offener Zugang
Dauer
00:07:56 Min
Aufnahmedatum
2020-08-10
Hochgeladen am
2020-10-16 18:16:32
Sprache
de-DE
Kontextsicherung und Parameterübergabe nach System-V-ABI für Aufgabe 4 der Lehrveranstaltung Betriebssysteme.
Folien und Transkript zum Video.