¿Por qué declarar main como una compilación de matriz?

9 minutos de lectura

avatar de usuario
Theodoros Chatzigiankis

Yo vi un fragmento de código en CodeGolf que pretende ser una bomba de compilación, donde main se declara como una gran matriz. Probé la siguiente versión (sin bomba):

int main[1] = { 0 };

Parece compilar bien bajo Clang y con solo una advertencia bajo GCC:

advertencia: ‘principal’ suele ser una función [-Wmain]

El binario resultante es, por supuesto, basura.

Pero, ¿por qué se compila en absoluto? ¿Está incluso permitido por la especificación C? La sección que creo que es relevante dice:

5.1.2.2.1 Inicio del programa

La función llamada al inicio del programa se llama main. La implementación no declara ningún prototipo para esta función. Se definirá con un tipo de retorno de int y sin parámetros. […] o con dos parámetros […] o de alguna otra manera definida por la implementación.

¿”Alguna otra manera definida por la implementación” incluye una matriz global? (Me parece que la especificación todavía se refiere a un función.)

Si no, ¿es una extensión del compilador? ¿O una característica de las cadenas de herramientas, que sirve para algún otro propósito y decidieron ponerla a disposición a través de la interfaz?

  • Eso no compilar. ISO C prohíbe matrices de tamaño cero.

    – Jens

    13/01/2016 a las 11:00

  • No está permitido por la especificación C. Los compiladores a menudo implementan cosas que no están cubiertas por la especificación.

    –MM

    13 de enero de 2016 a las 11:06

  • Pregunta relacionada: ¿Cómo puede funcionar un programa con una variable global llamada main en lugar de una función main?. Creo que también inspirado en una pregunta de codegolf.

    – Shafik Yaghmour

    06/10/2016 a las 20:21

  • @MM Especialmente en el caso de Malbolge

    – Vía Láctea90

    21 de junio de 2019 a las 21:56

avatar de usuario
Rey del cielo

Es porque C permite un entorno “no alojado” o independiente que no requiere la main función. Esto significa que el nombre main queda libre para otros usos. Es por esto que el lenguaje como tal permite tales declaraciones. La mayoría de los compiladores están diseñados para admitir ambos (la diferencia es principalmente cómo se realiza la vinculación) y, por lo tanto, no rechazan construcciones que serían ilegales en un entorno alojado.

La sección a la que hace referencia en el estándar se refiere al entorno alojado, la correspondiente para independiente es:

en un entorno independiente (en el que la ejecución del programa C puede tener lugar sin ningún beneficio de un sistema operativo), el nombre y el tipo de la función llamada al inicio del programa están definidos por la implementación. Todas las instalaciones de biblioteca disponibles para un programa independiente, que no sean el conjunto mínimo requerido por la cláusula 4, están definidas por la implementación.

Si luego lo vincula como de costumbre, saldrá mal ya que el vinculador normalmente tiene poco conocimiento sobre la naturaleza de los símbolos (qué tipo tiene o incluso si es una función o variable). En este caso, el enlazador resolverá gustosamente las llamadas a main a la variable nombrada main. Si no se encuentra el símbolo, se producirá un error de enlace.

Si lo está vinculando como de costumbre, básicamente está tratando de usar el compilador en una operación alojada y luego no define main como se supone que significa un comportamiento indefinido según el apéndice J.2:

el comportamiento es indefinido en las siguientes circunstancias:

  • El programa en un entorno alojado no define una función llamada principal usando una de las formas especificadas (5.1.2.2.1)

