Linux上的int 0x80总是调用32位ABI,不管它是从什么模式调用的:ebxecx中的args...还有/usr/include/asm/unistd_32.h的系统呼叫号码.(或在没有CONFIG_IA32_EMULATION的情况下编译的64位内核上崩溃).

64-bit code should use 101,电话号码是/usr/include/asm/unistd_64.h,args是rdirsi等.参见What are the calling conventions for UNIX & Linux system calls on i386 and x86-64.如果你的问题是重复的,如果你想知道到底发生了什么,继续阅读.

(有关32位与64位sys_write的示例,请参见Using interrupt 0x80 on 64-bit Linux)


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

相关:The Definitive Guide to Linux System Calls (on x86)用于如何进行int 0x80sysenter个32位系统调用,或syscall个64位系统调用,或调用vDSO进行gettimeofday等"虚拟"系统调用.加上系统调用的背景知识.


使用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 tag wiki for beginner guides, x86 manuals, official documentation, and performance optimization guides / resources.


但是,由于人们不断发布使用int 0x80 in 64-bit code位代码的问题,或者意外地使用来自32位源代码的building 64-bit binaries位代码,我想知道what exactly does happen on current Linux?

Does 100 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在正确使用时工作,只要任何指针适合32位(stack pointers don't fit).但是要小心,除非你有一个最近的strace+内核.

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

并非所有系统都支持int 0x80:Linux版本1(WSL1)的Windows子系统严格来说只有64位:int 0x80 doesn't work at all.也可以构建Linux内核without IA-32 emulation.(不支持32位可执行文件,不支持32位系统调用).请参阅this re:确保您的WSL实际上是WSL2(它在VM中使用实际的Linux内核)


详细信息:保存/还原了什么,内核使用了哪个regs的哪些部分

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

只传递arg寄存器的低32位.The upper halves of 100-101 are preserved, but ignored by 102 system calls.注意,向系统调用传递错误指针不会导致SIGSEGV;相反,系统调用返回-EFAULT.如果不(使用调试器或跟踪工具)判断错误返回值,它似乎会自动失败.

所有寄存器(当然,eax除外)都被保存/恢复(包括RFLAG和整数寄存器的上32个),除了r8-r11 are zeroed.在x86-64 SysV ABI的函数调用约定中,r12-r15是保留调用的,因此64位中归零为int 0x80的寄存器是AMD64添加的"新"寄存器的调用中断子集.

这种行为在内核内部如何实现寄存器保存的一些内部更改中得到了保留,内核中的注释提到它可以从64位开始使用,所以这个ABI可能是稳定的.(也就是说,你可以指望r8-r11被归零,而其他一切都被保留.)

返回值经过符号扩展以填充64位rax.这意味着指针返回值(如void *mmap())在64位寻址模式下使用之前需要进行零扩展

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


Older 102 decodes 103 incorrectly用于64位进程.它的解码过程就好像是用了syscall而不是int 0x80.This可以是very confusing.e、 g.straceeax=1/int $0x80打印write(0, NULL, 12 <unfinished ... exit status 1>,实际上是_exit(ebx),而不是write(rdi, rsi, rdx).

我不知道PTRACE_GET_SYSCALL_INFO特性添加的确切版本,但Linux内核5.5/strace 5.5可以处理它.它误导性地说,该过程"以32位模式运行",但解码正确.(Example).


101 works as long as all arguments (including pointers) fit in the low 32 of a register.默认代码模型("小")in the x86-64 SysV ABI中的静态代码和数据就是这种情况.(第3.5.1节)

但是this is not the case for 100, which many Linux distros now configure 102 to make by default(可执行文件是enable ASLR).例如,我在Arch Linux上编译了一个hello.c,并在main的开头设置了一个断点.传递给puts的字符串常量为0x555555554724,因此32位ABI write系统调用无法工作.(默认情况下,GDB禁用ASLR,因此如果从GDB中运行,则每次运行时都会看到相同的地址.)

Linux将堆栈设置为接近the "gap" between the upper and lower ranges of canonical addresses,即堆栈顶部为2^48-1.(或者在启用ASLR的情况下,随机 Select ).所以,在一个典型的静态链接可执行文件中,从rsp_start的条目类似于0x7fffffffe550,这取决于env变量和arg的大小.将该指针截断为esp不会指向任何有效内存,因此,如果试图传递截断的堆栈指针,则带有指针输入的系统调用通常会返回-EFAULT.(如果将rsp截断为esp,然后对堆栈执行任何操作,例如,如果将32位asm源代码构建为64位可执行文件,则程序将崩溃.)


它在内核中的工作方式:

在Linux源代码中,arch/x86/entry/entry_64_compat.S定义了

entry_64.S定义了64位内核的本机入口点,其中包括中断/故障处理程序和来自long mode (aka 64-bit mode)个进程的syscall个本机系统调用.

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

我猜64位模式下的possible use-caseint 0x80是指如果你想使用你安装的a custom code-segment descriptormodify_ldt.int 0x80推送段寄存器本身以供iret使用,Linux总是通过iretint 0x80次系统调用返回.64位syscall入口点将pt_regs->cs->ss设置为常数__USER_CS__USER_DS.(SS和DS使用相同的段描述符是正常的.权限差异是通过分页而不是分段完成的.)

entry_32.S定义32位内核的入口点,完全不涉及.

Linux 4.12's entry_64_compat.S年中的int 0x80个切入点:

/*
 * 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)

代码zero将eax扩展为rax,然后将所有寄存器推送到内核堆栈上,形成一个struct pt_regs.当系统调用返回时,它将从这里恢复.它是一个标准的布局,用于保存用户空间寄存器(对于任何入口点),因此来自其他进程(如gdb或strace)的ptrace将在系统调用中使用ptrace时读取和/或写入该内存.(ptrace寄存器的修改是使其他入口点的返回路径变得复杂的一件事.请参阅注释.)

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

我认为r8-r11的归零是为了符合历史行为.在Set up full pt_regs for all compat syscalls次提交之前,入口点只保存了C调用被删除的寄存器.它直接从asm发送call *ia32_sys_call_table(, %rax, 8),这些函数遵循调用约定,因此它们保留rbxrbprspr12-r15.从64位内核到32位用户空间(这可以将jmp扩展到64位代码段,以读取内核留在那里的任何内容),将r8-r11归零而不是不定义它们是to avoid info leaks.

当前的实现(Linux 4.12)从C发送32位ABI系统调用,从pt_regs重新加载保存的ebxecx等.(64位本机系统直接从asm调用dispatch,with only a mov %r10, %rcx需要考虑函数和syscall之间调用约定的微小差异.不幸的是,它不能总是使用sysret,因为CPU错误使非规范地址不安全.它确实try 了,所以快速路径非常快,尽管syscall本身仍然需要几十个循环.)

无论如何,在当前的Linux中,32位系统调用(包括来自64位的int 0x80个系统调用)最终会在do_syscall_32_irqs_on(struct pt_regs *regs)个系统调用中结束.它向函数指针ia32_sys_call_table发送6个零扩展参数.这可能避免了在更多情况下需要64位本机syscall函数的包装器来保持这种行为,因此ia32个表项中的更多项可以直接作为本机系统调用实现.

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中(就像在4.151之前仍然是64位的),int80入口点本身使用32位寄存器将arg放入正确的寄存器中,其中包含movxchg条指令.它甚至使用mov %edx,%edx到零将EDX扩展到RDX(因为在两种约定中arg3恰好使用相同的寄存器).code here.该代码在sysentersyscall32入口点重复.

脚注1:Linux4.15(我认为)引入了幽灵/熔毁缓解措施,并对入口点进行了重大修改,使其成为熔毁 case 的蹦床.它还对传入寄存器进行了清理,以避免在调用过程中(当某些Spectre小工具可能运行时)寄存器中出现实际参数以外的用户空间值,方法是存储这些值,将所有值归零,然后调用C包装器,从条目上保存的 struct 重新加载恰好宽度正确的参数.

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


简单示例/测试程序:

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

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

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

如果将其构建为独立于位置的可执行文件,第一个也会失败.(必须使用RIP relative lea而不是mov,才能将hello:的地址输入寄存器.)

我使用了gdb,但可以使用任何你喜欢的调试器.使用一个高亮显示自上一步以来更改的寄存器.gdbgui适用于调试asm源代码,但不适用于反汇编.尽管如此,它确实有一个寄存器窗格,至少对整型regs是有效的,在这个例子中效果很好.

See the inline 100 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转换为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.这种情况很容易发生,即使程序本身不打印到标准输出.

Linux相关问答推荐

Bash脚本用于在远程工作后关闭用户会话

无法下载Centos 7上的存储库的元数据

通过 ssh 在远程计算机上按索引访问数组元素

在 Linux 上的 std::threads 中创建子进程

ln命令报错target not a directory

使用 AWK 过滤 Linux 输出

c++进程状态中的+是什么意思

8 个半小时范围的 Crontab 表达式

使用 awk 将多行文本转换为 CSV

为什么我不能将 Unix Nohup 与 Bash For 循环一起使用?

如何在python中检索进程开始时间(或正常运行时间)

XML 编辑/查看软件

无法覆盖符号链接 RedHat Linux

如何在 Linux 中查找所有以 .rb 结尾的文件?

docker images显示图像,docker rmi表示没有这样的图像或参考不存在

Linux AMD64 中如何使用 fs/gs 寄存器?

qstat 和长作业(job)名称

如何使用 bash 在文件中间添加一行文本?

Linux:用户名不在 sudoers 文件中

使用 SED 将单词的首字母大写