C语言中的_start()函数有什么用?

157

我从同事那里学到,可以在不写main()函数的情况下编写和执行C程序。做法如下:

my_main.c

/* Compile this with gcc -nostartfiles */

#include <stdlib.h>

void _start() {
  int ret = my_main();
  exit(ret); 
}

int my_main() {
  puts("This is a program without a main() function!");
  return 0; 
}

使用以下命令编译:

gcc -o my_main my_main.c –nostartfiles

使用以下命令运行:

./my_main

何时需要做这种事?是否有任何真实世界的场景会用到这个功能?


1
远程相关:https://dev59.com/43E85IYBdhLWcg3w6oB0 - Mohit Jain
9
这是一篇经典文章,演示了程序启动的一些内部工作原理:在Linux上创建真正微小的ELF可执行文件的简要教程。它讨论了_start()和其他在main()之外的东西的一些细节,非常值得一读。 - user439793
1
C语言本身对_start或除了main之外的任何入口点都没有明确规定(除了对于自由站(嵌入式)实现,入口点的名称是实现定义的)。 - Keith Thompson
请注意,此 _start 不安全,调用 my_main 时违反了 ABI;您告诉编译器它是一个普通函数,但实际上它已经使用堆栈指针对齐(例如,在 x86-64 上,RSP%16 == 0),而不是 RSP%16 == 8,就像在调用推送 8 字节返回地址的普通函数之后进入一样。 您可以通过为 _start 使用 __attribute__((force_align_arg_pointer)) 来解决这个问题,告诉 GCC 在进入该“函数”时堆栈指针可能会“错位”,如 Get arg values with inline asm without Glibc? 中所示。 - Peter Cordes
在现代Linux发行版上,如果my_main使用scanf或printf(或任何可变参数函数)带有floatdouble FP参数,这将导致崩溃。当从不对齐RSP的函数调用glibc scanf时会出现分段错误 - Peter Cordes
4个回答

146
符号_start是您的程序的入口点。也就是说,该符号的地址是程序启动时跳转到的地址。_start函数通常由名为crt0.o的文件提供,其中包含用于C运行时环境的启动代码。它会设置一些内容,填充参数数组argv,计算有多少个参数,然后调用main函数。在main函数返回后,将调用exit函数。
如果程序不想使用C运行时环境,则需要提供自己的_start代码。例如,Go编程语言的参考实现这样做是因为它们需要一个非标准的线程模型,这需要一些关于堆栈的魔法操作。当您想要编写非常小的程序或进行非传统操作时,提供自己的_start也很有用。

2
另一个例子是Linux的动态链接器/装载器,它有自己定义的_start。 - P.P
2
@BlueMoon 但是那个 start 函数也来自于 crt0.o 目标文件。 - fuz
2
@ThomasMatthews 标准并没有规定 _start;事实上,在调用 main 之前会发生什么都没有具体说明,它只规定了在调用 main 时必须满足的条件。这更像是一个入口点的约定,而这个约定可以追溯到早期。 - fuz
9
许多编程语言都基于C运行时构建,因为这样实现语言更容易。Go语言不使用常规的线程模型。它们使用小的、堆分配的堆栈和自己的调度器。这显然不是标准的线程模型。 - fuz
1
一个有关在Linux上创建非常小的ELF可执行文件的旋风教程(或者说,“大小就是一切”) - SnakeDoc
显示剩余5条评论

54

main是程序员角度的程序入口点,而_start则是操作系统角度的通常入口点(即程序从操作系统启动后执行的第一条指令)。

在典型的C和尤其是C++程序中,在执行进入main函数之前会完成大量工作。这里有一个很好的解释介绍了从_start()main()以及在main函数退出后发生的一切(见下面的注释)。

通常,编译器的写者通过提供启动文件来提供必要的代码,但是在使用--nostartfiles标志时,你实际上告诉编译器:“不要打扰我给出标准的启动文件,让我完全控制从一开始发生的事情”。