El propósito de la posibilidad independiente es poder usar C en entornos donde (por ejemplo) no se proporcionan bibliotecas estándar o inicialización CRT. Esto significa que el código que se ejecuta antes main se llama (esa es la inicialización de CRT que inicializa el tiempo de ejecución de C) podría no proporcionarse y se esperaría que lo proporcione usted mismo (y puede decidir tener un main o puede decidir no hacerlo).

  • Esto compila y vincula bien (bueno, con una advertencia) con gcc 4.9.3 en cygwin: int f(int argc,char **argv) { return 0; } char *main = (char *)f;

    – Peter – Reincorporar a Mónica

    13 de enero de 2016 a las 11:05


  • @PeterA.Schneider Pero si funciona bien, es pura suerte. El CRT-init intentará llamar main que es donde se almacena el puntero y no a lo que apunta.

    – Rey del cielo

    13 de enero de 2016 a las 11:11

  • Se enlaza pero segfaults. Por cierto, no creo que la pregunta tenga mucho que ver con “independiente”. Por ejemplo, las siguientes compilaciones y enlaces (a un dll) en VS13: namespace Main_abused { class Program { int Main = 0; } } . Es más bien que main (y Main en C#) no son palabras clave, y los enlazadores de C son tontos, err, simples.

    – Peter – Reincorporar a Mónica

    13 de enero de 2016 a las 11:14


  • @PeterA.Schneider No estoy de acuerdo, un programa con main no estar definido de una manera diferente a lo que requiere el estándar (o la implementación especificada) está mal formado.

    – Rey del cielo

    13 de enero de 2016 a las 11:25

  • Esto no es realmente exacto. La sección alojada de C99/C11 tiene una oración retrasada “o de alguna otra manera definida por la implementación”, que no está del todo clara. Así que nadie sabe realmente qué formas de main están permitidas… Discutido en detalle aquí.

    – Lundin

    13 de mayo de 2016 a las 9:26

avatar de usuario
tymmej

Si está interesado en cómo crear un programa en la matriz principal: https://jroweboy.github.io/c/asm/2015/01/26/when-is-main-not-a-function.html. La fuente de ejemplo allí solo contiene una matriz char (y luego int) llamada main que está lleno de instrucciones de máquina.

Los principales pasos y problemas fueron:

  • Obtenga las instrucciones de la máquina de una función principal de un volcado de memoria gdb y cópielo en la matriz
  • Etiquetar los datos en main[] ejecutable declarándolo const (aparentemente, los datos son escribibles o ejecutables)
  • Último detalle: cambiar una dirección para datos de cadena reales.

El código C resultante es simplemente

const int main[] = {
    -443987883, 440, 113408, -1922629632,
    4149, 899584, 84869120, 15544,
    266023168, 1818576901, 1461743468, 1684828783,
    -1017312735
};

pero da como resultado un programa ejecutable en una PC de 64 bits:

$ gcc -Wall final_array.c -o sixth
final_array.c:1:11: warning: ‘main’ is usually a function [-Wmain]
 const int main[] = {
           ^
$ ./sixth 
Hello World!

avatar de usuario
Lundin

El problema es ese main no es un identificador reservado. El estándar C solo dice que en los sistemas alojados suele haber una función llamada main. Pero nada en el estándar le impide abusar del mismo identificador para otros fines siniestros.

GCC le da una advertencia engreída “principal suele ser una función”, insinuando que el uso del identificador main para otros fines no relacionados no es una idea brillante.


Ejemplo tonto:

#include <stdio.h>

int main (void)
{
  int main = 5;
  main:

  printf("%d\n", main);
  main--;

  if(main)
  {
    goto main;
  }
  else
  {
    int main (void);
    main();
  }
}

Este programa imprimirá repetidamente los números 5,4,3,2,1 hasta que se desborde la pila y se cuelgue (no intente esto en casa). Desafortunadamente, el programa anterior es un programa C estrictamente conforme y el compilador no puede evitar que lo escribas.

main es, después de compilar, solo otro símbolo en un archivo de objeto como muchos otros (funciones globales, variables globales, etc.).

El enlazador vinculará el símbolo. main independientemente de su tipo. De hecho, el enlazador no puede ver el tipo del símbolo en absoluto (él pueden mira, que no está en el .text-sección sin embargo, pero a él no le importa;))

Usando gcc, el punto de entrada estándar es _start, que a su vez llama a main() después de preparar el entorno de tiempo de ejecución. Por lo tanto, saltará a la dirección de la matriz de enteros, lo que generalmente dará como resultado una instrucción incorrecta, una falla de segmento o algún otro comportamiento incorrecto.

Todo esto, por supuesto, no tiene nada que ver con el estándar C.

Solo compila porque no usa las opciones adecuadas (y funciona porque los enlazadores a veces solo se preocupan por el nombres de símbolos, no sus escribe).

$ gcc -std=c89 -pedantic -Wall x.c
x.c:1:5: warning: ISO C forbids zero-size array ‘main’ [-Wpedantic]
 int main[0];
     ^
x.c:1:5: warning: ‘main’ is usually a function [-Wmain]

  • Todavía compila y enlaza. La única diferencia es que te avisa de eso. main suele ser una función (luego continúa y enlaza de todos modos).

    – Rey del cielo

    13 de enero de 2016 a las 11:14

  • @skyking ¿Quieres que la compilación/enlace falle? Agregar -Werror después.

    – Jens

    13 de enero de 2016 a las 12:22

  • Pero entonces (otros) programas C válidos tampoco se compilarían.

    – Rey del cielo

    13 de enero de 2016 a las 12:33

  • @skyking Luego agrega -Wno-* por las advertencias que eligió aceptar. La mayoría de las veces, las advertencias son fáciles de corregir y, si no lo son, algo está mal con el código, IMNSHO. yo suelo -Werror desde hace años y se ha demostrado su valor. Las nuevas advertencias son imposibles de perder y deben corregirse para continuar.

    – Jens

    13 de enero de 2016 a las 12:45

  • Estoy de acuerdo que -Werror y habilitar las advertencias es una buena idea, pero eso no contradice el hecho de que hacerlo hará que el compilador no pueda compilar programas C válidos.

    – Rey del cielo

    13 de enero de 2016 a las 12:46

avatar de usuario
zibrí

const int main[1] = { 0xc3c3c3c3 };

Esto compila y se ejecuta en x86_64… no hace nada, solo regresa: D

  • Todavía compila y enlaza. La única diferencia es que te avisa de eso. main suele ser una función (luego continúa y enlaza de todos modos).

    – Rey del cielo

    13 de enero de 2016 a las 11:14

  • @skyking ¿Quieres que la compilación/enlace falle? Agregar -Werror después.

    – Jens

    13 de enero de 2016 a las 12:22

  • Pero entonces (otros) programas C válidos tampoco se compilarían.

    – Rey del cielo

    13 de enero de 2016 a las 12:33

  • @skyking Luego agrega -Wno-* por las advertencias que eligió aceptar. La mayoría de las veces, las advertencias son fáciles de corregir y, si no lo son, algo está mal con el código, IMNSHO. yo suelo -Werror desde hace años y se ha demostrado su valor. Las nuevas advertencias son imposibles de perder y deben corregirse para continuar.

    – Jens

    13 de enero de 2016 a las 12:45

  • Estoy de acuerdo que -Werror y habilitar las advertencias es una buena idea, pero eso no contradice el hecho de que hacerlo hará que el compilador no pueda compilar programas C válidos.

    – Rey del cielo

    13 de enero de 2016 a las 12:46

¿Ha sido útil esta solución?