我在CodeGolf上看到了一段代码,它被用作编译器炸弹,其中main
被声明为一个巨大的数组。我尝试了以下(非炸弹)版本:
int main[1] = { 0 };
它似乎在Clang下编译得很好,在GCC下只有一个警告:
警告:"main"通常是一个函数[-Wmain]
生成的二进制文件当然是垃圾。
但为什么它要编译呢?C规范允许这样做吗?我认为相关的部分说:
5.1.2.2.1程序启动
程序启动时调用的函数名为main。该实现没有声明此函数的原型。它应使用int的返回类型进行定义,并且没有参数[…]或使用两个参数[……]或以其他实现定义的方式进行定义。
"其他实现定义的方式"是否包括全局数组?(在我看来,规范仍然引用了函数。)
如果不是,它是编译器扩展吗?或者是工具链的一个功能,它有其他用途,他们决定通过前端提供它?
这是因为C允许"非托管"或独立环境,而不需要main
功能。这意味着名称main
被释放用于其他用途。这就是为什么语言本身允许这样的声明。大多数编译器都是为支持这两种功能而设计的(区别主要在于链接的方式),因此它们不会禁止在托管环境中非法的构造。
您在标准中提到的部分是指托管环境,独立环境的对应部分是:
在独立的环境中(其中C程序的执行可以在没有任何操作系统的优点),程序调用的函数的名称和类型启动是实现定义的。任何可供独立式图书馆使用的图书馆设施除了第4条所要求的最小集之外,程序是由实现定义的。
如果你像往常一样链接它,它会出错,因为链接器通常对符号的性质(它有什么类型,甚至是函数或变量)知之甚少。在这种情况下,链接器将愉快地将对main
的调用解析为名为main
的变量。如果找不到符号,将导致链接错误。
如果你像往常一样链接它,你基本上是在尝试在托管操作中使用编译器,然后不定义main
,因为根据附录J.2:,这意味着未定义的行为
在以下情况下行为是未定义的:
- 宿主环境中的程序未定义名为主要的使用一个(5.1.2.2.1)
独立可能性的目的是能够在没有给出标准库或CRT初始化的环境中使用C。这意味着在调用main
之前运行的代码(即初始化C运行时的CRT初始化)可能没有提供,您可能会自己提供(您可能决定使用main
,也可能决定不使用)。
如果您对如何在主数组中创建程序感兴趣: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!
问题是main
不是保留标识符。C标准只是说在托管系统中通常有一个叫做main的函数。但该标准中没有任何内容可以防止您出于其他险恶目的滥用同一标识符。
GCC给您一个沾沾自喜的警告"main通常是一个函数",暗示将标识符main
用于其他不相关的目的不是一个好主意。
愚蠢的例子:
#include <stdio.h>
int main (void)
{
int main = 5;
main:
printf("%dn", main);
main--;
if(main)
{
goto main;
}
else
{
int main (void);
main();
}
}
该程序将重复打印数字5,4,3,2,1,直到堆栈溢出并崩溃(不要在家里尝试)。不幸的是,上面的程序是一个严格一致的C程序,编译器无法阻止您编写它。
main
在编译后,与许多其他符号(全局函数、全局变量等)一样,只是对象文件中的另一个符号。
链接器将链接符号main
,而不管其类型如何。事实上,链接器根本看不到符号的类型(他可以看到,但它不在.text
-部分,但他不在乎;)
使用gcc,标准入口点是_start,它在准备好运行时环境后调用main()。因此,它将跳转到整数数组的地址,这通常会导致错误的指令、segfault或其他一些错误行为。
当然,这一切都与C标准无关。
它之所以编译,是因为你没有使用正确的选项(之所以有效,是因为链接器有时只关心符号的名称,而不关心它们的type)。
$ 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]
const int main[1] = { 0xc3c3c3c3 };
这在x86_64上编译和执行…什么都不做,只返回:D