blog · git · desktop · images · contact


Linux-Kunde: main vs. _start

2011-03-31

Vor kurzem habe ich mir das Buch „Understanding the Linux Kernel“ zugelegt, was ich mal von vorne bis hinten durchlesen will. Infolge dessen wird es hier im Blog vermutlich gehäuft zu Postings aus der Kategorie „Linux-Kunde“ oder ähnlichem kommen.

Randnotiz: Ich habe die dritte Ausgabe des Buchs und obwohl das die neueste Ausgabe ist, wird „nur“ Linux 2.6.11 behandelt, denn das Buch ist von November 2005. Wer darüber nachdenkt, das Buch zu kaufen, sollte das wissen, denn das eine oder andere Detail stimmt heute nicht mehr.

Nachdem auf den ersten 30 Seiten einmal der Stoff der Vorlesung „Betriebssysteme“ einer gewöhnlichen Universität vermittelt wird (okay, ist übertrieben), geht es dann mit Speicheradressierung gleich richtig zur Sache. Es dauert dann auch nicht lange, bis drei Zeilen Assembler-Code zu sehen sind. Das hat sofort Lust geweckt und ich hab’s mal als Anlass genommen, wieder ein bisschen Assembler zu machen. Das letzte Mal ist schon eine Weile her und Linux war’s auch nicht.

Die erste Frage, um deren Beantwortung es hier gehen soll: Wo fängt ein Programm eigentlich an?

Ein einfaches C-Programm sieht gewöhnlich so aus:

int main(int argc, char *argv[])
{
    return 8;
}

Man könnte also naiverweise annehmen, dass „main“ der Anfang ist – immerhin ist C ja eine Low-Level-Sprache. ;) Sprich, der Kernel würde nach einem „exec*()“-Aufruf ein Symbol „main“ suchen und an dieser Adresse weitere Instruktionen ausführen. In der Tat findet man viele Beispiele im Netz, die diese Idee stützen. In Assembler sollte also obiger Code so aussehen:

.text
.global main
main:
    movl $8, %eax
    ret

(Weshalb meine 8 nach EAX geschoben werden muss und „wohin“ danach zurückgesprungen wird, dazu weiter unten mehr. Stichwort an dieser Stelle: „cdecl Calling Convention“.)

Wie übersetze ich das nun? Die Beispiele im Netz geben schlichtweg folgendes an:

$ gcc -o foo foo.s

Das funktioniert auch. Was dabei entsteht, ist allerdings eine gut 4 kB große Datei. Hmm. So viel für das bisschen Assembler da oben? Und was steht da so drin? Mal nachschauen:

$ objdump -d foo

Ich spare mir hier ein komplettes Listing. Es ist auf jeden Fall wesentlich mehr als einfach nur die zwei Instruktionen. Und wenn man genau darüber nachdenkt, war das auch zu erwarten, denn schon alleine das „ret“ ist verdächtig: Es nimmt die Rücksprungadresse vom Stack und springt dann dort hin. Ich habe aber weder selbst eine solche Adresse hinterlegt, noch ergäbe es Sinn, dass der Kernel in den von mir beschreibbaren Bereich eine Adresse schreibt, an der es nach meinem Tod weitergehen soll. Vorallem: Was sollte denn da weitergehen? Das Programm ist doch zuende. „mainmuss also in äußeren Code eingebettet sein.

main“ ist nur der Einsprungpunkt aus Sicht eines C-Programms, das die C-Standardbibliotheken verwendet, aber nicht aus Sicht des Kernels. Mir fehlt an dieser Stelle ein Link, der das mal schön erklärt, mit Bildchen und so. In „main“ landet man jedenfalls erst nach einer längeren Setup-Phase und auch nach dem Ende von „main“ ist es nicht vorbei. „main“ ist tatsächlich nur ein normaler Funktionsaufruf. Nur durch die Nähe von C und Assembler lässt sich dieses Symbol namens „main“ auch in einem Assembler-Programm verwenden. Den Code außenherum benötigt man aber immernoch, der gcc versteckt das beim Kompilieren lediglich vor mir.

