使用无效的系统调用号,这样分派代码就会直接返回
eax = -ENOSYS
,而不是调度到任何系统调用处理函数。除非这导致内核使用
iret
慢路径而不是
sysret
/
sysexit
。这可能解释了
测量结果,显示无效号码比
syscall(SYS_getpid)
慢 17 个周期,因为 glibc 错误处理(设置
errno
)可能无法解释它。但是从我阅读内核源代码的情况来看,我没有看到为什么在返回
-ENOSYS
的同时不使用
sysret
。
这个答案是针对sysenter
而不是syscall
。原来的问题中提到了sysenter
/sysret
(这很奇怪,因为sysexit
与sysenter
配合使用,而sysret
与syscall
配合使用)。我基于在x86-64内核上32位进程的sysenter
回答了这个问题。
本机64位syscall
在内核中处理更有效率。(更新:通过Meltdown / Spectre缓解补丁,它仍然通过C do_syscall_64
在4.16-rc2中分派)。
我的如果在64位代码中使用32位int 0x80 Linux ABI会发生什么?问答概述了从兼容模式到x86-64内核(entry_64_compat.S
)的系统调用入口点的内核方面。本答案只涉及相关部分。
该答案和此处的链接指向Linux 4.12源代码,其中不包含Meltdown缓解页表操作,因此会有显着的额外开销。
int 0x80
和
sysenter
有不同的入口点。您需要查找
entry_SYSENTER_compat
。据我所知,即使在64位用户空间进程中执行它,
sysenter
也始终会进入那里。Linux的入口点将常量
__USER32_CS
作为保存的CS值推送,因此它将始终以32位模式返回到用户空间。
在将寄存器推入内核堆栈上的
struct pt_regs
来构造后,有一个
TRACE_IRQS_OFF
钩子(不知道它包含多少条指令),然后是
call do_fast_syscall_32
,它是用C编写的。(本机64位
syscall
分派直接从asm执行,但32位兼容系统调用始终通过C进行分派)。
arch/x86/entry/common.c
中的 do_syscall_32_irqs_on
很轻量级:只是检查进程是否被跟踪(我认为这就是 strace
如何通过 ptrace
钩取系统调用),然后...
...
if (likely(nr < IA32_NR_syscalls)) {
regs->ax = ia32_sys_call_table[nr]( ... arg );
}
syscall_return_slowpath(regs);
}
据我所知,内核在该函数返回后可以使用 sysexit
。
因此,无论 EAX 是否具有有效的系统调用编号,返回路径都是相同的,并且显然在不进行任何分派的情况下返回是通过该函数的最快路径,特别是在具有 Spectre 缓解措施的内核中,其中函数指针表上的间接分支将经过 retpoline 且总是预测错误。
如果你想真正测试 sysenter/sysexit 而没有所有这些额外开销,那么你需要修改 Linux,将一个简化得多的入口点放入其中,而无需检查跟踪或推送/弹出所有寄存器。
你还可能想修改 ABI,在寄存器中传递返回地址(就像 syscall
自己所做的那样)而不是保存在用户空间栈上,这是 Linux 当前的 sysenter
ABI 所做的;它必须使用 get_user()
读取应返回到的 EIP 值。
如果你想测量所有这些开销,那么使用eax并获得
-ENOSYS
就足够了;最坏的情况是,如果分支预测器对该分支进行热处理,基于正常的32位系统调用,你将会多出一个分支未命中的范围检查。