首页 文章

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

提问于
浏览
29

Linux上的 int 0x80 总是调用32位ABI,无论它调用的是什么模式: ebx 中的args, ecx ,...和来自 /usr/include/asm/unistd_32.h 的系统调用号 . (或者在没有 CONFIG_IA32_EMULATION 的情况下编译的64位内核崩溃) .

64-bit code should use syscall ,电话号码来自 /usr/include/asm/unistd_64.h ,args位于 rdirsi 等 . 请参阅What are the calling conventions for UNIX & Linux system calls on i386 and x86-64 . 如果您的问题被标记为重复, see that link for details on how you should make system calls in 32 or 64-bit code. 如果您想了解究竟发生了什么,请继续阅读 .


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

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


使用 int 0x80 可以编写将在32位或64位模式下组合的内容,因此在微基准测试结束时使用 exit_group() 非常方便 .

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

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


但是,由于人们不断发布使用int 0x80 in 64-bit code的代码的问题,或者意外地从32位编写的源代码building 64-bit binaries,我想知道 what exactly does happen on current Linux?

Does int 0x80 save/restore all the 64-bit registers? Does it truncate any registers to 32-bit? What happens if you pass pointer args that have non-zero upper halves?

Does it work if you pass it 32-bit pointers?

1 回答

  • 34

    TL:DRint 0x80 正确使用时有效,只要任何指针适合32位( stack pointers don't fit ) . 另外, strace decodes it wrong ,解码寄存器内容,好像它是64位 syscall ABI . (有no simple/reliable way for strace to tell, yet . )

    int 0x80 零r8-r11,并保留其他所有内容 . 与32位代码一样使用它,使用32位呼叫号码 . (或者更好,不要使用它!)

    并非所有系统都支持 int 0x80 :Windows Ubuntu子系统仅严格为64位:int 0x80 doesn't work at all . 也可以构建Linux内核without IA-32 emulation . (不支持32位可执行文件,不支持32位系统调用) .


    详细信息:保存/恢复的内容,内核使用哪些部分

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

    只传递低32位的arg寄存器 . The upper halves of rbx-rbp are preserved, but ignored by int 0x80 system calls. 请注意,将错误的指针传递给系统调用不会导致SIGSEGV;而是系统调用返回 -EFAULT . 如果不检查错误返回值(使用调试器或跟踪工具),它将显示为静默失败 .

    除了 r8-r11 are zeroed 之外,所有寄存器(当然除了eax)都被保存/恢复(包括RFLAGS和整数寄存器的高32) . r12-r15 在x86-64 SysV ABI的函数调用约定中被调用保留,因此在64位中被 int 0x80 归零的寄存器是AMD64添加的"new"寄存器的调用被破坏的子集 .

    在内核中实现寄存器保存的一些内部更改中保留了这种行为,并且内核中的注释提到它可以从64位使用,因此这个ABI可能是稳定的 . (即,你可以指望r8-r11被归零,其他一切都被保留 . )

    返回值被符号扩展以填充64位 rax . (Linux declares 32-bit sys_ functions as returning signed long . )这意味着在64位寻址模式下使用之前,指针返回值(如来自 void *mmap() )需要进行零扩展

    sysenter 不同,它保留 cs 的原始值,因此它以与调用它相同的模式返回到用户空间 . (使用 sysenter 导致内核设置 cs$__USER32_CS ,它选择32位代码的描述符分割 . )


    strace decodes int 0x80 incorrectly 用于64位进程 . 它解码好像进程使用了 syscall 而不是 int 0x80 . This can be very confusing . 例如自 strace 打印 write(0, NULL, 12 <unfinished ... exit status 1> for eax=1 / int $0x80 ,这实际上是 _exit(ebx) ,而不是 write(rdi, rsi, rdx) .


    int 0x80 works as long as all arguments (including pointers) fit in the low 32 of a register . 静态代码和数据在默认代码模型("small")in the x86-64 SysV ABI中就是这种情况 . (第3.5.1节:已知所有符号都位于 0x000000000x7effffff 范围内的虚拟地址中,因此您可以执行 mov edi, hello (AT&T mov $hello, %edi )之类的操作来获取指向带有5字节指令的寄存器的指针) .

    this is not the case for position-independent executables, which many Linux distros now configure gcc to make by default (和可执行文件的enable ASLR) . 例如,我在Arch Linux上编译了一个 hello.c ,并在main的开头设置了一个断点 . 传递给 puts 的字符串常量为 0x555555554724 ,因此32位ABI write 系统调用无效 . (默认情况下,GDB会禁用ASLR,因此如果从GDB内部运行,则总是会在运行中看到相同的地址 . )

    Linux将堆栈放在the "gap" between the upper and lower ranges of canonical addresses附近,即堆栈顶部为2 ^ 48-1 . (或者在某个地方随机启用ASLR) . 所以 rsp 在一个典型的静态链接可执行文件中输入 _start 就像 0x7fffffffe550 ,这取决于env vars和args的大小 . 截断此指针指向 esp 并未指向任何有效内存,因此如果尝试传递截断的堆栈指针,则使用指针输入的系统调用通常会返回 -EFAULT . (如果将 rsp 截断为 esp 然后对堆栈执行任何操作,例如,如果您将32位asm源构建为64位可执行文件,则程序将崩溃 . )


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

    在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 定义了从compat模式到64位内核的系统调用入口点,以及64位进程中 int 0x80 的特殊情况 . (64位进程中的 sysenter 也可以进入该入口点,但它会推送 $__USER32_CS ,因此它将始终以32位模式返回 . )AMD CPU上支持32位版本的 syscall 指令, Linux也支持从32位进程进行快速32位系统调用 .

    我想在64位模式下 int 0x80possible use-case 是你想使用 modify_ldt 安装的a custom code-segment descriptor . int 0x80 推送段寄存器本身用于iret,Linux始终通过 iretint 0x80 系统调用返回 . 64位 syscall 入口点将 pt_regs->cs->ss 设置为常量, __USER_CS__USER_DS . (SS和DS使用相同的段描述符是正常的 . 权限差异是通过分页而不是分段完成的 . )

    entry_32.S 将入口点定义为32位内核,并且根本不涉及 .

    Linux 4.12的entry_64_compat.S中的int 0x80入口点:/ *

    • 32位遗留系统呼叫条目 .
    • 32位x86 Linux系统调用传统上使用INT $ 0x80
      *指示 . INT $ 0x80落在这里 .

    *此入口点可由32位和64位程序用于执行

    • 32位系统调用 . 可以在内联中找到INT $ 0x80的实例
      *各种程序和库 . 它也被vDSO使用
    • __kernel_vsyscall对不支持更快的硬件的回退
      *进场方式 . 重新启动的32位系统调用也会回退到INT
    • $ 0x80,无论最初用什么指令做什么
      *系统调用 .

    *这被认为是一条缓慢的道路 . 它不被大多数libc使用
    *除了在流程启动期间,在现代硬件上实现 .
    ...

    • /
      ENTRY(entry_INT80_compat)
      ...(请参阅完整源代码的github URL)

    代码将eax零扩展到rax,然后将所有寄存器推送到内核堆栈以形成struct pt_regs . 这是从系统调用返回时恢复的位置 . 它是用于保存的用户空间寄存器(对于任何入口点)的标准布局,因此来自其他进程的 ptrace (如gdb或 strace )将在该进程位于系统调用内时使用 ptrace 读取和/或写入该内存 . ( ptrace 寄存器的修改是使得其他入口点的返回路径复杂化的一件事 . 请参阅注释 . )

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

    我认为r8-r11的这个归零是为了匹配历史行为 . 在Set up full pt_regs for all compat syscalls commit之前,入口点仅保存了C call-clobbered寄存器 . 它使用 call *ia32_sys_call_table(, %rax, 8) 直接从asm调度,并且这些函数遵循调用约定,因此它们保留 rbxrbprspr12-r15 . 归零 r8-r11 而不是将它们保留为未定义可能是一种避免来自内核的信息泄漏的方法 . IDK如何处理 ptrace 如果用户空间的调用保留寄存器的唯一副本是在C函数保存它们的内核堆栈上 . 我怀疑它是否使用堆栈展开元数据在那里找到它们 .

    当前实现(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个零扩展args . 这可能避免在更多情况下需要围绕64位本机系统调用函数的包装来保留该行为,因此更多的 ia32 表条目可以直接是本机系统调用实现 .

    Linux 4.12 arch / x86 / entry / common.c if(可能(nr <IA32_NR_syscalls)){
    / *
    *有可能是32位系统调用实现
    *采用64位参数,但仍然假定
    *高位为零 . 确保我们对所有人进行零扩展

    • 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(寄存器);

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


    简单示例/测试程序:

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

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

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

    如果您将其构建为与位置无关的可执行文件,那么第一个也将失败 . (您必须使用RIP相对 lea 而不是 movhello: 的地址放入寄存器 . )

    我使用了gdb,但是使用你喜欢的调试器 . 使用自上一步以来突出显示已更改寄存器的寄存器 . gdbgui适用于调试asm源,但不适合反汇编 . 尽管如此,它确实有一个寄存器窗格,至少适用于整数寄存器,并且在这个例子中效果很好 .

    See the inline ;;; comments describing how register are changed by system calls

    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 中,如果您的 ~/.gdbinit 中没有,请运行 set disassembly-flavor intellayout reg . (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 . 这很容易发生,即使程序不打印到stdout本身也是如此 .

相关问题