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

Why does declaring main as an array compile?

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

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.

"some other implementation-defined manner"是否包含全局数组? (在我看来,规范仍然指的是 函数 。)

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

这是因为 C 允许 "non-hosted" 或不需要 main 函数的独立环境。这意味着名称 main 被释放用于其他用途。这就是为什么这种语言允许这样的声明。大多数编译器都设计为支持两者(不同之处主要在于 linking 的完成方式),因此它们不会禁止在托管环境中非法的构造。

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

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.

如果你然后 link 它像往常一样会变坏,因为 linker 通常对符号的性质知之甚少(它有什么类型,即使它是函数或多变的)。在这种情况下,linker 会愉快地将对 main 的调用解析为名为 main 的变量。如果找不到该符号,将导致 link 错误。

如果您 link 像往常一样使用它,您基本上是在尝试在托管操作中使用编译器,然后不定义 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 运行time 的 CRT 初始化)可能未提供,您需要自己提供(并且您可以决定要 main 也可以决定不要)。

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

linker 将 link 符号 main 而不管其类型。事实上,linker 根本看不到符号的类型(他 可以 看到,但是它不在 .text 部分,但是他不在乎 ;))

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

这一切当然与C-standard无关。

它之所以能够编译,是因为您没有使用正确的选项(并且之所以起作用,是因为链接器有时只关心符号的名称,而不是它们的类型).

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

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

主要步骤和问题是:

  • 从 gdb 内存转储中获取 main 函数的机器指令并将其复制到数组中
  • 通过将数据声明为 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 is usually a 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 的程序,编译器不能阻止你编写它。

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

这会在 x86_64 上编译和执行...什么都不做只是 return :D