Posted on 

to explore c va arg

之前写 go 语言时候发现 go 语言支持可变长参数,且写法与 c 语言类似,就好奇了 c 语言是如何实现可变长参数的。这里参考了[^this]。

C 语言可变参数通过三个宏(va_start、va_end、va_arg)和一个类型(va_list)实现的,

void va_start(va_list ap, paramN);
参数:
ap: 可变参数列表地址
paramN: 确定的参数
功能:初始化可变参数列表 (把函数在 paramN 之后的参数地址放到 ap 中)。

void va_end(va_list ap);
功能:关闭初始化列表 (将 ap 置空)。

type va_arg(va_list ap, type);
功能:返回下一个参数的值。

va_list:存储参数的类型信息。

综合上面 3 个宏和一个类型可以猜出如何实现 C 语言可变长参数函数:用 va_start 获取参数列表 (的地址) 存储到 ap 中,用 va_arg 逐个获取值,最后用 va_arg 将 ap 置空。

使用

使用范例,计算一组 int 数的和:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <stdarg.h>

#define END -1

int va_sum(int first_num, ...)
{
va_list ap;
va_start(ap, first_num);
int result = first_num;
int temp = 0;
while ((temp = va_arg(ap, int)) != END)
result += temp;
va_end(ap);
return result;
}

int main()
{
int sum_val = va_sum(1, 2, 3, 4, 5, END);
printf("%d", sum_val);
return 0;
}

分析

其实可变长参数的实现还是比较简单:不断从栈上根据参数字长取数据,因此不知道边界。

这一点在反汇编之下非常清楚!按照 x64 的约定传入参数

caller:

1
2
3
4
5
6
7
8
9
10
11
12
13
(gdb) disassemble main
Dump of assembler code for function main:
0x00000000004005f4 <+0>: push %rbp
0x00000000004005f5 <+1>: mov %rsp,%rbp
0x00000000004005f8 <+4>: sub $0x10,%rsp
0x00000000004005fc <+8>: mov $0xffffffff,%r9d
0x0000000000400602 <+14>: mov $0x5,%r8d
0x0000000000400608 <+20>: mov $0x4,%ecx
0x000000000040060d <+25>: mov $0x3,%edx
0x0000000000400612 <+30>: mov $0x2,%esi
0x0000000000400617 <+35>: mov $0x1,%edi
0x000000000040061c <+40>: mov $0x0,%eax
0x0000000000400621 <+45>: callq 0x4004d7 <va_sum>

callee:

1
2
3
4
5
6
7
8
9
10
11
(gdb) disassemble va_sum
Dump of assembler code for function va_sum:
0x00000000004004d7 <+0>: push %rbp
0x00000000004004d8 <+1>: mov %rsp,%rbp
=> 0x00000000004004db <+4>: sub $0xf0,%rsp
0x00000000004004e2 <+11>: mov %edi,-0xe4(%rbp) // 1
0x00000000004004e8 <+17>: mov %rsi,-0xa8(%rbp) // 2
0x00000000004004ef <+24>: mov %rdx,-0xa0(%rbp) // 3
0x00000000004004f6 <+31>: mov %rcx,-0x98(%rbp) // 4
0x00000000004004fd <+38>: mov %r8,-0x90(%rbp) // 5
0x0000000000400504 <+45>: mov %r9,-0x88(%rbp) // ?

根据参数的类型的字长从栈上取值。

[^this]: 深度探索 C 语言函数可变长参数