Pet*_*des 36 linux assembly x86-64 system-calls abi
int 0x80
在Linux上总是调用32位ABI,不管是什么模式,这就是所谓的:在args ebx
,ecx
...和系统调用号的/usr/include/asm/unistd_32.h
.(或者在没有编译的64位内核上崩溃CONFIG_IA32_EMULATION
).
64位代码应该使用syscall
,从呼叫号码/usr/include/asm/unistd_64.h
,并在args rdi
,rsi
等见什么调用约定UNIX和Linux系统上的i386和x86-64调用.如果您的问题被打上这样一个重复的,看你怎么说链接,细节应当使32位或64位代码的系统调用. 如果你想了解到底发生了什么,请继续阅读.
sys_write
系统调用比syscall
系统调用快,所以使用本机64位,int 0x80
除非你正在编写多格式机器代码,当执行32或64位时运行相同的机器代码.(syscall
始终以32位模式返回,因此它在64位用户空间中没有用,尽管它是有效的x86-64指令.)
相关:Linux系统的权威指南(在x86上)调用如何进行sysenter
或int 0x80
32位系统调用,或sysenter
64位系统调用,或调用vDSO进行"虚拟"系统调用syscall
.加上有关系统调用的背景知识.
使用gettimeofday
可以编写将以32位或64位模式组合的内容,因此它可以int 0x80
在微基准测试结束时使用.
标准化函数和系统调用约定的官方i386和x86-64 System V psABI文档的当前PDF文件链接自https://github.com/hjl-tools/x86-psABI/wiki/X86-psABI.
有关初学者指南,x86手册,官方文档和性能优化指南/资源,请参阅x86标记wiki.
但是,由于人们不断发布与使用代码的问题exit_group()
在64位代码,或不小心建立64位二进制文件从源代码对于32位写的,我不知道是什么确切不会对当前的Linux怎样呢?
是否int 0x80
保存/恢复所有的64位寄存器?它会将任何寄存器截断为32位吗?如果传递上半部分非零的指针args会发生什么?
如果你传递32位指针它是否有效?
Pet*_*des 40
TL:DR:int 0x80
正确使用时工作,只要任何指针适合32位(堆栈指针不适合).此外,strace
解码错误,解码寄存器内容,就好像它是64位syscall
ABI.(目前还没有简单/可靠的方式strace
来讲述.)
int 0x80
零点r8-r11,并保留其他所有内容.使用它与32位代码完全一样,使用32位呼叫号码.(或者更好,不要使用它!)
并非所有系统都支持int 0x80
:Windows Ubuntu子系统仅限64位:int 0x80
根本不起作用.也可以在没有IA-32仿真的情况下构建Linux内核.(不支持32位可执行文件,不支持32位系统调用).
int 0x80
使用eax
(不是完整的rax
)作为系统调用号,调度到32位用户空间int 0x80
使用的同一个函数指针表.(这些指针是sys_whatever
内核中本机64位实现的实现或包装器.系统调用实际上是跨用户/内核边界的函数调用.)
只传递低32位的arg寄存器. 上半部分rbx
- rbp
被保留,但被int 0x80
系统调用忽略. 请注意,将错误的指针传递给系统调用不会导致SIGSEGV; 而是系统调用返回-EFAULT
.如果不检查错误返回值(使用调试器或跟踪工具),它将显示为静默失败.
除了r8-r11归零外,所有寄存器(当然除了eax)都被保存/恢复(包括RFLAGS和整数寄存器的高32). r12-r15
在x86-64 SysV ABI的函数调用约定中调用保留,因此int 0x80
64位归零的寄存器是AMD64添加的"新"寄存器的调用破坏子集.
在内核中实现寄存器保存的一些内部更改中保留了这种行为,并且内核中的注释提到它可以从64位使用,因此这个ABI可能是稳定的.(即你可以指望r8-r11被归零,其他一切都被保留.)
返回值被符号扩展以填充64位rax
. (Linux声明32位sys_函数返回signedlong
.)这意味着void *mmap()
在64位寻址模式下使用之前,指针返回值(如from )需要进行零扩展
不像sysenter
,它保留的原始值cs
,所以它返回到在同一模式的用户空间,它被称为英寸(使用sysenter
在内核结果设定cs
到$__USER32_CS
,它选择为一个32位的代码段的描述符.)
strace
int 0x80
64位进程无法正确解码.它解码好像该过程使用syscall
而不是int 0x80
. 这可能非常令人困惑.例如,由于strace
打印write(0, NULL, 12 <unfinished ... exit status 1>
为eax=1
/ int $0x80
,这实际上是_exit(ebx)
,不write(rdi, rsi, rdx)
.
int 0x80
只要所有参数(包括指针)都适合寄存器的低32,就可以工作.这是x86-64 SysV ABI中默认代码模型("小")中的静态代码和数据的情况.(第3.5.1:所有的符号被称为是位于该范围内的虚拟地址0x00000000
到0x7effffff
,所以你可以做一样的东西mov edi, hello
(AT&T mov $hello, %edi
),以获得一个指针与5个字节的指令寄存器).
但是,这是不是对的情况下与位置无关的可执行文件,其中许多Linux发行版现在配置gcc
默认情况下,使(他们启用ASLR的可执行文件).例如,我hello.c
在Arch Linux上编译了一个,并在main的开头设置了一个断点.传递给的字符串常量puts
为at 0x555555554724
,因此32位ABI write
系统调用不起作用.(默认情况下,GDB会禁用ASLR,因此如果从GDB内部运行,则总是会在运行中看到相同的地址.)
Linux将堆栈放在规范地址的上下范围之间的"间隙"附近,即堆栈顶部为2 ^ 48-1.(或者在某个地方随机启用ASLR).因此,rsp
在进入_start
典型的静态链接可执行文件时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位内核的本机入口点,其中包括中断/故障处理程序和syscall
来自长模式(也称为64位模式)进程的本机系统调用.
entry_64_compat.S
定义从compat模式到64位内核的系统调用入口点,以及int 0x80
64位进程中的特殊情况.(sysenter
在64位进程中也可以进入该入口点,但它会推送$__USER32_CS
,因此它将始终以32位模式返回.)有一个32位版本的syscall
指令,AMD CPU支持,Linux支持它也适用于32位进程的快速32位系统调用.
我想一个可能的用例为int 0x80
在64位模式是,如果你想使用一个自定义代码段的描述符,你安装modify_ldt
. int 0x80
推送段寄存器本身以供使用iret
,Linux始终通过int 0x80
系统调用返回iret
.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
:Run Code Online (Sandbox Code Playgroud)/* * 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
和AMD syscall32
入口点为r8-r15存储零.)
我认为r8-r11的这个归零是为了匹配历史行为.在为所有compat系统调用提交设置完整的pt_regs之前,入口点只保存了C调用被破坏的寄存器.它直接从ASM出动call *ia32_sys_call_table(, %rax, 8)
,并且这些功能遵循调用约定,所以他们保留rbx
,rbp
,rsp
,和r12-r15
.归零r8-r11
而不是将它们保留为未定义可能是避免来自内核的信息泄漏的一种方法.IDK如何处理ptrace
用户空间的调用保留寄存器的唯一副本是在C函数保存它们的内核堆栈上.我怀疑它是否使用堆栈展开元数据在那里找到它们.
当前的实现(Linux的4.12)调度的32位-ABI系统从C调用,重新加载保存的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个零扩展args.这可能避免在更多情况下需要围绕64位本机系统调用函数的包装器来保留该行为,因此更多的ia32
表条目可以直接是本机系统调用实现.
Linux 4.12
arch/x86/entry/common.c
Run Code Online (Sandbox Code Playgroud)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);
在旧版本的Linux中,从asm调度32位系统调用(如64位仍然可以),int80入口点本身使用32位寄存器将args放入正确的寄存器mov
和xchg
指令.它甚至用于mov %edx,%edx
将EDX零扩展到RDX(因为arg3恰好在两种约定中使用相同的寄存器). 代码在这里.此代码是在复制sysenter
和syscall32
切入点.
我写了一个简单的Hello World(在NASM语法中),它将所有寄存器设置为非零上半部分,然后进行两次write()
系统调用int 0x80
,一次使用指向字符串的指针.rodata
(成功),第二次使用指向堆栈的指针(失败-EFAULT
).
然后它将本机64位syscall
ABI用于write()
堆栈中的字符(64位指针),然后再次退出.
因此所有这些示例都正确地使用了ABI,除了第二个int 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
Run Code Online (Sandbox Code Playgroud)
用它构建一个64位静态二进制文件
yasm -felf64 -Worphan-labels -gdwarf2 abi32-from-64.asm
ld -o abi32-from-64 abi32-from-64.o
Run Code Online (Sandbox Code Playgroud)
跑gdb ./abi32-from-64
.在gdb
,运行set disassembly-flavor intel
,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
Run Code Online (Sandbox Code Playgroud)
当gdb的TUI模式搞砸时按下control-L.这很容易发生,即使程序不打印到stdout本身也是如此.
归档时间: |
|
查看次数: |
4783 次 |
最近记录: |