x86_64汇编Linux系统调用混淆

18

我目前正在学习在Linux下的汇编语言。我一直在使用《从基础开始编程》这本书,而且所有的例子都是32位的。我的操作系统是64位的,我一直试图在64位中完成所有的例子。然而,我遇到了麻烦:

.section .data

.section .text
.global _start
_start:
movq $60, %rax
movq $2, %rbx
int $0x80

这仅仅调用了Linux的exit系统调用,或者应该是这样的。但实际上它导致了段错误,而当我采用这种方式时的结果是

.section .data

.section .text
.global _start
_start:
movq $1, %rax
movq $2, %rbx
int $0x80

代码可以运行。显然,问题出在我移动到% rax的值上。第二个示例中使用的$1是“从基础开始编程”建议使用的,但互联网上的多个来源称64位系统调用编号为$60。参考。我做错了什么?还有哪些问题需要注意?我应该使用什么参考资料? 以防万一您需要知道,我正在进行《从零开始编程》第5章的学习。


基本上是一个重复的问题:如果在64位代码中使用32位int 0x80 Linux ABI会发生什么? int $ 0x80仍然调用32位ABI,使用32位寄存器和调用号码。 只需使用在64位系统上组装32位二进制文件(GNU工具链)来遵循32位教程即可。 - Peter Cordes
5个回答

20
你遇到了i386和x86_64之间一个令人惊讶的不同点:它们不使用相同的系统调用机制。正确的代码是:
movq $60, %rax
movq $2,  %rdi   ; not %rbx!
syscall

中断0x80总是调用32位系统调用。它被用于允许32位应用程序在64位系统上运行。

为了学习的目的,你应该尝试按照教程的步骤进行,而不是即时转换为64位——你可能会遇到一些其他重要的行为差异。一旦你熟悉i386,然后你可以单独学习x86_64。


我可能最终会这样做。谢谢您的回复。 - Hudson Worden
应该使用%rdi作为第一个系统调用参数,而不是%rbx - Adam Zalcman
9
sysenter 在我的系统上无法工作,正确的指令是 syscall - hpsMouse
1
@hpsMouse,syscall是AMD发起的版本,而sysenter是Intel发起的版本。更多细节请参见:http://wiki.osdev.org/SYSENTER - Dan Lenski
澄清一下,syscall 总是用于本地 x86-64 调用(而不是 32 位兼容模式)。英特尔必须实现它。 - Yuhong Bao

14

请阅读此文UNIX 和 Linux 系统调用 x86-64 的调用约定是什么?

请注意,对于 x64 系统,使用 int 0x80 进行系统调用是旧的兼容性层。您应该在 x64 系统上使用 syscall 指令。

您仍然可以使用这种旧方法,但需要以 x86 模式编译二进制文件,请参考您的编译器或汇编器手册获取详细信息。


很高兴看到有人指出x86_64 Linux实际上使用的是syscall而不是sysenter!我写了一个更长的答案来解释这两者之间的混淆。 - Dan Lenski

7

duskwuff回答正确指出了64位x86 Linux与32位Linux系统调用机制不同。

然而,这个答案有几个不完整和误导性的地方:

正如评论中指出的那样SYSENTER实际上在许多64位Linux系统上无法工作,特别是64位AMD系统。

这是一个令人困惑的情况。详细信息在此处,但归根结底是这样的:

对于32位内核,SYSENTER/SYSEXIT是唯一兼容的一对[在AMD和英特尔CPU之间]

对于Long模式下的64位内核… SYSCALL/SYSRET是唯一兼容的一对[在AMD和英特尔CPU之间]

似乎在64位模式下的Intel CPU上,你可以使用SYSENTER,因为它与SYSCALL执行相同的操作,然而对于AMD系统来说并非如此。

结论:在64位x86系统上的Linux中,始终使用SYSCALL。这是x86-64 ABI实际规定的。(有关更多详细信息,请参见此维基答案)。

我的理解是,现代AMD CPU支持从32位用户空间(进入64位内核)的sysenter。但无论如何,您都不应直接使用sysenter;它没有得到官方支持。对于简单的初学者代码,请使用int $0x80,或调用VDSO以让内核提供的代码运行sysenter或在您的CPU上最有效的任何其他操作。https://blog.packagecloud.io/eng/2016/04/05/the-definitive-guide-to-linux-system-calls/ - Peter Cordes

5

在i386和x86_64之间有很多变化,包括进入内核所使用的指令以及用于传递系统调用参数的寄存器。以下是与您代码等效的代码:

.section .data

.section .text
.global _start
_start:
movq $60, %rax
movq $2, %rdi
syscall

引自这个答案,与相关问题有关:

系统调用号在Linux源代码的arch/x86/include/asm/unistd_64.h中。系统调用号传递到rax寄存器中。参数分别为rdi、rsi、rdx、r10、r8、r9。使用“syscall”指令调用该调用。系统调用将覆盖rcx寄存器。返回结果在rax中。


我也在做《Shellcoder's Handbook 2》中的东西。然而,我的操作系统也是64位的。我使用nasm选项-f elf32进行汇编,并且链接时也使用-melf_i386。我使用gdb和evans调试器(然而,它是为64位编译的,加载二进制文件后会提示错误,但在edb、gdb和执行中,错误都是相同的):在int 0x80之后的指令会导致段错误。我只想要退出。我已经看到,(r)ax寄存器必须包含0x3c而不是0x01(因为使用了64位的syscall文件),但我无法想象为什么在int 0x80之后会调用指令。有人知道原因吗? - icbytes

3

如果你检查 /usr/include/asm/unistd_32.h , exit 对应的是 1, 但是在 /usr/include/asm/unistd_64.h, exit 对应的是60


是的,int 0x80 总是使用 unistd_32 调用号(和 32 位寄存器)。只有在 64 位代码中的 syscall 使用 unistd_64.h 调用号和 RDI、RSI 等参数寄存器。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接