很明显,我是代表main发言的。我不关心x86的向后兼容性。只有x64 Linux的最佳方式。我很好奇。
如果使用printf
或其他libc函数,最好从main或call exit
调用ret
(它们是等价的;main的调用者将调用libcexit
函数。)
如果不是这样,如果您只使用syscall
进行其他原始系统调用,如write
,那么以这种方式退出也是合适且一致的,但无论哪种方式,或者call exit
基本上都是100%好的。
如果你想在没有libc的情况下工作,例如,把你的代码放在_start:
而不是main:
下,并用ld
或gcc -static -nostdlib
链接,那么你就不能使用ret
。使用mov eax, 231
(__NR_exit_group)/syscall
。
CCD_ 15是一个真实的&普通函数(使用有效的返回地址调用),但_start
(进程入口点)不是。在进入_start
时,堆栈保存argc
和argv
,因此尝试ret
将设置RIP=argc,然后代码提取将在未映射的地址上segfault。启动中RET上的Nasm分割故障
系统调用与来自main的ret
通过系统调用退出类似于在C-skipatexit()
和libc cleanup中调用_exit()
,特别是不要刷新任何缓冲的stdout输出(在终端上缓存行,否则为完全缓冲)。这会导致一些症状,例如在汇编中使用printf会导致管道时输出为空,但在终端上有效(或者如果输出没有以n
结束,即使在终端上也是如此)
main
是一个函数,从CRT启动代码中(间接)调用。(假设你的程序链接正常,就像你的C程序一样。)你手工编写的main函数的工作原理和编译器生成的main
函数完全一样。它的调用者(__libc_start_main
)确实做了类似于int result = main(argc, argv); exit(result);
的事情,
例如call rax
(由_start
传递的指针)/mov edi, eax
/call exit
因此,从main返回正是1类似于调用exit
。
exit
与_exit
与exit_group
以及底层asm系统调用。
C问题:退出和返回有什么区别?主要是关于exit()
与return
,尽管提到了直接调用_exit()
,即仅进行系统调用。它之所以适用,是因为C main编译成asm main就像手工编写一样。
脚注1:你可以发明一个假设的、故意奇怪的案例,其中它是不同的。例如,您使用main
中的堆栈空间作为sub rsp, 1024
/mov rsi, rsp
/…/的stdio缓冲区CCD_ 42。然后,从main返回将涉及将RSP置于该缓冲区之上,并且__libc_start_main对exit的调用可能会在执行到达fflush清理之前用返回地址和局部变量覆盖部分缓冲区。这个错误在asm中比在C中更明显,因为您需要leave
、mov rsp, rbp
或add rsp, 1024
或其他东西来将RSP指向您的返回地址。
在C++中,return from main为其局部运行析构函数(在全局/静态退出填充之前),而exit
则不运行。但这只是意味着编译器在实际运行ret
之前会生成做更多事情的asm,所以在asm中都是手动的,就像在C.中一样
另一个区别当然是asm/calling约定的细节:EAX(返回值)或EDI(第一个arg)中的退出状态,当然对于ret
,您必须让RSP指向您的返回地址,就像在函数条目中一样。有了call exit
,你就没有了,你甚至可以像jne exit
那样进行退出的条件尾调用。由于它是一个noretrurn函数,所以不需要RSP指向有效的返回地址。(不过,RSP应该在调用前对齐16,或者在尾调用前对齐RSP%16=8,与调用推送返回地址后的对齐相匹配。退出/flush清理不太可能对堆栈进行存储/加载所需的任何对齐,但这是一个好习惯。)
(整个脚注是关于ret
与call exit
,而不是syscall
的,所以它与答案的其余部分有点相切。您也可以运行syscall
,而不必关心堆栈指针指向哪里。)
SYS_exit
与SYS_exit_group
原始系统调用
原始SYS_exit
系统调用用于退出当前线程,如pthread_exit()
(eax=60/syscall
或eax=1/int 0x80
)。
SYS_exit_group
用于退出整个程序,与_exit
类似
(eax=231/syscall
或eax=252/int 0x80
)。
在单线程程序中,可以使用任何一种,但如果要使用原始系统调用,则概念上exit_group对我来说更有意义。glibc的_exit()
包装器函数实际上使用了exit_group
系统调用(自glibc 2.3以来)。有关更多详细信息,请参阅exit()的Syscall实现。
然而,您所看到的几乎所有手写的asm都使用SYS_exit
1。这不是";错误";,并且CCD_ 68对于没有启动更多线程的程序来说是完全可接受的。特别是如果您试图使用xor eax,eax
/inc eax
(32位模式下为3个字节)或push 60
/pop rax
(64位模式中为3个字符)来保存代码大小,而push 231
/pop rax
甚至会大于mov eax,231
,因为它不适合带符号的imm8。
注1:(通常实际对数字进行硬编码,不使用asm/unistd.h
中的__NR_
…常数或sys/syscall.h
中的它们的SYS_
…名称)
从历史上看,这就是一切。请注意,在unistd_32.h中,__NR_exit
的调用号为1,但直到几年后,内核才添加__NR_exit_group
=252,当时内核支持与其父级共享虚拟地址空间的任务,也就是由clone(2)
启动的线程。这是CCD_ 83在概念上变成";退出当前线程";。(但人们可以很容易地、令人信服地辩称,在单线程程序中,SYS_exit
仍然意味着退出整个程序,因为只有在有多个线程的情况下,它才与exit_group
不同。)
老实说,我从来没有在任何事情中使用过eax=252/int 0x80,只有eax=1。它只是在64位代码中,我经常使用CCD_ 86而不是CCD_;简单的";或者令人难忘的方式1,所以不妨做一个酷家伙,并使用";现代的";exit_group
方式在我的单线程玩具程序/实验/微基准标记/SO答案中。:P(如果我不喜欢在风车上倾斜,我就不会花那么多时间在组装上,尤其是在so上。)
顺便说一句,我通常使用NASM进行一次性实验,所以使用预定义的符号常量作为呼叫号码很不方便;使用GCC在运行GAS之前预处理.S
,您可以使用#include <sys/syscall.h>
使代码自我文档化,这样您就可以将mov $SYS_exit_group, %eax
(或$__NR_exit_group
)或mov eax, __NR_exit_group
与.intel_syntax noprefix
一起使用。
不要在64位代码中使用32位int 0x80
ABI:
如果在64位代码中使用32位int 0x80 Linux ABI,会发生什么?解释了如果在64位代码中使用COMPAT_IA32_EMULATIONint 0x80
ABI会发生什么。
只要您的内核已经编译了这种支持,就可以直接退出,否则它将像int 0x7f
等任何其他随机整数一样发生segfault。(例如,在WSL1上,或者构建自定义内核并禁用该支持的人。)
但在asm中这样做的唯一原因是,您可以使用nasm -felf32
或nasm -felf64
构建相同的源文件。(你不能在32位代码中使用syscall
,除非在一些有32位版本syscall
的AMD CPU上。而且32位ABI无论如何都使用不同的呼叫号码,所以这不会让同一个源对两种模式都有用。)
相关:
- 为什么允许我使用ret退出main?(CRT启动代码调用main,您不会将直接返回到内核。)
- _start中RET上的Nasm分段错误-无法从
_start
中ret
- 在程序集中使用printf会导致管道传输时输出为空,但在终端stdout缓冲区(而不是)上使用原始系统调用出口进行刷新
- exit()的系统调用实现
call exit
与mov eax,60
/syscall
(_exit)与mov eax,231
/syscall
(exit_group) - 可以';t从汇编(yasm)代码调用64位Linux上的C标准库函数-现代Linux发行版配置GCC,而
call exit
或call puts
不会与nasm -felf64 foo.asm
&;CCD_ 112 - main()真的是C++程序的开始吗?-Ciro的答案是深入研究glibc+其CRT启动代码实际上是如何调用main的(包括GDB中的x86-64 asm反汇编),并显示了__libc_start_main的glibc源代码
- Linux x86程序启动或者-我们到底该怎么去main()?32位asm,以及比您可能想要的更多的细节,直到您对asm更加熟悉为止,但如果您曾经想知道为什么CRT在进入main之前会运行这么多代码,它涵盖了从将GDB与
starti
(在进程入口点停止,例如在动态链接器的_start
中)和stepi
一起使用到您自己的_start
或main
,在一个级别上发生的事情 - https://stackoverflow.com/tags/x86/info很多关于这个和其他一切的好链接