在x64 Linux上,syscall、int 0x80和ret退出程序之间有什么区别


在学习了多年C++和Python之后,我昨天决定学习汇编(NASM语法),我已经对退出程序的方法感到困惑。它主要是关于ret的,因为它是SASM IDE上的建议指令。

很明显,我是代表main发言的。我不关心x86的向后兼容性。只有x64 Linux的最佳方式。我很好奇。

如果使用printf或其他libc函数,最好从main或call exit调用ret(它们是等价的;main的调用者将调用libcexit函数。)

如果不是这样,如果您只使用syscall进行其他原始系统调用,如write,那么以这种方式退出也是合适且一致的,但无论哪种方式,或者call exit基本上都是100%好的。

如果你想在没有libc的情况下工作,例如,把你的代码放在_start:而不是main:下,并用ldgcc -static -nostdlib链接,那么你就不能使用ret。使用mov eax, 231(__NR_exit_group)/syscall

CCD_ 15是一个真实的&普通函数(使用有效的返回地址调用),但_start(进程入口点)不是。在进入_start时,堆栈保存argcargv,因此尝试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()的系统调用实现,以比较相关的C函数exit_exitexit_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中更明显,因为您需要leavemov rsp, rbpadd 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清理不太可能对堆栈进行存储/加载所需的任何对齐,但这是一个好习惯。)

    (整个脚注是关于retcall exit,而不是syscall的,所以它与答案的其余部分有点相切。您也可以运行syscall,而不必关心堆栈指针指向哪里。)


    SYS_exitSYS_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_exit1。这不是";错误";,并且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 0x80ABI:

    如果在64位代码中使用32位int 0x80 Linux ABI,会发生什么?解释了如果在64位代码中使用COMPAT_IA32_EMULATIONint 0x80ABI会发生什么。

    只要您的内核已经编译了这种支持,就可以直接退出,否则它将像int 0x7f等任何其他随机整数一样发生segfault。(例如,在WSL1上,或者构建自定义内核并禁用该支持的人。)

    但在asm中这样做的唯一原因是,您可以使用nasm -felf32nasm -felf64构建相同的源文件。(你不能在32位代码中使用syscall,除非在一些有32位版本syscall的AMD CPU上。而且32位ABI无论如何都使用不同的呼叫号码,所以这不会让同一个源对两种模式都有用。)


    相关:

    • 为什么允许我使用ret退出main?(CRT启动代码调用main,您不会将直接返回到内核。)
    • _start中RET上的Nasm分段错误-无法从_startret
    • 在程序集中使用printf会导致管道传输时输出为空,但在终端stdout缓冲区(而不是)上使用原始系统调用出口进行刷新
    • exit()的系统调用实现call exitmov eax,60/syscall(_exit)与mov eax,231/syscall(exit_group)
    • 可以';t从汇编(yasm)代码调用64位Linux上的C标准库函数-现代Linux发行版配置GCC,而call exitcall puts不会与nasm -felf64 foo.asm&amp;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一起使用到您自己的_startmain,在一个级别上发生的事情
    • https://stackoverflow.com/tags/x86/info很多关于这个和其他一切的好链接

    最新更新