在C语言中编译和运行没有main()函数的程序

80

我试图在C语言中编译和运行以下没有main()函数的程序。我使用以下命令编译了我的程序。

gcc -nostartfiles nomain.c

编译器会发出警告。

/usr/bin/ld: warning: cannot find entry symbol _start; defaulting to 0000000000400340

好的,没问题。我运行了可执行文件(a.out),两个printf语句都成功打印出来,然后得到分段错误

所以我的问题是,为什么在成功执行打印语句后会发生分段错误?

我的代码:

#include <stdio.h>

void nomain()
{
        printf("Hello World...\n");
        printf("Successfully run without main...\n");
}

输出:

Hello World...
Successfully run without main...
Segmentation fault (core dumped)

注意:

这里,-nostartfiles GCC 标志防止编译器在链接时使用标准启动文件。


38
我很惊讶这真的能运作。坦白地说,我认为连接器的处理是错误的(或者至少是不好的):因为没有入口点,所以连接器只是从任何方便的函数中臆想出一个入口点。呃。 - geometrian
4
@imallett,至少链接器友善地用警告引起了注意,并解释了它采取的回退操作!你说得对,这可能更好地作为错误而不仅仅是警告。 - Toby Speight
为什么要使用无主函数? - Pieter B
4
@PieterB - 这对于关于Unix的讨论来说并不是非常相关,但Windows程序的入口点不一定是 main,而是 WinMainwWinMain - StoryTeller - Unslander Monica
@StoryTeller 实际上在 Windows 和 Linux 中,您都可以设置任意的入口点:对于 Linux 的 ld,它将是 -e 选项,对于 Windows 的 MSVC 链接器,它将是 /ENTRY 选项。 - Ruslan
2个回答

132

让我们来看一下你的程序生成的汇编代码

.LC0:
        .string "Hello World..."
.LC1:
        .string "Successfully run without main..."
nomain:
        push    rbp
        mov     rbp, rsp
        mov     edi, OFFSET FLAT:.LC0
        call    puts
        mov     edi, OFFSET FLAT:.LC1
        call    puts
        nop
        pop     rbp
        ret

注意ret语句。你的程序入口点被确定为nomain,这一切都没问题。但是一旦函数返回,它尝试跳转到调用栈上未被填充的地址... 这是非法访问,随之而来的是分段错误。
一个快速的解决方案是在程序末尾调用exit()(并假设C11我们也可以将该函数标记为_Noreturn):
#include <stdio.h>
#include <stdlib.h>

_Noreturn void nomain(void)
{
    printf("Hello World...\n");
    printf("Successfully run without main...\n");
    exit(0);
}

实际上,现在你的函数行为非常类似于一个普通的main函数,因为在从main返回后,将使用main的返回值调用exit函数。


6
我认为有些架构/操作系统组合可以让你直接“返回”程序,例如MS-DOS的.COM可执行文件。不管怎样,我们已经深入到实现特定的行为中了。 - pjc50
4
我们确实是。尽管原始帖子中的路径建议使用Unix变体。这与某些架构和指令集的流行程度结合在一起,是我感到舒适地在答案中呈现生成汇编代码的唯一原因。 - StoryTeller - Unslander Monica
1
只是一个观察。-nostartfiles也可能使C库无法使用。如果没有执行_C_启动,后续对_C_库函数的调用可能会意外失败。在Linux上,如果您使用-nostartupfiles-static编译,您可能会发现程序会出错。有一些_C_库(如MUSL)不需要预先初始化,可以在这种环境中使用。 - Michael Petch

22

在C语言中,当函数/子程序被调用时,栈按以下顺序填充:

  1. 参数,
  2. 返回地址,
  3. 局部变量, --> 栈的顶部

由于main()是起点,ELF以这样的方式构建程序,即无论什么指令先出现都会被先压入栈中,此处是printf。

现在,程序没有返回地址或__end__,实际上它假设位于该位置(__end__)的堆栈上的任何内容均为返回地址,但不幸的是,它并不是,因此它会崩溃。


4
栈数据的顺序是否由C标准定义?我认为这取决于系统架构。 - Délisson Junio
1
这就是为什么我提到了ELF(可执行和可链接文件格式),它是通过在所需的操作系统上进行特定ARCH类型的交叉编译而生成的。 - Milind Deore
1
挑剔一点的话,即使在没有堆栈的系统上,您也可以使用ELF格式。这样的系统之一是使用Codewarrior编译器的Freescale RS08,它生成ELF链接器文件。 - Lundin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接