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

2023-11-25

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

64位代码应该使用syscall,电话号码来自/usr/include/asm/unistd_64.h,和参数rdi, rsi等参见i386 和 x86-64 上的 UNIX 和 Linux 系统调用的调用约定是什么。如果您的问题被标记为与此重复,请参阅该链接以了解有关如何进行操作的详细信息should使用 32 位或 64 位代码进行系统调用。如果您想了解到底发生了什么,请继续阅读。

(例如 32 位与 64 位sys_write, see 在 64 位 Linux 上使用中断 0x80)


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

有关的:Linux 系统调用权威指南(在 x86 上)如何制作int 0x80 or sysenter32 位系统调用,或syscall64 位系统调用,或调用 vDSO 进行“虚拟”系统调用,例如gettimeofday。加上系统调用的背景知识。


Using int 0x80使得编写可以在 32 位或 64 位模式下汇编的东西成为可能,因此对于exit_group()在微基准测试或其他测试结束时。

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

See the x86 tag wiki初学者指南、x86 手册、官方文档和性能优化指南/资源。


但由于人们不断用使用的代码发布问题int 0x80在 64 位代码中,或者不小心构建 64 位二进制文​​件来自为 32 位编写的源代码,我想知道what exactly当前的 Linux 上会发生这种情况吗?

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

如果你向它传递 32 位指针,它会起作用吗?


TL:DR: int 0x80只要使用正确,任何指针都适合 32 位(堆栈指针不适合)。但要注意的是strace解码错误除非你有一个最近的 strace + 内核。

int 0x80零 r8-r11由于原因,并保留其他一切。使用它就像在 32 位代码中一样,使用 32 位调用号。 (或者更好的是,不要使用它!)

并非所有系统都支持int 0x80:适用于 Linux 版本 1 (WSL1) 的 Windows 子系统仅限 64 位:int 0x80根本不起作用。也可以构建 Linux 内核没有 IA-32 仿真任何一个。 (不支持 32 位可执行文件,不支持 32 位系统调用)。看this回复:确保您的 WSL 实际上是 WSL2(它在虚拟机中使用实际的 Linux 内核。)


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

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

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

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

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

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

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


Older strace解码int 0x80不正确地对于 64 位进程。它解码就好像该过程已经使用了syscall代替int 0x80. This can be 很混乱. e.g. strace prints write(0, NULL, 12 <unfinished ... exit status 1> for eax=1 / int $0x80,这实际上是_exit(ebx), not write(rdi, rsi, rdx).

我不知道具体版本在哪里PTRACE_GET_SYSCALL_INFO添加了该功能,但 Linux 内核 5.5 / strace 5.5 可以处理它。它误导性地表示该进程“以 32 位模式运行”,但确实解码正确。 (Example).


int 0x80只要所有参数(包括指针)都位于寄存器的低 32 位即可工作。这是默认代码模型(“小”)中的静态代码和数据的情况在 x86-64 SysV ABI 中。 (第 3.5.1 节 :已知所有符号都位于该范围内的虚拟地址中0x00000000 to 0x7effffff,所以你可以做类似的事情mov edi, hello(美国电话电报公司mov $hello, %edi)以使用 5 字节指令获取指向寄存器的指针)。

But this is not案例为位置无关的可执行文件,许多 Linux 发行版现在都配置了gcc默认设置(和他们启用ASLR对于可执行文件)。例如我编译了一个hello.c在 Arch Linux 上,并在 main 的开头设置断点。传递给的字符串常量puts was at 0x555555554724,所以 32 位 ABIwrite系统调用不起作用。 (GDB 默认情况下禁用 ASLR,因此如果您从 GDB 中运行,则每次运行时您总是会看到相同的地址。)

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


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

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

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

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

我猜一个可能的用例 for int 0x80在 64 位模式下,如果您想使用a 自定义代码段描述符你安装的modify_ldt. int 0x80推送段寄存器本身以供使用iret,Linux 总是从int 0x80系统调用通过iret。 64 位syscall入口点集pt_regs->cs and ->ss到常数,__USER_CS and __USER_DS。 (SS和DS使用相同的段描述符是正常的。权限差异是通过分页而不是分段来完成的。)

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

The int 0x80入口点在Linux 4.12 的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。 (sysenter和AMDsyscall32r8-r15 的入口点存储零。)

我认为 r8-r11 的归零是为了匹配历史行为。之前为所有兼容系统调用设置完整的 pt_regs提交时,入口点仅保存了 C 调用破坏的寄存器。它直接从 asm 发送call *ia32_sys_call_table(, %rax, 8),并且这些函数遵循调用约定,因此它们保留rbx, rbp, rsp, and r12-r15。归零r8-r11而不是让它们未定义以避免信息泄露从 64 位内核到 32 位用户空间(可以远远跳转到 64 位代码段以读取内核留下的任何内容)。

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

无论如何,在当前的 Linux 中,32 位系统调用(包括int 0x80从 64 位开始)最终会出现在do_syscall_32_irqs_on(struct pt_regs *regs)。它分派到函数指针ia32_sys_call_table,带有 6 个零扩展参数。这可能会避免在更多情况下需要围绕 64 位本机系统调用函数进行包装来保留该行为,因此更多ia32表项可以直接由本机系统调用实现。

Linux 4.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);

