Linux上的int 0x80始终调用32位ABI,无论从什么模式调用它:ebx中的args,ecx ...和/usr/include/asm/unistd_32.h中的syscall编号。 (否则,在未使用CONFIG_IA32_EMULATION编译的64位内核上会崩溃)。

64位代码应使用syscall,其调用编号为/usr/include/asm/unistd_64.h,而args为rdirsi等。请参阅调用约定在i386和x86-64上进行UNIX和Linux系统调用。如果您的问题被标记为与此重复,请参阅该链接以获取有关如何以32位或64位代码进行系统调用的详细信息。如果您想了解到底发生了什么,请继续阅读。

(有关32位和64位sys_write的示例,请参阅在64位Linux上使用中断0x80)


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

相关:Linux系统调用权威指南x86),以了解如何进行int 0x80sysenter 32位系统调用,或syscall 64位系统调用,或调用vDSO进行gettimeofday之类的“虚拟”系统调用。加上有关系统调用的全部内容的背景信息。


使用int 0x80可以编写可以在32或64位模式下汇编的内容,因此最后使用exit_group()十分方便微型基准之类的东西。

有关标准化功能和syscall调用约定的i386和x86-64 System V正式psABI官方文档的最新PDF链接,网址为https://github.com/hjl-tools/ x86-psABI / wiki / X86-psABI。

有关初学者指南,x86手册,官方文档以及性能优化指南/资源,请参见x86标签Wiki。


但是,由于人们一直在用在64位代码中使用int 0x80的代码发布问题,或者意外地从为32位编写的源构建64位二进制文​​件,所以我想知道在当前的Linux上究竟会发生什么?

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

如果传递32位的指针会起作用吗?

评论

@IwillnotexistIdonotexist:破坏r8-r11是最不重要的原因! #1是:仅支持32位args /返回值。 #2是:strace会将其解码为错误,因此很难调试。 #3是:性能低下。 #4是:寄存器args与x86-64函数调用不匹配,并且它使用ebx保留调用。 (排名因使用案例/初学者而异,但我认为所有这些总是比清除r8-r11更为重要。)无论如何,我会考虑一个更好的介绍。

#1 楼

TL:DR:只要正确使用32位(堆栈指针不适合)的指针,int 0x80即可正常工作。但是请注意,除非您使用的是最新的strace +内核,否则strace会将其解码为错误。

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

并非所有系统甚至都支持int 0x80:Linux的Windows子系统(WSL)严格仅是64位的:int 0x80根本不起作用。也可以在没有IA-32仿真的情况下构建Linux内核。 (不支持32位可执行文件,不支持32位系统调用。)


详细信息:保存/还原了什么,内核使用了哪些部分进行管理

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

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

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

通过对内核内部实现寄存器保存的方式进行一些内部更改,保留了此行为,并且内核中的注释提到它可以在64位上使用,因此此ABI可能是稳定的。 (即,您可以指望r8-r11被清零,并且所有其他内容都将保留。)

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

sysenter不同,它保留了cs的原始值,因此以与调用它时相同的方式返回到用户空间。(使用sysenter会导致内核将cs设置为$__USER32_CS,从而为32位代码段选择一个描述符。)


