linux - 在 x64 Linux 上,syscall、int 0x80 和 ret 退出程序有什么区别?
问题描述
经过多年的 C++ 和 Python,我昨天决定学习汇编(NASM 语法),我已经对退出程序的方式感到困惑。它主要是关于 ret 因为它是 SASM IDE 上的建议指令。
我显然是在为主要发言。我不关心 x86 向后兼容性。只有 x64 Linux 的最佳方式。我很好奇。
解决方案
如果你使用printf
或其他 libc 函数,最好ret
从 main 或call exit
. (它们是等价的;main 的调用者将调用 libcexit
函数。)
如果不是,如果您只是进行其他原始系统调用,例如write
with syscall
,那么以这种方式退出也是适当且一致的,但无论哪种方式,或者call exit
在 main 中都可以 100% 正常。
如果你想在没有 libc 的情况下工作,例如将你的代码放在_start:
而不是main:
和链接下ld
or gcc -static -nostdlib
,那么你不能使用ret
. 使用mov eax, 231
(__NR_exit_group) / syscall
。
main
是一个真实且正常的函数,就像任何其他函数一样(使用有效的返回地址调用),但_start
(进程入口点)不是。在进入 时_start
,堆栈保持argc
和argv
,因此尝试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
(通过的指针_start
)mov edi, eax
// call exit
。
所以从 main 返回就像调用1exit
一样。
exit() 的系统调用实现,用于比较相关的 C 函数、
exit
vs_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 中更明显,因为您需要leave
or mov rsp, rbp
or or add rsp, 1024
or 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 不太可能执行任何需要对齐的存储/加载到堆栈,但正确处理它是一个好习惯。)
(整个脚注是关于ret
vs. 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()的系统调用实现。
但是,您将看到的几乎所有手写 asm 都使用SYS_exit
1。这不是“错误的”,SYS_exit
对于没有启动更多线程的程序来说是完全可以接受的。特别是如果您尝试使用 /(32 位模式下为 3 个字节)或 /(64 位模式下为 3 个字节)来保存代码大小xor eax,eax
,inc eax
而push 60
/pop rax
会push 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 -felf32
or构建相同的源文件nasm -felf64
。(您不能syscall
在 32 位代码中使用,除非在某些具有 32 位版本的 AMD CPU 上syscall
。而且 32 位 ABI 无论如何都使用不同的调用号,因此这不会让相同的源对两者都有用模式。)
有关的:
- 为什么允许我使用 ret 退出 main?(CRT 启动代码调用 main,您不会直接返回内核。)
- _start 中 RET 上的 Nasm 分段错误- 你不能
ret
从_start
- 在汇编中使用 printf 会导致管道输出为空,但可以在终端标准输出缓冲区(不)上使用原始系统调用退出刷新
- exit()
call exit
vs.mov eax,60
/syscall
(_exit) vs.mov eax,231
/syscall
(exit_group) 的系统调用实现。 - 无法从汇编(yasm)代码在 64 位 Linux 上调用 C 标准库函数- 现代 Linux 发行版以与&&链接
call exit
或call puts
不链接的方式配置 GCC 。nasm -felf64 foo.asm
gcc foo.o
- 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很多关于这个和其他一切的好链接。
推荐阅读
- angular - 使用 socket.io 在 Angular 聊天应用程序中实现“有人在打字……”功能
- javascript - Javascript/Typescript 从另一个嵌套数组中更新数据
- java - File.delete() 返回 false?
- angularjs - AngularJS 将数据广播到 iframe
- selenium-webdriver - Chrome 版本 71.x 未知错误:调用函数结果缺少“值”
- python - 如何在 scipy.integrate.ode (或其他)函数中编写初始条件?
- java - 如何使用构造函数打印出每本书的主题、颜色和页面?
- angular - Angular @Input,将对象绑定到同一对象但具有不同的属性名称
- php - PHP Array Goup 使用具有多个键的日期并分配键的总和
- stanford-nlp - 错误:无法找到或加载主类 edu.stanford.nlp.ie.machinereading.MachineReading