如果在 64 位代码中使用 32 位 int 0x80 Linux ABI 会发生什么情况?

What happens if you use the 32-bit int 0x80 Linux ABI in 64-bit code?

int 0x80 on Linux 总是调用 32 位 ABI,不管它是从什么模式调用的:args in ebx, ecx, ... and来自 /usr/include/asm/unistd_32.h 的系统调用编号。 (或者在没有 CONFIG_IA32_EMULATION 编译的 64 位内核上崩溃)。

64 位代码应使用 syscall,调用编号来自 /usr/include/asm/unistd_64.h,args 在 rdirsi,等等。参见 What are the calling conventions for UNIX & Linux system calls on i386 and x86-64。如果您的问题被标记为与此重复,请参阅 link 以了解有关您如何应该 在 32 位或 64 位代码中进行系统调用的详细信息。 如果您想了解到底发生了什么,请继续阅读。

(有关 32 位与 64 位 sys_write 的示例,请参阅 Using interrupt 0x80 on 64-bit Linux


syscall 系统调用比 int 0x80 系统调用快,因此请使用本机 64 位 syscall 除非您编写的多语言机器代码在以 32 位执行时运行相同或 64 位。 (sysenter 在 32 位模式下总是 returns,因此它在 64 位用户空间中没有用,尽管它是一个有效的 x86-64 指令。)

相关:The Definitive Guide to Linux System Calls (on x86) 如何进行 int 0x80sysenter 32 位系统调用,或 syscall 64 位系统调用,或调用 vDSO "virtual" 系统调用如 gettimeofday。加上系统调用的背景知识。


使用 int 0x80 可以在 32 位或 64 位模式下编写 assemble 的东西,因此在微基准测试或其他东西的末尾使用 exit_group() 很方便.

标准化函数和系统调用约定的官方 i386 和 x86-64 System V psABI 文档的当前 PDF 是 link 来自 https://github.com/hjl-tools/x86-psABI/wiki/X86-psABI

请参阅 tag wiki 获取新手指南、x86 手册、官方文档和性能优化指南/资源。


但是由于人们一直在发布有关使用为 32 位编写的源代码中的 int 0x80 in 64-bit code, or accidentally building 64-bit binaries 的代码的问题,我想知道 到底发生了什么 当前 Linux?

int 0x80save/restore都是64位寄存器吗?它是否将任何寄存器截断为 32 位?如果传递具有非零上半部分的指针参数会发生什么情况?

传32位指针能行吗?

TL:DR: int 0x80 在正确使用时工作,只要任何指针适合 32 位( 堆栈指针不适合)。但是请注意 strace 解码错误 除非你有一个非常新的 strace + 内核。

int 0x80 将 r8-r11 置零,并保留其他所有内容。使用它就像在 32 位代码中一样,使用 32 位索书号。 (或者更好的是,不要使用它!)

并非所有系统甚至都支持 int 0x80:Linux 版本 1 (WSL1) 的 Windows 子系统严格仅限 64 位:. It's also possible to build Linux kernels without IA-32 emulation either. (No support for 32-bit executables, no support for 32-bit system calls). See 回复:确保您的 WSL 实际上是 WSL2(它在 VM 中使用实际的 Linux 内核。)


细节:什么是saved/restored,内核使用了哪些regs

int 0x80 使用 eax(不是完整的 rax)作为系统调用号,分派到与 32 位相同的 table 函数指针user-space int 0x80 使用。 (这些指针指向内核中本机 64 位实现的 sys_whatever 实现或包装器。系统调用实际上是跨越 user/kernel 边界的函数调用。)

只传递arg寄存器的低32位。 rbx-rbp 的上半部分被保留,但被 int 0x80 系统调用忽略。 请注意,将错误的指针传递给系统调用不会不会导致 SIGSEGV;取而代之的是系统调用 returns -EFAULT。如果您不检查错误 return 值(使用调试器或跟踪工具),它将似乎无声地失败。

所有寄存器(当然除了eax)都是saved/restored(包括RFLAGS,以及整数regs的高位32),除了r8-r11被置零r12-r15 在 x86-64 SysV ABI 的函数调用约定中保留了调用,因此在 64 位中被 int 0x80 清零的寄存器是 AMD64 的“新”寄存器的调用破坏子集已添加。

此行为在内核内部实现寄存器保存的一些内部更改后得以保留,内核中的注释提到它可以从 64 位开始使用,因此此 ABI 可能是 stable . (即,您可以指望 r8-r11 被归零,其他所有内容都被保留。)

return 值经过符号扩展以填充 64 位 rax(Linux declares 32-bit sys_ functions as returning signed long.) 这意味着指针 return 值(如来自 void *mmap())在用于 64 位寻址模式

之前需要进行零扩展

sysenter 不同,它保留了 cs 的原始值,因此它 return 以与调用它时相同的模式发送给用户 - space。 (使用 sysenter 导致内核设置 cs$__USER32_CS,它为 32 位代码段选择一个描述符。)


较旧的 strace 对 64 位进程 int 0x80 解码不正确 。它解码时就好像进程使用了​​ syscall 而不是 int 0x80This can be very confusing。例如straceeax=1 / int [=46=]x80 打印 write(0, NULL, 12 <unfinished ... exit status 1>,实际上是 _exit(ebx),而不是 write(rdi, rsi, rdx)

我不知道添加 PTRACE_GET_SYSCALL_INFO 功能的确切版本,但 Linux 内核 5.5 / strace 5.5 处理它。它误导性地表示进程“运行s in 32-bit mode”但解码正确。 ().


int 0x80 只要所有参数(包括指针)都在寄存器 的低 32 位就可以工作。默认代码模型(“小”)中的静态代码和数据就是这种情况 in the x86-64 SysV ABI。 (第 3.5.1 节 : 所有符号都位于 0x000000000x7effffff 范围内的虚拟地址中,所以你可以做类似 mov edi, hello 的事情(AT&T mov $hello, %edi) 使用 5 字节指令获取指向寄存器的指针。

但是 不是 position-independent executables, which many Linux distros now configure gcc to make by default (and they enable ASLR for executables 的情况)。比如我在ArchLinux上编译了一个hello.c,在main的开头设置了断点。传递给 puts 的字符串常量位于 0x555555554724,因此 32 位 ABI write 系统调用将不起作用。 (GDB 默认禁用 ASLR,因此如果您在 GDB 中 运行,您总是会看到来自 运行 运行 的相同地址。)

Linux 将堆栈放在 附近,即堆栈顶部位于 2^48-1。 (或者在某个随机的地方,启用 ASLR)。因此,在典型的静态链接 executable 中进入 _startrsp 类似于 0x7fffffffe550,具体取决于环境变量和参数的大小。 T运行将此指针指向 esp 并不指向任何有效内存,因此如果您尝试传递 t运行 指定的堆栈指针。 (如果你 t运行cate rspesp 然后你的程序会崩溃,然后对堆栈做任何事情,例如,如果你将 32 位 asm 源构建为 64 位 executable.)


它在内核中是如何工作的:

在Linux源代码中,arch/x86/entry/entry_64_compat.S定义 ENTRY(entry_INT80_compat)。 32 位和 64 位进程在执行 int 0x80.

时使用相同的入口点

entry_64.S 定义了 64 位内核的本机入口点,其中包括中断/故障处理程序和来自 long mode (aka 64-bit mode) 个进程的 syscall 本机系统调用。

entry_64_compat.S 定义从兼容模式到 64 位内核的系统调用入口点,以及 64 位进程中 int 0x80 的特殊情况。 (64 位进程中的 sysenter 也可能会到达该入口点,但它会推送 $__USER32_CS,因此在 32 位模式下它将始终 return。)有一个 32- syscall 指令的位版本,在 AMD CPUs 上受支持,Linux 也支持来自 32 位进程的快速 32 位系统调用。

我想在 64 位模式下 int 0x80 可能用例 是如果您想使用 a custom code-segment descriptor that you installed with modify_ldt. int 0x80 pushes segment registers itself for use with iret 和 Linux 总是 returns 从 int 0x80 系统调用通过 iret。 64 位 syscall 入口点将 pt_regs->cs->ss 设置为常量 __USER_CS__USER_DS。 (SS和DS使用相同的段描述符是正常的。权限差异是通过分页完成的,而不是分段。)

entry_32.S 定义 32 位内核的入口点,完全不涉及。

The int 0x80 entry point in Linux 4.12's entry_64_compat.S:

/*
 * 32-bit legacy system call entry.
 *
 * 32-bit x86 Linux system calls traditionally used the INT [=10=]x80
 * instruction.  INT [=10=]x80 lands here.
 *
 * This entry point can be used by 32-bit and 64-bit programs to perform
 * 32-bit system calls.  Instances of INT [=10=]x80 can be found inline in
 * various programs and libraries.  It is also used by the vDSO's
 * __kernel_vsyscall fallback for hardware that doesn't support a faster
 * entry method.  Restarted 32-bit system calls also fall back to INT
 * [=10=]x80 regardless of what instruction was originally used to do the
 * system call.
 *
 * This is considered a slow path.  It is not used by most libc
 * implementations on modern hardware except during process startup.
 ...
 */
 ENTRY(entry_INT80_compat)
 ...  (see the github URL for the full source)

代码将eax零扩展为rax,然后将所有寄存器压入内核栈,形成struct pt_regs。这是系统调用 returns 时它将恢复的位置。它处于保存用户 space 寄存器(对于任何入口点)的标准布局中,因此来自其他进程(如 gdb 或 strace)的 ptrace 将读取 and/or 写入内存,如果他们在系统调用中使用 ptrace。 (ptrace 修改寄存器是使 return 路径对于其他入口点变得复杂的一件事。请参阅评论。)

但它推动 [=96=] 而不是 r8/r9/r10/r11。 (sysenter 和 AMD syscall32 入口点存储 r8-r15 的零。)

我认为 r8-r11 的这种归零是为了匹配历史行为。在 Set up full pt_regs for all compat syscalls commit, the entry point only saved the C call-clobbered registers. It dispatched directly from asm with call *ia32_sys_call_table(, %rax, 8), and those functions follow the calling convention, so they preserve rbx, rbp, rsp, and r12-r15. Zeroing r8-r11 instead of leaving them undefined was 从 64 位内核到 32 位用户 space 之前(这可能会跳转到 64 位代码段以读取内核留在那里的任何内容)。

当前实现 (Linux 4.12) 从 C 调度 32 位 ABI 系统调用,从 pt_regs 重新加载保存的 ebxecx 等. (64 位本机系统调用直接从 asm 调度,with only a mov %r10, %rcx 需要考虑函数和 syscall 之间调用约定的微小差异。不幸的是它不能总是使用 sysret,因为CPU 错误使非规范地址不安全。它确实尝试这样做,所以快速路径非常快,尽管 syscall 本身仍然需要数十个周期。)

无论如何,在当前 Linux 中,32 位系统调用(包括来自 64 位的 int 0x80)最终会在 do_syscall_32_irqs_on(struct pt_regs *regs) 中结束。它分派给函数指针 ia32_sys_call_table,带有 6 个零扩展参数。这可能避免在更多情况下需要围绕 64 位本机系统调用函数的包装器来保留该行为,因此更多 ia32 table 条目可以直接是本机系统调用实现。

Linux 4.12 arch/x86/entry/common.c

if (likely(nr < IA32_NR_syscalls)) {
  /*
   * It's possible that a 32-bit syscall implementation
   * takes a 64-bit parameter but nonetheless assumes that
   * the high bits are zero.  Make sure we zero-extend all
   * of the args.
   */
  regs->ax = ia32_sys_call_table[nr](
      (unsigned int)regs->bx, (unsigned int)regs->cx,
      (unsigned int)regs->dx, (unsigned int)regs->si,
      (unsigned int)regs->di, (unsigned int)regs->bp);
}

syscall_return_slowpath(regs);

在旧版本的 Linux 中,从 asm 调度 32 位系统调用(就像 64 位系统在 4.151 之前一样),int80 入口点本身使用 32 位寄存器,使用 movxchg 指令将 args 放入正确的寄存器中。它甚至使用 mov %edx,%edx 将 EDX 零扩展到 RDX(因为 arg3 恰好在两个约定中使用相同的寄存器)。 code here。此代码在 sysentersyscall32 入口点中重复。

脚注 1:Linux 4.15(我认为)引入了 Spectre / Meltdown 缓解措施,并对入口点进行了重大改造,使它们成为崩溃案例的蹦床。它还清理了传入的寄存器,以避免在调用期间将用户 space 值以外的实际参数存在寄存器中(当某些 Spectre 小工具可能 运行 时),方法是存储它们,将所有内容归零,然后调用从条目上保存的结构中重新加载正确宽度的 args 的 C 包装器。

我打算留下这个答案来描述更简单的机制,因为这里在概念上有用的部分是系统调用的内核端涉及使用 EAX 或 RAX 作为 table 函数指针的索引,其他传入的寄存器值被复制到调用约定希望 args 去的地方。即 syscall 只是调用内核及其调度代码的一种方式。


简单示例/测试程序:

我写了一个简单的 Hello World(在 NASM 语法中),它将所有寄存器设置为具有非零的上半部分,然后使用 int 0x80 进行两次 write() 系统调用,一次带有指向 .rodata 中字符串的指针(成功),第二个带有指向堆栈的指针(失败 -EFAULT)。

然后它使用本机 64 位 syscall ABI write() 堆栈中的字符(64 位指针),然后再次退出。

因此,所有这些示例都正确使用了 ABI,除了第二个 int 0x80 尝试传递 64 位指针并对其进行处理 运行。

如果您将其构建为与位置无关的执行程序table,第一个也会失败。 (您必须使用相对于 RIP 的 lea 而不是 mov 才能将 hello: 的地址放入寄存器中。)

我使用了 gdb,但可以使用您喜欢的任何调试器。使用一个突出显示自上一步以来更改的寄存器。 gdbgui 适用于调试 asm 源代码,但不适用于反汇编。不过,它确实有一个寄存器面板,至少适用于整数寄存器,并且在这个例子中效果很好。

查看内联 ;;; 注释,描述系统调用如何更改寄存器

global _start
_start:
    mov  rax, 0x123456789abcdef
    mov  rbx, rax
    mov  rcx, rax
    mov  rdx, rax
    mov  rsi, rax
    mov  rdi, rax
    mov  rbp, rax
    mov  r8, rax
    mov  r9, rax
    mov  r10, rax
    mov  r11, rax
    mov  r12, rax
    mov  r13, rax
    mov  r14, rax
    mov  r15, rax

    ;; 32-bit ABI
    mov  rax, 0xffffffff00000004          ; high garbage + __NR_write (unistd_32.h)
    mov  rbx, 0xffffffff00000001          ; high garbage + fd=1
    mov  rcx, 0xffffffff00000000 + .hello
    mov  rdx, 0xffffffff00000000 + .hellolen
    ;std
after_setup:       ; set a breakpoint here
    int  0x80                   ; write(1, hello, hellolen);   32-bit ABI
    ;; succeeds, writing to stdout
;;; changes to registers:   r8-r11 = 0.  rax=14 = return value

    ; ebx still = 1 = STDOUT_FILENO
    push 'bye' + (0xa<<(3*8))
    mov  rcx, rsp               ; rcx = 64-bit pointer that won't work if truncated
    mov  edx, 4
    mov  eax, 4                 ; __NR_write (unistd_32.h)
    int  0x80                   ; write(ebx=1, ecx=truncated pointer,  edx=4);  32-bit
    ;; fails, nothing printed
;;; changes to registers: rax=-14 = -EFAULT  (from /usr/include/asm-generic/errno-base.h)

    mov  r10, rax               ; save return value as exit status
    mov  r8, r15
    mov  r9, r15
    mov  r11, r15               ; make these regs non-zero again

    ;; 64-bit ABI
    mov  eax, 1                 ; __NR_write (unistd_64.h)
    mov  edi, 1
    mov  rsi, rsp
    mov  edx, 4
    syscall                     ; write(edi=1, rsi='bye\n' on the stack,  rdx=4);  64-bit
    ;; succeeds: writes to stdout and returns 4 in rax
;;; changes to registers: rax=4 = length return value
;;; rcx = 0x400112 = RIP.   r11 = 0x302 = eflags with an extra bit set.
;;; (This is not a coincidence, it's how sysret works.  But don't depend on it, since iret could leave something else)

    mov  edi, r10d
    ;xor  edi,edi
    mov  eax, 60                ; __NR_exit (unistd_64.h)
    syscall                     ; _exit(edi = first int 0x80 result);  64-bit
    ;; succeeds, exit status = low byte of first int 0x80 result = 14

section .rodata
_start.hello:    db "Hello World!", 0xa, 0
_start.hellolen  equ   $ - _start.hello

Build it 使用

转换为 64 位静态二进制文件
yasm -felf64 -Worphan-labels -gdwarf2 abi32-from-64.asm
ld -o abi32-from-64 abi32-from-64.o

运行gdb ./abi32-from-64。在 gdb、运行 set disassembly-flavor intellayout reg 中,如果您的 ~/.gdbinit 中还没有。 (GAS .intel_syntax 类似于 MASM,而不是 NASM,但它们足够接近,如果您喜欢 NASM 语法,它很容易阅读。)

(gdb)  set disassembly-flavor intel
(gdb)  layout reg
(gdb)  b  after_setup
(gdb)  r
(gdb)  si                     # step instruction
    press return to repeat the last command, keep stepping

当 gdb 的 TUI 模式混乱时按 control-L。这很容易发生,即使程序本身不打印到标准输出。