Dieser verschleierte C-Code behauptet, ohne main () zu laufen, aber was macht er wirklich?

84
#include <stdio.h>
#define decode(s,t,u,m,p,e,d) m##s##u##t
#define begin decode(a,n,i,m,a,t,e)

int begin()
{
    printf("Ha HA see how it is?? ");
}

Ruft dies indirekt auf main? Wie?

Rajeev Singh
quelle
146
Die definierten Makros expand beginnen "main" zu sagen. Es ist nur ein Trick. Nichts Interessantes.
rghome
10
Ihre Toolchain sollte die Option haben, den vorverarbeiteten Code in einer Datei zu
@rghome Warum nicht als Antwort posten? Und es ist angesichts der Anzahl der positiven Stimmen eindeutig interessant.
Matsemann
3
@ Matsemann Wow! Ich habe die Up-Votes nicht bemerkt. Ich könnte es in eine Antwort ändern, und wenn die Kommentar-Up-Votes Antwort-Up-Votes wären, wäre dies bei weitem meine beste Punktzahl, aber es gibt bereits eine detaillierte Antwort. Ich denke, der Punkt meines Kommentars ist, dass es nicht wirklich interessant ist und daher als Alternative für Leute fungiert, die die Antwort nicht abstimmen wollen. Vielen Dank für den Hinweis.
rghome
Leute, es liegt am Linker als Betriebssystem-Tool, den Einstiegspunkt und nicht die Sprache selbst festzulegen. Sie können sogar unseren eigenen Einstiegspunkt festlegen und eine Bibliothek erstellen, die auch ausführbar ist! unix.stackexchange.com/a/223415/37799
Ho1

Antworten:

193

Die Sprache C definiert die Ausführungsumgebung in zwei Kategorien: freistehend und gehostet . In beiden Ausführungsumgebungen wird von der Umgebung eine Funktion zum Programmstart aufgerufen.
In einer freistehenden Umgebung kann die Startfunktion eines Programms definiert werden, während dies in einer gehosteten Umgebung der Fall sein sollte main. Kein Programm in C kann ohne Programmstartfunktion in den definierten Umgebungen ausgeführt werden.

In Ihrem Fall mainwird durch die Präprozessordefinitionen ausgeblendet. begin()wird erweitert, auf decode(a,n,i,m,a,t,e)die weiter erweitert wird main.

int begin() -> int decode(a,n,i,m,a,t,e)() -> int m##a##i##n() -> int main() 

decode(s,t,u,m,p,e,d)ist ein parametrisiertes Makro mit 7 Parametern. Ersatzliste für dieses Makro ist m##s##u##t. m, s, uund tgibt 4 th , 1 st , 3 rd und 2 nd Parameter in der Ersatzliste verwendet.

s, t, u, m, p, e, d
1  2  3  4  5  6  7

Rest nützt nichts ( nur um zu verschleiern ). Argument übergeben decodeist " a , n , i , m , a, t, e" , so werden die Kennungen m, s, uund tersetzt werden mit Argumenten m, a, iund n, respectively.

 m --> m  
 s --> a 
 u --> i 
 t --> n
Haccks
quelle
11
@GrijeshChauhan Alle C-Compiler verarbeiten die Makros. Sie werden von allen C-Standards seit C89 benötigt.
jdarthenay
17
Das ist eindeutig falsch. Unter Linux kann ich verwenden _start(). Oder noch niedriger kann ich versuchen, den Start meines Programms einfach an der Adresse auszurichten, auf die die IP nach dem Start eingestellt ist. main()ist C Standard Bibliothek . C selbst unterwirft dies nicht.
ljrk
1
@haccks Die Standardbibliothek hat einen Eintrittspunkt definieren. Die Sprache selbst ist egal
ljrk
3
Können Sie bitte erklären, wie decode(a,n,i,m,a,t,e)es wird m##a##i##n? Ersetzt es Zeichen? Können Sie einen Link zur Dokumentation der decodeFunktion bereitstellen ? Vielen Dank.
AL
1
@AL First beginist so definiert, dass es durch decode(a,n,i,m,a,t,e)das zuvor definierte ersetzt wird. Diese Funktion nimmt die Argumente s,t,u,m,p,e,dund verkettet sie in dieser Form m##s##u##t( ##bedeutet verketten). Das heißt, es ignoriert die Werte von p, e und d. Wie Sie "Call" decodemit s = a, t = n, u = i, m = m ersetzt er effektiv beginmit main.
ljrk
71

Versuchen Sie es mit gcc -E source.c, die Ausgabe endet mit:

int main()
{
    printf("Ha HA see how it is?? ");
}

Eine main()Funktion wird also tatsächlich vom Präprozessor erzeugt.

jdarthenay
quelle
37

