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

时间:2017-09-07 04:20:41

标签: linux assembly x86-64 system-calls abi

Linux上的

int 0x80始终调用32位ABI,无论调用何种模式:ebx中的args,ecx,...和来自{{1的系统调用号码}}。 (或者在没有/usr/include/asm/unistd_32.h编译的64位内核上崩溃。)

64位代码应使用CONFIG_IA32_EMULATION ,来自syscall的电话号码,以及/usr/include/asm/unistd_64.hrdi中的args等。 What are the calling conventions for UNIX & Linux system calls on i386 and x86-64。如果您的问题被标记为重复,请参阅该链接,详细了解应如何以32或64位代码进行系统调用。如果您想了解什么确实发生了,继续阅读。

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

相关:The Definitive Guide to Linux System Calls (on x86)了解如何进行sysenterint 0x80 32位系统调用,或sysenter 64位系统调用,或将vDSO调用为“虚拟“系统调用syscall。加上有关系统调用的背景知识。

使用gettimeofday可以编写将以32位或64位模式汇编的内容,因此在微基准测试结束时可以方便地使用int 0x80

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

有关初学者指南,x86手册,官方文档和效果优化指南/资源,请参阅代码wiki

但是,由于人们不断发布使用int 0x80 in 64-bit code的代码或使用32位编写的源代码意外building 64-bit binaries的问题,我不知道 究竟确实发生了什么在当前的Linux上?

exit_group()是否保存/恢复所有64位寄存器?它会将任何寄存器截断为32位吗?如果传递上半部分非零的指针args会发生什么?

如果你传递32位指针会有效吗?

1 个答案:

答案 0 :(得分:36)

TL:DR int 0x80在正确使用时有效,只要任何指针适合32位(堆栈指针不适合) 。此外, strace解码错误,解码寄存器内容,就好像它是64位syscall ABI一样。 (有no simple/reliable way for strace to tell, yet。)

