在包括C在内的大多数语言中,堆栈用于函数调用。这就是为什么你会得到一个";堆栈溢出";如果您在递归中不小心,则会出现错误。(双关语并非有意)。
如果这是真的,那么asmlinkage
GCC指令有什么特别之处。
上面写着,来自#kernelnewbies
asmlinkage标签是我们应该注意的另一件事这个简单的函数。这是告诉一些gcc魔术的#define函数不应期望在其寄存器中的参数(一种常见的优化),但仅在CPU上堆栈
我的意思是,我不认为寄存器用于正常的函数调用。
更奇怪的是,当您了解到它是使用x86上的GCCregparm
函数属性实现的。
regparm
的文档如下:
在x86-32目标上,regparm属性导致编译器通过如果参数为中的整型,则从1到数字寄存器EAX、EDX和ECX,而不是在堆栈上。
这基本上是在说与asmlinkage
正在尝试做的相反的事情
那么会发生什么呢?它们是在堆栈上还是在寄存器中。
我哪里错了?
信息不是很清楚。
在x86 32位上,asmlinkage
宏扩展到__attribute__((regparam(0)))
,这基本上告诉GCC,不应通过寄存器传递参数(0
是重要部分)。从Linux 5.17开始,x86-32和Itanium64似乎是唯一两个重新定义该宏的体系结构,默认情况下,该宏扩展为完全没有属性。
因此CCD_ 8本身并不意味着";参数在堆栈上传递";。默认情况下,使用正常的调用约定。这包括x86 64位,它遵循System V AMD64 ABI调用约定,通过RDI、RSI、RDX、RCX、R8、R9、[XYZ]MM0–7传递函数参数。
HOWEVER需要做一个重要的澄清:即使没有特殊的__attribute__
来强制编译器使用堆栈中的参数,最新内核版本中的系统调用仍然通过指向pt_regs
结构的指针(在系统调用条目中保存堆栈上保存的所有用户空间寄存器)间接地从堆栈中获取参数。这是通过一组适度复杂的宏(SYSCALL_DEFINEx
)来实现的,这些宏透明地执行所有操作。
因此,技术上,尽管asmlinkage
不会更改调用约定,但参数是而不是在寄存器内传递的,正如人们通过简单查看syscall函数签名所认为的那样。
例如,以下系统调用:
SYSCALL_DEFINE3(something, int, one, int, two, int, three)
{
// ...
do_something(one, two, three);
// ...
}
实际变成(大致):
asmlinkage __x64_sys_something(struct pt_regs *regs)
{
// ...
do_something(regs->di, regs->si, regs->dx);
// ...
}
它编译成类似于:
/* ... */
mov rdx,QWORD PTR [rdi+0x60]
mov rsi,QWORD PTR [rdi+0x68]
mov rdi,QWORD PTR [rdi+0x70]
call do_something
/* ... */
至少在i386和x86-64上,asmlinkage
意味着使用没有GCC选项和__attribute__
的标准调用约定。(就像用户空间程序通常用于该目标一样。)
对于i386,这意味着只有堆栈参数。对于x86-64,它仍然是和往常一样的寄存器
对于x86-64,没有差异;内核已经在所有地方使用了AMD64 System V ABI文档中的标准调用约定,因为它设计得很高效,在寄存器中传递前6个整数参数。
但是i386有更多的历史包袱,标准调用约定(i386SysVABI)低效地传递堆栈上的所有arg。据推测,在古代历史的某个时刻,Linux是由GCC使用这种约定编译的,而手工编写的调用C函数的asm入口点已经使用了这种约定。
因此(我猜在这里),当Linux想要从gcc -m32
切换到gcc -m32 -mregparm=3
以构建具有更高效调用约定的内核时,他们可以选择要么同时修改手工编写的asm以使用新约定,要么强制一些特定的C函数仍然使用传统的调用约定,这样手工编写的sm就可以保持不变。
如果他们做出了前一个选择,那么i386的asmlinkage
将是__attribute__((regparm(3)))
,以强制执行该约定,即使内核是以不同的方式编译的。
但是,他们选择保持asm不变,并立即使用堆栈为i386保留#define asmlinkage __attribute__((regparm(0)))
,这实际上是零寄存器args。
我不知道这是否有任何调试好处,比如可以看到哪些参数从asm传递到C函数中,而不会立即修改唯一的副本。
如果-mregparm=3
和相应的属性是新的GCC特性,那么Linus可能希望保留使用旧GCC构建内核的可能性。这将排除将asm更改为需要__attribute__((regparm(3)))
的可能性。他们实际做出的asmlinking=regparm(0)选择还有一个优点,那就是不必修改任何asm,这意味着没有正确性问题,而且在调用约定时使用new(?)可以消除任何可能的GCC错误。
在这一点上,我认为完全可以修改调用asmlinkage
函数的asm代码,并将其交换为regparm(3)。但这是一件很小的事情。现在不值得做,因为32位x86内核在几乎所有的用例中都早已过时。即使使用32位用户空间,您也几乎总是想要64位内核。
如果在系统调用入口点保存寄存器涉及到在最低地址使用EBX保存寄存器,那么堆栈参数甚至可能有效率优势,因为在最低地址,寄存器已经准备好用作函数参数。您将全部设置为call *ia32_sys_call_table(,%eax,4)
。但这实际上并不安全,因为被调用者拥有他们的堆栈参数,并被允许写入它们,尽管GCC通常不使用传入的堆栈参数位置作为暂存空间。所以我怀疑Linux是否会做到这一点。
其他ISAs可以很好地处理asmlinkage在寄存器中传递参数的问题,因此堆栈参数对Linux的工作方式没有什么重要的基础。(可能除了i386特定的代码,但我对此表示怀疑。)
整个"asmclinkage意味着在堆栈上传递args";纯粹是i386的东西
Linux运行的大多数其他ISAs都比32位x86更新(和/或类似RISC,有更多寄存器),并且有一个标准的调用约定,在现代CPU中更高效,在前几个参数中使用寄存器。其中包括x86-64。