这有时是必要的,并且在嵌入式系统上经常使用。例如,如果没有操作系统,您必须在全局对象初始化之前手动启用某些部分的存储器系统(例如缓存)。


1
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Cheiron
@Cheiron:抱歉,我的错误。在C++中,全局变量通常由构造函数初始化,该构造函数在_start()内运行(或实际上是由其调用的另一个函数),在许多裸机程序中,您需要首先将所有全局数据从闪存显式地复制到RAM中,这也发生在_start()中,但是这个问题既不涉及C++也不涉及裸机代码。 - MikeMB
1
请注意,在提供自己的 _start 的程序中,除非您采取特殊步骤自行初始化 C 库,否则 C 库将不会被初始化 - 在这样的程序中使用任何非异步信号安全函数可能是不安全的。(没有官方保证任何库函数都能正常工作,但异步信号安全函数根本不能引用任何全局数据,因此它们必须费尽心思才能出现故障。) - zwol
1
@FUZxxl 话说,我注意到异步信号安全函数确实允许修改errno(例如,readwrite是异步信号安全的,可以设置errno),这可能根据线程何时分配每个线程的errno位置而产生问题。 - zwol
1
@FUZxxl 我确信 glibc 的 malloc 不是异步信号安全的,但我接受对抽象情况的更正。 - zwol
显示剩余5条评论

14

这里是一个很好的概述,在main之前程序启动时会发生什么。特别地,它显示__start是从操作系统视角下你的程序的实际入口点

这是指令指针在你的程序中开始计数的第一个地址。

那里的代码调用一些C运行时库例程来进行一些清理工作,然后调用你的main,最后关闭并使用main返回的任何退出码调用exit


一张图片胜过千言万语:

C runtime startup diagram


P.S:此答案是从另一个问题转移过来的,Stack Overflow已将其作为该问题的重复关闭。


1
为了保留优秀的分析和精美的图片,此文已被转载。原文链接 - ulidtko

2

什么时候需要这样做?

当您想要为程序创建自己的启动代码时。

main 不是 C 程序的第一个入口点,_start 是幕后的第一个入口点。

Linux 中的示例:

_start: # _start is the entry point known to the linker
    xor %ebp, %ebp            # effectively RBP := 0, mark the end of stack frames
    mov (%rsp), %edi          # get argc from the stack (implicitly zero-extended to 64-bit)
    lea 8(%rsp), %rsi         # take the address of argv from the stack
    lea 16(%rsp,%rdi,8), %rdx # take the address of envp from the stack
    xor %eax, %eax            # per ABI and compatibility with icc
    call main                 # %edi, %rsi, %rdx are the three args (of which first two are C standard) to main

    mov %eax, %edi    # transfer the return of main to the first argument of _exit
    xor %eax, %eax    # per ABI and compatibility with icc
    call _exit        # terminate the program

有没有实际应用场景可以使用这个功能?
如果你的意思是,实现我们自己的 _start
是的,在我所工作过的大多数商业嵌入式软件中,我们需要根据特定的内存和性能要求来实现我们自己的 _start
如果你的意思是,放弃 main 函数并将其更改为其他内容:
不,我认为这样做没有任何好处。

通常情况下,_start 应该调用 exit 而不是 _exit(如果你链接了 libc),以确保 stdio 缓冲区被刷新。例如,如果你将 stdout 重定向到文件,则 puts("hello") 在 main 函数返回时仍然会被缓冲,因为 stdout 是全缓冲的。这还会调用已注册的 atexit 函数。 - Peter Cordes
一个完整的 _start 还会在进入时检查 RDX 是否为非空,并将其注册到 atexit 中。这是动态链接器的回调函数,如果任何库有任何析构函数,则使其变为非空。 (静态可执行文件直接从内核输入自己的 _start,所有寄存器都为0,没有机会让库先运行启动代码。即使 _start 不调用其函数,在动态链接的可执行文件中,glibc 也会初始化自己。使用此静态可执行文件中的 _start,glibc 函数(如 printf)将崩溃;FILE *stdout 甚至不会被初始化。) - Peter Cordes

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