¿Cómo puede funcionar un programa con una variable global llamada main en lugar de una función principal?

97

Considere el siguiente programa:

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

Usando g ++ 4.8.1 (mingw64) en el sistema operativo Windows 7, el programa se compila y se ejecuta bien, imprimiendo:

¡C ++ es excelente!

a la consola. mainparece ser una variable global más que una función; ¿Cómo se puede ejecutar este programa sin la función main()? ¿Este código se ajusta al estándar C ++? ¿Está bien definido el comportamiento del programa? También utilicé la -pedantic-errorsopción, pero el programa aún se compila y se ejecuta.

Incinerador de basuras
fuente
11
@ πάνταῥεῖ: ¿por qué es necesaria la etiqueta de abogado de idiomas?
Destructor
14
Tenga en cuenta que 195es el código de operación de la RETinstrucción y que en la convención de llamadas de C, el llamador borra la pila.
Brian
2
@PravasiMeet "entonces cómo se ejecuta este programa": ¿no crees que el código de inicialización de una variable debería ejecutarse (incluso sin la main()función? De hecho, no tienen ninguna relación)
El croissant paramagnético
4
Yo soy de los que encontraron que el programa segfaults como está (Linux de 64 bits, g ++ 5.1 / clang 3.6). Sin embargo, puedo rectificar esto modificándolo int main = ( std::cout << "C++ is excellent!\n", exit(0),1 );(e incluido <cstdlib>), aunque el programa sigue estando legalmente mal formado.
Mike Kinghan
11
@Brian Deberías mencionar la arquitectura al hacer declaraciones como esa. Todo el mundo no es un VAX. O x86. O lo que sea.
dmckee --- ex-moderador gatito

Respuestas:

84

Antes de entrar en el meollo de la pregunta sobre lo que está sucediendo, es importante señalar que el programa está mal formado según el informe de defectos 1886: Enlace de idioma para main () :

[...] Un programa que declara una variable main en el ámbito global o que declara el nombre main con enlace en lenguaje C (en cualquier espacio de nombres) está mal formado. [...]

Las versiones más recientes de clang y gcc hacen que esto sea un error y el programa no se compilará ( vea el ejemplo en vivo de gcc ):

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

Entonces, ¿por qué no había ningún diagnóstico en las versiones anteriores de gcc y clang? Este informe de defectos ni siquiera tuvo una propuesta de resolución hasta finales de 2014, por lo que este caso fue muy recientemente explícitamente mal formado, lo que requiere un diagnóstico.

Antes de esto, parece que esto sería un comportamiento indefinido, ya que estamos violando un deberán requisito del proyecto de C ++ estándar de la sección 3.6.1 [basic.start.main] :

Un programa debe contener una función global llamada main, que es el inicio designado del programa. [...]

El comportamiento indefinido es impredecible y no requiere un diagnóstico. La inconsistencia que vemos con la reproducción del comportamiento es un comportamiento indefinido típico.

Entonces, ¿qué está haciendo realmente el código y por qué en algunos casos produce resultados? Veamos lo que tenemos:

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

Tenemos maincuál es un int declarado en el espacio de nombres global y se está inicializando, la variable tiene una duración de almacenamiento estática. La implementación define si la inicialización se llevará a cabo antes de que se realice un intento de llamada, mainpero parece que gcc hace esto antes de llamar.main .

El código usa el operador de coma , el operando de la izquierda es una expresión de valor descartada y se usa aquí únicamente para el efecto secundario de la llamada std::cout. El resultado del operador de coma es el operando derecho, que en este caso es el valor pr 195que se asigna a la variable main.

Podemos ver que Sergej señala el ensamblado generado muestra que coutse llama durante la inicialización estática. Aunque el punto más interesante de discusión para ver la sesión de godbolt en vivo sería este:

main:
.zero   4

y el siguiente:

movl    $195, main(%rip)

El escenario probable es que el programa salte al símbolo mainesperando que haya un código válido allí y, en algunos casos, se producirá un error de segmentación . Entonces, si ese es el caso, esperaríamos que almacenar código de máquina válido en la variable mainpodría conducir a un programa viable , asumiendo que estamos ubicados en un segmento que permite la ejecución de código. Podemos ver que esta entrada del IOCCC de 1984 hace precisamente eso .

Parece que podemos hacer que gcc haga esto en C usando ( verlo en vivo ):

const int main = 195 ;

Se produce un error de segmentación si la variable mainno es constante, presumiblemente porque no está ubicada en una ubicación ejecutable, Hat Tip para este comentario aquí que me dio esta idea.

También vea la respuesta FUZxxl aquí a una versión específica de C de esta pregunta.

Shafik Yaghmour
fuente
Por qué la implementación no da ninguna advertencia también. (Cuando uso -Wall & -Wextra todavía no da una advertencia única). ¿Por qué? ¿Qué opinas de la respuesta de @Mark B a esta pregunta?
Destructor
En mi humilde opinión, el compilador no debería dar una advertencia porque mainno es un identificador reservado (3.6.1 / 3). En este caso, creo que el manejo de VS2013 de este caso (ver la respuesta de Francis Cugler) es más correcto en su manejo que gcc & clang.
cdmh
@PravasiMeet Actualicé mi respuesta sobre por qué las versiones anteriores de gcc no daban un diagnóstico.
Shafik Yaghmour
2
... y de hecho, cuando pruebo el programa del OP en Linux / x86-64, con g ++ 5.2 (que acepta el programa, supongo que no bromeaba sobre la "versión más reciente"), se bloquea exactamente donde lo esperaba haría.
zwol
1
@Walter No creo que estos sean duplicados, el primero está haciendo una pregunta mucho más específica. Claramente, hay un grupo de usuarios de SO que tienen una visión más reduccionista de los duplicados, lo cual no tiene mucho sentido para mí, ya que podríamos reducir la mayoría de las preguntas de SO a alguna versión de preguntas más antiguas, pero entonces SO no sería muy útil.
Shafik Yaghmour
20

