Was ist die Verwendung von _start () in C?

125

Ich habe von meinem Kollegen gelernt, dass man ein C-Programm schreiben und ausführen kann, ohne eine main()Funktion zu schreiben . Es kann so gemacht werden:

my_main.c

/* Compile this with gcc -nostartfiles */

#include <stdlib.h>

void _start() {
  int ret = my_main();
  exit(ret); 
}

int my_main() {
  puts("This is a program without a main() function!");
  return 0; 
}

Kompilieren Sie es mit diesem Befehl:

gcc -o my_main my_main.c nostartfiles

Führen Sie es mit diesem Befehl aus:

./my_main

Wann müsste man so etwas tun? Gibt es ein reales Szenario, in dem dies nützlich wäre?

Einfacher Kerl
quelle
7
Klassischer Artikel, der einige der inneren Funktionsweisen beim Starten von Programmen demonstriert: Ein Wirbelwind-Tutorial zum Erstellen wirklich teensy ELF Executables für Linux . Dies ist eine gute Lektüre, in der einige der Feinheiten _start()und andere Dinge außerhalb von besprochen werden main().
1
Die C-Sprache selbst sagt nichts über _startoder über einen anderen Einstiegspunkt aus main(außer dass der Name des Einstiegspunkts für freistehende (eingebettete) Implementierungen implementierungsdefiniert ist).
Keith Thompson

Antworten:

107

Das Symbol _startist der Einstiegspunkt Ihres Programms. Das heißt, die Adresse dieses Symbols ist die Adresse, zu der beim Programmstart gesprungen wurde. Normalerweise wird die Funktion mit dem Namen _startvon einer aufgerufenen Datei bereitgestellt, crt0.odie den Startcode für die C-Laufzeitumgebung enthält. Es richtet einige Dinge ein, füllt das Argumentarrayargv , zählt, wie viele Argumente vorhanden sind, und ruft dann auf main. Nach mainRückgabe exitwird aufgerufen.

Wenn ein Programm die C-Laufzeitumgebung nicht verwenden möchte, muss es seinen eigenen Code für bereitstellen _start. Die Referenzimplementierung der Programmiersprache Go tut dies beispielsweise, weil sie ein nicht standardmäßiges Threading-Modell benötigen, das etwas Magie mit dem Stapel erfordert. Es ist auch nützlich, eigene zu liefern, _startwenn Sie wirklich kleine Programme oder Programme schreiben möchten, die unkonventionelle Dinge tun.

fuz
quelle
2
Ein weiteres Beispiel ist der dynamische Linker / Loader von Linux, für den ein eigener _start definiert ist.
PP
2
@BlueMoon Aber das _startkommt auch aus der Objektdatei crt0.o.
Fuz
2
@ThomasMatthews Der Standard spezifiziert nicht _start; Tatsächlich wird nicht angegeben, was passiert, bevor überhaupt mainaufgerufen wird, sondern nur, welche Bedingungen erfüllt sein müssen, wenn mainaufgerufen wird. Es ist eher eine Konvention für den Einstiegspunkt _start, der aus alten Zeiten stammt.
Fuz
1
"Die Referenzimplementierung der Programmiersprache Go tut dies, weil sie ein nicht standardmäßiges Threading-Modell benötigen." crt0.o ist C-spezifisch (crt-> C-Laufzeit). Es gibt keinen Grund zu der Annahme, dass es für eine andere Sprache verwendet wird. Und Go's Threading-Modell ist völlig standardkonform
Steve Cox
8
@SteveCox Viele Programmiersprachen basieren auf der C-Laufzeit, da es einfacher ist, Sprachen auf diese Weise zu implementieren. Go verwendet nicht das normale Threading-Modell. Sie verwenden kleine, Heap-zugewiesene Stapel und ihren eigenen Scheduler. Dies ist sicherlich kein Standard-Gewindemodell.
Fuz
45

Während dies aus mainSicht des Programmierers der Einstiegspunkt für Ihr Programm _startist , ist dies aus Sicht des Betriebssystems der übliche Einstiegspunkt (die erste Anweisung, die ausgeführt wird, nachdem Ihr Programm vom Betriebssystem aus gestartet wurde).

In einem typischen C- und insbesondere C ++ - Programm wurde viel Arbeit geleistet, bevor die Ausführung in main eingeht. Besonders Dinge wie die Initialisierung globaler Variablen. Hier finden Sie eine gute Erklärung für alles finden , was los ist auf zwischen _start()und main()und auch nach der Haupt hat wieder verlassen (siehe Kommentar unten).
Der dafür erforderliche Code wird normalerweise von den Compiler-Autoren in einer Startdatei bereitgestellt, aber mit dem Flag –nostartfilesteilen Sie dem Compiler im Wesentlichen mit: "Geben Sie mir nicht die Standard-Startdatei , sondern geben Sie mir die volle Kontrolle darüber, was direkt von der Anfang".

