Questo codice C offuscato afferma di funzionare senza main (), ma cosa fa veramente?


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?? ");
}

Questo chiama indirettamente main? Come?


146
Le macro definite expand iniziano a dire "main". È solo un trucco. Niente di interessante.
rghome

10
La tua toolchain dovrebbe avere un'opzione per lasciare il codice preelaborato in un file - il file effettivo che è stato compilato - dove lo vedrai, infatti, ha una main ()

@rghome Perché non pubblicare come risposta? Ed è chiaramente interessante, dato il numero di voti positivi.
Matsemann

3
@Matsemann Wow! Non ho notato i voti favorevoli. Potrei cambiarlo in una risposta, e se il commento positivo fosse una risposta positiva, sarebbe di gran lunga il mio miglior punteggio, ma c'è già una risposta dettagliata. Penso che il punto del mio commento sia che non è molto interessante e quindi funge da alternativa per le persone che non vogliono votare la risposta. Grazie per averlo fatto notare però.
rghome

Ragazzi, spetta al linker come strumento del sistema operativo impostare il punto di ingresso e non la lingua stessa. Puoi persino impostare il nostro punto di ingresso e puoi creare una libreria che sia anche eseguibile! unix.stackexchange.com/a/223415/37799
Ho1

Risposte:


193

Il linguaggio C definisce l'ambiente di esecuzione in due categorie: indipendente e ospitato . In entrambi gli ambienti di esecuzione una funzione viene chiamata dall'ambiente per l'avvio del programma.
In un ambiente indipendente , la funzione di avvio del programma può essere definita implementazione mentre in un ambiente ospitato dovrebbe essere main. Nessun programma in C può essere eseguito senza la funzione di avvio del programma negli ambienti definiti.

Nel tuo caso, mainè nascosto dalle definizioni del preprocessore.begin()si espanderà a decode(a,n,i,m,a,t,e)cui verrà ulteriormente espanso 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)è una macro parametrizzata con 7 parametri. L'elenco di sostituzione per questa macro è m##s##u##t. m, s, ue tsono 4 ° , 1 ° , 3 ° e 2 ° parametro utilizzato nella lista di sostituzione.

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

Il resto non serve ( solo per offuscare ). L'argomento passato a decodeè " a , n , i , m , a, t, e" quindi, gli identificatori m, s, ue tvengono sostituiti con argomenti m, a, ie n, rispettivamente.

 m --> m  
 s --> a 
 u --> i 
 t --> n

11
@GrijeshChauhan tutti i compilatori C elaborano le macro, è richiesto da tutti gli standard C a partire da C89.
jdarthenay

17
È chiaramente sbagliato. Su Linux posso usare _start(). O ancora più di basso livello posso provare ad allineare semplicemente l'inizio del mio programma con l'indirizzo a cui è impostato l'IP dopo l'avvio. main()è C standard biblioteca . C stesso non impone restrizioni su questo.
ljrk

1
@haccks La libreria standard definisce un punto di ingresso. Alla lingua in sé non interessa
ljrk

3
Puoi spiegare come si decode(a,n,i,m,a,t,e)diventa m##a##i##n? Sostituisce i personaggi? Potete fornire un collegamento alla documentazione della decodefunzione? Grazie.
AL

1
@AL First beginè definito per essere sostituito da quello decode(a,n,i,m,a,t,e)definito prima. Questa funzione prende gli argomenti s,t,u,m,p,e,de li concatena in questa forma m##s##u##t( ##significa concatenare). Cioè, ignora i valori di p, e e d. Quando "chiami" decodecon s = a, t = n, u = i, m = m, effettivamente si sostituisce begincon main.
ljrk

71

Prova a utilizzare gcc -E source.c, l'output termina con:

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

Quindi una main()funzione è effettivamente generata dal preprocessore.


37

Il programma in questione fa chiamata main()a causa di espansione di macro, ma la tua ipotesi è difettoso - è non dover chiamare main()a tutti!

A rigor di termini, puoi avere un programma C ed essere in grado di compilarlo senza avere un mainsimbolo. mainè qualcosa a cui si c libraryaspetta di passare, dopo aver terminato la propria inizializzazione. Di solito si salta maindal simbolo libc noto come _start. È sempre possibile avere un programma molto valido, che esegue semplicemente assembly, senza avere un main. Guarda questo:

/* 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 */
}

Compilare quanto sopra con gcc -nostdlib without_main.ce vederlo in stampaHello World! sullo schermo semplicemente emettendo chiamate di sistema (interrupt) in assembly inline.

Per ulteriori informazioni su questo particolare problema, controlla il blog di ksplice

Un altro problema interessante è che puoi anche avere un programma che si compila senza che il mainsimbolo corrisponda a una funzione C. Ad esempio, puoi avere quanto segue come programma C molto valido, che fa lamentare il compilatore solo quando si sale al livello di avvertenza.