Desde 3.6.1 / 1:

Un programa debe contener una función global llamada main, que es el inicio designado del programa. Se define por implementación si se requiere un programa en un entorno autónomo para definir una función principal.

A partir de esto, parece que g ++ permite un programa (presumiblemente como la cláusula "independiente") sin una función principal.

Luego desde 3.6.1 / 3:

La función main no se utilizará (3.2) dentro de un programa. El enlace (3.5) de main está definido por la implementación. Un programa que declara que main está en línea o estático está mal formado. El nombre principal no está reservado de otro modo.

Así que aquí aprendemos que está perfectamente bien tener una variable entera nombrada main.

Finalmente, si se pregunta por qué se imprime la salida, la inicialización del int mainusa el operador de coma para ejecutar couten static init y luego proporcionar un valor integral real para realizar la inicialización.

Marca B
fuente
7
Es interesante notar que la vinculación falla si cambia el nombre maina otra cosa: (.text+0x20): undefined reference to principal '' '
Fred Larson
1
¿No tiene que especificar a gcc que su programa es independiente?
Shafik Yaghmour
9

gcc 4.8.1 genera el siguiente ensamblado x86:

.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

Tenga en cuenta que coutse llama durante la inicialización, no en la mainfunción.

.zero 4declara 4 bytes (inicializados con 0) comenzando en la ubicación main, donde maines el nombre de la variable [!] .

El mainsímbolo se interpreta como el inicio del programa. El comportamiento depende de la plataforma.

sergej
fuente
1
Tenga en cuenta que, como señala Brian, 195 es el código de operación de retalgunas arquitecturas. Por lo tanto, decir cero instrucciones puede no ser exacto.
Shafik Yaghmour
@ShafikYaghmour Gracias por tu comentario, tienes razón. Me equivoqué con las directivas del ensamblador.
sergej
8

Ese es un programa mal formado. Se bloquea en mi entorno de prueba, cygwin64 / g ++ 4.9.3.

Del estándar:

3.6.1 Función principal [basic.start.main]

1 Un programa debe contener una función global denominada main, que es el inicio designado del programa.

R Sahu
fuente
Creo que antes del informe de defectos que cité, esto era simplemente un comportamiento indefinido.
Shafik Yaghmour
@ShafikYaghmour, ¿Es ese el principio general que se aplicará en todos los lugares donde se aplicarán los usos estándar ?
R Sahu
Quiero decir que sí, pero no veo una buena descripción de la diferencia. Por lo que puedo decir de esta discusión , NDR mal formado y comportamiento indefinido probablemente sean sinónimos, ya que ninguno de los dos requiere un diagnóstico. Esto parecería implicar que las UB están mal formadas y son distintas, pero no están seguras.
Shafik Yaghmour
3
La sección 4 de C99 ("Conformidad") hace que esto sea inequívoco: "Si se viola un requisito de 'debe' o 'no debe' que aparece fuera de una restricción, el comportamiento no está definido". No puedo encontrar una redacción equivalente en C ++ 98 o C ++ 11, pero sospecho firmemente que el comité quería que estuviera allí. (Los comités de C y C ++ realmente necesitan sentarse y resolver todas las diferencias terminológicas entre los dos estándares.)
zwol
7

La razón por la que creo que esto funciona es que el compilador no sabe que está compilando la main()función, por lo que compila un entero global con efectos secundarios de asignación.

El formato de objeto en el que se compila esta unidad de traducción no es capaz de diferenciar entre un símbolo de función y un símbolo de variable .

Entonces, el enlazador se vincula felizmente al símbolo principal (variable) y lo trata como una llamada a función. Pero no hasta que el sistema de ejecución haya ejecutado el código de inicialización de la variable global.

Cuando ejecuté la muestra, se imprimió pero luego causó una falla de segmentación . Supongo que fue entonces cuando el sistema de tiempo de ejecución intentó ejecutar una variable int como si fuera una función .

Galik
fuente
4

Probé esto en un sistema operativo Win7 de 64 bits usando VS2013 y se compila correctamente, pero cuando intento compilar la aplicación, aparece este mensaje en la ventana de salida.

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
fuente
2
FWIW, eso es un error del vinculador, no un mensaje del depurador. La compilación se realizó correctamente, pero el vinculador no pudo encontrar una función main()porque es una variable de tipoint
cdmh
Gracias por la respuesta, reformularé mi respuesta inicial para reflejar esto.
Francis Cugler
-1

Estás haciendo un trabajo complicado aquí. Como principal (de alguna manera) podría declararse un número entero. Usó el operador de lista para imprimir el mensaje y luego asignarle 195. Como dijo alguien a continuación, que no se siente cómodo con C ++, es cierto. Pero como el compilador no encontró ningún nombre definido por el usuario, main, no se quejó. Recuerde que main no es una función definida por el sistema, su función definida por el usuario y la cosa desde la cual el programa comienza a ejecutarse es el Módulo Principal, no main (), específicamente. De nuevo, main () es llamado por la función de inicio que es ejecutada por el cargador intencionalmente. Luego, todas sus variables se inicializan y, mientras se inicializan, se generan así. Eso es. El programa sin main () está bien, pero no es estándar.

Vikas.Ghode
fuente