gcc x86_64中调用函数时多传了几个参数会发生什么

文章通过示例和反汇编代码解释了在调用只有3个参数的函数时,即使传入6个参数,程序仍能正常工作的原因。这主要是因为x86_64架构允许在寄存器中传递额外的参数,而未使用的参数并未在函数内部使用。此外,文章提到了Linux内核如何处理系统调用时的参数,即便大多数系统调用不需要6个参数,内核的机制也能确保正确执行。
摘要由CSDN通过智能技术生成

先说结论: 多传几个参数没有任何问题

验证代码如下, 我们先定义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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值