从汇编代码分析可变参数原理
C语言可变参数经常会使用到,比如在写回调函数时,有时候会使用不定参数的函数。另外经常使用的linux printf、sprintf都是不定参数的函数。但是函数是如何解析到这些不定参数的呢?有什么手段可以实现呢?如果我们把这些不定参数都统一放在某个能找到的地方,不就可以根据参数占用的内存大小,找到并一一解析出来。我们都知道函数调用过程中有两大法宝:函数栈和寄存器。实际上不定参数函数解析参数的时候也是使用这两大法宝。
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
double average(int num, ...)
{
double res = 0;
va_list args;
va_start(args, num);
for (int i = 0; i < num; ++i) {
int arg = va_arg(args, int);
res += arg;
}
va_end(args);
return res * 1.0 / num;
}
int main()
{
double res = 0;
res = average(10, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11);
printf("%f\n", res);
return 0;
}
查看main函数的反汇编代码:
int main()
{
...
12f5: 6a 0b pushq $0xb
12f7: 6a 0a pushq $0xa
12f9: 6a 09 pushq $0x9
12fb: 6a 08 pushq $0x8
12fd: 6a 07 pushq $0x7
12ff: 41 b9 06 00 00 00 mov $0x6,%r9d
1305: 41 b8 05 00 00 00 mov $0x5,%r8d
130b: b9 04 00 00 00 mov $0x4,%ecx
1310: ba 03 00 00 00 mov $0x3,%edx
1315: be 02 00 00 00 mov $0x2,%esi
131a: bf 0a 00 00 00 mov $0xa,%edi
131f: b8 00 00 00 00 mov $0x0,%eax
1324: e8 5a fe ff ff callq 1183 <average>
...
...
}
这里只展示了部分相关的汇编代码。可以看到在调用average函数前,会先把函数入参放入栈和寄存器中。函数入参是函数参数列表从右到左依次放入栈中,剩余的6个放入寄存器中(如果没有超过六个则都存放在寄存器中)。

左边的参数表示放在栈中,右边的参数放在寄存器中。
1324: e8 5a fe ff ff callq 1183 <average>
调用average函数,call指令会把下调指令存放到栈中,在average函数返回时,会把指令地址从栈中弹出,赋值给pc指针,继续执行。average函数的汇编代码分成下面几个部分:
1 把存放在寄存器的参数放入栈中
double average(int num, ...)
{
1183: f3 0f 1e fa endbr64
1187: 55 push %rbp
1188: 48 89 e5 mov %rsp,%rbp
118b: 48 81 ec f0 00 00 00 sub $0xf0,%rsp
1192: 89 bd 1c ff ff ff mov %edi,-0xe4(%rbp)
1198: 48 89 b5 58 ff ff ff mov %rsi,-0xa8(%rbp)
119f: 48 89 95 60 ff ff ff mov %rdx,-0xa0(%rbp)
11a6: 48 89 8d 68 ff ff ff mov %rcx,-0x98(%rbp)
11ad: 4c 89 85 70 ff ff ff mov %r8,-0x90(%rbp)
11b4: 4c 89 8d 78 ff ff ff mov %r9,-0x88(%rbp)
...
}
经过上面的操作函数栈变化如下:

在把函数参数入栈的时候使用的是mov指令,因此rbp的值不会改变。
2 取参
取参分为两个部分rbp下面部分的参数和rbp上面部分的参数。此时打印rbp的值为:
(gdb) p /x $rbp
$35 = 0x7fffffffdaa0
取rbp下面部分参数的汇编代码:
1236: 8b 85 30 ff ff ff mov -0xd0(%rbp),%eax
123c: 83 f8 2f cmp $0x2f,%eax
123f: 77 23 ja 1264 <average+0xe1>
1241: 48 8b 85 40 ff ff ff mov -0xc0(%rbp),%rax
1248: 8b 95 30 ff ff ff mov -0xd0(%rbp),%edx
124e: 89 d2 mov %edx,%edx
1250: 48 01 d0 add %rdx,%rax
1253: 8b 95 30 ff ff ff mov -0xd0(%rbp),%edx
1259: 83 c2 08 add $0x8,%edx
125c: 89 95 30 ff ff ff mov %edx,-0xd0(%rbp)
mov -0xc0(%rbp),%rax是把-0xc0(%rbp)存放内容放到rax寄存器中。-0xc0(%rbp)放的是下面参数的基地址。add %rdx,%rax是把基地址加上偏移计算出实际存放的地址。
-0xd0(%rbp)出存放的是每次取参的offset,因为这里每个参数存在8个字节长度的栈上,所以看到add $0x8,%edx,然后再存放会-0xd0(%rbp)中。
取rbp上面部分参数的汇编代码:
1264: 48 8b 85 38 ff ff ff mov -0xc8(%rbp),%rax
126b: 48 8d 50 08 lea 0x8(%rax),%rdx
126f: 48 89 95 38 ff ff ff mov %rdx,-0xc8(%rbp)
-0xc8(%rbp)是存放上部分参数的基地址。mov -0xc8(%rbp),%rax是把基地址放到rax寄存器中。lea 0x8(%rax),%rdx基地址加偏移(大小为0x8)计算等到实际地址。mov %rdx,-0xc8(%rbp)保存该地址,便于下次计算代取参数的实际地址。下面这个图片展示的是栈rbp下面的存放的内容


浙公网安备 33010602011771号