stdarg.h 声明了 va_list 类型,并定义了 va_start()、va_arg()、va_end() 三个宏。其用于在,访问可变数量的参数,可变参数的函数通常是通过在函数参数列表的末尾用省略号定义。
stdarg.h 由编译器提供,GCC 用各种内建实现来提供,暴露给 stdarg.h。也就是说,可变参数是编译层面的事情。编译器为不同的 ABI 实现了特定的可变参数,这里就不深入研究,要涉及到各种 ABI 的函数参数约定挺麻烦的。
你以为呢,stdarg.h 其实就是 define 到内建结构而已:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
#ifndef __GNUC_VA_LIST
#define __GNUC_VA_LIST
typedef __builtin_va_list __gnuc_va_list;
#endif
#define va_start(...) __builtin_c23_va_start(__VA_ARGS__)
#else
#define va_start(v,l) __builtin_va_start(v,l)
#endif
#define va_end(v) __builtin_va_end(v)
#define va_arg(v,l) __builtin_va_arg(v,l)
#if !defined(__STRICT_ANSI__) || __STDC_VERSION__ + 0 >= 199900L \
|| __cplusplus + 0 >= 201103L
#define va_copy(d,s) __builtin_va_copy(d,s)
#endif
#define __va_copy(d,s) __builtin_va_copy(d,s)
|
在 man stdarg 里这样写的。宏为了描述把宏加上类型,其实它们都是宏:
1
2
3
4
5
6
|
#include <stdarg.h>
void va_start(va_list ap, last);
type va_arg(va_list ap, type);
void va_end(va_list ap);
void va_copy(va_list dest, va_list src);
|
va_list
va_list 存储可变参数信息的类型供后面三个宏使用,在 x86 实现大致如下:
1
2
3
4
5
6
|
struct __va_list_tag {
unsigned int gp_offset;
unsigned int fp_offset;
void *overflow_arg_area;
void *reg_save_area;
}
|
总之,要挖编译器源码,还得翻各 ABI 的实现,我就找博客贴着凑合着看吧。
用法就是声明声明一个用于后面给宏传参。
va_start
初始化 va_list ap 还需要有一个固定参数放在 last。
va_arg
在可变参数中依次获取每个可变参数的值,传入 ap ,然后把类型传入 type ,返回当前可变参数中的下一个参数。
va_end
用于清理 va_list 变量,使其不再指向任何有效的内存位置,在可变参数函数列表中使用,结束可变参数的处理。
示例
最经典的参数求和:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
int sum(int count, ...)
{
int total = 0;
va_list args;
va_start(args, count);
for (int i = 0; i < count; i++)
total += va_arg(args, int);
va_end(args);
return total;
}
int main(int argc, char *argv[])
{
printf("Sum of 1, 2, 3: %d\n", sum(3, 1, 2, 3));
printf("Sum of 4, 5, 6, 7: %d\n", sum(4, 4, 5, 6, 7));
return EXIT_SUCCESS;
}
|
甚至可以实现自己的 printf,我目前就只实现了 %d 和 %s:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
void my_putnbr(int val)
{
unsigned int unum;
if (val < 0)
{
putchar('-');
/*
* 直接取反会导致溢出
*/
unum = (unsigned int)(-(val + 1)) + 1;
}
else
unum = (unsigned int)(val);
if (unum >= 10)
my_putnbr(unum / 10);
putchar('0' + unum % 10);
}
void my_putstr(const char *str)
{
for (; *str != '\0'; str++)
putchar(*str);
putchar('\0');
}
void my_printf(const char *fmt, ...)
{
va_list arg;
va_start(arg, fmt);
for (const char *p = fmt; *p != '\0'; p++)
{
if (*p != '%')
{
putchar(*p);
continue;
}
p++;
if (*p == 'd')
my_putnbr(va_arg(arg, int));
else if (*p == 's')
my_putstr(va_arg(arg, char *));
else if (*p == '%')
putchar('%');
}
va_end(arg);
}
int main(int argc, char *argv[])
{
my_printf("int:%d, string: %s %", -2131, "adas");
return EXIT_SUCCESS;
}
|
更好的 printf 可以看我的 ics2024 实验 stdio.c 实现:
https://github.com/storm-1614/ics2024/blob/master/abstract-machine/klib/src/stdio.c
参考资料
- 从frida到va_list
- 揭秘X86架构C可变参数函数实现原理
- gcc-mirror/gcc
- C 标准库 - <stdarg.h>