Was Linux anspringt, ist das Symbol „_start“. Möchte ich also mein Assembler-Programm ohne die C-Standardbibliothek betreiben (und das will ich – der Assembler-Code, den ich schreibe, soll alles sein, was ausgeführt wird, denn sonst könnte ich auch direkt C nehmen), muss es so aussehen:

.text
.global _start
_start:
     movl $1, %eax
     movl $8, %ebx
     int  $0x80

Einerseits hat sich offensichtlich der Symbolname geändert. Andererseits sieht der Code auch ganz anders aus. Wieso?

main“ ist, wie gesagt, eine Funktion. Ihr werden gemäß der cdecl Calling Convention über den Stack Parameter übergeben. Zusätzlich liegt auf dem Stack eine Rücksprungadresse, an der nach Ende der Funktion weitergemacht werden soll. Was nach „main“ passiert, interessiert aus der Sicht eines C-Programms meistens nicht weiter und man geht davon aus, dass das Programm zuende ist. „main“ kann also gemäß der Konvention seinen Rückgabewert in EAX platzieren und dann zurückspringen. Genau das tut das erste Programm oben.

_start“ ist keine Funktion und wird auch nicht so aufgerufen. Das ist nur der Einsprungpunkt ins Programm, an dessen Ende es kein „Zurück“ gibt. Möchte ich das Programm sauber beenden, dann muss ich dem Kernel das explizit sagen – ich kann nicht einfach eine Funktion verlassen und gut ist’s. Daher muss ein Syscall erfolgen und eben das tun die drei Instruktionen:

Dieses zweite Programm kann ich auf zwei Arten übersetzen. Einerseits direkt mit „as“ und „ld“:

$ as -o foo.o foo.s
$ ld -o foo foo.o

Oder über den gcc, wenn ich ihm sage, dass er keine Standardbibliotheken verwenden soll:

$ gcc -nostdlib -o foo foo.s

Man könnte natürlich auch das „main“-Beispiel mit „as“ und „ld“ übersetzen, der Aufruf wird aber sehr lang und hässlich. Siehe zum Beispiel: http://stackoverflow.com/questions/3577922/linking-a-gas-assembly-file-as-a-c-program-without-using-gcc

Was nun ohne Standardbibliothek entsteht, ist eine etwa 460 Byte große Datei. Das ist schon eher das, was ich mir vorgestellt habe. 460 Byte sind für drei Instruktionen zwar immernoch recht viel, aber das liegt schlichtweg an ELF.

Zusammenfassung

Noch einmal beide Varianten in C und Assembler:

C

main

int main(int argc, char *argv[])
{
    return 8;
}

Übersetzung:

$ gcc -o foo foo.c

_start

void _start()
{
    asm(
        "movl $1, %eax \n"
        "movl $8, %ebx \n"
        "int  $0x80    \n"
       );
}

Übersetzung:

$ gcc -nostdlib -o foo foo.c

Assembler

main

.text
.global main
main:
    movl $8, %eax
    ret

Übersetzung entweder:

$ as -o foo.o foo.s
$ ld -o foo -dynamic-linker \
    /lib/ld-linux.so.2 \
    /usr/lib/crt1.o \
    /usr/lib/crti.o \
    -lc foo.o \
    /usr/lib/crtn.o

Oder:

$ gcc -o foo foo.s

_start

.text
.global _start
_start:
     movl $1, %eax
     movl $8, %ebx
     int  $0x80

Übersetzung entweder:

$ as -o foo.o foo.s
$ ld -o foo foo.o

Oder:

$ gcc -nostdlib -o foo foo.s

Es fällt noch auf, dass die „_start“-Variante in C eigentlich auch nur aus Assembler besteht. Da kann man nichts machen – man hat keine Bibliotheken zur Verfügung, also muss man sich selbst um den Syscall kümmern. C an sich kann einem da auch nicht helfen, da Syscalls auf jeder Plattform anders aussehen.

Comments?