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

我在CodeGolf上看到了一段代码,它被用作编译器炸弹,其中main被声明为一个巨大的数组。 我尝试了以下(非炸弹)版本:

 int main[1] = { 0 }; 

它似乎在Clang下编译得很好,在GCC下面只有一个警告:

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

由此产生的二进制文件当然是垃圾。

但为什么它编译? C规范甚至允许吗? 我觉得有关的部分是说:

5.1.2.2.1程序启动

程序启动时调用的函数名为main。 这个实现声明了这个函数没有原型。 它应该用一个返回typesint来定义,并且不带任何参数或者带有两个参数,或者以某种其他实现定义的方式来定义。

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

如果不是,它是一个编译器扩展吗? 还是工具链的一个function,这是为其他目的服务的,他们决定通过前端提供它?

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

您在标准中提到的部分是指托pipe环境,相应的独立的是:

在一个独立的环境中(C程序的执行可能没有任何操作系统的好处),程序启动时调用的函数的名称和types是实现定义的。 任何独立程序提供的图书馆设施,除第4条所要求的最低限度设置外,都是实施定义的。

如果你像往常一样链接它,它会变坏,因为链接器通常对符号的性质(它具有什么types或甚至是函数或variables)知之甚less。 在这种情况下,链接器将愉快地将调用mainparsing为名为main的variables。 如果找不到符号,将导致链接错误。

如果你像往常一样链接,你基本上是在托pipe操作中使用编译器,然后不定义main因为你应该这样认为是不确定的行为,如附录J.2所示:

在以下情况下行为是不确定的:

  • 程序在托pipe环境中没有使用指定的表单之一(5.1.2.2.1)定义名为main的函数,

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

如果你有兴趣如何在主数组中创build程序: https : //jroweboy.github.io/c/asm/2015/01/26/when-is-main-not-a-function.html 。 那里的示例源只包含一个名为main的char(以及后面的int)数组,它充满了机器指令。

主要的步骤和问题是:

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

由此产生的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! 

main是在编译之后,就像其他许多其他的符号(全局函数,全局variables等)一样。

链接器将链接符号main不pipe其types。 事实上,链接器根本看不到符号的types(他可以看到,它不在.text ,但是他不在乎;))

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

这当然与C标准无关。

问题是main不是保留的标识符。 C标准只是说在托pipe系统中通常有一个叫做main的函数。 但标准中没有任何内容可以防止您为了其他恶意目的而滥用相同的标识符。

海湾合作委员会给你一个自鸣得意的警告“主要通常是一个function”,暗示使用标识符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程序,编译器不能阻止你写它。

它只编译,因为你不使用正确的选项(因为连接符有时只关心符号的名称 ,而不是它们的types )。

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