Das betreffende Programm wirdmain() aufgrund einer Makroerweiterung aufgerufen , aber Ihre Annahme ist fehlerhaft - es muss überhaupt nicht aufgerufen werden main()!

Genau genommen können Sie ein C-Programm haben und es kompilieren können, ohne ein mainSymbol zu haben. mainist etwas, in das der c libraryerwartet, zu springen, nachdem er seine eigene Initialisierung abgeschlossen hat. Normalerweise springt man mainvom libc-Symbol, das als bekannt ist _start. Es ist immer möglich, ein sehr gültiges Programm zu haben, das einfach die Assembly ausführt, ohne ein Hauptprogramm zu haben. Schau dir das an:

/* This must be compiled with the flag -nostdlib because otherwise the
 * linker will complain about multiple definitions of the symbol _start
 * (one here and one in glibc) and a missing reference to symbol main
 * (that the libc expects to be linked against).
 */

void
_start ()
{
    /* calling the write system call, with the arguments in this order:
     * 1. the stdout file descriptor
     * 2. the buffer we want to print (Here it's just a string literal).
     * 3. the amount of bytes we want to write.
     */
    asm ("int $0x80"::"a"(4), "b"(1), "c"("Hello world!\n"), "d"(13));
    asm ("int $0x80"::"a"(1), "b"(0)); /* calling exit syscall, with the argument to be 0 */
}

Kompilieren Sie das Obige mit gcc -nostdlib without_main.cund sehen Sie, wie es Hello World!auf dem Bildschirm gedruckt wird, indem Sie Systemaufrufe (Interrupts) in der Inline-Assembly ausgeben.

Weitere Informationen zu diesem speziellen Problem finden Sie im ksplice-Blog

Ein weiteres interessantes Problem ist, dass Sie auch ein Programm haben können, das kompiliert wird, ohne dass das mainSymbol einer C-Funktion entspricht. Zum Beispiel können Sie Folgendes als sehr gültiges C-Programm verwenden, das den Compiler nur dann zum Jammern bringt, wenn Sie die Warnstufe erhöhen.

/* These values are extracted from the decimal representation of the instructions
 * of a hello world program written in asm, that gdb provides.
 */
const int main[] = {
    -443987883, 440, 113408, -1922629632,
    4149, 899584, 84869120, 15544,
    266023168, 1818576901, 1461743468, 1684828783,
    -1017312735
};

Die Werte im Array sind Bytes, die den Anweisungen zum Drucken von Hello World auf dem Bildschirm entsprechen. Um eine detailliertere Darstellung der Funktionsweise dieses speziellen Programms zu erhalten, werfen Sie einen Blick auf diesen Blog-Beitrag , in dem ich ihn auch zuerst gelesen habe.

Ich möchte noch einen letzten Hinweis zu diesen Programmen geben. Ich weiß nicht, ob sie sich gemäß der C-Sprachspezifikation als gültige C-Programme registrieren, aber das Kompilieren und Ausführen dieser Programme ist sicherlich sehr gut möglich, selbst wenn sie gegen die Spezifikation selbst verstoßen.

NlightNFotis
quelle
1
Ist der Name eines _startTeils eines definierten Standards oder ist das nur implementierungsspezifisch? Sicherlich ist Ihr "main als Array" architekturspezifisch. Wichtig ist auch, dass es nicht unangemessen ist, wenn Ihr Trick "main as a array" zur Laufzeit aufgrund von Sicherheitsbeschränkungen fehlschlägt (obwohl dies wahrscheinlicher wäre, wenn Sie das constQualifikationsmerkmal nicht verwenden würden und viele Systeme dies dennoch zulassen würden).
Mah
1
@mah: _startist nicht im ELF-Standard enthalten, obwohl der AMD64 psABI einen Verweis auf _startbei 3.4 Process Initialization enthält . Offiziell kennt ELF nur die Adresse e_entryim ELF-Header, _startist nur ein Name, den die Implementierung gewählt hat.
Ninjalj
1
@mah Wichtig ist auch, dass es nicht unangemessen ist, dass Ihr Trick "main as a array" zur Laufzeit aufgrund von Sicherheitsbeschränkungen fehlschlägt (obwohl dies wahrscheinlicher wäre, wenn Sie das const-Qualifikationsmerkmal nicht verwenden würden und dennoch viele Systeme dies zulassen würden es). Nur wenn die endgültige ausführbare Datei in irgendeiner Weise als etwas Unsicheres unterscheidbar ist - eine binäre ausführbare Datei ist eine binäre ausführbare Datei, egal wie sie dort ankam. Und spielt constkeine Rolle - der Symbolname in dieser ausführbaren Binärdatei lautet main. Nicht mehr und nicht weniger. constist ein C-Konstrukt, das zur Ausführungszeit nichts bedeutet.
Andrew Henle
1
@Stewart: Bei ARMv6l (Segmentierungsfehler) schlägt dies sicherlich fehl. Es sollte jedoch auf jeder x86-64-Architektur funktionieren.
links um den
@ AndrewHenle Eine binäre ausführbare Datei ist eine binäre ausführbare Datei, egal wie sie dort ankam - nicht genau wahr. Eine binäre ausführbare Datei ist kein einzelner Blob ausführbarer Anweisungen, sondern ein sorgfältig zugeordneter Blob von Partitionen, von denen einige Anweisungen sind, von denen einige schreibgeschützte Daten sind und von denen einige Daten sind, die in Lese- / Schreibdaten initialisiert werden sollen. (Einige) Sicherheitshardware-MMUs können die Ausführung von Seiten verhindern, die nicht als solche gekennzeichnet sind. Dies ist eine gute Funktion, um beispielsweise Stapelüberläufe zu verhindern, die zur Ausführung von Code auf dem Stapel führen. Leider ist dies manchmal legitim oder oft nicht aktiviert.
Mah
30

