Linux 内核中的函数传参规约

一个月前的6月27号,在一个内核技术交流群里有朋友提出了一个问题,详细情况如下:

在《Linux 源代码内核情景分析》的第 372 页,有一段进程切换的汇编函数 switch_to,在这里面调用了一个C语言函数 __switch_to(374 页),这个C语言函数需要接受两个参数,奇怪的是在汇编代码里看不到这两个参数,虽然有一次压栈动作,但那是为 __switch_toreturn 语句准备的,那这个C语言函数的两个参数到底是哪里传过来的呢。

这使我想起了在《Linux Inside》里面有一节提到的一种传参方式,名字叫 fastcall calling conventions,也就是使用 axdxcx 这三个寄存器来传递第一二三个参数。如果真是这样的话,那就可以解释这个函数的参数传递了,因为这段汇编的输入数据恰好是放在 EAXEDX 寄存器里的,而且数据类型和含义也与C语言函数 __switch_to 相匹配。可是这种传参方式我当初刚看到时就用 Google 搜索了很久,根本找不到任何依据,反而找到了反驳它的资料。任何地方的任何讲述调用规约的资料都是说 fastcall calling conventions 是使用 ECXEDX 传递前两个参数,剩下的使用堆栈传递,这跟《Linux Inside》里讲的完全不一样,那时我就非常的矛盾,只不过当时忙着往下看书没有继续追查,这次一定要搞明白到底是怎么回事。

我继续搜索着资料,然后就搜到了 GCC 的一节在线文档,文档中对 fastcall 的解释和其他资料一样,完全找不到使用过 EAX 的痕迹,我更加坚信《Linux Inside》写错了。我随手在页面上搜索了 EAX 这个关键词,结果看到了 regparm (number) 这个语法的解释段落里连续三次提到了 EAX 寄存器,开头一句话是这么写的:

On x86-32 targets, the regparm attribute causes the compiler to pass arguments number one to number if they are of integral type in registers EAX, EDX, and ECX instead of on the stack.

看完我非常兴奋,这不是就是 switch_to 的传参过程吗,原来是这么设置的。

但紧接着问题又来了,如果真的用了这个属性,那么必然有类似 regparm (3) 这样的属性标识,然而我并没有看到代码里有任何地方用了这个属性,那这种传值方式到底是如何生效的呢,难不成这是 GCC 默认的设置?我在内核的 GitHub 仓库里搜了下,只在一个头文件里看到了 regparm(3),在其他文件里还有几处 regparm(0),显然,这个属性并没有发挥多大作用。于是我又开始了一番 Google 搜索,最终找到了一个 Stack Overflow 的问答,答案里提到,除了使用 __attribute__(regparm(3)) 这种方式设置传参属性外,还有一种就是给 GCC 传递 -mregparm=3 选项。于是我在内核的 GitHub 仓库里搜索了一番,果然在 Makefile 文件里找到了这个选项。很明显,这个选项才是将内核的汇编-C语言传参方式设置成使用 EAXEDXECX 的根本途径。

至此,问题解决。另外,根据上面那份 GCC 文档可以看出,我们最熟悉的通过堆栈传参的方式,其名字叫做 cdecl

Show Comments