我正在用 C 编写一个仅依赖于 Linux 内核的独立程序。
我研究了相关manual pages并了解到在 x86-64 上,Linux 系统调用入口点通过七个寄存器 rax
接收系统调用号和六个参数。 , rdi
, rsi
, rdx
, r10
, r8
, 和 r9
.
这是否意味着每个系统调用都接受六个参数?
我研究了几个 libc 实现的源代码,以了解它们如何执行系统调用。有趣的是,musl包含两种不同的系统调用方法:
src/internal/x86_64/syscall.s
这个汇编源文件定义了 一
__syscall
功能移动系统调用号和正好六个参数 到 ABI 中定义的寄存器。函数的通用名称暗示它可以与任何系统调用一起使用,尽管它总是向内核传递六个参数。 arch/x86_64/syscall_arch.h
这个 C 头文件定义了 七分
__syscallN
功能 , 与 N
指定它们的数量。这表明仅传递系统调用所需的确切数量的参数的好处超过了拥有和维护七个几乎相同的函数的成本。 所以我自己试了一下:
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;
}
我运行了这个程序和 verified它确实写了
It works!\n
到标准输出。这给我留下了以下问题:0
好的? 最佳答案
系统调用最多接受 6 个参数,通过寄存器传递(几乎与 SysV x64 C ABI 相同的寄存器,用 r10
替换 rcx
但它们在系统调用情况下被调用者保留),并且“额外”参数被简单地忽略。
以下是对您问题的一些具体回答。src/internal/x86_64/syscall.s
只是一个“thunk”,它把所有的参数都转移到了正确的地方。也就是说,它从带有系统调用号和 6 个以上参数的 C-ABI 函数转换为具有相同 6 个参数和 rax
中的系统调用号的“系统调用 ABI”函数。 .对于任意数量的参数,它都“很好”工作 - 如果不使用这些参数,系统调用将简单地忽略额外的寄存器移动。
由于在 C-ABI 中所有的参数寄存器都被认为是临时的(即调用者保存),如果你假设这个 __syscall
,破坏它们是无害的。方法是从 C 中调用的。实际上内核对被破坏的寄存器做出了更强的保证,仅破坏 rcx
和 r11
所以假设 C 调用约定是安全但悲观的。特别是调用__syscall
的代码尽管内核 promise 保留它们,但在此处实现将不必要地根据 C ABI 保存任何参数和暂存寄存器。arch/x86_64/syscall_arch.h
file 几乎是一样的东西,但在 C 头文件中。在这里,您需要所有七个版本(零到六个参数),因为如果您调用带有错误数量参数的函数,现代 C 编译器会发出警告或错误。因此,在装配案例中,没有真正的选择可以让“一个功能来统治所有这些”。这也有一个优点,即执行少于 6 个参数的工作系统调用。
您列出的问题,已回答:
因为调用约定主要是基于寄存器和调用者清理。在这种情况下(包括在 C ABI 中),您始终可以传递更多参数,而其他参数将被调用者忽略。自
syscall
机制在 C 和 .asm 级别是通用的,编译器没有真正的方法可以确保您传递正确数量的参数 - 您需要传递正确的系统调用 ID 和 正确数量的参数。如果传递的少,内核会看到垃圾,如果传递的多,它们将被忽略。是的,当然 - 因为整个
syscall
机制是进入内核的“通用门”。 99% 的情况下您不会使用它:glibc
使用正确的签名将绝大多数有趣的系统调用包装在 C ABI 包装器中,因此您不必担心。这些是系统调用访问安全发生的方式。你没有将它们设置为任何东西。如果您使用 C 原型(prototype)
arch/x86_64/syscall_arch.h
编译器只是为您处理它(它不会将它们设置为任何内容),如果您正在编写自己的 asm,则不会将它们设置为任何内容(并且您应该假设它们在系统调用之后被破坏了)。可以自由地使用它想要的所有寄存器,但将遵守内核调用约定,即在 x86-64 上除
rax
之外的所有寄存器, rcx
和 r11
被保留(这就是为什么你会在 C 内联汇编的 clobber 列表中看到 rcx
和 r11
)。是的,但差异非常小,因为 reg-reg
mov
在最近的英特尔架构上,指令通常具有零延迟和高吞吐量(高达 4 个/周期)。因此,对于系统调用来说,移动额外的 6 个寄存器可能需要 1.5 个周期,即使它什么都不做,通常也至少需要 50 个周期。所以影响很小,但可能是可以衡量的(如果你非常仔细地衡量!)。我不确定你的意思,但其他寄存器可以像所有 GP 寄存器一样使用,如果内核想要保留它们的值(例如,通过
push
将它们放在堆栈上然后 pop
使用它们之后)。
关于linux-kernel - 为什么 x86-64 Linux 系统调用使用 6 个寄存器集工作?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45664535/