如果以64位代码使用32位int 0x80 Linux ABI,会发生什么情况? [英] What happens if you use the 32-bit int 0x80 Linux ABI in 64-bit code?

查看:105
本文介绍了如果以64位代码使用32位int 0x80 Linux ABI,会发生什么情况?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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等.请参见UNIX&的调用约定是什么? Linux系统在i386和x86-64上调用.如果您的问题被标记为与此重复,请查看该链接,以详细了解应该如何使用32位或64位代码进行系统调用.确实发生了,请继续阅读.

(有关32位和64位sys_write的示例,请参见 《 Linux系统调用的权威指南》 (在x86上),说明如何进行int 0x80sysenter 32位系统调用,或syscall 64位系统调用或调用vDSO用于虚拟"系统调用,例如gettimeofday.加上有关系统调用的全部内容的背景.


使用int 0x80可以编写以32位或64位模式汇编的内容,因此对于exit_group()在微基准测试末尾或其他内容时非常方便.

标签wiki ,用于初学者指南,x86手册,官方文档以及性能优化指南/资源.


但是,由于人们一直使用使用的代码发布问题c0>(以64位代码显示),或偶然地解决方案

TL:DR :int 0x80在正确使用时有效,只要任何指针都适合32位(堆栈指针不合适).同样, strace解码错误,将寄存器内容解码为好像是64位的syscall ABI. (有没有简单/可靠的方法让strace知道.)

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

并非所有系统甚至都支持int 0x80:Linux的Windows子系统(WSL)严格仅是64位的:而无需IA-32仿真. (不支持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被清零.在x86-64 SysV ABI的函数调用约定中,r12-r15被保留了呼叫,因此在64位中被int 0x80清零的寄存器是AMD64添加的新"寄存器的调用子集. >

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

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

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


对于64位进程,

strace错误地解码了int 0x80 .它解码为好像该进程已使用syscall而不是int 0x80. 为可执行文件启用ASLR ).例如,我在Arch Linux上编译了hello.c,并在main的开始处设置了一个断点.传递给puts的字符串常量位于0x555555554724,因此32位ABI write系统调用将不起作用. (默认情况下,GDB禁用ASLR,因此,如果您从GDB内部运行,则每次运行时总是看到相同的地址.)

Linux将堆栈放在顶部和底部之间的间隙"附近规范地址的范围较小,即堆栈顶部为2 ^ 48-1. (或者是随机的,启用了ASLR).因此,在典型的静态链接可执行文件中,进入_startrsp类似于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位内核定义了本机入口点,其中包括中断/错误处理程序和您随modify_ldt一起安装的自定义代码段描述符 . int 0x80推送段寄存器自身以供 iret 使用,并且Linux总是返回通过iretint 0x80系统调用. 64位syscall入口点将pt_regs->cs->ss设置为常量__USER_CS__USER_DS. (SS和DS使用相同的段描述符是正常的.权限差异是通过分页而不是分段来完成的.)

entry_32.S定义了32位内核的入口点,并且完全没有涉及.

中的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,然后将所有寄存器压入内核堆栈以形成为所有兼容的系统调用设置完整的pt_regs 提交之前,入口点仅保存了C调用密集型寄存器.它使用call *ia32_sys_call_table(, %rax, 8)从asm直接调度,并且这些函数遵循调用约定,因此它们保留了rbxrbprspr12-r15.调零r8-r11而不是使其保持未定义状态可能是避免内核泄漏信息的一种方法.如果用户空间的调用保留寄存器的唯一副本位于C函数将其保存的内核堆栈上,则IDK如何处理ptrace.我怀疑它使用堆栈展开元数据在此处找到它们.

当前实现(Linux 4.12)从C分派32位ABI系统调用,从pt_regs重新加载保存的ebxecx等. (64位本机系统调用直接从asm分派, Linux 4.12 arch/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);

在从asm分派32位系统调用的旧版Linux中(就像64位仍然如此),int80入口点本身使用32位的movxchg指令将args放入正确的寄存器中.位寄存器.它甚至使用mov %edx,%edx将EDX零扩展为RDX(因为arg3在两种约定中都使用相同的寄存器). 在此处编码.此代码在sysentersyscall32入口点中重复.


简单的示例/测试程序:

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

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

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

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

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

请参阅内嵌的;;;注释,其中描述了系统调用如何更改寄存器

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中,如果尚未在~/.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.即使程序无法自行打印输出,这也很容易发生.

int 0x80 on Linux always invokes the 32-bit ABI, regardless of what mode it's called from: args in ebx, ecx, ... and syscall numbers from /usr/include/asm/unistd_32.h. (Or crashes on 64-bit kernels compiled without CONFIG_IA32_EMULATION).

64-bit code should use syscall, with call numbers from /usr/include/asm/unistd_64.h, and args in rdi, rsi, etc. See What are the calling conventions for UNIX & Linux system calls on i386 and x86-64. If your question was marked a duplicate of this, see that link for details on how you should make system calls in 32 or 64-bit code. If you want to understand what exactly happened, keep reading.

(For an example of 32-bit vs. 64-bit sys_write, see Using interrupt 0x80 on 64-bit Linux)


syscall system calls are faster than int 0x80 system calls, so use native 64-bit syscall unless you're writing polyglot machine code that runs the same when executed as 32 or 64 bit. (sysenter always returns in 32-bit mode, so it's not useful from 64-bit userspace, although it is a valid x86-64 instruction.)

