为啥将 main 声明为数组编译?

Posted

技术标签:

【中文标题】为啥将 main 声明为数组编译?【英文标题】:Why does declaring main as an array compile?为什么将 main 声明为数组编译? 【发布时间】:2016-04-18 07:34:27 【问题描述】:

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

int main[1] =  0 ;

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

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

生成的二进制文件当然是垃圾。

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

5.1.2.2.1 程序启动

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

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

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

【问题讨论】:

编译。 ISO C 禁止零大小的数组。 C 规范不允许。编译器通常会实现规范未涵盖的内容。 相关问题:How can a program with a global variable called main instead of a main function work?。我认为也受到了 codegolf 问题的启发。 @M.M 特别是Malbolge的情况 【参考方案1】:

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

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

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

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

如果您像往常一样链接它,您基本上是在尝试在托管操作中使用编译器,然后没有定义 main,因为您应该按照附录 J.2 表示未定义的行为:

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

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

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

【讨论】:

这可以在 cygwin 上使用 gcc 4.9.3 编译和链接(好吧,带有警告):int f(int argc,char **argv) return 0; char *main = (char *)f; @PeterA.Schneider 但如果运行正常,那只是运气。 CRT-init 将尝试调用 main,这是存储指针的位置,而不是它指向的位置。 它链接但有段错误。顺便说一句,我认为这个问题与“独立”没有太大关系。例如,以下在 VS13 中编译和链接(到 dll):namespace Main_abused class Program int Main = 0; 。而是 main(和 C# 中的 Main)不是关键字,而 C 链接器是愚蠢的、错误的、简单的。 @PeterA.Schneider 我不同意,main 的程序没有以不同于标准(或指定的实现)要求的方式定义。 这并不准确。 C99/C11 的托管部分有一个迟钝的句子“或以某种其他实现定义的方式”,这完全不清楚。所以没有人真正知道允许哪些形式的 main...Discussed in detail here.【参考方案2】:

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

主要步骤和问题是:

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

生成的 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!

【讨论】:

【参考方案3】:

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

GCC 给你一个自鸣得意的警告“main 通常是一个函数”,暗示将标识符 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 程序,编译器无法阻止你编写它。

【讨论】:

【参考方案4】:

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

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

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

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

【讨论】:

我作为对 skyking 的答案链接的评论发布的最小示例,但有段错误。使用内联汇编器等进行任何调整吗? @PeterA.Schneider 它会出现段错误,因为它会跳转到指针的 地址 而不是它的内容。 谢谢!我想我仍然希望这些工具链的 C 前端会抛出,即使链接器在看到目标文件时并不关心。【参考方案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]

【讨论】:

它仍然可以编译和链接。唯一的区别是它会警告您 main 通常是一个函数(然后它会继续并链接)。 @skyking 你想让编译/​​链接失败?然后添加-Werror 但是(其他)有效的 C 程序也将无法编译。 @skyking 然后为您选择接受的警告添加-Wno-*。通常,警告很容易修复,如果不是,则代码 IMNSHO 有问题。我多年来一直使用-Werror,它已被证明很有价值。新警告不容错过,必须修复才能继续。 我同意-Werror 并启用警告是个好主意,但这并不矛盾这样做会导致编译器无法编译有效的 C 程序。【参考方案6】:
const int main[1] =  0xc3c3c3c3 ;

这在 x86_64 上编译和执行...什么都不做,只是返回 :D

【讨论】:

有趣,它是如何工作的?它仍然适用于 ASLR 吗? C3 只是一个回报。所以它执行并返回。

以上是关于为啥将 main 声明为数组编译?的主要内容,如果未能解决你的问题,请参考以下文章

在我将编译器设置设置为支持 c++11 后,我仍然收到错误“stoi”未声明。为啥? [复制]

为啥以下代码编译失败(C++ lambda问题)

为啥 int main() 编译?

服务器main函数里消息主循环函数为啥不会卡死

为数组分配空间

为啥手机编译器class后面默认是Main呢