较旧的strace对于64位进程错误地解码了int 0x80。它进行解码,就好像该进程已使用syscall而不是int 0x80一样。这可能非常令人困惑。例如strace打印write(0, NULL, 12 <unfinished ... exit status 1> / eax=1int _exit(ebx)x80,实际上是write(rdi, rsi, rdx),而不是PTRACE_GET_SYSCALL_INFO

我不知道添加int 0x80功能的确切版本,但是Linux内核5.5 / strace 5.5可以处理它。它误导性地表示该进程“以32位模式运行”,但确实解码正确。 (示例)。只要所有参数(包括指针)都适合寄存器的低32位,0x00000000都可以工作。 x86-64 SysV ABI中默认代码模型(“小”)中的静态代码和数据就是这种情况。 (第3.5.1节
:已知所有符号都位于0x7effffffmov edi, hello范围内的虚拟地址中,因此您可以执行mov $hello, %edi(AT&T gcc)之类的操作来将指针指向具有5个字节的寄存器说明)。

但是,与位置无关的可执行文件不是这种情况,许多Linux发行版现在都将hello.c配置为默认生成(它们为可执行文件启用了ASLR)。例如,我在Arch Linux上编译了一个puts,并在main的开始处设置了一个断点。传递给0x555555554724的字符串常量位于write,因此32位ABI rsp系统调用将不起作用。 (默认情况下,GDB禁用ASLR,因此,如果您从GDB内部运行,则每次运行都会看到相同的地址。)

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


如何它可以在内核中运行:

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

syscall定义了64位内核的本机入口点,其中包括中断/错误处理程序和来自的entry_64_compat.S本机系统调用长模式(也称为64位模式)进程。

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

如果您要使用自定义代码,我想modify_ldt在64位模式下可能的用例是-与int 0x80一起安装的段描述符。 iret推送段寄存器本身以供int 0x80使用,Linux总是通过iretsyscall系统调用返回。 64位pt_regs->cs入口点将->ss__USER_CS设置为常数__USER_DSentry_32.S。 (SS和DS使用相同的段描述符是正常的。权限差异是通过分页而不是分段来完成的。)

int 0x80定义了32位内核的入口点,根本没有涉及。


Linux 4.12的entry_64_compat.S中的struct pt_regs入口点:

/*
 * 32-bit legacy system call entry.
 *
 * 32-bit x86 Linux system calls traditionally used the INT 
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);
x80 * instruction. INT
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
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
yasm -felf64 -Worphan-labels -gdwarf2 abi32-from-64.asm
ld -o abi32-from-64 abi32-from-64.o
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 *
(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
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,然后将所有寄存器压入内核堆栈以形成ptrace。这是从系统调用返回时恢复的位置。它采用已保存用户空间寄存器(用于任何入口点)的标准布局,因此,如果其他进程(例如gdb或strace)在系统调用内使用ptrace时使用它们,则它们将读取和/或写入该内存。 (修改寄存器ptrace是一回事,它会使其他入口点的返回路径变得复杂。请参阅注释。)但是,它推送了sysenter而不是r8 / r9 / r10 / r11。 (syscall32和AMD call *ia32_sys_call_table(, %rax, 8)入口点为r8-r15存储零。)

我认为r8-r11的调零与历史行为相符。在为所有compat syscall设置完整的pt_regs之前,入口点仅保存了C调用密集的寄存器。它使用rbx从asm直接调度,并且这些函数遵循调用约定,因此它们保留rbprspr12-r15r8-r11。调零ptrace而不是使其保持未定义状态可能是避免内核泄漏信息的一种方法。如果用户空间的调用保留寄存器的唯一副本位于C函数将其保存的内核堆栈上,则IDK如何处理ebx。我怀疑它是否使用堆栈展开元数据在此处找到它们。

当前实现(Linux 4.12)从C调度32位ABI系统调用,从ecx重新加载保存的pt_regsmov %r10, %rcx等。 。 (64位本机系统调用直接从asm分派,仅需syscall即可解决函数与sysret之间的调用约定中的微小差异。不幸的是,它不能始终使用syscall,因为CPU错误使它在使用非规范时不安全它确实会尝试,所以尽管int 0x80本身仍需要数十个周期,但快速路径还是非常快。)

无论如何,在当前的Linux中,32位系统调用(包括64位的do_syscall_32_irqs_on(struct pt_regs *regs))位)最终以ia32_sys_call_table结尾。它分派给具有6个零扩展args的函数指针ia32。这样可以避免在更多情况下需要围绕64位本机syscall函数进行包装以保留该行为,因此,更多arch/x86/entry/common.c表条目可以直接作为本机系统调用实现。


Linux 4.12 mov

q4312078q


在从asm分派32位系统调用的旧版Linux中(就像64位仍然如此),int80入口点本身使用32位寄存器通过xchgmov %edx,%edx指令将args放入正确的寄存器中。它甚至使用sysenter将EDX零扩展为RDX(因为arg3在两种约定中碰巧使用相同的寄存器)。代码在这里。此代码在syscall32write()入口点中重复。


简单示例/测试程序:

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

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

所以所有这些示例正在正确使用ABI,除了第二个lea尝试传递64位指针并将其截断。

如果将其构建为与位置无关的可执行文件,则第一个也将失败。 (您必须使用相对RIP的mov而不是hello:才能将gdbgui的地址保存到寄存器中。)

我使用了gdb,但请使用您喜欢的任何调试器。使用一个突出显示自上一步以来已更改的寄存器的寄存器。 ;;;非常适合调试asm源,但不适用于反汇编。不过,它确实具有一个至少适用于整数reg的寄存器窗格,并且在此示例中效果很好。

请参阅内联gdb ./abi32-from-64注释,其中描述了系统调用如何更改寄存器

q4312078q

使用
将其构建为64位静态二进制文件

q4312078q

运行gdb。在set disassembly-flavor intel中,如果尚未在layout reg中运行~/.gdbinit.intel_syntax,请运行它们。 (GAS q4312079q就像MASM,而不是NASM,但它们足够接近,如果您喜欢NASM语法,则很容易阅读。)

q4312078q

gdb的TUI模式陷入混乱。即使程序无法自行打印输出,也很容易发生这种情况。

评论


@MatteoItalia:受到这个问题的启发,这个问题要求得到关于以32位模式构建的答案的解释,以解释64位模式的情况。 (错误的系统调用号和错误的strace解码导致非常混乱的结果)。无论如何,这让我感到好奇,并让我想写一个规范的答案。

– Peter Cordes
17年9月7日在6:01



@EOF:是的,疯狂和危险(对于您的过程,而不是系统)。在没有内核帮助的情况下,您已经可以做到这一点,只需使用内核的段选择器的已知值(jumpoverflow.com/questions/34467092/…)进行一次jmp。 AFAIK没什么用,或者在Linux下不支持,但是您可以做到。甚至还有一个Modify_ldt系统调用,它可以让您以“标准”方式进行操作:stackoverflow.com/a/13355668/224132

– Peter Cordes
17年7月7日在6:08

我不相信r8-r11的调零功能可以在旧的未修补内核上得到保证。似乎回想起这曾经是数据泄漏漏洞的一部分,当您让32位程序执行int 0x80然后切换到64位代码并获得对那些寄存器的先前值的访问权限时,我认为它们已修复在归还之前将它们归零。您在回答中提到了它,但是我相信如果您进行搜索,将会发现一个与之相关的实际漏洞。

– Michael Petch
17年11月4日在18:16



我终于找到了安全问题。这里有一篇文章。还有一些演示它的代码。

– Michael Petch
17年11月4日在18:32

@ameed:-38是-ENOSYS。 man7.org/linux/man-pages/man2/getuid.2.html解释说,从Linux 2.4开始,您应该使用getuid32。显然,对旧的getuid调用号的内核支持已删除,因为在Arch Linux桌面上也得到了同样的东西。您将在32位代码中看到具有相同int 0x80系统调用的相同内容,可以在其中跟踪它,并看到glibc使用getuid32,而您的代码使用getuid。 PS,您可以像普通人一样使用“ = a”(ret)。是的,您确实需要在r8..r11上使用clipper,但是在任何低8 regs上都不需要(使用eax作为输出操作数)

– Peter Cordes
18-11-23在23:13