Warum wird das Deklarieren von main als Array kompiliert?

Lesezeit: 8 Minuten

Benutzeravatar von Theodoros Chatzigiannakis
Theodoros Chatzigiannakis

ich sah ein Codeschnipsel auf CodeGolf das ist als Compiler-Bombe gedacht, wo main wird als riesiges Array deklariert. Ich habe die folgende (Nicht-Bomben-) Version ausprobiert:

int main[1] = { 0 };

Es scheint unter Clang und mit nur einer Warnung unter GCC gut zu kompilieren:

Warnung: ‘main’ ist normalerweise eine Funktion [-Wmain]

Die resultierende Binärdatei ist natürlich Müll.

Aber warum kompiliert es überhaupt? Ist es überhaupt von der C-Spezifikation erlaubt? Der Abschnitt, den ich für relevant halte, lautet:

5.1.2.2.1 Programmstart

Die beim Programmstart aufgerufene Funktion heißt main. Die Implementierung deklariert keinen Prototyp für diese Funktion. Es muss mit dem Rückgabetyp int und ohne Parameter definiert werden […] oder mit zwei Parametern […] oder auf eine andere implementierungsdefinierte Weise.

Enthält “eine andere implementierungsdefinierte Weise” ein globales Array? (Mir scheint, dass sich die Spezifikation immer noch auf a bezieht Funktion.)

Wenn nicht, ist es eine Compiler-Erweiterung? Oder ein Feature der Toolchains, das einem anderen Zweck dient und sie sich entschieden haben, es über das Frontend verfügbar zu machen?

  • Es nicht kompilieren. ISO C verbietet Arrays der Größe Null.

    – Jens

    13. Januar 2016 um 11:00 Uhr

  • Dies ist gemäß der C-Spezifikation nicht zulässig. Compiler implementieren oft Dinge, die nicht von der Spezifikation abgedeckt werden.

    – MM

    13. Januar 2016 um 11:06 Uhr

  • Verwandte Frage: Wie kann ein Programm mit einer globalen Variable namens main anstelle einer main-Funktion funktionieren?. Ich denke auch inspiriert von einer Codegolf-Frage.

    – Shafik Yaghmour

    6. Oktober 2016 um 20:21 Uhr

  • @MM Besonders im Fall von Malbolge

    – Milchstraße90

    21. Juni 2019 um 21:56 Uhr

Benutzeravatar von skyking
Himmel König

Dies liegt daran, dass C eine “nicht gehostete” oder freistehende Umgebung zulässt, die die nicht erfordert main Funktion. Das bedeutet, dass der Name main wird für andere Verwendungen freigegeben. Deshalb erlaubt die Sprache als solche solche Erklärungen. Die meisten Compiler sind so konzipiert, dass sie beides unterstützen (der Unterschied besteht hauptsächlich darin, wie das Linken erfolgt) und verbieten daher keine Konstrukte, die in einer gehosteten Umgebung illegal wären.

Der Abschnitt, auf den Sie sich im Standard beziehen, bezieht sich auf gehostete Umgebungen, das entsprechende für freistehende ist:

In einer freistehenden Umgebung (in der die C-Programmausführung ohne den Nutzen eines Betriebssystems stattfinden kann) sind Name und Typ der beim Programmstart aufgerufenen Funktion implementierungsdefiniert. Alle Bibliotheksfunktionen, die für ein freistehendes Programm verfügbar sind, mit Ausnahme des in Abschnitt 4 erforderlichen Mindestsatzes, sind implementierungsdefiniert.

Wenn Sie es dann wie gewohnt verknüpfen, wird es schlecht, da der Linker normalerweise wenig Wissen über die Natur der Symbole hat (welchen Typ es hat oder sogar, ob es eine Funktion oder Variable ist). In diesem Fall löst der Linker gerne Aufrufe an main zu der benannten Variablen main. Wenn das Symbol nicht gefunden wird, führt dies zu einem Verbindungsfehler.

Wenn Sie es wie gewohnt verknüpfen, versuchen Sie im Grunde, den Compiler im gehosteten Betrieb zu verwenden und dann nicht zu definieren main wie Sie es sollen bedeutet undefiniertes Verhalten gemäß Anhang J.2:

das Verhalten ist unter den folgenden Umständen undefiniert:

  • Programm in einer gehosteten Umgebung definiert keine Funktion namens main mit einer der angegebenen Formen (5.1.2.2.1)

