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

Linux上的int 0x80总是调用32位ABI,而不pipe它是从哪个模式调用的:args in ebxecx ,…和/usr/include/asm/unistd_32.h系统调用号。 (或在没有CONFIG_IA32_EMULATION情况下编译的64位内核崩溃)。

64位代码应该使用syscall ,来自/usr/include/asm/unistd_64.h调用编号以及rdirsi等中的参数。请参阅x86-64上的UNIX和Linux系统调用的调用约定是什么 。 如果您的问题被标记为重复的, 请参阅该链接了解如何以32位或64位代码进行系统调用。 如果你想了解究竟发生了什么,请继续阅读。


syscall系统调用比int 0x80系统调用要快,因此除非要编写多边形机器代码,而是在32位或64位执行时运行,否则请使用本机64位syscall 。 ( sysenter始终以32位模式返回,所以从64位用户空间无用,尽pipe它是有效的x86-64指令。)

使用int 0x80可以编写以32位或64位模式组装的东西,所以在microbenchmark之后的exit_group()方便使用。

当前正式的i386和x86-64 System V的PDF文件,psABI文档标准化的函数和系统调用约定从https://github.com/hjl-tools/x86-psABI/wiki/X86-psABI链接。

有关初学者指南,x86手册,官方文档和性能优化指南/资源,请参阅x86标记wiki。


但是,由于人们不断地用64位代码中使用int 0x80代码发布问题,或者意外地从32位源代码 构build了64位二进制代码,我不知道现在的Linux 究竟发生了什么?

int 0x80保存/恢复所有的64位寄存器? 它是否截断任何寄存器到32位? 如果你传递了非零上半部分的指针参数,会发生什么?

如果你通过它的32位指针是否工作?

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

TL:DRint 0x80在正确使用的情况下工作,只要任何指针适合32位(堆栈指针不适用)。 strace解码错误 ,解码寄存器内容就好像它是64位syscall ABI。

int 0x80零r8-r11,并保留一切。 使用它就像在32位的代码,与32位的电话号码。 (或者更好,不要使用它!)


int 0x80使用eax (而不是完整的rax )作为系统调用号,调度到32位用户空间int 0x80使用的同一个函数指针表。 (这些指针指向sys_whatever生64位实现的sys_whatever实现或包装器,系统调用实际上是跨越用户/内核边界的函数调用。)

只有arg寄存器的低32位被传递。 rbxrbp的上半部分被保留,但被int 0x80系统调用忽略。 请注意,将错误指针传递给系统调用不会导致SIGSEGV; 而是系统调用返回-EFAULT 。 如果您不检查错误返回值(使用调试器或跟踪工具),它将会以静默方式失败。

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

这种行为已经被保留了一些内部的改变,如何在内核中实现注册保存,而内核中的注释提到它可以从64位使用,所以这个ABI可能是稳定的。 (也就是说,你可以指望r8-r11被清零,其他所有的东西都被保存下来。)

返回值是符号扩展以填充64位rax 。 (Linux声明32位sys_函数返回的是signed long 。)这意味着在使用64位寻址模式之前,指针返回值(例如void *mmap() )需要被零扩展

sysenter不同的是,它保留了cs的原始值,所以它以与调用它相同的模式返回到用户空间。(使用sysenter结果将内核设置为cs$__USER32_CS ,它选择一个32位的描述符代码段)。