Dies ist manchmal notwendig und wird häufig auf eingebetteten Systemen verwendet. Wenn Sie beispielsweise kein Betriebssystem haben und bestimmte Teile Ihres Speichersystems (z. B. Caches) vor der Initialisierung Ihrer globalen Objekte manuell aktivieren müssen.

MikeMB
quelle
Die globalen Variablen sind Teil des Datenabschnitts und werden daher beim Laden des Programms eingerichtet (wenn sie const sind, sind sie Teil des Textabschnitts, dieselbe Geschichte). Die _start-Funktion hat damit nichts zu tun.
Cheiron
@Cheiron: Entschuldigung, mein Emistake In c ++ werden globale Variablen häufig von einem Konstruktor initialisiert, der in _start()(oder einer anderen von ihm aufgerufenen Funktion) ausgeführt wird, und in vielen Bare-Metal-Programmen kopieren Sie explizit alle globalen Daten von Flash in den RAM Erstens, was auch in passiert _start(), aber diese Frage betraf weder C ++ noch Bare-Metal-Code.
MikeMB
1
Beachten Sie, dass in einem eigenen Programm _startdie C-Bibliothek nur initialisiert wird, wenn Sie spezielle Schritte ausführen, um dies selbst zu tun. Es kann durchaus unsicher sein, eine nicht asynchronsignalsichere Funktion eines solchen Programms zu verwenden. (Es gibt keine offizielle Garantie dafür, dass eine Bibliotheksfunktion funktioniert, aber asynchronsignalsichere Funktionen können überhaupt nicht auf globale Daten verweisen, sodass sie sich um Fehlfunktionen
bemühen müssen
@zwol das ist nur teilweise richtig. Beispielsweise könnte eine solche Funktion Speicher zuweisen. Das Zuweisen von Speicher ist problematisch, wenn die internen Datenstrukturen für mallocnicht initialisiert werden.
Fuz
1
@FUZxxl auch sagen , dass ich feststellen , dass Async-Signal sichere Funktionen werden zu modifizieren erlaubt errno(zB readund writebin Async-Signal-safe und einstellen kann errno) und das könnte möglicherweise ein Problem sein , genau je nachdem , wann der pro Thread errnoStandort zugeordnet ist .
zwol
2

Hier ist ein guter Überblick, was beim Programmstart geschieht vor main . Insbesondere zeigt es , dass __startist der eigentliche Einstiegspunkt zu Ihrem Programm von OS Sicht.

Dies ist die allererste Adresse, von der aus der Anweisungszeiger in Ihrem Programm zu zählen beginnt.

Der dortige Code ruft einige Routinen der C-Laufzeitbibliothek auf, um die Verwaltung zu übernehmen. Rufen Sie dann Ihre mainan und bringen Sie die Dinge herunter und rufen Sie exitmit dem zurückgegebenen Exit-Code auf main.


Ein Bild sagt mehr als tausend Worte:

C Laufzeit-Startdiagramm


PS: Diese Antwort wurde aus einer anderen Frage übernommen, die SO als Duplikat dieser Frage hilfreich geschlossen hat.

ulidtko
quelle
Cross-Posting, um die hervorragende Analyse und das schöne Bild zu bewahren .
Ulidtko
1

Wann müsste man so etwas tun?

Wenn Sie Ihren eigenen Startcode für Ihr Programm wünschen.

mainist nicht der erste Eintrag für ein C-Programm, sondern der erste _startEintrag hinter dem Vorhang.

Beispiel unter Linux:

_start: # _start is the entry point known to the linker
    xor %ebp, %ebp            # effectively RBP := 0, mark the end of stack frames
    mov (%rsp), %edi          # get argc from the stack (implicitly zero-extended to 64-bit)
    lea 8(%rsp), %rsi         # take the address of argv from the stack
    lea 16(%rsp,%rdi,8), %rdx # take the address of envp from the stack
    xor %eax, %eax            # per ABI and compatibility with icc
    call main                 # %edi, %rsi, %rdx are the three args (of which first two are C standard) to main

    mov %eax, %edi    # transfer the return of main to the first argument of _exit
    xor %eax, %eax    # per ABI and compatibility with icc
    call _exit        # terminate the program

Gibt es ein reales Szenario, in dem dies nützlich wäre?

Wenn Sie meinen, implementieren Sie unsere eigenen _start:

Ja, in den meisten kommerziellen Embedded-Programmen, mit denen ich gearbeitet habe, müssen wir unsere eigenen implementieren, _startum unsere spezifischen Speicher- und Leistungsanforderungen zu erfüllen.

Wenn Sie meinen, lassen Sie die mainFunktion fallen und ändern Sie sie in etwas anderes:

Nein, ich sehe keinen Vorteil darin.

Trevor
quelle