Der Zweck der freistehenden Möglichkeit besteht darin, C in Umgebungen verwenden zu können, in denen (beispielsweise) Standardbibliotheken oder CRT-Initialisierung nicht gegeben sind. Dies bedeutet, dass der Code, der zuvor ausgeführt wird main aufgerufen wird (das ist die CRT-Initialisierung, die die C-Laufzeitumgebung initialisiert) wird möglicherweise nicht bereitgestellt, und von Ihnen wird erwartet, dass Sie diese selbst bereitstellen (und Sie können sich für eine main oder kann sich dagegen entscheiden).

  • Dies kompiliert und verlinkt gut (na ja, mit einer Warnung) mit gcc 4.9.3 auf cygwin: int f(int argc,char **argv) { return 0; } char *main = (char *)f;

    – Peter – Setzen Sie Monica wieder ein

    13. Januar 2016 um 11:05 Uhr


  • @PeterA.Schneider Aber wenn es gut läuft, ist es reines Glück. Die CRT-Init wird versuchen anzurufen main wo der Zeiger gespeichert ist und nicht worauf er zeigt.

    – Himmel König

    13. Januar 2016 um 11:11 Uhr

  • Es verbindet aber Segfaults. Übrigens, ich glaube nicht, dass die Frage viel mit “freistehend” zu tun hat. Zum Beispiel die folgenden Kompilierungen und Links (zu einer DLL) in VS13: namespace Main_abused { class Program { int Main = 0; } } . Es ist eher so, dass main (und Main in C#) keine Schlüsselwörter sind und die C-Linker dumm, ähm, einfach sind.

    – Peter – Setzen Sie Monica wieder ein

    13. Januar 2016 um 11:14 Uhr


  • @PeterA.Schneider Ich stimme nicht zu, ein Programm mit main nicht anders definiert zu sein, als es der Standard (oder die angegebene Implementierung) vorschreibt, ist fehlerhaft.

    – Himmel König

    13. Januar 2016 um 11:25 Uhr

  • Das ist nicht wirklich genau. Der gehostete Abschnitt von C99/C11 enthält einen verzögerten Satz „oder auf eine andere implementierungsdefinierte Weise“, was völlig unklar ist. Also weiß niemand wirklich, welche Formen von main erlaubt sind … Hier im Detail diskutiert.

    – Ludin

    13. Mai 2016 um 9:26 Uhr

Benutzeravatar von tymmej
tymmej

Wenn Sie daran interessiert sind, wie Sie ein Programm im Hauptarray erstellen: https://jroweboy.github.io/c/asm/2015/01/26/when-is-main-not-a-function.html. Die Beispielquelle dort enthält nur ein char (und später int)-Array namens main die mit Maschinenanweisungen gefüllt ist.

Die wichtigsten Schritte und Probleme waren:

  • Holen Sie sich die Maschinenanweisungen einer Hauptfunktion aus einem gdb-Speicherauszug und kopieren Sie sie in das Array
  • Taggen Sie die Daten ein main[] ausführbar, indem es als const deklariert wird (Daten sind anscheinend entweder beschreibbar oder ausführbar)
  • Letztes Detail: Ändern Sie eine Adresse für tatsächliche String-Daten.

Der resultierende C-Code ist einfach

const int main[] = {
    -443987883, 440, 113408, -1922629632,
    4149, 899584, 84869120, 15544,
    266023168, 1818576901, 1461743468, 1684828783,
    -1017312735
};

führt aber zu einem ausführbaren Programm auf einem 64-Bit-PC:

$ gcc -Wall final_array.c -o sixth
final_array.c:1:11: warning: ‘main’ is usually a function [-Wmain]
 const int main[] = {
           ^
$ ./sixth 
Hello World!

Benutzeravatar von Lundin
Lundin

Das Problem ist, dass main ist kein reservierter Bezeichner. Der C-Standard sagt nur, dass es in gehosteten Systemen normalerweise eine Funktion namens main gibt. Aber nichts im Standard hindert Sie daran, dieselbe Kennung für andere finstere Zwecke zu missbrauchen.

GCC gibt Ihnen eine selbstgefällige Warnung “main ist normalerweise eine Funktion”, die auf die Verwendung des Bezeichners hinweist main für andere nicht verwandte Zwecke ist keine brillante Idee.


Blödes Beispiel:

#include <stdio.h>

int main (void)
{
  int main = 5;
  main:

  printf("%d\n", main);
  main--;

  if(main)
  {
    goto main;
  }
  else
  {
    int main (void);
    main();
  }
}

Dieses Programm gibt wiederholt die Zahlen 5,4,3,2,1 aus, bis es einen Stapelüberlauf bekommt und abstürzt (versuchen Sie das nicht zu Hause). Leider ist das obige Programm ein strikt konformes C-Programm und der Compiler kann Sie nicht daran hindern, es zu schreiben.

main ist – nach dem Kompilieren – nur ein weiteres Symbol in einer Objektdatei wie viele andere (globale Funktionen, globale Variablen usw.).

Der Linker verknüpft das Symbol main unabhängig von seiner Art. Tatsächlich kann der Linker den Typ des Symbols überhaupt nicht sehen (he kann sehen, dass es nicht in der ist .text-Abschnitt, aber das ist ihm egal ;))

Bei Verwendung von gcc ist der Standard-Einstiegspunkt _start, der wiederum main() aufruft, nachdem er die Laufzeitumgebung vorbereitet hat. Es springt also zur Adresse des Integer-Arrays, was normalerweise zu einer fehlerhaften Anweisung, einem Segfault oder einem anderen schlechten Verhalten führt.

Das alles hat natürlich nichts mit der C-Norm zu tun.

Es wird nur kompiliert, weil Sie nicht die richtigen Optionen verwenden (und funktioniert, weil Linker sich manchmal nur um die Namen von Symbolen, nicht ihre Typ).

$ gcc -std=c89 -pedantic -Wall x.c
x.c:1:5: warning: ISO C forbids zero-size array ‘main’ [-Wpedantic]
 int main[0];
     ^
x.c:1:5: warning: ‘main’ is usually a function [-Wmain]

  • Es wird immer noch kompiliert und verlinkt. Der einzige Unterschied besteht darin, dass Sie davor gewarnt werden main ist normalerweise eine Funktion (dann geht es weiter und verlinkt trotzdem).

    – Himmel König

    13. Januar 2016 um 11:14 Uhr

  • @skyking Sie möchten, dass das Kompilieren/Linken fehlschlägt? Hinzufügen -Werror dann.

    – Jens

    13. Januar 2016 um 12:22 Uhr

  • Aber dann würden auch (andere) gültige C-Programme nicht kompiliert werden.

    – Himmel König

    13. Januar 2016 um 12:33 Uhr

  • @skyking Dann füge hinzu -Wno-* für die Warnungen, die Sie akzeptiert haben. Meistens sind Warnungen leicht zu beheben, und wenn nicht, stimmt etwas mit dem Code IMNSHO nicht. ich benutze -Werror seit Jahren und hat sich bewährt. Neue Warnungen sind nicht zu übersehen und müssen behoben werden, um fortzufahren.

    – Jens

    13. Januar 2016 um 12:45 Uhr

  • I stimme zu -Werror und das Aktivieren von Warnungen ist eine gute Idee, aber das widerspricht nicht der Tatsache, dass der Compiler dadurch keine gültigen C-Programme kompilieren kann.

    – Himmel König

    13. Januar 2016 um 12:46 Uhr

Benutzeravatar von Zibri
Zibri

const int main[1] = { 0xc3c3c3c3 };

Dies wird auf x86_64 kompiliert und ausgeführt … gibt nichts zurück: D

  • Es wird immer noch kompiliert und verlinkt. Der einzige Unterschied besteht darin, dass Sie davor gewarnt werden main ist normalerweise eine Funktion (dann geht es weiter und verlinkt trotzdem).

    – Himmel König

    13. Januar 2016 um 11:14 Uhr

  • @skyking Sie möchten, dass das Kompilieren/Linken fehlschlägt? Hinzufügen -Werror dann.

    – Jens

    13. Januar 2016 um 12:22 Uhr

  • Aber dann würden auch (andere) gültige C-Programme nicht kompiliert werden.

    – Himmel König

    13. Januar 2016 um 12:33 Uhr

  • @skyking Dann füge hinzu -Wno-* für die Warnungen, die Sie akzeptiert haben. Meistens sind Warnungen leicht zu beheben, und wenn nicht, stimmt etwas mit dem Code IMNSHO nicht. ich benutze -Werror seit Jahren und hat sich bewährt. Neue Warnungen sind nicht zu übersehen und müssen behoben werden, um fortzufahren.

    – Jens

    13. Januar 2016 um 12:45 Uhr

  • I stimme zu -Werror und das Aktivieren von Warnungen ist eine gute Idee, aber das widerspricht nicht der Tatsache, dass der Compiler dadurch keine gültigen C-Programme kompilieren kann.

    – Himmel König

    13. Januar 2016 um 12:46 Uhr

1413900cookie-checkWarum wird das Deklarieren von main als Array kompiliert?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy