Ce code C obscurci prétend fonctionner sans main (), mais que fait-il vraiment?


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

Cela appelle-t-il indirectement main? Comment?


146
Les macros définies expand commencent à dire "main". C'est juste un truc. Rien d'interessant.
rghome

10
Votre chaîne d'outils devrait avoir une option pour laisser le code prétraité dans un fichier - le fichier réel qui est compilé - où vous le verrez, en effet, a un main ()

@rghome Pourquoi ne pas publier comme réponse? Et c'est clairement intéressant, compte tenu du nombre de votes positifs.
Matsemann

3
@Matsemann Wow! Je n'ai pas remarqué les votes positifs. Je pourrais le changer en réponse, et si les votes positifs des commentaires étaient des votes positifs, ce serait de loin mon meilleur score, mais il y a déjà une réponse détaillée. Je pense que le point de mon commentaire est que ce n’est pas vraiment intéressant et qu’il s’agit donc d’une alternative pour les personnes qui ne veulent pas voter pour la réponse. Merci de l'avoir signalé.
rghome

Les gars, c'est à l'éditeur de liens en tant qu'outil du système d'exploitation de définir le point d'entrée, et non la langue elle-même. Vous pouvez même définir notre propre point d'entrée, et vous pouvez créer une bibliothèque qui est également exécutable! unix.stackexchange.com/a/223415/37799
Ho1

Réponses:


193

Le langage C définit l'environnement d'exécution en deux catégories: autonome et hébergé . Dans les deux environnements d'exécution, une fonction est appelée par l'environnement pour le démarrage du programme.
Dans un environnement autonome, la fonction de démarrage du programme peut être définie par l'implémentation alors que dans un environnement hébergé , elle devrait l'être main. Aucun programme en C ne peut s'exécuter sans la fonction de démarrage du programme sur les environnements définis.

Dans votre cas, mainest masqué par les définitions du préprocesseur. begin()s'étendra à decode(a,n,i,m,a,t,e)laquelle sera étendu davantage 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)est une macro paramétrée avec 7 paramètres. La liste de remplacement pour cette macro est m##s##u##t. m, s, uet tsont 4 e , 1 er , 3 e et 2 e paramètre utilisé dans la liste de remplacement.

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

Le repos n'est d'aucune utilité ( juste pour obscurcir ). L'argument passé à decodeest " a , n , i , m , a, t, e" donc, les identificateurs m, s, uet tsont remplacés par des arguments m, a, iet n, respectivement.

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

11
@GrijeshChauhan tous les compilateurs C traitent les macros, il est requis par tous les standards C depuis C89.
jdarthenay

17
C'est tout simplement faux. Sur Linux, je peux utiliser _start(). Ou encore plus bas niveau, je peux essayer d'aligner simplement le début de mon programme avec l'adresse à laquelle l'adresse IP est définie après le démarrage. main()est la bibliothèque C Standard . C lui-même n'impose aucune restriction à ce sujet.
ljrk

1
@haccks La bibliothèque standard définit un point d'entrée. La langue elle-même ne se soucie pas
ljrk

3
Pouvez-vous s'il vous plaît expliquer comment decode(a,n,i,m,a,t,e)devenir m##a##i##n? Remplace-t-il les caractères? Pouvez-vous fournir un lien vers la documentation de la decodefonction? Merci.
AL

1
@AL First beginest défini pour être remplacé par decode(a,n,i,m,a,t,e)ce qui est défini avant. Cette fonction prend les arguments s,t,u,m,p,e,det les concatène sous cette forme m##s##u##t( ##signifie concaténer). C'est-à-dire qu'il ignore les valeurs de p, e et d. Lorsque vous «appelez» decodeavec s = a, t = n, u = i, m = m, il remplace effectivement beginpar main.
ljrk

71

Essayez d'utiliser gcc -E source.c, la sortie se termine par:

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

Ainsi, une main()fonction est en fait générée par le préprocesseur.


37

Le programme en question ne appel en main()raison de l' expansion macro, mais votre hypothèse est erronée - il n'a pas dû appeler main()du tout!

À proprement parler, vous pouvez avoir un programme C et être capable de le compiler sans avoir de mainsymbole. mainest quelque chose auquel le c librarys'attend à sauter, après avoir terminé sa propre initialisation. Habituellement, vous sautez à mainpartir du symbole libc connu sous le nom de _start. Il est toujours possible d'avoir un programme très valide, qui exécute simplement l'assembly, sans avoir de main. Regarde ça:

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

Compilez ce qui précède avec gcc -nostdlib without_main.cet voyez-le imprimerHello World! à l'écran simplement en émettant des appels système (interruptions) dans l'assemblage en ligne.

Pour plus d'informations sur ce problème particulier, consultez le blog ksplice

Un autre problème intéressant est que vous pouvez également avoir un programme qui compile sans que le mainsymbole corresponde à une fonction C. Par exemple, vous pouvez avoir ce qui suit comme programme C très valide, qui ne fait gémir le compilateur que lorsque vous augmentez le niveau d'avertissement.

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

Les valeurs du tableau sont des octets qui correspondent aux instructions nécessaires pour imprimer Hello World à l'écran. Pour un compte rendu plus détaillé du fonctionnement de ce programme spécifique, jetez un œil à ce billet de blog , où je l'ai également lu en premier.

Je veux faire un dernier avis sur ces programmes. Je ne sais pas s'ils s'enregistrent en tant que programmes C valides selon la spécification du langage C, mais les compiler et les exécuter est certainement très possible, même s'ils enfreignent la spécification elle-même.


1
Le nom fait-il _startpartie d'une norme définie ou est-ce uniquement spécifique à la mise en œuvre? Votre "main en tant que tableau" est certainement spécifique à l'architecture. Également important, il ne serait pas déraisonnable que votre astuce "main en tant que tableau" échoue au moment de l'exécution en raison de restrictions de sécurité (bien que cela soit plus probable si vous n'utilisiez pas le constqualificatif, et que de nombreux systèmes le permettraient).
mah

1
@mah: _startn'est pas dans le standard ELF, bien que le psABI AMD64 contienne une référence _startà 3.4 Process Initialization . Officiellement, ELF ne connaît que l'adresse à e_entrydans l'en-tête ELF, _startc'est juste un nom que l'implémentation a choisi.
ninjalj

1
@mah Également important, il ne serait pas déraisonnable que votre astuce "main as an array" échoue au moment de l'exécution en raison de restrictions de sécurité (bien que ce soit plus probable si vous n'utilisiez pas le qualificatif const, et que de nombreux systèmes le permettraient il). Uniquement si l'exécutable final peut être distingué comme quelque chose de non sécurisé - un exécutable binaire est un exécutable binaire, peu importe comment il y est arrivé. Et constpeu importe un peu - le nom du symbole dans ce fichier exécutable binaire est main. Ni plus ni moins. constest une construction C qui ne veut rien dire au moment de l'exécution.
Andrew Henle

1
@Stewart: il échoue certainement sur ARMv6l (faute de segmentation). Mais cela devrait fonctionner sur n'importe quelle architecture x86-64.
gauche autour du

@AndrewHenle un exécutable binaire est un exécutable binaire, peu importe comment il y est arrivé - pas tout à fait vrai. Un exécutable binaire n'est pas un seul blob d'instructions exécutables, c'est un blob soigneusement mappé de partitions, dont certaines sont des instructions, dont certaines sont des données en lecture seule, et dont certaines sont des données à initialiser en données en lecture-écriture. (Certaines) MMU matérielles de sécurité peuvent empêcher l'exécution à partir de pages non marquées comme telles, et c'est une bonne fonctionnalité pour éviter, par exemple, les débordements de pile conduisant à l'exécution de code sur la pile, mais malheureusement, c'est parfois légitime ou souvent non activé.
mah

30

Quelqu'un essaie d'agir comme un magicien. Il pense qu'il peut nous tromper. Mais nous le savons tous, l'exécution du programme c commence par main().

Le int begin()sera remplacé decode(a,n,i,m,a,t,e)par un passage d'étape de préprocesseur. Là encore, decode(a,n,i,m,a,t,e)sera remplacé par m ## a ## i ## n. Comme par association positionnelle d'appel de macro, la svolonté a une valeur de caractère a. De même, usera remplacé par «i» et tsera remplacé par «n». Et c'est ainsi m##s##u##tque deviendramain

En ce qui concerne le ##symbole dans l'expansion de macro, c'est l'opérateur de prétraitement et il effectue le collage de jetons. Lorsqu'une macro est développée, les deux jetons de chaque côté de chaque opérateur '##' sont combinés en un seul jeton, qui remplace alors '##' et les deux jetons d'origine dans l'expansion de la macro.

Si vous ne me croyez pas, vous pouvez compiler votre code avec -Eflag. Il arrêtera le processus de compilation après le prétraitement et vous pourrez voir le résultat du collage de jetons.

gcc -E FILENAME.c

11

decode(a,b,c,d,[...])mélange les quatre premiers arguments et les joint pour obtenir un nouvel identifiant, dans l'ordre dacb. (Les trois arguments restants sont ignorés.) Par exemple, decode(a,n,i,m,[...])donne l'identifiant main. Notez que c'est ce que lebegin macro est définie.

Par conséquent, la beginmacro est simplement définie comme main.


2

Dans votre exemple, la main()fonction est réellement présente, car il begins'agit d'une macro que le compilateur remplace par une decodemacro qui à son tour est remplacée par l'expression m ## s ## u ## t. En utilisant l'expansion macro ##, vous atteindrez le mot à mainpartir de decode. Ceci est une trace:

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

C'est juste une astuce à avoir main(), mais l'utilisation du nom main()de la fonction d'entrée du programme n'est pas nécessaire en langage de programmation C. Cela dépend de vos systèmes d'exploitation et de l'éditeur de liens comme l'un de ses outils.

Sous Windows, vous n'utilisez pas toujours main(), mais plutôt WinMainouwWinMain , bien que vous puissiez utiliser main(), même avec la chaîne d'outils de Microsoft . Sous Linux, on peut utiliser _start.

C'est à l'éditeur de liens en tant qu'outil du système d'exploitation de définir le point d'entrée, et non la langue elle-même. Vous pouvez même définir notre propre point d'entrée, et vous pouvez créer une bibliothèque qui est également exécutable !


@vaxquis Vous avez raison, mais c'est une réponse partielle que j'ai écrite pour compléter / corriger la première réponse qui lie la main()fonction au langage de programmation C, ce qui n'est pas correct.
Ho1

@vaxquis J'ai supposé qu'expliquer "la fonction main () n'est pas essentielle dans les programmes C" serait une réponse partielle. J'ai ajouté un paragraphe pour compléter la réponse. - Ho1 il y a 16 minutes
Ho1
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.