首页 > 解决方案 > 在 x64 Linux 上,syscall、int 0x80 和 ret 退出程序有什么区别?

问题描述

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

我显然是在为主要发言。我不关心 x86 向后兼容性。只有 x64 Linux 的最佳方式。我很好奇。

标签: linuxassemblyx86-64system-callsexit

解决方案


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

如果不是,如果您只是进行其他原始系统调用,例如writewith syscall,那么以这种方式退出也是适当且一致的,但无论哪种方式,或者call exit在 main 中都可以 100% 正常。

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

main是一个真实且正常的函数,就像任何其他函数一样(使用有效的返回地址调用),但_start(进程入口点)不是。在进入 时_start,堆栈保持argcargv,因此尝试ret设置 RIP=argc,然后代码提取将在该未映射地址上发生段错误。 _start 中 RET 上的 Nasm 分段错误


系统调用与 ret-from-main

通过系统调用退出就像_exit()在 C 中调用 -跳过 atexit()和 libc 清理,特别是不刷新任何缓冲的 stdout 输出(在终端上缓冲的行,否则为全缓冲)。这会导致诸如在装配中使用 printf 导致管道时输出为空,但在终端上工作(或者如果您的输出不以 结尾\n,即使在终端上也是如此。)

main是一个函数,从 CRT 启动代码中(间接)调用。(假设您正常链接您的程序,就像您链接 C 程序一样。)您的手写 main 的工作方式与编译器生成的 Cmain函数完全一样。它的调用者 ( __libc_start_main) 确实做了类似的事情int result = main(argc, argv); exit(result);
例如call rax(通过的指针_startmov edi, eax// call exit
所以从 main 返回就像调用1exit一样。

  • exit() 的系统调用实现,用于比较相关的 C 函数、exitvs _exit.exit_group和底层 asm 系统调用。

  • C题:exit和return有什么区别?主要是关于exit()vs. return,虽然有提到_exit()直接调用,即只是进行系统调用。它是适用的,因为 C main 编译为 asm main 就像您手动编写的一样。

脚注1:您可以发明一个假设的故意奇怪的案例,它是不同的。例如,您使用堆栈空间main作为您的 stdio 缓冲区sub rsp, 1024/ mov rsi, rsp/ ... / call setvbuf。然后从 main 返回将涉及将 RSP 放在该缓冲区之上,并且 __libc_start_main 的 exit 调用可能会在执行到达 fflush 清理之前用返回地址和本地值覆盖该缓冲区的某些内容。这个错误在 asm 中比在 C 中更明显,因为您需要leaveor mov rsp, rbpor or add rsp, 1024or something 将 RSP 指向您的返回地址。

在 C++ 中,从主要运行析构函数返回其本地(在全局/静态退出之前),exit不会。但这只是意味着编译器在实际运行之前使 asm 做更多的事情ret,所以它在 asm 中都是手动的,就像在 C 中一样。

另一个区别当然是 asm / 调用约定细节:EAX(返回值)或 EDI(第一个参数)中的退出状态,当然ret你必须让 RSP 指向你的返回地址,就像它在函数入口一样. 如果你不这样做,call exit你甚至可以像jne exit. 由于它是一个 noreturn 函数,因此您实际上并不需要 RSP 指向有效的返回地址。(不过,RSP应该在 call 之前对齐 16,或者 RSP%16 = 8 在 tailcall 之前对齐,在调用推送返回地址之后匹配对齐。exit / fflush cleanup 不太可能执行任何需要对齐的存储/加载到堆栈,但正确处理它是一个好习惯。)

(整个脚注是关于retvs. call 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()的系统调用实现。

但是,您将看到的几乎所有手写 asm 都使用SYS_exit1。这不是“错误的”,SYS_exit对于没有启动更多线程的程序来说是完全可以接受的。特别是如果您尝试使用 /(32 位模式下为 3 个字节)或 /(64 位模式下为 3 个字节)来保存代码大小xor eax,eaxinc eaxpush 60/pop raxpush 231比它pop rax更大,mov eax,231因为它不适合有符号的 imm8 .

注 1:(通常实际上是对数字进行硬编码,而不是使用__NR_... 常量 fromasm/unistd.h或它们的SYS_... 名称 from sys/syscall.h

从历史上看,这就是一切。请注意,在 unistd_32.h 中,__NR_exit调用编号为 1,但__NR_exit_group直到几年后内核获得对与其父级共享虚拟地址空间的任务的支持时才添加 = 252,即由clone(2). 这是SYS_exit概念上成为“退出当前线程”的时候。(但人们可以轻松而令人信服地争辩说,在单线程程序中,SYS_exit 仍然意味着退出整个程序,因为它仅与exit_group有多个线程时不同。)

老实说,我从来没有在任何事情上使用过 eax=252 / int 0x80,只有 eax=1。它只是在我经常使用的 64 位代码中,mov eax,231而不是mov eax,60因为两个数字都不像 1 那样“简单”或令人难忘,所以还不如成为一个很酷的人并exit_group在我的单线程玩具程序中使用“现代”方式/实验/微基准/ SO答案。:P(如果我不喜欢在风车上倾斜,我就不会花这么多时间在组装上,尤其是在 SO 上。)

顺便说一句,我通常使用 NASM 进行一次性实验,因此使用预定义的符号常量作为电话号码很不方便;使用 GCC.S在运行 GAS 之前对 a 进行预处理,#include <sys/syscall.h>您可以使用mov $SYS_exit_group, %eax(或$__NR_exit_group) 或mov eax, __NR_exit_group使用.intel_syntax noprefix.


不要int 0x80在 64 位代码中使用 32 位 ABI:

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

只要您的内核编译了该支持,退出就完全没问题,否则它将像任何其他随机 int 数一样出现段错误,例如int 0x7f. (例如在 WSL1 上,或者构建自定义内核并禁用该支持的人。)

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


有关的:


推荐阅读