为什么x86-64 Linux系统调用可以设置6个寄存器?

时间:2017-08-13 20:15:01

标签: linux-kernel operating-system x86-64 system-calls abi

我在C中编写一个仅依赖Linux内核的独立程序。

我研究了相关的manual pages并了解到,在x86-64上,Linux系统调用入口点通过七个寄存器rax接收系统调用号和六个参数,{{ 1}},rdirsirdxr10r8

这是否意味着每个系统调用都接受六个参数?

我研究了几个 libc 实现的源代码,以了解它们如何执行系统调用。有趣的是, musl 包含两种不同的系统调用方法:

  1. src/internal/x86_64/syscall.s

    此汇编源文件定义一个r9函数,它将系统调用号和正好六个参数移动到ABI中定义的寄存器。该函数的通用名称暗示它可以与任何系统调用一起使用,尽管它总是将六个参数传递给内核。

  2. arch/x86_64/syscall_arch.h

    此C头文件定义七个单独的__syscall函数__syscallN指定其arity。这表明只传递系统调用所需的确切数量的参数的好处超过了拥有和维护七个几乎相同的函数的成本。

  3. 所以我自己尝试了一下:

    N

    我运行了这个程序并verified它将long system_call(long number, long _1, long _2, long _3, long _4, long _5, long _6) { long value; register long r10 __asm__ ("r10") = _4; register long r8 __asm__ ("r8") = _5; register long r9 __asm__ ("r9") = _6; __asm__ volatile ( "syscall" : "=a" (value) : "a" (number), "D" (_1), "S" (_2), "d" (_3), "r" (r10), "r" (r8), "r" (r9) : "rcx", "r11", "cc", "memory"); return value; } int main(void) { static const char message[] = "It works!" "\n"; /* system_call(write, standard_output, ...); */ system_call(1, 1, message, sizeof message, 0, 0, 0); return 0; } 写入标准输出。这给我留下了以下问题:

    • 为什么我可以传递比系统调用更多的参数?
    • 这是合理的,记录在案的行为吗?
    • 我应该将未使用的寄存器设置为什么?
      • It works!\n好吗?
    • 内核对它没有使用的寄存器有什么作用?
      • 会忽略它们吗?
    • 由于指令较少,七功能是否更快?
      • 这些功能中的其他寄存器会发生什么变化?

1 个答案:

答案 0 :(得分:6)

系统调用接受最多6个参数,传入寄存器(与SysV x64 C ABI几乎相同的寄存器,r10替换rcx,但它们是 callee保留在系统调用案例中),“额外”参数被忽略。

以下问题的一些具体答案。

src/internal/x86_64/syscall.s只是一个“thunk”,它将所有参数都转移到正确的位置。也就是说,它从一个带有系统调用号和另外6个参数的C-ABI函数转换为具有相同6个参数的“系统调用ABI”函数和rax中的系统调用号。它对任意数量的参数都“正常” - 如果不使用这些参数,系统调用将简单地忽略额外的寄存器移动。

因为在C-ABI中所有的参数寄存器都被认为是临时的(即调用者保存),如果你假设从C调用这个__syscall方法,那么破坏它们是无害的。实际上内核对被破坏的寄存器提供了更强的保证,只有rcxr11,所以假设C调用约定是安全但是悲观。特别是,这里实现的调用__syscall的代码将根据C ABI不必要地保存任何参数和临时寄存器,尽管内核承诺保留它们。

arch/x86_64/syscall_arch.h文件几乎是一样的,但在C头文件中。在这里,您需要所有七个版本(对于零到六个参数),因为如果您使用错误数量的参数调用函数,现代C编译器将发出警告或错误。因此,在装配案例中没有“一个函数来统治它们”的实际选择。这样做的好处是可以减少少于6个参数的系统调用。

您列出的问题,已回答:

  • 为什么我可以传递比系统调用更多的参数?

因为调用约定主要是基于寄存器和调用者清理。在这种情况下(包括在C ABI中)总是可以传递更多的参数,而被调用者只会忽略其他参数。由于syscall机制在C和.asm级别是泛型,因此编译器无法确保传递正确数量的参数 - 您需要传递正确的系统调用id 正确的参数数量。如果你传递的更少,内核将看到垃圾,如果你传递更多,它们将被忽略。

  • 这是合理的,记录在案的行为吗?

是的,确定 - 因为整个syscall机制是内核的“通用门”。 99%的时间你不打算使用它:glibc使用正确的签名包装C ABI包装器中绝大多数有趣的系统调用,这样你就不必担心了。这些是系统调用访问安全发生的方式。

  • 我应该将未使用的寄存器设置为什么?

您不会将它们设置为任何内容。如果您使用C原型arch/x86_64/syscall_arch.h,编译器会为您处理它(它不会将它们设置为任何东西),如果您正在编写自己的asm,则不要将它们设置为任何东西(并且您应该假设它们在系统调用之后被破坏了。

  • 内核对它不使用的寄存器有什么作用?

可以自由使用它想要的所有寄存器,但是会遵守内核调用约定,即x86-64上除raxrcxr11之外的所有寄存器的约定。保留(这就是为什么你在C inline asm中的clobber列表中看到rcxr11。)

  • 由于指令较少,七功能是否更快?

是的,但差异非常小,因为reg-reg mov指令通常具有零延迟并且在最近的英特尔架构上具有高吞吐量(最多4个/周期)。因此,移动额外的6个寄存器可能需要1.5个周期来进行系统调用,即使它什么也不做,通常需要至少50个周期。所以影响很小,但可能是可测量的(如果你非常仔细地测量!)。

  • 这些功能中的其他寄存器会发生什么变化?

我不确定你的意思是什么,但其他寄存器可以像所有GP寄存器一样使用,如果内核想要保留它们的值(例如,通过push将它们放在堆栈上然后稍后pop