【问题标题】: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 的返回类型并且没有参数 [...] 或使用两个参数 [...] 或以其他一些实现定义的方式来定义。

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

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

【问题讨论】:

标签: c gcc clang main


【解决方案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 只是一个回报。所以它执行并返回。
      猜你喜欢
      • 2011-02-01
      • 2010-09-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-02-17
      • 2018-03-28
      • 1970-01-01
      • 2015-11-26
      相关资源
      最近更新 更多