原因与 64 位模式下的 why RCX is not used for passing parameters to system calls, being replaced with R10 相同:因为 sysenter 和 sysexit 指令的工作方式。即,来自英特尔文档sysexit 指令:
在执行 SYSEXIT 之前,软件必须指定特权级 3 代码段和代码入口点,以及特权级 3 堆栈段和堆栈指针,通过将值写入以下 MSR 和通用
注册:
• 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 指令都不支持在
堆栈。
这很明显,RSP 在sysenter 上被IA32_SYSENTER_ESP 替换,因此操作系统甚至不知道用户空间堆栈应该在哪里,至少这不是易学的。所以 Linux 保留ebp 正是为了这个目的:为操作系统提供用户堆栈。现在调用者必须保存ebp,因为它必须在执行sysenter 之前用esp 覆盖它。
为什么 Linux 不使用 edx 或 ecx 来传递堆栈指针——这两个寄存器不会在 sysenter 上被覆盖?我认为这是为了速度:ebp,当用于在通常的int 0x80 调用中传递参数时,是最后一个可能的(第六个)参数。系统调用很少需要超过 5 个参数,因此无需为几乎所有系统调用读取用户空间堆栈(如果 edx 或 ecx 用于堆栈指针),Linux 只需为具有 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.
*/