为什么声明main作为数组编译?

时间:2016-01-13 10:55:05

标签: c gcc clang main

我看到a snippet of code on CodeGolf用作编译器炸弹,其中main被声明为一个巨大的数组。我试过以下(非炸弹)版本:

int main[1] = { 0 };

似乎在Clang下编译正常,只有GCC下的警告:

  

警告:'main'通常是函数[-Wmain]

结果二进制文件当然是垃圾。

但为什么要编译呢?是否允许C规范?我认为相关的部分说:

  

5.1.2.2.1程序启动

     

程序启动时调用的函数名为main。该实现声明此函数没有原型。它应定义为返回类型为int且没有参数[...]或具有两个参数[...]或以其他一些实现定义的方式。

“其他一些实现定义的方式”是否包含全局数组? (在我看来,规范仍然引用了函数。)

如果没有,它是编译器扩展吗?或者是工具链的一个功能,它可以用于其他目的,他们决定通过前端提供它?

6 个答案:

答案 0 :(得分:38)

这是因为C允许“非托管”或独立环境,不需要main功能。这意味着名称main被释放用于其他用途。这就是为什么这样的语言允许这样的声明。大多数编译器都是为了支持两者而设计的(差异主要在于如何完成链接),因此它们不会禁止在托管环境中非法的构造。

您在标准中引用的部分是指托管环境,对应的独立部分是:

  

在独立环境中(C程序执行可能在没有任何情况下执行)   操作系统的好处),程序调用的函数的名称和类型   startup是实现定义的。任何独立的图书馆设施   程序,除了第4条要求的最小集合外,都是实现定义的。

如果你像往常一样链接它会变坏,因为链接器通常对符号的性质(它有什么类型,甚至它是函数或变量)知之甚少。在这种情况下,链接器将很乐意解析对main到名为main的变量的调用。如果未找到符号,则会导致链接错误。

如果你像往常一样链接它,你基本上是在托管操作中使用编译器,然后不定义main,因为你应该意味着未定义的行为,如附录J.2:

  

在以下情况下,行为未定义:

     
      
  • ...
  •   
  • 托管环境中的程序未定义名为的函数   主要   用一个   指明表格(5.1.2.2.1)
  •   

独立可能性的目的是能够在没有给出(例如)标准库或CRT初始化的环境中使用C.这意味着可能没有提供在main之前运行的代码(即初始化C运行时的CRT初始化),并且您可能希望自己提供(并且您可能决定拥有{{1}或者可能决定不这样做。

答案 1 :(得分:24)

如果您对如何在主阵列中创建程序感兴趣:https://jroweboy.github.io/c/asm/2015/01/26/when-is-main-not-a-function.html。那里的示例源只包含一个名为main的char(以及后来的int)数组,其中包含机器指令。

主要步骤和问题是:

  • 从gdb内存转储中获取主函数的机器指令并将其复制到数组
  • 通过声明const(数据显然是可写的或可执行的)来标记main[]可执行文件中的数据
  • 最后详细信息:更改实际字符串数据的地址。

生成的C代码只是

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

但在64位PC上产生可执行程序:

$ 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!

答案 2 :(得分:9)

问题是main不是保留标识符。 C标准只说在托管系统中通常有一个名为main的函数。但是标准中的任何内容都不会阻止您滥用相同的标识符以用于其他险恶目的。

GCC给你一个自鸣得意的警告“主要通常是一个功能”,暗示使用标识符main用于其他不相关的目的并不是一个好主意。

愚蠢的例子:

#include <stdio.h>

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

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

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

该程序将重复打印数字5,4,3,2,1,直到它出现堆栈溢出并崩溃(不要在家中尝试)。不幸的是,上面的程序是严格符合C程序的,编译器不能阻止你编写它。

答案 3 :(得分:8)

main - 在编译之后 - 在对象文件中只是另一个符号,就像许多其他符号一样(全局函数,全局变量等)。

链接器将链接符号main,无论其类型如何。实际上,链接器根本看不到符号的类型(他可以看,它不在.text - 部分,但他没有护理;))

使用gcc,标准入口点是_start,它在准备运行时环境后又调用main()。所以它会跳转到整数数组的地址,这通常会导致错误的指令,段错误或其他一些不良行为。

这一切当然与C标准无关。

答案 4 :(得分:3)

它只是编译,因为你没有使用正确的选项(并且因为链接器有时只关心符号的名称,而不是类型)。

$ 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]

答案 5 :(得分:1)

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

这在x86_64上编译并执行...什么都不做就返回:D