int 0x80 zeros 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位用户空间{{1}相同的函数指针表使用。 (这些指针是针对内核中本机64位实现的int 0x80实现或包装器。系统调用实际上是跨用户/内核边界的函数调用。)

仅传递低32位的arg寄存器。 sys_whatever - rbx的上半部分被保留,但被rbp系统调用忽略。请注意,将错误的指针传递给系统调用并不会。导致SIGSEGV;而是系统调用返回int 0x80。如果您没有检查错误返回值(使用调试器或跟踪工具),它将显示为无提示失败。

所有寄存器(当然除了eax)都被保存/恢复(包括RFLAGS和整数寄存器的高32),除了 r8-r11归零。在{x86-64 SysV ABI的函数调用约定中调用保留-EFAULT,因此在64位中被r12-r15置零的寄存器是&#的调用被破坏的子集34;新"注册AMD64添加。

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

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

void *mmap()不同,它保留sysenter的原始值,因此它以与调用它相同的模式返回用户空间。(使用cs导致内核将sysenter设置为cs,为32位代码段选择一个描述符。)

对于64位进程,

$__USER32_CS错误地解码strace 。它解码好像该进程使用了​​int 0x80而不是syscallThis can be very confusing。例如由于int 0x80打印strace / write(0, NULL, 12 <unfinished ... exit status 1> eax=1,实际上是int $0x80,而不是_exit(ebx)

只要所有参数(包括指针)都适合寄存器的低32 ,

write(rdi, rsi, rdx)就会起作用。静态代码和数据在默认代码模型中就是这种情况(&#34; small&#34;)in the x86-64 SysV ABI。 (第3.5.1节 :已知所有符号都位于int 0x800x00000000 范围内的虚拟地址中,因此您可以执行0x7effffff之类的内容(AT&amp; T { {1}})使用5字节指令获取指向寄存器的指针。)

position-independent executables的情况,许多Linux发行版现在默认配置mov edi, hello (并且enable ASLR the "gap" between the upper and lower ranges of canonical addresses 3}}可执行文件)。例如,我在Arch Linux上编译了一个mov $hello, %edi,并在main的开头设置了一个断点。传递给gcc的字符串常量为hello.c,因此32位ABI puts系统调用不起作用。 (默认情况下,GDB会禁用ASLR,因此如果从GDB内部运行,则始终会在运行中看到相同的地址。)

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

它在内核中的工作原理:

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

int 0x80定义了64位内核的本机入口点,其中包括来自a custom code-segment descriptor进程的中断/错误处理程序和entry_64.S本机系统调用。

syscall定义从compat模式到64位内核的系统调用入口点,以及64位进程中entry_64_compat.S的特殊情况。 (64位进程中的int 0x80也可以转到该入口点,但它会推送sysenter,因此它将始终以32位模式返回。)其中有32位AMD CPU支持的$__USER32_CS指令的版本,Linux也支持32位进程的快速32位系统调用。

我想64位模式下syscall可能的用例是您要使用int 0x80安装的iretmodify_ldt推送段寄存器本身以用于Linux 4.12's entry_64_compat.S,Linux始终通过int 0x80int 0x80系统调用返回。 64位iret入口点将syscallpt_regs->cs设置为常量->ss__USER_CS。 (SS和DS使用相同的段描述符是正常的。权限差异是通过分页而不是分段来完成的。)

__USER_DS将入口点定义为32位内核,完全不参与。

  

struct pt_regs中的entry_32.S入口点:

int 0x80

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

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

我认为r8-r11的归零是为了匹配历史行为。在with only a mov %r10, %rcx提交之前,入口点仅保存了C call-clobbered寄存器。它使用syscall32直接从asm发送,并且这些函数遵循调用约定,因此它们保留call *ia32_sys_call_table(, %rax, 8)rbxrbprsp。将r12-r15归零而不是将它们保留为未定义可能是避免来自内核的信息泄漏的一种方法。 IDK如何处理r8-r11如果用户空间的调用保留寄存器的唯一副本在C函数保存它们的内核堆栈上。我怀疑它是否使用堆栈展开元数据在那里找到它们。

当前实现(Linux 4.12)从C调度32位ABI系统调用,从ptrace重新加载已保存的ebxecx等。 (64位本机系统调用直接从asm调度,do_syscall_32_irqs_on(struct pt_regs *regs)需要考虑函数与pt_regs之间调用约定的微小差异。不幸的是,它不能始终使用syscall ,因为CPU错误会使非规范地址变得不安全。它会尝试,所以快速路径非常快,尽管sysret本身仍然需要数十个周期。)

无论如何,在当前的Linux中,32位系统调用(包括64位的syscall)最终会在Linux 4.12 arch/x86/entry/common.c中结束。它调度到函数指针int 0x80,具有6个零扩展args。这可能避免在更多情况下需要围绕64位本机系统调用函数的包装来保留该行为,因此更多ia32_sys_call_table表条目可以直接作为本机系统调用实现。

  

code here

ia32

在旧版本的Linux中,从asm调度32位系统调用(如64位仍然可以),int80入口点本身将args放入正确的寄存器中,并带有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); mov指令,使用32位寄存器。它甚至使用xchg将EDX零扩展到RDX(因为arg3碰巧在两个约定中使用相同的寄存器)。 gdbgui。此代码在mov %edx,%edxsysenter入口点重复。

简单示例/测试程序:

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

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

因此所有这些示例都正确使用了ABI,除了第二个write()尝试传递64位指针并将其截断。

如果您将其构建为与位置无关的可执行文件,那么第一个也将失败。 (您必须使用RIP相对int 0x80代替lea才能将mov的地址输入到寄存器中。)

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

请参阅内联hello:评论,说明系统调用如何更改注册

;;;
使用

{{3}}转换为64位静态二进制文件

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

运行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 intel,请运行layout 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。这很容易发生,即使程序不打印到stdout自己。