Kompilieren Sie das Programm und führen Sie es ohne main () in C aus

78

Ich versuche folgendes Programm ohne main()Funktion in zu kompilieren und auszuführen C. Ich habe mein Programm mit dem folgenden Befehl kompiliert.

gcc -nostartfiles nomain.c

Und der Compiler warnt

/usr/bin/ld: warning: cannot find entry symbol _start; defaulting to 0000000000400340

OK, kein Problem. Dann habe ich eine ausführbare Datei (a.out) ausgeführt, beide printfAnweisungen wurden erfolgreich gedruckt und erhalten dann einen Segmentierungsfehler .

Meine Frage lautet also: Warum Segmentierungsfehler nach erfolgreicher Ausführung von Druckanweisungen?

Mein Code:

#include <stdio.h>

void nomain()
{
        printf("Hello World...\n");
        printf("Successfully run without main...\n");
}

Ausgabe:

Hello World...
Successfully run without main...
Segmentation fault (core dumped)

Hinweis:

Hier -nostartfilesverhindert das gcc-Flag, dass der Compiler beim Verknüpfen Standardstartdateien verwendet

msc
quelle
36
Ich bin überrascht, dass das überhaupt funktioniert. Ehrlich gesagt halte ich diese Behandlung durch den Linker für fehlerhaft (oder zumindest für eine schlechte Sache): Es gab keinen Einstiegspunkt, daher halluzinierte der Linker sie nur aus einer beliebigen Funktion heraus. Blech.
Imallett
4
@imallett, zumindest war der Linker so freundlich, mit einer Warnung darauf aufmerksam zu machen und zu erklären, welche Fallback-Aktion er unternahm! Sie haben Recht, dass dies als Fehler besser ist als nur als Warnung.
Toby Speight
Warum würden Sie keine Hauptleitung verwenden?
Pieter B
4
@PieterB - Nicht übermäßig relevant für eine Diskussion über Unices, aber der Einstiegspunkt für Windows-Programme ist nicht unbedingt main, aber WinMainoder wWinMain.
Geschichtenerzähler - Unslander Monica
@StoryTeller Sie können sowohl unter Windows als auch unter Linux einen beliebigen Einstiegspunkt festlegen: Für Linux ldwäre dies eine -eOption, für den MSVC-Linker von Windows eine /ENTRYOption.
Ruslan

Antworten:

130

Werfen wir einen Blick auf die generierte Assembly Ihres Programms:

.LC0:
        .string "Hello World..."
.LC1:
        .string "Successfully run without main..."
nomain:
        push    rbp
        mov     rbp, rsp
        mov     edi, OFFSET FLAT:.LC0
        call    puts
        mov     edi, OFFSET FLAT:.LC1
        call    puts
        nop
        pop     rbp
        ret

Beachten Sie die retAussage. Der Einstiegspunkt Ihres Programms ist bestimmt, damit nomainist alles in Ordnung. Sobald die Funktion zurückkehrt, versucht sie, in eine Adresse auf dem Aufrufstapel zu springen, die nicht ausgefüllt ist. Das ist ein illegaler Zugriff und es folgt ein Segmentierungsfehler.

Eine schnelle Lösung wäre, exit()am Ende Ihres Programms aufzurufen (und unter der Annahme von C11 können wir die Funktion genauso gut als markieren _Noreturn):

#include <stdio.h>
#include <stdlib.h>

_Noreturn void nomain(void)
{
    printf("Hello World...\n");
    printf("Successfully run without main...\n");
    exit(0);
}

Tatsächlich verhält sich Ihre Funktion jetzt ziemlich ähnlich wie eine reguläre mainFunktion, da maindie exitFunktion nach der Rückkehr von mit maindem Rückgabewert aufgerufen wird.

Geschichtenerzähler - Unslander Monica
quelle
6
Ich denke, es gibt einige Architektur / Betriebssystem-Kombinationen, bei denen Sie einfach aus einem Programm "zurückkehren" können. MS-DOS .COM ausführbare Dateien? Wie auch immer, wir beschäftigen uns intensiv mit implementierungsspezifischem Verhalten.
pjc50
4
@ pjc50 - Wir sind in der Tat. Obwohl der Pfad im OP eine Unix-Variante vorschlug. Dies, zusammen mit der Popularität bestimmter Architekturen und Befehlssätze, war der einzige Grund, warum ich mich wohl fühlte, generierte Baugruppen in der Antwort zu präsentieren.
Geschichtenerzähler - Unslander Monica
1
Nur eine Beobachtung. -nostartfileskann auch die C-Bibliothek unbrauchbar machen. Ohne den C- Start können nachfolgende Aufrufe der C- Bibliotheksfunktionen unerwartet fehlschlagen. Wenn Sie unter Linux kompilieren -nostartupfilesund -staticfeststellen, dass das Programm fehlerhaft ist. Es gibt C- Bibliotheken wie MUSL, für die keine Vorabinitialisierung erforderlich ist und die für diese Umgebung ausgelegt sind.
Michael Petch
21

Wenn in C Funktionen / Unterprogramme aufgerufen werden, wird der Stapel wie folgt gefüllt (in der Reihenfolge):

  1. Die Argumente,
  2. Absender,
  3. Lokale Variablen, -> Stapeloberseite

Als Startpunkt main () strukturiert ELF das Programm so, dass alle Anweisungen, die zuerst kommen, zuerst gepusht werden, in diesem Fall printfs.

Jetzt wird das Programm ohne Rücksprungadresse ODER abgeschnitten, __end__und tatsächlich wird davon ausgegangen, dass alles, was sich an diesem ( __end__) Speicherort auf dem Stapel befindet, die Rücksprungadresse ist, aber leider nicht und daher stürzt es ab.

Milind Deore
quelle
4
Ist die Reihenfolge der Stapeldaten durch den C-Standard definiert? Ich dachte, es liegt an der Systemarchitektur
Délisson Junio
1
Aus diesem Grund habe ich ELF (ausführbares und verknüpfbares Dateiformat) erwähnt. Dies wird durch Cross-Compilieren für einen bestimmten ARCH-Typ auf dem erforderlichen Betriebssystem generiert.
Milind Deore
1
Um wählerisch zu sein, können Sie das ELF-Format auch auf Systemen ohne Stapel verwenden. Ein Beispiel für ein solches System ist Freescale RS08 mit dem Codewarrior-Compiler, der ELF-Linkerdateien generiert.
Lundin