为啥将 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
的调用解析为名为@987654325@ 的变量。如果找不到符号,将导致链接错误。
如果您像往常一样链接它,您基本上是在尝试在托管操作中使用编译器,然后没有定义 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 声明为数组编译?的主要内容,如果未能解决你的问题,请参考以下文章