Return main 与 _start 中的值

Return values in main vs _start

注意,这个问题在这里已经有类似的答案,我想指出的是:

然而,这个问题更多地询问了它们的 return 格式以及它们之间的关系(我认为上述问题并未完全涵盖)。


_startmain 有什么区别?在我看来 ld 使用 _start,但 gcc 使用 main 作为入口点。我注意到的另一个区别是 main 似乎 return %rax 中的值,而 _start return 是 [=24= 中的值]

以下是我看到的两种方式的示例:

.globl _start
_start:
    mov , %rax
    mov , %rbx
    int [=11=]x80

然后 运行 它:

$ as script.s -o script.o; ld script.o -o script; ./script; echo $?
# 2

反之:

.globl main
main:
    mov , %rax
    ret

然后 运行 它:

$ gcc script.s -o script; ./script; echo $?
3

这两种方法有什么区别? main 是否会在某处自动调用 _start,或者它们之间有何关系?为什么一个 return 它们的值在 rbx 而另一个 return 它在 rax 中?

TL:DR: 函数 return 值和 system-call 参数使用不同的寄存器,因为它们完全不相关。


当您使用 gcc 进行编译时,它会链接定义 _start 的 CRT 启动代码。 _start(间接)调用 main,并将 main 的 return 值(主要留在 EAX 中)传递给 exit()库函数。 (在执行任何必要的 libc 清理(如刷新 stdio 缓冲区)后,最终会退出 系统调用 。)

另见 Return vs Exit from main function in C - this is exactly analogous to what you're doing, except you're using _exit() which bypasses libc cleanup, instead of exit().

An int [=18=]x80 system call takes its argument in EBX, as per the 32-bit system-call ABI (). It's not a return value from a function, it's the process exit status. See 了解更多关于系统调用的信息。

注意 _start 不是一个函数;从这个意义上说,它不能 return 因为堆栈上没有 return 地址。 您正在随意描述“return 到 OS”,并将其与函数的“return 值”混为一谈。 您 [=如果需要,50=]可以 从 main 调用 exit,但不能从 _start.

调用 ret

EAX 是 return-value 寄存器,用于 function-calling 约定 int 大小的值。 (RAX的高32位被忽略,因为main returns int。而且,$?退出状态只能得到传递给[的值的低8位=15=].)

相关:

  • What happens with the return value of main()?
  • 解释了为什么应该使用 syscall,并展示了系统调用后内核内部发生的一些内核方面的情况。

_start 是二进制文件的入口点。 Main 是 C 代码的入口点。

_start特定于工具链,main()特定于语言。

你不能简单地开始执行编译后的 C 代码,你需要一个 bootstrap,一些代码准备了像这样的高级语言所需的最少东西,其他语言有更长的要求列表但是对于 C,您需要通过加载程序(如果在操作系统上)或 bootstrap 或两者都是堆栈指针的解决方案,以便有一个堆栈,read/write 全局数据(通常称为 .data)被初始化并且归零(通常称为.bss)数据被归零。然后 bootstrap 可以调用 main().

因为大多数代码 运行 在某些操作系统上,并且操作系统 can/does 将该代码加载到 ram 中,所以不需要硬入口点要求,因为您需要启动处理器,例如有硬入口点或硬向量 table 地址的地方。所以 gnu 足够灵活,一些操作系统也足够灵活,代码的入口点不必是二进制文件中的第一个 machine 代码。现在这并不意味着 _start 表示入口点 本身 因为您需要告诉链接器入口点 ENTRY(_start) 例如,如果您使用 gnu ld 的链接器脚本。但是这些工具确实希望找到一个名为 _start 的标签,如果链接器没有找到它就会发出警告,它会继续运行但会发出警告。

main() 特定于 C 语言作为 C 入口点,标签 bootstrap 在完成其工作并准备好 [=7​​1=] 已编译的 C 代码后调用。

如果加载到 ram 并且二进制文件格式支持它并且操作系统的加载程序支持它,则二进制文件的入口点可以在二进制文件中的任何位置,在二进制文件中指示。

您可以将 _start 视为二进制文件的入口点,将 main 视为编译后的 C 代码的入口点。