strace为64位进程正确解码int 0x80 。 它解码好像进程已经使用syscall而不是int 0x80 。 这可能是非常混乱的 。 例如自strace打印write(0, NULL, 12 <unfinished ... exit status 1>eax=1 / int $0x80 ,实际上是_exit(ebx) ,而不是write(rdi, rsi, rdx)


只要所有参数(包括指针)适合寄存器的低32位, int 0x80可以工作 。 这是x86-64 SysV ABI中默认代码模型(“小”) 中静态代码和数据的情况。 (第3.5.1节: 已知所有符号位于范围为0x000000000x7effffff的虚拟地址中 ,因此您可以执行像mov edi, hello (AT&T mov $hello, %edi )来获取指向寄存器的指针用5字节的指令)。

但是位置无关的可执行文件 并不是这种情况,现在许多Linux发行版默认配置gcc (并且它们启用ASLR来执行可执行文件)。 例如,我在Arch Linux上编译了一个hello.c文件,并在main的开头设置了一个断点。 传递给puts的字符串常量是0x555555554724 ,所以32位ABI写入系统调用将不起作用。 (即使gcc在默认情况下生成了一个PIE可执行文件,但是它并没有为它启用ASLR,与/bin/ls ,它每次都在相同的地址加载。

Linux将堆栈放在规范地址的上下范围之间的“间隙”附近,即堆栈的顶部为2 ^ 48-1。 (或随机的地方,启用ASLR)。 因此,对于典型的静态链接可执行文件中的_start条目, _start类似于0x7fffffffe550 ,具体取决于env vars和args的大小。 将此指针截断为esp并不指向任何有效的内存,因此如果您尝试传递截断的堆栈指针,则带有指针输入的系统调用通常会返回-EFAULT 。 (如果您将rsp截断为esp ,然后对堆栈执行任何操作,例如,如果将32位asm源作为64位可执行文件构建,则程序将崩溃。)


它如何在内核中工作:

在Linux源代码中, arch/x86/entry/entry_64_compat.S定义了ENTRY(entry_INT80_compat) 。 当他们执行int 0x80时,32位和64位进程都使用相同的入口点。

entry_64.S定义了64位内核的原生入口点,它包括来自长模式(又称64位模式)进程的中断/错误处理程序和syscall本地系统调用。

entry_64_compat.S将系统调用入口点从compat模式定义到64位内核,以及在64位进程中使用int 0x80的特殊情况。 (64位进程中的$__USER32_CS也可以进入这个入口点,但是它会推送$__USER32_CS ,所以它总是以32位模式返回。)有一个32位版本的syscall指令,在AMD CPU上支持,而且Linux也支持来自32位进程的快速32位系统调用。

我猜想在64位模式 int 0x80一个可能的用例是,如果你想使用你用modify_ldt安装的自定义代码 modify_ldtint 0x80将段寄存器本身用于与iret一起使用,而Linux始终通过iretint 0x80系统调用中iret 。 64位syscall入口点将pt_regs->cs->ss为常量, __USER_CS__USER_DS 。 (SS和DS使用相同的段描述符是很正常的,权限的区别是分页,而不是分段。

entry_32.S将入口点定义到32位内核中,根本不涉及。

Linux 4.12的 int 0x80入口点entry_64_compat.S

 /* * 32-bit legacy system call entry. * * 32-bit x86 Linux system calls traditionally used the INT $0x80 * instruction. INT $0x80 lands here. * * This entry point can be used by 32-bit and 64-bit programs to perform * 32-bit system calls. Instances of INT $0x80 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 * $0x80 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 。 这是从系统调用返回时恢复的位置。 它是用于保存用户空间寄存器(对于任何入口点)的标准布局,所以如果在进程处于系统调用中时使用ptrace ,来自其他进程(如gdb或strace )的ptrace将读取和/或写入该内存。 ( ptrace修改寄存器是使其他入口点返回路径变得复杂的一件事情,请参阅注释。)

但它推动$0而不是r8 / r9 / r10 / r11。 ( syscall32和AMD syscall32入口点存储r8-r15的零。)

我认为r8-r11的调零是为了匹配历史行为。 在为所有的compat系统调用建立完整的pt_regs之前,入口点只保存了C调用的缓存寄存器。 它直接从asm call *ia32_sys_call_table(, %rax, 8)call *ia32_sys_call_table(, %rax, 8) ,这些函数遵循调用约定,因此它们保留rbxrbprbxr12-r15 。 对r8-r11清零而不是将其保留为未定义状态可能是避免内核信息泄漏的一种方法。 IDK如何处理ptrace如果用户空间的调用保存寄存器的唯一副本是在C函数保存它们的内核堆栈上。 我怀疑它使用堆栈展开元数据在那里找到它们。

当前的实现(Linux 4.12)从C调度32位ABI系统调用,从pt_regs重新加载保存的ebxecx等。 (64位原生系统调用直接从asm调用, 只有mov %r10, %rcx需要考虑函数和syscall调用之间的调用约定中的小差异。不幸的是,它不能总是使用sysret ,因为CPU的bug使得它对于非规范地址是不安全的,它确实试图去做,所以快速路径非常快,尽管syscall本身仍然需要几十个周期。)

无论如何,在当前的Linux中,32位系统调用(包括来自64位的int 0x80 )最终在do_syscall_32_irqs_on(struct pt_regs *regs) 。 它调度到一个函数指针ia32_sys_call_table ,有6个零扩展参数。 这可能会避免在更多情况下需要在64位本机系统调用函数中使用包装来保留该行为,所以更多的ia32表项可以直接作为本地系统调用实现。

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位仍然如此),int80入口点本身使用32位寄存器,通过movxchg指令将args放入正确的寄存器中。 它甚至使用mov %edx,%edx将EDX零扩展为RDX(因为arg3碰巧在两个约定中都使用相同的寄存器)。 代码在这里 。 此代码在syscall32syscall32入口点中重复。


简单的例子/测试程序:

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

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

因此,所有这些示例都正确使用ABI,除了第二个int 0x80试图传递一个64位指针并截断它。

如果您将其构建为独立于位置的可执行文件,则第一个文件也将失败。 (您必须使用RIP相关的lea而不是mov来获取hello:的地址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 

将它构建成一个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。 这种情况很容易发生,即使程序不能打印出标准输出。