x86_64 Linux 函数和系统调用之间的 ABI 差异

Difference in ABI between x86_64 Linux functions and syscalls

x86_64 SysV ABI 的函数调用约定定义了要在 rcx 寄存器中传递的整数参数 #4。另一方面,Linux 内核系统调用 ABI 使用 r10 来达到同样的目的。对于函数和系统调用,所有其他参数都在相同的寄存器中传递。

这导致了一些奇怪的事情。例如,查看 x32 平台的 glibc 中 mmap 的实现(存在相同的差异):

00432ce0 <__mmap>:
  432ce0:       49 89 ca                mov    %rcx,%r10
  432ce3:       b8 09 00 00 40          mov    [=12=]x40000009,%eax
  432ce8:       0f 05                   syscall

所以所有寄存器都已经到位,除了我们将 rcx 移动到 r10

我想知道为什么不将系统调用 ABI 定义为与函数调用 ABI 相同,考虑到它们已经如此相似。

AMD 的 syscall 破坏了 rcx 寄存器,因此使用 r10 代替。

syscall instruction 旨在提供一种更快的进入 Ring-0 以执行系统调用的方法。这是对旧方法的改进,旧方法是引发软件中断(int 0x80 on Linux)。

指令速度更快的部分原因是因为它不更改内存,甚至不更改 rsp 以指向内核堆栈。与软件中断不同,在软件中断中 CPU 被强制允许 OS 恢复操作而不会破坏任何东西,对于此命令,CPU 被允许假设软件知道正在发生某些事情这里。

特别是,syscall 在寄存器中存储用户-space 状态的两部分。调用后的RIP到return存放在rcx,标志存放在R11because RFLAGS is masked with a kernel-supplied value before entry to the kernel)。这意味着这两个寄存器都被指令破坏了。

因为它们被破坏了,系统调用 ABI 使用另一个寄存器而不是 rcx,因此使用 r10 作为第四个参数。

r10 是一个自然的选择,因为 in the x86-64 SystemV ABI 它不用于传递函数参数,并且函数不需要保留其调用者的值 r10。因此,系统调用包装函数可以 mov %rcx, %r10 而无需任何 save/restore。对于 6-arg 系统调用和 SysV ABI 的函数调用约定,这对于任何其他寄存器都是不可能的。


顺便说一句,32 位系统调用 ABI 也可以通过 sysenter 访问,这需要 user-space 和 kernel-space 之间的合作才能允许 return在 sysenter 之后转到用户 space。 (即在 运行 sysenter 之前在 user-space 中存储一些状态)。这比 int 0x80 的性能更高,但很笨拙。 glibc 仍然使用它(通过跳转到 vdso 页面中的 user-space 代码,内核映射到每个进程的地址 space)。

AMD 的 syscall 是另一种实现与英特尔 sysenter 相同想法的方法:通过不保留所有内容来使内核的 entry/exit 成本更低。