Jemand versucht, sich wie ein Magier zu verhalten. Er glaubt, er kann uns austricksen. Aber wir alle wissen, dass die Ausführung des c-Programms mit beginnt main().

Das int begin()wird decode(a,n,i,m,a,t,e)durch einen Durchgang der Präprozessorstufe ersetzt. Andererseits decode(a,n,i,m,a,t,e)wird durch m ## a ## i ## n ersetzt. Wie durch die Positionszuordnung des Makroaufrufs hat der sWille einen Zeichenwert a. Ebenso uwird durch 'i' und tdurch 'n' ersetzt. Und so m##s##u##twird es werdenmain

In Bezug auf das ##Symbol in der Makroerweiterung ist es der Vorverarbeitungsoperator und führt das Einfügen von Token durch. Wenn ein Makro erweitert wird, werden die beiden Token auf beiden Seiten jedes '##'-Operators zu einem einzigen Token kombiniert, das dann die' ## 'und die beiden ursprünglichen Token in der Makroerweiterung ersetzt.

Wenn Sie mir nicht glauben, können Sie Ihren Code mit -Eflag kompilieren . Der Kompilierungsprozess wird nach der Vorverarbeitung gestoppt und Sie können das Ergebnis des Einfügens von Token sehen.

gcc -E FILENAME.c
abhiarora
quelle
11

decode(a,b,c,d,[...])mischt die ersten vier Argumente und verbindet sie, um eine neue Kennung in der Reihenfolge zu erhalten dacb. (Die verbleibenden drei Argumente werden ignoriert.) Gibt beispielsweise decode(a,n,i,m,[...])den Bezeichner an main. Beachten Sie, dass das beginMakro so definiert ist.

Daher wird das beginMakro einfach definiert als main.

Frxstrem
quelle
2

In Ihrem Beispiel ist die main()Funktion tatsächlich vorhanden, da begines sich um ein Makro handelt, das der Compiler durch ein Makro ersetzt, decodedas wiederum durch den Ausdruck m ## s ## u ## t ersetzt wird. Mit der Makro-Erweiterung ##erreichen Sie das Wort mainvon decode. Dies ist eine Spur:

begin --> decode(a,n,i,m,a,t,e) --> m##parameter1##parameter3##parameter2 ---> main

Es ist nur ein Trick main(), aber die Verwendung des Namens main()für die Eingabefunktion des Programms ist in der Programmiersprache C nicht erforderlich. Dies hängt von Ihren Betriebssystemen und dem Linker als einem seiner Tools ab.

Unter Windows verwenden Sie nicht immer main(), sondern eher WinMainoderwWinMain , obwohl Sie verwenden können main(), auch mit Microsofts Toolchain . Unter Linux kann man verwenden _start.

Es liegt am Linker als Betriebssystem-Tool, den Einstiegspunkt und nicht die Sprache selbst festzulegen. Sie können sogar unseren eigenen Einstiegspunkt festlegen und eine Bibliothek erstellen, die auch ausführbar ist !

Ho1
quelle
@ vaxquis Sie haben Recht, aber dies ist eine Teilantwort, die ich geschrieben habe, um die erste Antwort zu ergänzen / zu korrigieren, die die main()Funktion an die Programmiersprache C bindet , die nicht korrekt ist.
Ho1
@ vaxquis Ich ging davon aus, dass die Erklärung von "main () - Funktion in C-Programmen nicht unbedingt erforderlich ist" eine teilweise Antwort wäre. Ich habe einen Absatz hinzugefügt, um die Antwort zu vervollständigen. - Ho1 vor 16 Minuten
Ho1