【问题标题】:What is the use of _start() in C?C 中 _start() 的用途是什么?
【发布时间】:2015-06-24 00:44:44
【问题描述】:

我从同事那里了解到,无需编写 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

什么时候需要做这种事情?是否有任何现实世界的场景可以使用它?

【问题讨论】:

标签: c gcc startup


【解决方案1】:

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

在一个典型的 C 尤其是 C++ 程序中,在执行进入 main 之前已经做了很多工作。 特别是像初始化全局变量这样的东西。 Here 你可以很好地解释_start()main() 之间发生的一切,以及在 main 再次退出之后(参见下面的评论)。
必要的代码通常由编译器编写者在启动文件中提供,但使用标志–nostartfiles 基本上是在告诉编译器:“不要费心给我标准启动文件,让我完全控制正在发生的事情从一开始”。

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

【讨论】:

  • 全局变量是数据部分的一部分,因此在程序加载期间设置(如果它们是 const,它们是文本部分的一部分,同样的故事)。 _start 函数与此完全无关。
  • @Cheiron: 抱歉,我的错误在 c++ 中,全局变量通常由在 _start()(或实际上由它调用的另一个函数)和许多裸机程序中运行的构造函数初始化,您首先将所有全局数据从闪存显式复制到 RAM,这也发生在 _start() 中,但这个问题既不是关于 c++ 也不是关于裸机代码。
  • 请注意,在提供自己的_start 的程序中,除非您自己采取特殊步骤,否则 C 库不会被初始化——使用任何非来自此类程序的异步信号安全功能。 (没有官方保证 any 库函数会起作用,但是异步信号安全函数根本不能引用任何全局数据,因此它们必须竭尽全力才能发生故障.)
  • @FUZxxl 话虽如此,我注意到异步信号安全函数允许修改errno(例如readwrite是异步信号-safe 并且可以设置 errno),这可能是一个问题,具体取决于每个线程 errno 位置的确切分配时间。
  • @FUZxxl 我知道 glibc 的 mallocnot 异步信号安全的,但我接受关于抽象情况的更正。
【解决方案2】:

符号_start 是您程序的入口点。也就是说,该符号的地址是程序开始时跳转到的地址。通常,名称为_start 的函数由一个名为crt0.o 的文件提供,该文件包含C 运行时环境的启动代码。它设置了一些东西,填充参数数组argv,计算有多少参数,然后调用mainmain 返回后,exit 被调用。

如果程序不想使用 C 运行时环境,它需要为_start 提供自己的代码。例如,Go 编程语言的参考实现这样做是因为它们需要一个非标准的线程模型,这需要堆栈的一些魔力。当您想编写非常小的程序或执行非常规操作的程序时,提供您自己的_start 也很有用。

【讨论】:

  • 另一个例子是 Linux 的动态链接器/加载器,它定义了自己的 _start。
  • @BlueMoon 但是那个_start也来自目标文件crt0.o
  • @ThomasMatthews 标准没有指定_start;实际上,它根本没有指定在调用main 之前会发生什么,它只是指定了调用main 时必须满足哪些条件。入口点是 _start 的惯例,这可以追溯到过去。
  • “Go 编程语言的参考实现这样做是因为它们需要一个非标准的线程模型” crt0.o 是 C 特定的(crt->C 运行时)。没有理由期望它被用于任何其他语言。 Go 的线程模型完全符合标准
  • @SteveCox 许多编程语言都构建在 C 运行时之上,因为以这种方式实现语言更容易。 Go 不使用普通的线程模型。他们使用小的堆分配堆栈和自己的调度程序。这当然不是标准的线程模型。
【解决方案3】:

什么时候需要做这种事情?

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

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 函数。
  • 功能齐全的_start 也会在入口处检查 RDX,如果非 NULL 将其注册到 atexit。如果任何库有任何析构函数,动态链接器就会将其设为非 NULL 回调。 (静态可执行文件直接从内核输入自己的_start,所有寄存器都为0,库没有机会首先运行启动代码。这就是glibc在动态链接的可执行文件中初始化自身的方式,即使_start没有调用它的函数。在静态可执行文件中使用这个 _start 时,像 printf 这样的 glibc 函数会崩溃;FILE *stdout 甚至不会被初始化。)
【解决方案4】:

Here 很好地概述了程序启动期间发生的情况之前 main。特别是,它表明__start 是从操作系统的角度您的程序的实际入口点

这是instruction pointer 在您的程序中开始计数的第一个地址。

那里的代码调用一些 C 运行时库例程只是为了做一些内务处理,然后调用您的 main,然后停止运行并使用返回的退出代码 main 调用 exit


一张图胜过千言:


P.S:这个答案是从another question 移植过来的,所以作为这个答案的副本已经有用地关闭了。

【讨论】:

  • 交叉发布以保留优秀的analysis和漂亮的图片。
猜你喜欢
  • 1970-01-01
  • 2010-09-09
  • 2020-10-23
  • 2013-11-03
  • 1970-01-01
  • 1970-01-01
  • 2014-01-15
  • 2011-08-07
相关资源
最近更新 更多