In older versions of Linux that dispatch 32-bit system calls from asm (like 64-bit still did until 4.151), the int80 entry point itself puts args in the right registers with mov and xchg instructions, using 32-bit registers. It even uses mov %edx,%edx to zero-extend EDX into RDX (because arg3 happen to use the same register in both conventions). code here. This code is duplicated in the sysenter and syscall32 entry points.

脚注 1:Linux 4.15(我认为)引入了 Spectre / Meltdown 缓解措施,并对入口点进行了重大修改,使它们成为崩溃案例的蹦床。它还清理传入的寄存器,以避免在调用期间(当某些 Spectre 小工具可能运行时)寄存器中存在除实际参数之外的用户空间值,方法是存储它们,将所有内容清零,然后调用重新加载正确宽度的 C 包装器入口时保存的结构体中的 args。

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


简单示例/测试程序:

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

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

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

如果将其构建为与位置无关的可执行文件,则第一个也会失败。 (您必须使用 RIP 相关的lea代替mov获取地址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

Build it转换为 64 位静态二进制文件

yasm -felf64 -Worphan-labels -gdwarf2 abi32-from-64.asm
ld -o abi32-from-64 abi32-from-64.o

Run gdb ./abi32-from-64. In gdb, run set disassembly-flavor intel and layout reg如果你的里面没有这个~/.gdbinit已经。 (气体.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。即使程序本身不打印到标准输出,这种情况也很容易发生。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

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

  • 了解 Linux oom-killer 日志

    我的应用程序被 oom killer 杀死了 它是在实时 USB 上运行的 Ubuntu 11 10 无需交换 PC 具有 1 Gig 的 RAM 唯一运行的应用程序 除了所有内置的 Ubuntu 东西 是我的程序 flasherav 请注
  • Grep 递归和计数

    需要在具有大量子目录的目录中搜索文件内的字符串 我在用着 grep c r string here 我怎样才能找到总数量 如何仅输出至少具有一个实例的文件 使用 Bash 的进程替换 这给出了我认为是您想要的输出 如果不是 请澄清问题 gr
  • 在 qemu 中将扇区加载到 RAM

    我编写了一个简单的程序 将扇区 扇区编号 2 加载到 RAM 但什么也没打印 首先 我尝试了以下引导扇区代码 org 0x7c00 mov ax 0x1000 ES BX 1000 0000 mov es ax mov bx 0x00 Lo
  • 在嵌入式系统上将内核控制台发送到哪里?

    我正在开发一个嵌入式系统 该系统当前通过串行端口 1 上的控制台输出启动 Linux 使用启动加载程序中的控制台启动参数 然而 最终我们将使用这个串行端口 内核控制台输出的最佳解决方案是什么 dev null 能否以某种方式将其放在 pty
  • 亚马逊 Linux - 安装 openjdk-debuginfo?

    我试图使用jstack在 ec2 实例上amazon linux 所以我安装了openjdk devel包裹 sudo yum install java 1 7 0 openjdk devel x86 64 但是 jstack 引发了异常j
  • 如何恢复 x86-64 寄存器保存约定

    fibonacci cmpq 1 rdi ja recursive movl 1 eax ret recursive push rbp push r10 movq rdi r10 leaq 2 rdi rdi call fibonacci
  • 从哪里获取 iostream.h

    我正在尝试在 Linux 中做一些事情 但它抱怨找不到 iostream h 我需要安装什么才能获取此文件 这个标准头的正确名称是iostream没有扩展名 如果您的编译器仍然找不到它 请尝试以下操作 find usr include na
  • 测试 xmm/ymm 寄存器是否为零的更快方法?

    It s fortunate that PTEST does not affect the carry flag but only sets the rather awkward ZF also affects both CF and ZF
  • 由于 abi::cxx11 符号导致的链接问题?

    我们最近收到一份报告 因为GCC 5 1 libstdc 和双 ABI http gcc gnu org onlinedocs libstdc manual using dual abi html 它似乎Clang 不知道 GCC 内联名称
  • 如果输入被重定向则执行操作

    我想知道如果我的输入被重定向 我应该如何在 C 程序中执行操作 例如 假设我有已编译的程序 prog 并且我将输入 input txt 重定向到它 我这样做 prog lt input txt 我如何在代码中检测到这一点 一般来说 您无法判
  • NASM:如何正确访问SSD驱动器?

    我需要使用 NASM 16 位代码访问 SSD 驱动器 访问普通硬盘时 需要设置寄存器AX DX CX来选择柱面 磁道 扇区 扇区数 AH 选择读扇区功能 DL 选择驱动器号 CH 选择气缸 DH 选择磁盘上的一侧 CL 选择步入正轨的部门
  • 命名互斥体的 Mono 替代方案

    在 Windows NET 上 命名的互斥体可用于同步多个进程 不幸的是 Mono 在 Linux 上不太支持这一点 他们的发行说明 http www mono project com Release Notes Mono 2 8 Shar
  • 无法连接到 Azure Ubuntu VM - 公钥被拒绝

    我们在 Azure 上使用 Ubuntu VM 一段时间了 很少遇到任何问题 然而 其中一台虚拟机最近出现了问题 出乎意料的是 Ubuntu VM 开始拒绝公钥 ssh i azure key email protected cdn cgi
  • 如何在 Linux 中向热敏打印机发送 ESC/POS 命令

    我正在尝试在热敏打印机上发送 ESC POS 命令 但每当我发送它们时 热敏打印机都会将它们打印为文本 而不是作为命令执行它们 我在 prn 文件中编写这些命令 每当我执行 lp 命令来打印文件时 这些 prn 文件也会被打印 但作为文本
  • 如何使 gcc 为 -fpatchable-function-entry 发出多字节 NOP?

    gcc确实有能力使用多字节用于对齐循环和函数的 NOP 然而当我尝试 fpatchable function entry option https gcc gnu org onlinedocs gcc Instrumentation Opt
  • 如何将命令输出作为多个参数传递给另一个命令

    我想将命令的每个输出作为多个参数传递给第二个命令 例如 grep pattern input returns file1 file2 file3 我想复制这些输出 例如 cp file1 file1 bac cp file2 file2 b
  • 安装 JDK 时出错:keytool 命令需要已安装的 proc fs (/proc)。 Linux 的 Windows 子系统

    我尝试在 Linux 的 Windows 子系统 Ubuntu 14 04 上安装 Oracle JDK 1 7 但出现以下错误 the keytool command requires a mounted proc fs proc Jav
  • 我可以在 Ubuntu 上使用 Homebrew 吗?

    我只是尝试使用 Homebrew 和 Linuxbrew 在我的 Ubuntu 服务器上安装软件包 但都失败了 这就是我尝试安装它们的方法 sudo apt get install build essential curl git m4 r
  • 使用 Python 将阿拉伯语或任何从右到左书写系统的字符串打印到 Linux 终端

    非常简单的例子是 city print city 我期望输出是 但实际上输出是相反的字符串 字母看起来有点不同 因为它们有开始 中间和结束形式 我无法将其粘贴到此处 因为复制粘贴会再次更正字符串的顺序 如何在 Linux 终端上正确打印阿拉
  • 在 Linux 服务器上创建和编辑 MS-Word 文档?

    希望开发处理文档的服务器端应用程序 源文档大多是MS Word 2003 2007 即MS版本的Docx 希望服务器应用程序能够在linux或windows上运行 想知道在linux下读写MS Word文件最好的工具或库是什么 兼容性是最重

随机推荐