当前位置: 首页 > 知识库问答 >
问题:

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

水恩
2023-03-14

Linux上的<code>int 0x80

64位代码应该使用< code>syscall,调用号来自< code >/usr/include/ASM/unistd _ 64 . h ,args在< code>rdi、< code>rsi等。了解UNIX的调用约定是什么

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

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

相关内容:Linux系统调用(x86上)的权威指南,介绍如何进行< code>int 0x80或< code>sysenter 32位系统调用,或< code>syscall 64位系统调用,或调用vDSO进行“虚拟”系统调用,如< code>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 tag wiki。

但是,由于人们不断发布在64位代码中使用int 0x80的代码的问题,或者意外地从为32位编写的源代码构建64位二进制文件,我想知道当前Linux到底发生了什么?

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

如果将它传递给 32 位指针,它是否有效?

共有1个答案

姜淇
2023-03-14

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

由于某些原因,int 0x80零 r8-r11,并保留其他所有内容。使用方法与 32 位代码中的 32 位呼叫号码完全相同。(或者更好的是,不要使用它!

甚至不是所有的系统都支持< code>int 0x80:Linux版本1的Windows子系统(WSL1)严格来说只是64位的:< code > int 0x 80 根本不起作用。没有IA-32仿真也可以构建Linux内核。(不支持32位可执行文件,不支持32位系统调用)。参见这篇re:确保你的WSL实际上是WSL2(它在一个VM中使用一个实际的Linux内核。)

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

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

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

这种行为在内核内部对寄存器保存的实现方式进行了一些内部更改后得以保留,并且内核中的注释提到它可以从64位开始使用,因此此ABI可能是稳定的。(也就是说,您可以指望 r8-r11 被归零,而其他所有内容都被保留下来。

返回值经过符号扩展以填充 64 位 rax。(Linux 将 32 位sys_函数声明为返回有符号长整型。这意味着指针返回值(如来自 void *mmap())在 64 位寻址模式下使用之前需要为零扩展

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

较旧的 strace 对 64 位进程的 int 0x80进行不正确的解码。它解码时,就好像该进程使用了 syscall 而不是 int 0x80。这可能非常令人困惑。例如,打印写(0, NULL, 12

我不知道添加PTRACE_GET_SYSCALL_INFO功能的确切版本,但Linux内核5.5/strace 5.5处理它。它误导性地说进程“以32位模式运行”,但确实解码正确。(示例)。

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

但是对于位置无关的可执行文件却不是这样,许多Linux发行版现在将gcc配置为默认设置(并且它们为可执行文件启用ASLR)。例如,我在ArchLinux上编译了一个hello. c,并在main的开头设置了一个断点。传递给put的字符串常量位于0x5555554724,因此32位ABIwrite系统调用将不起作用。(GDB默认禁用ASLR,因此如果您从GDB内部运行,则每次运行都会看到相同的地址。)

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

在Linux源代码中,< code > arch/x86/entry/entry _ 64 _ compat。S定义< code >条目(entry_INT80_compat)。32位和64位进程在执行< code>int 0x80时使用相同的入口点。

<code>entry_64.S</code>定义了64位内核的本机入口点,其中包括中断/故障处理程序和长模式(又名64位模式)进程的本机系统调用。

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

我猜64位模式下的< code>int 0x80的一个可能的用例是,如果您想使用随< code>modify_ldt一起安装的自定义代码段描述符。< code>int 0x80推送段寄存器本身,以便与< code>iret一起使用,Linux总是通过< code>iret从< code>int 0x80系统调用返回。64位< code>syscall入口点设置< code>pt_regs-

<代码>条目_32。S定义了进入32位内核的入口点,与此无关。

Linux 4.12的<code>entry_64_compat.s<code>中的<code>int 0x80<code>入口点:

/*
 * 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,然后将所有寄存器推送到内核堆栈上,形成一个< code>struct pt_regs。当系统调用返回时,它将从这里恢复。它是保存用户空间寄存器的标准布局(对于任何入口点),所以当其他进程(如gdb或< code>strace)在系统调用中使用< code>ptrace时,它们将读取和/或写入内存。(< code>ptrace寄存器的修改是使返回路径对于其他入口点变得复杂的一个原因。见评论。)

但它将<code>推送$0</code>而不是r8/r9/r10/r11。(<code>syscenter</code>和AMD<code>syscall32</code>入口点为r8-r15存储零。)

我认为r8-r11的归零是为了匹配历史行为。在为所有compat syscalls提交设置完整pt_regs之前,切入点只保存了C调用阻塞的寄存器。它使用call*ia32_sys_call_table(,%rax,8)直接从ami调度,这些函数遵循调用约定,因此它们保留了rbxrbprspr12-r15。归零r8-r11而不是让它们未定义是为了避免信息从64位内核泄漏到32位用户空间(这可能会远jmp到64位代码段以读取内核留在那里的任何内容)。

当前的实现(Linux 4.12)从C发送32位ABI系统调用,从<code>pt_regs、<code>ecx,等等。(64位本机系统调用直接从asm发送,只需要一个<code>mov%r10,%rcx<code>来解释函数和<code>syscall<code>之间调用约定的微小差异。不幸的是,它不能总是使用<code>sysret<code>因为CPU错误使其不安全,使用非规范地址。它确实尝试了,因此快速路径非常快,尽管<code>系统调用 本身仍然需要几十个周期。)

无论如何,在当前的Linux中,32位系统调用(包括<code>int 0x80结束。它向函数指针ia32_sys_call_table发送6个零扩展参数。这可能会在更多情况下避免在64位本机系统调用函数周围使用包装器来保留该行为,因此更多的ia32表条目可以直接作为本机系统呼叫实现。

Linux4.12arch/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);

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

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

我编写了一个简单的Hello World(在NASM语法中),它将所有寄存器设置为具有非零上半部,然后使用<code>int 0x80系统调用,其中一个带有指向<code>中字符串的指针。rodata(成功),第二个具有指向堆栈的指针(失败时为-默认值)。

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

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

如果您将它构建为独立于位置的可执行文件,第一个也会失败。(您必须使用RIP相关的< code>lea而不是< code>mov将< code>hello:的地址放入寄存器中。)

我使用了gdb,但使用您喜欢的任何调试器。使用一个突出显示自上次单步以来更改的寄存器。gdbgui适用于调试ami源代码,但不适用于反汇编。尽管如此,它确实有一个至少适用于整数regs的寄存器窗格,并且在这个示例中效果很好。

请参阅内联 说明系统调用如何更改寄存器的注释

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 intel and 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。即使程序本身没有打印到标准输出,这种情况也很容易发生。

 类似资料:
  • 问题内容: 在Linux上总是调用32位ABI,不管是什么模式,这就是所谓的:ARGS中,…和系统调用号的。(或者在没有编译的64位内核上崩溃)。 64位代码应该使用,从呼叫号码,并在args ,等见什么是在i386和x86-64 UNIX和Linux系统调用的调用约定 。如果您的问题被打上这样一个重复的, 看你怎么说链接,细节 应当 使32位或64位代码的系统调用。 如果您想了解到底发生了什么,

  • 问题内容: 最近,我一直在对我公司的数据库产品的写入性能进行一些基准测试,并且发现仅切换到64位JVM可以使性能持续提高20-30%。 我不允许详细介绍我们的产品,但基本上它是面向列的数据库,已针对存储日志进行了优化。基准测试包括向其提供几GB的原始日志,并确定分析它们并将其作为结构化数据存储在DB中所需的时间。CPU和I / O的处理非常繁重,尽管很难说是什么比例。 有关设置的一些注意事项: 两

  • 问题内容: 我已经使用Java一段时间了,而我典型的设置新开发机的习惯要求从Oracle站点下载并安装最新的JDK。 今天这引发了一个不寻常的问题, 回想起来,我已经安装了之前的两个版本,并且很高兴将普通的工具链插入(Eclipse)。在我的日常编程中,我不会回想起曾经因为使用64位JRE(或为此目的而针对64位JRE)而不得不以其他方式进行更改或思考的事情。 根据我对64位和32位的理解- 确实

  • 问题内容: 我的电脑正在使用Windows 7 64位。但是将要部署我的jsp Web应用程序的服务器是32位。 我需要在PC上安装32位JDK / JRE才能进行开发吗?我正在使用Eclipse。 非常感谢你。 问题答案: 您绝对不需要安装32位JRE即可进行开发。您构建的Java代码不会跟踪您的64位。(我假设您没有使用JNI,这会使事情变得有些复杂。) 不过,您 可能 需要安装32位JRE进

  • 问题内容: 我正在创建一个非常简单的应用程序,该应用程序可以读取和显示文本文件并进行搜索。 我问自己是否有兴趣向用户提出32位和64位版本。 区别仅在于使用64位版本访问更多的内存堆大小,还是还有其他兴趣? 32位编译程序是否可以在64位JVM上运行(我认为是) 问题答案: 任何 程序的32位和64位版本之间的唯一区别是机器字的大小,可寻址内存的数量以及所使用的操作系统ABI。对于Java,语言规

  • 问题内容: 我想知道x86和x64中的64位长吗? 问题答案: 是。Java 在任何JVM上都是64位,无一例外。所有Java原语类型都是完全可移植的,并且在所有实现中都具有固定的大小。