先说结论: 多传几个参数没有任何问题
验证代码如下, 我们先定义2个函数, 一个有6个参数, 一个有3个参数
long func6(long a, long b, long c, long d, long e, long f)
{
return a+b+c+d+e+f;
}
long func3(long a, long b, long c)
{
return a+b+c;
}
然后我们在main
函数中这样调用
- 定义一个
func6()
类型的函数指针 ->f6
- 我们把
func3()
赋值给f6
(强制类型转换, 骗过gcc编译器) - 这时
f6
实际上指向的func3()
函数 - 我们使用6个参数调用
f6
(实际是调用func3()
)
int main(void)
{
long result;
typeof(func6) *f6;
f6 = (typeof(func6)*)&func3;
result = (*f6)(1, 2, 3, 4, 5, 6);
return 0;
}
结果是程序执行完全正常, 我们也得到了正确的结果, 和直接使用3个参数调用func3()
效果一样
为什么会这样
我们反汇编看一下就很容易得出结论了
关键函数反汇编如下, 为了方便阅读我删除了无关紧要的代码
int main(void)
{
f6 = (typeof(func6)*)&func3;
40055e: 48 c7 45 f0 32 05 40 movq $0x400532,-0x10(%rbp)
400565: 00
result = (*f6)(1, 2, 3, 4, 5, 6);
400566: 48 8b 45 f0 mov -0x10(%rbp),%rax
40056a: 41 b9 06 00 00 00 mov $0x6,%r9d
400570: 41 b8 05 00 00 00 mov $0x5,%r8d
400576: b9 04 00 00 00 mov $0x4,%ecx
40057b: ba 03 00 00 00 mov $0x3,%edx
400580: be 02 00 00 00 mov $0x2,%esi
400585: bf 01 00 00 00 mov $0x1,%edi
40058a: ff d0 callq *%rax
long func3(long a, long b, long c)
{
400532: 55 push %rbp
400533: 48 89 e5 mov %rsp,%rbp
400536: 48 89 7d f8 mov %rdi,-0x8(%rbp)
40053a: 48 89 75 f0 mov %rsi,-0x10(%rbp)
40053e: 48 89 55 e8 mov %rdx,-0x18(%rbp)
return a+b+c;
400542: 48 8b 45 f0 mov -0x10(%rbp),%rax
400546: 48 8b 55 f8 mov -0x8(%rbp),%rdx
40054a: 48 01 c2 add %rax,%rdx
40054d: 48 8b 45 e8 mov -0x18(%rbp),%rax
400551: 48 01 d0 add %rdx,%rax
}
我们从main()
函数的汇编看起, 可以看到
f6 = (typeof(func6)*)&func3;
这一句的汇编代码movq $0x400532,-0x10(%rbp)
把func3
的地址$0x400532
保存到了f6中, 下面是函数调用的过程
- 把
func3
函数的地址放到rax
寄存器 - 把第6个参数放到
r9d
寄存器 - 把第5个参数放到
r8d
寄存器 - 把第4个参数放到
ecx
寄存器 - 把第3个参数放到
edx
寄存器 - 把第2个参数放到
esi
寄存器 - 把第1个参数放到
edi
寄存器 - 最后
callq *%rax
跳转到func3
函数
我们再看看func3
函数做了什么
开头的2条指令保存堆栈
push %rbp
mov %rsp,%rbp
关键是后面3条指令
mov %rdi,-0x8(%rbp)
mov %rsi,-0x10(%rbp)
mov %rdx,-0x18(%rbp)
分别把寄存器rdi
, rsi
, rdx
中的参数保存到栈上, 这里的rdi
其实就是上面edi
(详见参考资料)
然后就结束了
return a+b+c;
后面的汇编代码是计算a+b+c
的过程, 与函数调用无关
所以, 虽然我们传入了6个参数, 但是实际上它只使用了前3个
所以, 除了浪费点资源, 没有任何问题
当然这里的例子中函数参数类型是一样的, 如果参数类型不同情况可能略有不同, 但是不影响我们的分析过程
后记
为什么会研究这个看似无聊的问题
因为在看到Linux内核实现系统调用表时一个疑惑: 在int80中断处理函数中, Linux内核根据系统调用号调用对应的系统调用函数时, 都是传递的6个参数
regs->ax = sys_call_table[nr](regs->di, regs->si, regs->dx, regs->r10, regs->r8, regs->r9)
但是我们都知道Linux中的几百个系统调用不全是有6个参数(实际上没有多少系统调用有6个参数), 内核这样操作难道不会出问题?
事实上确实没问题, 而且现在看来内核的这个操作相当高级, 并且非常简洁
参考资料
x86_64及aarch64架构传参规则
https://elixir.bootlin.com/linux/v4.16.18/source/arch/x86/entry/common.c