Comment un programme avec une variable globale appelée main au lieu d'une fonction principale peut-il fonctionner?

97

Considérez le programme suivant:

#include <iostream>
int main = ( std::cout << "C++ is excellent!\n", 195 ); 

En utilisant g ++ 4.8.1 (mingw64) sur le système d'exploitation Windows 7, le programme se compile et fonctionne correctement, imprimant:

Le C ++ est excellent!

à la console. mainsemble être une variable globale plutôt qu'une fonction; comment ce programme peut-il s'exécuter sans la fonction main()? Ce code est-il conforme à la norme C ++? Le comportement du programme est-il bien défini? J'ai également utilisé l' -pedantic-errorsoption mais le programme se compile et s'exécute toujours.

Destructeur
la source
11
@ πάνταῥεῖ: pourquoi la balise d'avocat de langue est-elle nécessaire?
Destructor
14
Notez qu'il 195s'agit de l'opcode pour l' RETinstruction et que dans la convention d'appel C, l'appelant efface la pile.
Brian le
2
@PravasiMeet "alors comment ce programme s'exécute" - ne pensez-vous pas que le code d'initialisation d'une variable devrait être exécuté (même sans la main()fonction? En fait, ils ne sont absolument pas liés.)
Le Croissant Paramagnétique
4
Je fais partie de ceux qui ont trouvé que le programme se segmentait tel quel (Linux 64 bits, g ++ 5.1 / clang 3.6). Je peux cependant rectifier cela en le modifiant int main = ( std::cout << "C++ is excellent!\n", exit(0),1 );(et en l'incluant <cstdlib>), même si le programme reste juridiquement mal formé.
Mike Kinghan le
11
@Brian Vous devriez mentionner l'architecture lorsque vous faites de telles déclarations. Tout le monde n'est pas un VAX. Ou x86. Ou peu importe.
dmckee --- ex-moderator chaton

Réponses:

85

Avant d'entrer dans le vif du sujet de la question de ce qui se passe, il est important de souligner que le programme est mal formé selon le rapport de défaut 1886: Lien de langue pour main () :

[...] Un programme qui déclare une variable main à portée globale ou qui déclare le nom main avec une liaison en langage C (dans n'importe quel espace de noms) est mal formé. [...]

Les versions les plus récentes de clang et gcc en font une erreur et le programme ne se compilera pas ( voir l'exemple gcc live ):

error: cannot declare '::main' to be a global variable
int main = ( std::cout << "C++ is excellent!\n", 195 ); 
    ^

Alors pourquoi n'y avait-il pas de diagnostic dans les anciennes versions de gcc et clang? Ce rapport de défaut n'avait même pas de résolution proposée jusqu'à la fin de 2014 et ce cas n'était donc que très récemment explicitement mal formé, ce qui nécessite un diagnostic.

Avant cela, il semble que ce soit un comportement indéfini puisque nous violons une exigence de shall du projet de norme C ++ de la section 3.6.1 [basic.start.main] :

Un programme doit contenir une fonction globale appelée main, qui est le début désigné du programme. [...]

Un comportement non défini est imprévisible et ne nécessite pas de diagnostic. L'incohérence que nous constatons avec la reproduction du comportement est un comportement non défini typique.

Alors, que fait réellement le code et pourquoi dans certains cas produit-il des résultats? Voyons ce que nous avons:

declarator  
|        initializer----------------------------------
|        |                                           |
v        v                                           v
int main = ( std::cout << "C++ is excellent!\n", 195 ); 
    ^      ^                                   ^
    |      |                                   |
    |      |                                   comma operator
    |      primary expression
global variable of type int

Nous avons mainqui est un int déclaré dans l'espace de noms global et qui est en cours d'initialisation, la variable a une durée de stockage statique. C'est l'implémentation qui définit si l'initialisation aura lieu avant qu'une tentative d'appel ne mainsoit faite, mais il semble que gcc le fasse avant d'appeler main.

Le code utilise l' opérateur virgule , l'opérande gauche est une expression de valeur ignorée et est utilisé ici uniquement pour l'effet secondaire de l'appel std::cout. Le résultat de l'opérateur virgule est l'opérande de droite qui, dans ce cas, est la prvalue 195qui est affectée à la variable main.

Nous pouvons voir que sergej indique que l'assembly généré montre qu'il coutest appelé lors de l'initialisation statique. Bien que le point le plus intéressant pour la discussion voir la session de godbolt en direct serait le suivant:

main:
.zero   4

et le suivant:

movl    $195, main(%rip)

Le scénario probable est que le programme saute au symbole mains'attendant à ce qu'un code valide soit présent et, dans certains cas, seg-fault . Donc, si tel est le cas, nous nous attendons à ce que le stockage de code machine valide dans la variable mainpuisse conduire à un programme exploitable , en supposant que nous soyons situés dans un segment qui permet l'exécution de code. Nous pouvons voir que cette entrée du IOCCC de 1984 fait exactement cela .

Il semble que nous pouvons demander à gcc de le faire en C en utilisant ( voir en direct ):

const int main = 195 ;

Il seg-faille si la variable mainn'est pas const vraisemblablement car elle n'est pas située dans un emplacement exécutable, Hat Tip à ce commentaire ici qui m'a donné cette idée.

Voir également la réponse FUZxxl ici à une version spécifique C de cette question.

Shafik Yaghmour
la source
Pourquoi la mise en œuvre ne donne aucun avertissement également. (Quand j'utilise -Wall & -Wextra, cela ne donne toujours pas un seul avertissement). Pourquoi? Que pensez-vous de la réponse de @Mark B à cette question?
Destructor
À mon humble avis, le compilateur ne devrait pas donner d'avertissement car ce mainn'est pas un identifiant réservé (3.6.1 / 3). Dans ce cas, je pense que la gestion de ce cas par VS2013 (voir la réponse de Francis Cugler) est plus correcte dans sa gestion que gcc & clang.
cdmh
@PravasiMeet J'ai mis à jour ma réponse pour expliquer pourquoi les versions antérieures de gcc ne donnaient pas de diagnostic.
Shafik Yaghmour
2
... et en effet, quand je teste le programme de l'OP sur Linux / x86-64, avec g ++ 5.2 (qui accepte le programme - je suppose que vous ne plaisantiez pas sur la "version la plus récente"), il plante exactement là où je l'attendais aurait.
zwol
1
@Walter Je ne pense pas que ce soient des doublons, le premier pose une question beaucoup plus étroite. Il y a clairement un groupe d'utilisateurs de SO qui ont une vision plus réductrice des doublons, ce que je n'ai pas beaucoup de sens pour moi puisque nous pourrions résumer la plupart des questions de SO à une version de questions plus anciennes, mais alors SO ne serait pas très utile.
Shafik Yaghmour
20

À partir du 3.6.1 / 1:

Un programme doit contenir une fonction globale appelée main, qui est le début désigné du programme. C'est la mise en œuvre définie si un programme dans un environnement autonome est nécessaire pour définir une fonction principale.

A partir de là, il semble que g ++ autorise un programme (vraisemblablement comme la clause "autonome") sans fonction principale.

Puis à partir du 3.6.1 / 3:

La fonction main ne doit pas être utilisée (3.2) dans un programme. Le lien (3.5) de main est la mise en œuvre définie. Un programme qui déclare que main est en ligne ou statique est mal formé. Le nom main n'est pas réservé autrement.

Donc, ici, nous apprenons qu'il est parfaitement bien d'avoir une variable entière nommée main.

Enfin, si vous vous demandez pourquoi la sortie est imprimée, l'initialisation de l ' int mainutilise l'opérateur virgule pour s'exécuter coutà l'initialisation statique, puis fournit une valeur intégrale réelle pour effectuer l'initialisation.

Marque B
la source
7
Il est intéressant de noter que la liaison échoue si vous renommez mainavec quelque chose d'autre: (.text+0x20): undefined reference to main ''
Fred Larson
1
N'avez-vous pas à spécifier à gcc que votre programme est autonome?
Shafik Yaghmour
9

gcc 4.8.1 génère l'assembly x86 suivant:

.LC0:
    .string "C++ is excellent!\n"
    subq    $8, %rsp    #,
    movl    std::__ioinit, %edi #,
    call    std::ios_base::Init::Init() #
    movl    $__dso_handle, %edx #,
    movl    std::__ioinit, %esi #,
    movl    std::ios_base::Init::~Init(), %edi  #,
    call    __cxa_atexit    #
    movl    $.LC0, %esi #,
    movl    std::cout, %edi #,
    call    std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*)   #
    movl    $195, main(%rip)    #, main
    addq    $8, %rsp    #,
    ret
main:
    .zero   4

Notez qu'il coutest appelé lors de l'initialisation, pas dans la mainfonction!

.zero 4déclare 4 octets (initialisés à 0) à partir de l'emplacement main, où mainest le nom de la variable [!] .

Le mainsymbole est interprété comme le début du programme. Le comportement dépend de la plate-forme.

Sergej
la source
1
Notez, comme le souligne Brian, 195 l'opcode pour retcertaines architectures. Donc, dire zéro instruction peut ne pas être exact.
Shafik Yaghmour
@ShafikYaghmour Merci pour votre commentaire, vous avez raison. Je me suis trompé avec les directives assembleur.
sergej
8

C'est un programme mal formé. Il plante sur mon environnement de test, cygwin64 / g ++ 4.9.3.

De la norme:

3.6.1 Fonction principale [basic.start.main]

1 Un programme doit contenir une fonction globale appelée main, qui est le début désigné du programme.

R Sahu
la source
Je pense qu'avant le rapport de défaut que j'ai cité, il s'agissait simplement d'un comportement indéfini.
Shafik Yaghmour
@ShafikYaghmour, est - ce que le principe général à appliquer à tous les endroits où les utilisations standard doit ?
R Sahu
Je veux dire oui mais je n'ai pas vu une bonne description de la différence. D'après ce que je peux dire de cette discussion , un NDR mal formé et un comportement indéfini sont probablement synonymes puisque ni l'un ni l'autre ne nécessitent un diagnostic. Cela semblerait impliquer mal formé et UB sont distincts mais pas sûrs.
Shafik Yaghmour
3
La section 4 de C99 («Conformité») rend cela sans ambiguïté: «Si une exigence« doit »ou« ne doit pas »qui apparaît en dehors d'une contrainte est violée, le comportement n'est pas défini.» Je ne trouve pas de libellé équivalent en C ++ 98 ou C ++ 11, mais je soupçonne fortement que le comité a voulu qu'il soit là. (Les comités C et C ++ ont vraiment besoin de s'asseoir et d'aplanir toutes les différences terminologiques entre les deux normes.)
zwol
7

La raison pour laquelle je pense que cela fonctionne est que le compilateur ne sait pas qu'il compile la main()fonction afin qu'il compile un entier global avec des effets secondaires d'affectation.

Le format d'objet dans lequel cette unité de traduction est compilée n'est pas capable de faire la différence entre un symbole de fonction et un symbole variable .

Ainsi, l' éditeur de liens se connecte volontiers au symbole principal (variable) et le traite comme un appel de fonction. Mais pas tant que le système d'exécution n'a pas exécuté le code d'initialisation de la variable globale.

Lorsque j'ai analysé l'échantillon, il s'est imprimé, mais cela a provoqué un défaut de segmentation . Je suppose que c'est à ce moment que le système d'exécution a essayé d'exécuter une variable int comme s'il s'agissait d'une fonction .

Galik
la source
4

J'ai essayé cela sur un système d'exploitation Win7 64 bits utilisant VS2013 et il se compile correctement, mais lorsque j'essaye de créer l'application, je reçois ce message de la fenêtre de sortie.

1>------ Build started: Project: tempTest, Configuration: Debug Win32 ------
1>LINK : fatal error LNK1561: entry point must be defined
========== Build: 0 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========
Francis Cugler
la source
2
FWIW, c'est une erreur de l'éditeur de liens, pas un message du débogueur. La compilation a réussi, mais l'éditeur de liens n'a pas pu trouver de fonction main()car il s'agit d'une variable de typeint
cdmh
Merci pour la réponse. Je reformulerai ma réponse initiale pour refléter cela.
Francis Cugler
-1

Vous faites un travail délicat ici. Comme main (en quelque sorte) pourrait être déclaré entier. Vous avez utilisé l'opérateur de liste pour imprimer le message, puis lui attribuer 195. Comme l'a dit quelqu'un ci-dessous, le fait que cela ne soit pas à l'aise avec C ++ est vrai. Mais comme le compilateur n'a trouvé aucun nom défini par l'utilisateur, main, il ne s'est pas plaint. Rappelez-vous que main n'est pas une fonction définie par le système, sa fonction définie par l'utilisateur et la chose à partir de laquelle le programme commence à s'exécuter est le module principal, pas main (), en particulier. Encore une fois, main () est appelée par la fonction de démarrage qui est exécutée intentionnellement par le chargeur. Ensuite, toutes vos variables sont initialisées, et lors de l'initialisation, la sortie est comme ça. C'est tout. Le programme sans main () est correct, mais pas standard.

Vikas.Ghode
la source