为什么Linux内核在使用Sysenter/sysexit时不恢复所有寄存器?



在linux内核2.6.11中,当使用sysenter进行系统调用时,它几乎与init 0x80相同,使用saveall将所有寄存器推送到内核堆栈上,但在调用完成后,如果未设置相关标志,我们使用sysexit返回,但不恢复已保存在堆栈上的所有寄存器。

某些系统调用可能会更改寄存器值,为什么我们不需要重新记录所有寄存器

我读过相应的i386文档,上面写着

Intel386上的所有寄存器都是全局的,因此对调用函数和被调用函数都可见。寄存器%ebp、%ebx、%edi、%esi和%esp"属于"调用函数。换句话说,被调用函数必须为其调用者保留这些寄存器的值。剩余的寄存器"属于"如果调用函数想在函数调用中保留这样的寄存器值,它必须将该值保存在本地堆栈帧中

因此,glibc包装器函数有责任进行保存工作,我已经阅读了一些glibc代码来确保这一点。因此,当使用sysenter/sysexit进行系统调用时,我们首先在用户堆栈上推送%ebp、%edx、%ecx由于%edx和%ecx不在保留寄存器中,我们需要稍后在完成系统调用后恢复它们我们还使用%ebp在调用系统服务例程之前保存用户堆栈指针,因此我们需要将其还原以传递参数

原因与RCX不用于向系统调用传递参数的原因相同,在64位模式下被R10取代:因为sysentersysexit指令的工作方式。也就是说,从英特尔文档上的sysexit指令:

在执行SYSEXIT之前,软件必须通过将值写入以下MSR和通用程序来指定权限级别3代码段和代码入口点,以及权限级别3堆栈段和堆栈指针寄存器:

•IA32_SYSENTER_CS(MSR地址174H)—包含用于确定段的32位值权限级别3代码和堆栈段的选择器(请参阅操作部分)

RDX--此寄存器中的规范地址加载到RIP中(因此,此值引用第一条指令将在用户代码中执行)。如果返回不是64位模式,则只加载位31:0。

ECX--此寄存器中的规范地址加载到RSP中(因此,此值包含特权级别3堆栈)。如果返回不是64位模式,则只加载位31:0。

因此rdx(edx)和rcx(ecx)由指令保留。那么ebp呢?好吧,从sysenter指令的文档来看:

SYSENTER和SYSEXIT指令是伴随指令,但它们不构成调用/返回对。当执行SYSENTER指令时,处理器不保存用户代码的状态信息(例如指令指针),并且SYSENTER和SYSEXIT指令都不支持在堆栈

这一点很明显,因为sysenter上的RSPIA32_SYSENTER_ESP取代了,所以操作系统甚至不知道用户空间堆栈应该在哪里,至少这对学习来说不是微不足道的。因此Linux保留ebp正是为了这个目的:为操作系统提供用户堆栈。现在调用者必须保存ebp,因为在执行sysenter之前必须用esp覆盖它。

为什么Linux没有将edxecx用于传递堆栈指针——这两个寄存器在sysenter上没有被覆盖?我认为这是为了速度:ebp,在通常的int 0x80调用中用于参数传递时,是最后一个可能的(第六个)参数。系统调用很少需要超过5个参数,因此Linux不必为几乎所有的系统调用读取用户空间堆栈(如果堆栈指针使用edxecx),而只需为具有6个参数的系统调用执行此操作。(请注意,在执行sysenter之前,必须最后推ebp——这正是因为内核必须知道在哪里可以找到第六个参数)。

这一切都在Linux源代码arch/x86/entry/vdso/vdso32/sysenter.S:中进行了总结

/*
* The caller puts arg2 in %ecx, which gets pushed. The kernel will use
* %ecx itself for arg2. The pushing is because the sysexit instruction
* (found in entry.S) requires that we clobber %ecx with the desired %esp.
* User code might expect that %ecx is unclobbered though, as it would be
* for returning via the iret instruction, so we must push and pop.
*
* The caller puts arg3 in %edx, which the sysexit instruction requires
* for %eip. Thus, exactly as for arg2, we must push and pop.
*
* Arg6 is different. The caller puts arg6 in %ebp. Since the sysenter
* instruction clobbers %esp, the user's %esp won't even survive entry
* into the kernel. We store %esp in %ebp. Code in entry.S must fetch
* arg6 from the stack.
*
* You can not use this vsyscall for the clone() syscall because the
* three words on the parent stack do not get copied to the child.
*/

这应该由所使用的ABI(调用约定)定义。有些寄存器在函数调用之间保留,而有些则不保留。您可以查看平台上使用的ABI。

对于X64,http://x86-64.org/documentation/abi.pdf记录。见图3.4

保留跨调用意味着寄存器被调用者保存,因此函数应在返回之前将其还原;

未保存表示调用程序已保存,因此函数可以直接使用它,但不能还原它。

最新更新