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
,标志存放在R11
(because 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 成本更低。
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
,标志存放在R11
(because 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 成本更低。