C 函数的 return 由 C 编译器使用的调用约定定义,编译器作者可以自由地做任何他们想做的事,但现代他们通常符合定义的目标(ARM 、x86、MIPS 等)定义的约定。因此,C 调用约定确切地定义了如何根据事物 return 进行某些操作,因此 int main () 是 int 的 return 但 float myfun() 在约定中可能有不同的规则。

二进制文件中的 return 甚至 return 是由独立于高级语言的操作系统或操作环境定义的。所以在 x86 处理器上的 mac 上规则可能是一回事在 x86 上的 Windows 上规则可能是另一回事,在同一个 x86 上的 Ubuntu Linux 上可能是另一个,bsd,另一个,可能不是 Mint Linux 另一个,等等。

规则和系统调用特定于操作系统,而不是处理器或计算机,或者肯定不是不直接接触操作系统的高级语言(在 bootstrap 中处理或库代码不在高级语言代码)。其中一些您应该进行系统调用,而不仅仅是 return 寄存器中的值,但显然操作系统需要足够强大以处理不正确的 return,用于格式错误的二进制文件。 And/or 允许它作为一个合法的 return 而无需退出系统调用,并且在这种情况下将定义如何在没有系统调用的情况下 return 的规则。

至于主调用_start,你自己可以很容易地看到:

int main ( void )
{
    return(5);
}

readelf 显示:

  Entry point address:               0x500

objdump 显示(这里不是全部输出)

Disassembly of section .init:

00000000000004b8 <_init>:
 4b8:   48 83 ec 08             sub    [=12=]x8,%rsp
 4bc:   48 8b 05 25 0b 20 00    mov    0x200b25(%rip),%rax        # 200fe8 <__gmon_start__>
 4c3:   48 85 c0                test   %rax,%rax
 4c6:   74 02                   je     4ca <_init+0x12>
 4c8:   ff d0                   callq  *%rax
 4ca:   48 83 c4 08             add    [=12=]x8,%rsp
 4ce:   c3                      retq   

...

Disassembly of section .text:

00000000000004f0 <main>:
 4f0:   b8 05 00 00 00          mov    [=12=]x5,%eax
 4f5:   c3                      retq   
 4f6:   66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
 4fd:   00 00 00 

...

0000000000000500 <_start>:
 500:   31 ed                   xor    %ebp,%ebp
 502:   49 89 d1                mov    %rdx,%r9
 505:   5e                      pop    %rsi
 506:   48 89 e2                mov    %rsp,%rdx
 509:   48 83 e4 f0             and    [=12=]xfffffffffffffff0,%rsp
 50d:   50                      push   %rax
 50e:   54                      push   %rsp
 50f:   4c 8d 05 6a 01 00 00    lea    0x16a(%rip),%r8        # 680 <__libc_csu_fini>
 516:   48 8d 0d f3 00 00 00    lea    0xf3(%rip),%rcx        # 610 <__libc_csu_init>
 51d:   48 8d 3d cc ff ff ff    lea    -0x34(%rip),%rdi        # 4f0 <main>
 524:   ff 15 b6 0a 20 00       callq  *0x200ab6(%rip)        # 200fe0 <__libc_start_main@GLIBC_2.2.5>
 52a:   f4                      hlt    
 52b:   0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)

所以你可以看到我上面提到的一切。二进制文件的入口点不在二进制文件的开头。入口点(对于二进制文件)是 _start,位于二进制文件中间的某个位置。在 _start 之后的某处(不一定像这里看到的那样接近,可能隐藏在其他嵌套调用之下)main 从 bootstrap 代码中调用。假设 .data 和 .bss 以及堆栈是由加载程序设置的,而不是由 bootstrap 在调用 C 入口点之前设置的。

所以在这种情况下,典型的 _start 是二进制文件的入口点,在它之后的某处 bootstraps 对于 C,它调用 C 入口点 main()。作为程序员,虽然您可以控制使用哪个链接器脚本和 bootstrap,因此不必使用 _start 作为入口点,您可以创建自己的入口点(当然不能是 main(),除非您不完全支持 C 以及可能与操作系统相关的其他异常)。