Related: The Definitive Guide to Linux System Calls (on x86) for how to make int 0x80 or sysenter 32-bit system calls, or syscall 64-bit system calls, or calling the vDSO for "virtual" system calls like gettimeofday. Plus background on what system calls are all about.


Using int 0x80 makes it possible to write something that will assemble in 32 or 64-bit mode, so it's handy for an exit_group() at the end of a microbenchmark or something.

Current PDFs of the official i386 and x86-64 System V psABI documents that standardize function and syscall calling conventions are linked from https://github.com/hjl-tools/x86-psABI/wiki/X86-psABI.

See the tag wiki for beginner guides, x86 manuals, official documentation, and performance optimization guides / resources.


But since people keep posting questions with code that uses int 0x80 in 64-bit code, or accidentally building 64-bit binaries from source written for 32-bit, I wonder 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?

解决方案

TL:DR: int 0x80 works when used correctly, as long as any pointers fit in 32 bits (stack pointers don't fit). Also, strace decodes it wrong, decoding register contents as if it was the 64-bit syscall ABI. (There is no simple/reliable way for strace to tell, yet.)

int 0x80 zeros r8-r11, and preserves everything else. Use it exactly like you would in 32-bit code, with the 32-bit call numbers. (Or better, don't use it!)

Not all systems even support int 0x80: The Windows Subsystem for Linux (WSL) is strictly 64-bit only: int 0x80 doesn't work at all. It's also possible to build Linux kernels without IA-32 emulation either. (No support for 32-bit executables, no support for 32-bit system calls).


The details: what's saved/restored, which parts of which regs the kernel uses

int 0x80 uses eax (not the full rax) as the system-call number, dispatching to the same table of function-pointers that 32-bit user-space int 0x80 uses. (These pointers are to sys_whatever implementations or wrappers for the native 64-bit implementation inside the kernel. System calls are really function calls across the user/kernel boundary.)

Only the low 32 bits of arg registers are passed. The upper halves of rbx-rbp are preserved, but ignored by int 0x80 system calls. Note that passing a bad pointer to a system call doesn't result in SIGSEGV; instead the system call returns -EFAULT. If you don't check error return values (with a debugger or tracing tool), it will appear to silently fail.

All registers (except eax of course) are saved/restored (including RFLAGS, and the upper 32 of integer regs), except that r8-r11 are zeroed. r12-r15 are call-preserved in the x86-64 SysV ABI's function calling convention, so the registers that get zeroed by int 0x80 in 64-bit are the call-clobbered subset of the "new" registers that AMD64 added.

This behaviour has been preserved over some internal changes to how register-saving was implemented inside the kernel, and comments in the kernel mention that it's usable from 64-bit, so this ABI is probably stable. (I.e. you can count on r8-r11 being zeroed, and everything else being preserved.)

The return value is sign-extended to fill 64-bit rax. (Linux declares 32-bit sys_ functions as returning signed long.) This means that pointer return values (like from void *mmap()) need to be zero-extended before use in 64-bit addressing modes

Unlike sysenter, it preserves the original value of cs, so it returns to user-space in the same mode that it was called in. (Using sysenter results in the kernel setting cs to $__USER32_CS, which selects a descriptor for a 32-bit code segment.)


strace decodes int 0x80 incorrectly for 64-bit processes. It decodes as if the process had used syscall instead of int 0x80. This can be very confusing. e.g. since strace prints write(0, NULL, 12 <unfinished ... exit status 1> for eax=1 / int $0x80, which is actually _exit(ebx), not write(rdi, rsi, rdx).


int 0x80 works as long as all arguments (including pointers) fit in the low 32 of a register. This is the case for static code and data in the default code model ("small") in the x86-64 SysV ABI. (Section 3.5.1 : all symbols are known to be located in the virtual addresses in the range 0x00000000 to 0x7effffff, so you can do stuff like mov edi, hello (AT&T mov $hello, %edi) to get a pointer into a register with a 5 byte instruction).

But this is not the case for position-independent executables, which many Linux distros now configure gcc to make by default (and they enable ASLR for executables). For example, I compiled a hello.c on Arch Linux, and set a breakpoint at the start of main. The string constant passed to puts was at 0x555555554724, so a 32-bit ABI write system call would not work. (GDB disables ASLR by default, so you always see the same address from run to run, if you run from within GDB.)

Linux puts the stack near the "gap" between the upper and lower ranges of canonical addresses, i.e. with the top of the stack at 2^48-1. (Or somewhere random, with ASLR enabled). So rsp on entry to _start in a typical statically-linked executable is something like 0x7fffffffe550, depending on size of env vars and args. Truncating this pointer to esp does not point to any valid memory, so system calls with pointer inputs will typically return -EFAULT if you try to pass a truncated stack pointer. (And your program will crash if you truncate rsp to esp and then do anything with the stack, e.g. if you built 32-bit asm source as a 64-bit executable.)


How it works in the kernel:

In the Linux source code, arch/x86/entry/entry_64_compat.S defines ENTRY(entry_INT80_compat). Both 32 and 64-bit processes use the same entry point when they execute int 0x80.

entry_64.S is defines native entry points for a 64-bit kernel, which includes interrupt / fault handlers and syscall native system calls from long mode (aka 64-bit mode) processes.

entry_64_compat.S defines system-call entry-points from compat mode into a 64-bit kernel, plus the special case of int 0x80 in a 64-bit process. (sysenter in a 64-bit process may go to that entry point as well, but it pushes $__USER32_CS, so it will always return in 32-bit mode.) There's a 32-bit version of the syscall instruction, supported on AMD CPUs, and Linux supports it too for fast 32-bit system calls from 32-bit processes.

I guess a possible use-case for int 0x80 in 64-bit mode is if you wanted to use a custom code-segment descriptor that you installed with modify_ldt. int 0x80 pushes segment registers itself for use with iret, and Linux always returns from int 0x80 system calls via iret. The 64-bit syscall entry point sets pt_regs->cs and ->ss to constants, __USER_CS and __USER_DS. (It's normal that SS and DS use the same segment descriptors. Permission differences are done with paging, not segmentation.)

entry_32.S defines entry points into a 32-bit kernel, and is not involved at all.

The int 0x80 entry point in Linux 4.12's 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)

The code zero-extends eax into rax, then pushes all the registers onto the kernel stack to form a struct pt_regs. This is where it will restore from when the system call returns. It's in a standard layout for saved user-space registers (for any entry point), so ptrace from other process (like gdb or strace) will read and/or write that memory if they use ptrace while this process is inside a system call. (ptrace modification of registers is one thing that makes return paths complicated for the other entry points. See comments.)

But it pushes $0 instead of r8/r9/r10/r11. (sysenter and AMD syscall32 entry points store zeros for r8-r15.)

I think this zeroing of r8-r11 is to match historical behaviour. Before the Set up full pt_regs for all compat syscalls commit, the entry point only saved the C call-clobbered registers. It dispatched directly from asm with call *ia32_sys_call_table(, %rax, 8), and those functions follow the calling convention, so they preserve rbx, rbp, rsp, and r12-r15. Zeroing r8-r11 instead of leaving them undefined was probably a way to avoid info-leaks from the kernel. IDK how it handled ptrace if the only copy of user-space's call-preserved registers was on the kernel stack where a C function saved them. I doubt it used stack-unwinding metadata to find them there.

The current implementation (Linux 4.12) dispatches 32-bit-ABI system calls from C, reloading the saved ebx, ecx, etc. from pt_regs. (64-bit native system calls dispatch directly from asm, with only a mov %r10, %rcx needed to account for the small difference in calling convention between functions and syscall. Unfortunately it can't always use sysret, because CPU bugs make it unsafe with non-canonical addresses. It does try to, so the fast-path is pretty damn fast, although syscall itself still takes tens of cycles.)

Anyway, in current Linux, 32-bit syscalls (including int 0x80 from 64-bit) eventually end up indo_syscall_32_irqs_on(struct pt_regs *regs). It dispatches to a function pointer ia32_sys_call_table, with 6 zero-extended args. This maybe avoids needing a wrapper around the 64-bit native syscall function in more cases to preserve that behaviour, so more of the ia32 table entries can be the native system call implementation directly.

Linux 4.12 arch/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 does), 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.


Simple example / test program:

I wrote a simple Hello World (in NASM syntax) which sets all registers to have non-zero upper halves, then makes two write() system calls with int 0x80, one with a pointer to a string in .rodata (succeeds), the second with a pointer to the stack (fails with -EFAULT).

Then it uses the native 64-bit syscall ABI to write() the chars from the stack (64-bit pointer), and again to exit.

So all of these examples are using the ABIs correctly, except for the 2nd int 0x80 which tries to pass a 64-bit pointer and has it truncated.

If you built it as a position-independent executable, the first one would fail too. (You'd have to use a RIP-relative lea instead of mov to get the address of hello: into a register.)

I used gdb, but use whatever debugger you prefer. Use one that highlights changed registers since the last single-step. gdbgui works well for debugging asm source, but is not great for disassembly. Still, it does have a register pane that works well for integer regs at least, and it worked great on this example.

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 into a 64-bit static binary with

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 if you don't have that in your ~/.gdbinit already. (GAS .intel_syntax is like MASM, not NASM, but they're close enough that it's easy to read if you like NASM syntax.)

(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

Press control-L when gdb's TUI mode gets messed up. This happens easily, even when programs don't print to stdout themselves.

这篇关于如果以64位代码使用32位int 0x80 Linux ABI,会发生什么情况?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