在C中不使用main()编译并运行程序


78

我正在尝试编译和运行以下没有main()功能的程序C。我已经使用以下命令编译了程序。

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)

注意:

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


37
我很惊讶这一切都奏效。坦白地说,我认为链接器的这种处理是错误的(或至少是一件不好的事情):没有入口点,因此链接器只是将其从任何方便的功能中幻化了。布莱克
imallett

4
@imallett,至少该链接器足够友好,可以通过警告来引起注意并解释其正在执行的后备操作!您说对了,尽管这样做可能比错误更好,而不只是警告。
Toby Speight

为什么不使用main?
Pieter B

4
@PieterB-与有关unices的讨论不太相关,但是Windows程序的切入点不一定是mainWinMain或者是wWinMain
StoryTeller-Unslander Monica's

实际上,在Windows和Linux中,@ StoryTeller都可以设置任意入口点:对于Linux ld,这是-e选项,对于Windows的MSVC链接器,则可以/ENTRY选择。
Ruslan

Answers:


130

让我们看一下程序的生成程序集

.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,该exit函数将使用main返回值进行调用。


6
我认为有些架构/ OS组合可以让您“退出”程序。MS-DOS .COM可执行文件?无论如何,我们正在深入研究特定于实现的行为。
pjc50

4
@ pjc50-的确如此。尽管OP中的路径建议使用Unix变体。加上某些体系结构和指令集的流行,这是我感到很舒服的唯一答案,可以在答案中呈现生成的程序集。
StoryTeller-Unslander Monica's

1
只是一个观察。-nostartfiles也会使C库不可用。如果不执行C启动,则随后对C库函数的调用可能会意外失败。在Linux上,如果你是用汇编-nostartupfiles-static你会发现程序将故障。有像MUSL这样的C库,它们不需要在这种环境下工作的预先初始化。
Michael Petch

21

在C语言中,当调用函数/子例程时,堆栈填充为(按顺序):

  1. 论据,
  2. 退货地址,
  3. 局部变量->堆栈顶部

main()是起点,ELF以这样的方式构造程序:首先执行的是任何指令,在这种情况下,首先是printfs。

现在,程序被截断了一些,没有返回地址或__end__,实际上它假定堆栈中that(__end__)位置上的任何内容都是返回地址,但不幸的是它没有,因此崩溃了。


4
堆栈数据的顺序是否由C标准定义?我认为这是到系统架构
DélissonJUNIO

1
那就是为什么我提到ELF(可执行和可链接文件格式)的原因,它是通过在所需OS上针对特定ARCH类型进行交叉编译而生成的。
米林德·迪奥

1
要挑剔,即使在没有堆栈的系统上也可以使用ELF格式。这种系统的一个示例是带有Codewarrior编译器的Freescale RS08,该编译器生成ELF链接器文件。
伦丁
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.