/* 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
};

I valori nell'array sono byte che corrispondono alle istruzioni necessarie per stampare Hello World sullo schermo. Per un resoconto più dettagliato di come funziona questo programma specifico, dai un'occhiata a questo post del blog , dove l'ho letto per primo.

Voglio fare un ultimo avviso su questi programmi. Non so se si registrano come programmi C validi secondo la specifica del linguaggio C, ma compilarli ed eseguirli è certamente molto possibile, anche se violano la specifica stessa.


1
Il nome fa _startparte di uno standard definito o è solo specifico dell'implementazione? Certamente il tuo "main as an array" è specifico dell'architettura. Inoltre, non sarebbe irragionevole che il trucco "main as an array" fallisse in fase di esecuzione a causa di restrizioni di sicurezza (sebbene ciò sarebbe più probabile se non si usasse il constqualificatore, e molti sistemi lo consentirebbero).
mah

1
@mah: _startnon è nello standard ELF, sebbene la psABI AMD64 contenga un riferimento _starta 3.4 Inizializzazione del processo . Ufficialmente, ELF conosce solo l'indirizzo e_entrynell'intestazione ELF, _startè solo un nome scelto dall'implementazione.
ninjalj

1
@mah È anche importante, non sarebbe irragionevole che il tuo trucco "main as an array" fallisse in fase di esecuzione a causa di restrizioni di sicurezza (anche se sarebbe più probabile se non usassi il qualificatore const, e molti sistemi lo permetterebbero it). Solo se l'eseguibile finale è in qualche modo distinguibile come qualcosa di insicuro - un eseguibile binario è un eseguibile binario indipendentemente da come sia arrivato lì. E constnon importa un bit: il nome del simbolo in quel file eseguibile binario è main. Ne più ne meno. constè un costrutto C che non significa nulla al momento dell'esecuzione.
Andrew Henle

1
@Stewart: sicuramente fallisce su ARMv6l (errore di segmentazione). Ma dovrebbe funzionare su qualsiasi architettura x86-64.
sinistra circa l'

@AndrewHenle un eseguibile binario è un eseguibile binario, non importa come sia arrivato lì - non esattamente vero. Un eseguibile binario non è un singolo blob di istruzioni eseguibili, è un blob di partizioni accuratamente mappate, alcune delle quali sono istruzioni, alcune delle quali sono dati di sola lettura e alcuni dei quali sono dati da inizializzare in dati di lettura-scrittura. (Alcune) MMU hardware di sicurezza possono impedire l'esecuzione da pagine non contrassegnate come tali, e questa è una buona caratteristica per prevenire, ad esempio, overflow dello stack che portano all'esecuzione di codice sullo stack, ma purtroppo a volte è legittimo o spesso non abilitato.
mah

30

Qualcuno sta cercando di comportarsi come un mago. Pensa di poterci ingannare. Ma sappiamo tutti, l'esecuzione del programma c inizia con main().

Il int begin()sarà sostituito decode(a,n,i,m,a,t,e)da un passaggio di fase di preprocessore. Poi di nuovo, decode(a,n,i,m,a,t,e)sarà sostituito con m ## a ## i ## n. Come per l'associazione posizionale della chiamata macro, swill ha un valore di carattere a. Allo stesso modo, usarà sostituito da "i" e tsarà sostituito da "n". Ed è così m##s##u##tche diventeràmain

Per quanto riguarda, ##simbolo in espansione macro, è l'operatore di preelaborazione ed esegue il token paste. Quando una macro viene espansa, i due token su entrambi i lati di ciascun operatore "##" vengono combinati in un unico token, che quindi sostituisce "##" e i due token originali nell'espansione della macro.

Se non mi credi, puoi compilare il tuo codice con -Eflag. Fermerà il processo di compilazione dopo la preelaborazione e potrai vedere il risultato dell'incollaggio del token.

gcc -E FILENAME.c

11

decode(a,b,c,d,[...])mescola i primi quattro argomenti e li unisce per ottenere un nuovo identificatore, nell'ordine dacb. (Gli altri tre argomenti vengono ignorati.) Ad esempio, decode(a,n,i,m,[...])fornisce l'identificatore main. Nota che questo è ciò come beginviene definita la macro.

Pertanto, la beginmacro è semplicemente definita come main.


2

Nel tuo esempio, la main()funzione è effettivamente presente, perché beginè una macro che il compilatore sostituisce con una decodemacro che a sua volta sostituita dall'espressione m ## s ## u ## t. Usando l'espansione macro ##, raggiungerai la parola mainda decode. Questa è una traccia:

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

È solo un trucco da avere main(), ma l'uso del nome main()per la funzione di immissione del programma non è necessario nel linguaggio di programmazione C. Dipende dai tuoi sistemi operativi e dal linker come uno dei suoi strumenti.

In Windows, non si usa sempre main(), ma piuttosto WinMainowWinMain , sebbene sia possibile utilizzare main(), anche con la toolchain di Microsoft . In Linux, si può usare _start.

Spetta al linker come strumento del sistema operativo impostare il punto di ingresso e non la lingua stessa. Puoi persino impostare il nostro punto di ingresso e puoi creare una libreria che sia anche eseguibile !


@vaxquis Hai ragione, ma questa è una risposta parziale che ho scritto per complimentarmi / correggere la prima risposta che lega la main()funzione al linguaggio di programmazione C, che non è corretto.
Ho1

@vaxquis Ho assunto che spiegare "la funzione main () non è essenziale nei programmi C" sarebbe una risposta parziale. Ho aggiunto un paragrafo per completare la risposta. - Ho1 16 minuti fa
Ho1
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.