关于C#:为什么声明main作为数组编译?

Why does declaring main as an array compile?

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

1
int main[1] = { 0 };

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

warning: 'main' is usually a function [-Wmain]

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

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

5.1.2.2.1 Program startup

The function called at program startup is named main. The implementation declares no prototype for this function. It shall be defined with a return type of int and with no parameters [...] or with two parameters [...] or in some other implementation-defined manner.

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

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


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

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

in a freestanding environment (in which C program execution may take place without any
benefit of an operating system), the name and type of the function called at program
startup are implementation-defined. Any library facilities available to a freestanding
program, other than the minimal set required by clause 4, are implementation-defined.

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

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

the behavior is undefined in the following circumstances:

  • ...
  • program in a hosted environment does not define a function named
    main
    using one
    of the specified forms (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代码就是

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

但导致64位PC上的可执行程序:

1
2
3
4
5
6
$ 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用于其他不相关的目的并不是一个好主意。

愚蠢的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>

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

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

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

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


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

链接器将链接符号main,无论其类型如何。实际上,链接器根本看不到符号的类型(他可以看到,它不在.text -section中,但他并不关心;))

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

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


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

1
2
3
4
5
$ 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]


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

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