1 - Aufrufkonvention [ID:21394]
50 von 91 angezeigt

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

Teil einer Videoserie :
Teil eines Kapitels:
Threadumschaltung

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.

Tags

betriebssysteme operating systems stubs
Einbetten
Wordpress FAU Plugin
iFrame
Teilen