Linux上的 int 0x80
总是调用32位ABI,无论它调用什么模式: ebx
中的args, ecx
,...和来自 /usr/include/asm/unistd_32.h
的系统调用号 . (或者在没有 CONFIG_IA32_EMULATION
的情况下编译的64位内核崩溃) .
64-bit code should use syscall ,电话号码来自 /usr/include/asm/unistd_64.h
,args位于 rdi
, rsi
等 . 请参阅What are the calling conventions for UNIX & Linux system calls on i386 and x86-64 . 如果您的问题被标记为重复, see that link for details on how you should make system calls in 32 or 64-bit code. 如果您想了解究竟发生了什么,请继续阅读 .
syscall
系统调用比 int 0x80
系统调用快,因此请使用本机64位 syscall
,除非您正在编写在执行32或64位时运行相同的多语言机器代码 . ( sysenter
始终以32位模式返回,因此它在64位用户空间中没有用,尽管它是有效的x86-64指令 . )
相关:The Definitive Guide to Linux System Calls (on x86)有关如何进行 int 0x80
或 sysenter
32位系统调用,或 syscall
64位系统调用,或调用vDSO进行"virtual"系统调用,如 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标记wiki .
但是,由于人们不断发布使用int 0x80 in 64-bit code的代码的问题,或者从32位的源代码中意外building 64-bit binaries,我想知道 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?
1 回答
TL:DR :
int 0x80
正确使用时有效,只要任何指针适合32位( stack pointers don't fit ) . 另外, strace decodes it wrong ,解码寄存器内容,好像它是64位syscall
ABI . (有no simple/reliable way for strace to tell, yet . )int 0x80
零r8-r11,并保留其他所有内容 . 与32位代码一样使用它,使用32位呼叫号码 . (或者更好,不要使用它!)并非所有系统都支持
int 0x80
:Windows Ubuntu子系统仅严格为64位:int 0x80 doesn't work at all . 也可以构建Linux内核without IA-32 emulation . (不支持32位可执行文件,不支持32位系统调用) .详细信息:保存/恢复的内容,内核使用哪些部分
int 0x80
使用eax
(不是完整的rax
)作为系统调用号,调度到32位用户空间int 0x80
使用的同一个函数指针表 . (这些指针是针对内核中本机64位实现的sys_whatever
实现或包装器 . 系统调用实际上是跨用户/内核边界的函数调用 . )只传递低32位的arg寄存器 . The upper halves of rbx-rbp are preserved, but ignored by int 0x80 system calls. 请注意,将错误指针传递给系统调用不会导致SIGSEGV;而是系统调用返回
-EFAULT
. 如果不检查错误返回值(使用调试器或跟踪工具),它将显示为静默失败 .除了 r8-r11 are zeroed 之外,所有寄存器(当然除了eax)都被保存/恢复(包括RFLAGS和整数寄存器的高32) . _868467_在x86-64 SysV ABI的函数调用约定中被调用保留,因此64位中被
int 0x80
归零的寄存器是AMD64添加的"new"寄存器的调用被破坏的子集 .在内核中实现寄存器保存的一些内部更改中保留了这种行为,并且内核中的注释提到它可以从64位使用,因此这个ABI可能是稳定的 . (即,你可以指望r8-r11被归零,其他一切都被保留 . )
返回值被符号扩展以填充64位
rax
. (Linux declares 32-bit sys_ functions as returning signed long . )这意味着在64位寻址模式下使用之前,指针返回值(如来自void *mmap()
)需要进行零扩展与
sysenter
不同,它保留cs
的原始值,因此它以与调用它相同的模式返回到用户空间 . (使用sysenter
导致内核设置cs
到$__USER32_CS
,它选择32位代码的描述符分割 . )strace decodes int 0x80 incorrectly 用于64位进程 . 它解码好像进程使用了
syscall
而不是int 0x80
. This can be very confusing . 例如因为strace
打印write(0, NULL, 12 <unfinished ... exit status 1>
foreax=1
/int $0x80
,这实际上是_exit(ebx)
,而不是write(rdi, rsi, rdx)
.int 0x80 works as long as all arguments (including pointers) fit in the low 32 of a register . 静态代码和数据在默认代码模型("small")in the x86-64 SysV ABI中就是这种情况 . (第3.5.1节:已知所有符号都位于
0x00000000
到0x7effffff
范围内的虚拟地址中,因此您可以执行mov edi, hello
(AT&Tmov $hello, %edi
)之类的操作来获取指向带有5字节指令的寄存器的指针 .但 this is not the case for position-independent executables, which many Linux distros now configure gcc to make by default (和可执行文件的enable ASLR) . 例如,我在Arch Linux上编译了一个
hello.c
,并在main的开头设置了一个断点 . 传递给puts
的字符串常量为0x555555554724
,因此32位ABIwrite
系统调用无效 . (默认情况下,GDB会禁用ASLR,因此如果从GDB内部运行,则总是会在运行中看到相同的地址 . )Linux将堆栈放在the "gap" between the upper and lower ranges of canonical addresses附近,即堆栈顶部为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位内核的本机入口点,其中包括来自long mode (aka 64-bit mode)进程的中断/故障处理程序和syscall
本机系统调用 .entry_64_compat.S
定义了从compat模式到64位内核的系统调用入口点,以及64位进程中int 0x80
的特殊情况 . (64位进程中的sysenter
也可能会进入该入口点,但它会推送$__USER32_CS
,因此它将始终以32位模式返回 . )有一个32位版本的syscall
指令,在AMD CPU上支持, Linux也支持从32位进程进行快速32位系统调用 .我想在64位模式下
int 0x80
的 possible use-case 是你想使用modify_ldt
安装的a custom code-segment descriptor .int 0x80
推送段寄存器本身用于iret,Linux始终通过iret
从int 0x80
系统调用返回 . 64位syscall
入口点将pt_regs->cs
和->ss
设置为常量,__USER_CS
和__USER_DS
. (SS和DS使用相同的段描述符是正常的 . 权限差异是通过分页而不是分段完成的 . )entry_32.S
将入口点定义为32位内核,并且根本不涉及 .*指示 . INT $ 0x80落在这里 .
*此入口点可由32位和64位程序用于执行
*各种程序和库 . 它也被vDSO使用
*进场方式 . 重新启动的32位系统调用也会回退到INT
*系统调用 .
*这被认为是一条缓慢的道路 . 它不被大多数libc使用
*除了在流程启动期间,在现代硬件上实现 .
...
ENTRY(entry_INT80_compat)
...(请参阅完整源代码的github URL)
代码将eax零扩展到rax,然后将所有寄存器推送到内核堆栈以形成struct pt_regs . 这是从系统调用返回时恢复的位置 . 它是用于保存的用户空间寄存器(对于任何入口点)的标准布局,因此来自其他进程的
ptrace
(如gdb或strace
)将读取和/或写入该内存(如果它们在系统调用中使用ptrace
) . (ptrace
寄存器的修改是使得其他入口点的返回路径复杂化的一件事 . 请参阅注释 . )但它推动
$0
而不是r8 / r9 / r10 / r11 . (sysenter
和AMDsyscall32
入口点为r8-r15存储零 . )我认为r8-r11的这个归零是为了匹配历史行为 . 在Set up full pt_regs for all compat syscalls commit之前,入口点仅保存了C调用被破坏的寄存器 . 它使用
call *ia32_sys_call_table(, %rax, 8)
直接从asm调度,并且这些函数遵循调用约定,因此它们保留rbx
,rbp
,rsp
和r12-r15
. 归零r8-r11
而不是将它们保留为未定义可能是一种避免来自内核的信息泄漏的方法 . IDK如何处理ptrace
如果用户空间的调用保留寄存器的唯一副本是在C函数保存它们的内核堆栈上 . 我怀疑它是否使用堆栈展开元数据在那里找到它们 .当前实现(Linux 4.12)从C调度32位ABI系统调用,从
pt_regs
重新加载保存的ebx
,ecx
等 . (64位本机系统调用直接从asm调度,with only a mov %r10, %rcx需要考虑函数之间调用约定的微小差异和syscall
. 不幸的是它不能总是使用sysret
,因为CPU错误使得它与非规范地址不安全 . 确实尝试过,所以快速路径非常快,尽管syscall
本身仍需要数十个周期 . )无论如何,在当前的Linux中,32位系统调用(包括64位的
int 0x80
)最终会在do_syscall_32_irqs_on(struct pt_regs *regs)中结束 . 它调度到一个函数指针ia32_sys_call_table
,带有6个零扩展args . 这可能避免在更多情况下需要围绕64位本机系统调用函数的包装来保留该行为,因此更多的ia32
表条目可以直接是本机系统调用实现 .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(寄存器);
在旧版本的Linux中,从asm调度32位系统调用(就像64位仍然一样),int80入口点本身使用32位寄存器将args放入正确的寄存器中
mov
和xchg
指令 . 它甚至使用mov %edx,%edx
将EDX零扩展到RDX(因为arg3碰巧在两个约定中使用相同的寄存器) . code here . 此代码在sysenter
和syscall32
入口点重复 .简单示例/测试程序:
我写了一个简单的Hello World(在NASM语法中),它将所有寄存器设置为非零上半部分,然后使用
int 0x80
进行两次write()
系统调用,一次使用指向.rodata
(成功)中字符串的指针,第二次使用指针到堆栈(与-EFAULT
失败) .然后它使用本机64位
syscall
ABI来堆栈中的字符(64位指针),然后再次退出 .因此所有这些示例都正确地使用了ABI,除了第二个
int 0x80
,它试图传递一个64位指针并将其截断 .如果您将其构建为与位置无关的可执行文件,那么第一个也将失败 . (您必须使用RIP相对
lea
而不是mov
来将hello:
的地址输入到寄存器中 . )我使用了gdb,但是使用你喜欢的调试器 . 使用自上一步以来突出显示已更改寄存器的寄存器 . gdbgui适用于调试asm源,但不适合反汇编 . 尽管如此,它确实有一个寄存器窗格,至少适用于整数寄存器,并且在这个例子中效果很好 .
See the inline ;;; comments describing how register are changed by system calls
Build it成64位静态二进制文件
运行
gdb ./abi32-from-64
. 在gdb
中,如果您的~/.gdbinit
中已经没有,请运行set disassembly-flavor intel
和layout reg
. (GAS.intel_syntax
就像MASM,而不是NASM,但如果您喜欢NASM语法,它们很容易阅读 . )当gdb的TUI模式搞砸时按下control-L . 这很容易发生,即使程序不打印到stdout本身也是如此 .