va_list系列的使用及问题

本文最后更新于:2022年6月8日 晚上

va_list

简介

引自 https://www.runoob.com/cprogramming/c-standard-library-stdarg-h.html

stdarg.h 头文件定义了一个变量类型 va_list 和三个宏,这三个宏可用于在参数个数未知(即参数个数可变)时获取函数中的参数。

可变参数的函数通在参数列表的末尾是使用省略号(,…)定义的。

库变量

下面是头文件 stdarg.h 中定义的变量类型:

序号 变量 & 描述
1 va_list 这是一个适用于 va_start()、va_arg()va_end() 这三个宏存储信息的类型。

库宏

下面是头文件 stdarg.h 中定义的宏:

序号 宏 & 描述
1 void va_start(va_list ap, last_arg) 这个宏初始化 ap 变量,它与 va_argva_end 宏是一起使用的。last_arg 是最后一个传递给函数的已知的固定参数,即省略号之前的参数。
2 type va_arg(va_list ap, type) 这个宏检索函数参数列表中类型为 type 的下一个参数。
3 void va_end(va_list ap) 这个宏允许使用了 va_start 宏的带有可变参数的函数返回。如果在从函数返回之前没有调用 va_end,则结果为未定义。

使用

  1. 首先包含stdarg.h头文件
  2. 使用va_list初始化 指针
  3. 使用va_start 将 第2步中创建的指针添加、还有第一个参数
  4. 通过va_arg 将 第2步中创建的指针添加、以及要取的数据的类型
  5. 不使用va_list后,释放va_start的指针

示例如下:

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

void test(int p1, ...){
va_list ap;
int a = p1;
va_start(ap, p1);
double b = va_arg(ap,double);
char c = va_arg(ap,int);
char *d = va_arg(ap,char *);
printf("a:%d\r\nb:%lf\r\nc:%d\r\nd:%s\r\n",a,b,c,d);
va_end(ap);
}

int main(){
int a = 1;
double b = 2.2;
char c = 0x33;
char *d = "abcderf";
test(a,b,c,d);
}

将上述代码保存为test.c,使用命令 gcc test.c 然后 ./a.out执行

正常输出

1
2
3
4
a:1
b:2.200000
c:51
d:abcderf

遇到的问题

第一次写的代码为以下,编译时爆出几个警告,刚开始没注意,后面运行时发现core dumped. 遂回头查产生改现象的原因。

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

void test(int p1, ...){
va_list ap;
int a = p1;
va_start(ap, p1);
float b = va_arg(ap,float);
char c = va_arg(ap,char);
char *d = va_arg(ap,char *);
printf("a:%d\r\nb:%lf\r\nc:%d\r\nd:%s\r\n",a,b,c,d);
va_end(ap);
}

int main(){
int a = 1;
float b = 2.2;
char c = 0x33;
char *d = "abcderf";
test(a,b,c,d);
}

我们注意到有两个warning,翻译一下:当通过...时,float被提升为double,另一个也是类似,char 被提升为int所以使用va_arg访问时,传入参数类型为char,需要用va_arg(ap,int);传入参数为float时,需使用va_arg(arg,double)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[gonglja@archlinux hurlex-doc]$ gcc -g0 test.c 
In file included from test.c:2:
test.c: In function ‘test’:
test.c:8:25: warning: ‘float’ is promoted to ‘double’ when passed through ‘...’
8 | float b = va_arg(ap,float);
| ^
test.c:8:25: note: (so you should pass ‘double’ not ‘float’ to ‘va_arg’)
test.c:8:25: note: if this code is reached, the program will abort
test.c:9:25: warning: ‘char’ is promoted to ‘int’ when passed through ‘...’
9 | char c = va_arg(ap,char);
| ^
test.c:9:25: note: if this code is reached, the program will abort
[gonglja@archlinux hurlex-doc]$ ./a.out
Illegal instruction (core dumped)
[gonglja@archlinux hurlex-doc]$

修改后,程序正常运行。

为什么 当通过...时,float被提升为doublechar 提升为int呢?==猜测是由于字节对齐引起的==

为了验证我们的猜测,在这个地方,我们看一下汇编代码,

由于涉及到浮点数,为了简单,我们简化一下代码,去掉浮点数相关。

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

void test(int p1, ...){
va_list ap;
int a = p1;
va_start(ap, p1);
char c = va_arg(ap,char); // 此处存在问题,应改为va_arg(ap,int)
char *d = va_arg(ap,char *);
printf("a:%d\r\nc:%d\r\nd:%s\r\n",a,c,d);
va_end(ap);
}

int main(){
int a = 1;
char c = 51;
char *d = "abcderf";
test(a,c,d);
}

通过 gcc -S test.c 生成 test.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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
	.file	"test.c"
.text
.globl test
.type test, @function
test:
.LFB0:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $120, %rsp
movl %edi, -228(%rbp)
movq %rsi, -168(%rbp)
movq %rdx, -160(%rbp)
movq %rcx, -152(%rbp)
movq %r8, -144(%rbp)
movq %r9, -136(%rbp)
testb %al, %al
je .L2
movaps %xmm0, -128(%rbp)
movaps %xmm1, -112(%rbp)
movaps %xmm2, -96(%rbp)
movaps %xmm3, -80(%rbp)
movaps %xmm4, -64(%rbp)
movaps %xmm5, -48(%rbp)
movaps %xmm6, -32(%rbp)
movaps %xmm7, -16(%rbp)
.L2:
movq %fs:40, %rax
movq %rax, -184(%rbp)
xorl %eax, %eax
movl -228(%rbp), %eax
movl %eax, -212(%rbp)
movl $8, -208(%rbp)
movl $48, -204(%rbp)
leaq 16(%rbp), %rax
movq %rax, -200(%rbp)
leaq -176(%rbp), %rax
movq %rax, -192(%rbp)
ud2
.cfi_endproc
.LFE0:
.size test, .-test
.section .rodata
.LC0:
.string "abcderf"
.text
.globl main
.type main, @function
main:
.LFB1:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $16, %rsp
movl $1, -12(%rbp)
movb $51, -13(%rbp)
leaq .LC0(%rip), %rax
movq %rax, -8(%rbp)
movsbl -13(%rbp), %ecx
movq -8(%rbp), %rdx
movl -12(%rbp), %eax
movl %ecx, %esi
movl %eax, %edi
movl $0, %eax
call test
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE1:
.size main, .-main
.ident "GCC: (GNU) 11.2.0"
.section .note.GNU-stack,"",@progbits

只关注重点地方代码,test调用之前,test调用之后。

找到 call test ,在call test之前,

rdi中存着第一个参数,值为1

rsi中存着第二个参数,值为2

rdx中存着第三个参数,值为字符串 的地址

接着调用call test,跳转至test中,首先保存rbp寄存器,更新rbp指针为rsp(栈顶指针),接着栈顶下移(分配空间),将edirsiraxrcxr8r9保存到栈区。为什么是edi呢?因为你的第一个参数为int,而非int64_t

继续往下走,test %al,%al 其行为类似and(TEST %SRC, %DEST 目的寄存器与源寄存器进行逻辑与操作,其结果不更新目的寄存器),je 如果等于0,跳转。

继续往下走,FS:0x28在 Linux 上存储一个特殊的哨兵堆栈保护值,并且代码正在执行堆栈保护检查。

==FS:0x28 之下与 ud2 之前代码未分析。(留个坑)==

然后后面 执行ud2,UD2是一种让CPU产生invalid opcode exception的软件指令. 内核发现CPU出现这个异常, 会立即停止运行。此时就Core Dumped.

以上猜测错误。

**==其实是c99标准中的默认参数提升==**,具体标准如下。

C99标准 6.5.2.2 函数调用 6、7、8节

6 If the expression that denotes the called function has a type that does not include a prototype, the integer promotions are performed on each argument, and arguments that have type float are promoted to double. These are called the default argument promotions. If the number of arguments does not equal the number of parameters, the behavior is undefined. If the function is defined with a type that includes a prototype, and either the prototype ends with an ellipsis (, …) or the types of the arguments after promotion are not compatible with the types of the parameters, the behavior is undefined. If the function is defined with a type that does not include a prototype, and the types of the arguments after promotion are not compatible with those of the parameters after promotion, the behavior is undefined, except for the following cases:

​ — one promoted type is a signed integer type, the other promoted type is the corresponding unsigned integer type, and the value is representable in both types;

​ — both types are pointers to qualified or unqualified versions of a character type or void.

7 If the expression that denotes the called function has a type that does include a prototype, the arguments are implicitly converted, as if by assignment, to the types of the corresponding parameters, taking the type of each parameter to be the unqualified version of its declared type. The ellipsis notation in a function prototype declarator causes argument type conversion to stop after the last declared parameter. The default argument promotions are performed on trailing arguments.

8 No other conversions are performed implicitly; in particular, the number and types of arguments are not compared with those of the parameters in a function definition that does not include a function prototype declarator

6如果表示被调用函数的表达式具有不包含原型的类型,则对每个参数执行整数提升,并将具有float类型的参数提升为double。这些被称为默认参数提升。如果参数的数量不等于参数的数量,则行为是未定义的。如果函数是用包含原型的类型定义的,并且原型以省略号(,…)结尾或者升级后的参数类型与参数类型不兼容,行为未定义。如果函数定义的类型不包括原型,且升级后参数的类型与升级后参数的类型不兼容,则行为未定义,但以下情况除外:

  • 一个升级类型是有符号整数类型,另一个升级类型是相应的无符号整数类型,值在两种类型中都可以表示;
  • 这两种类型都是指向字符类型或void的限定或非限定版本的指针。

7 如果表示被调用函数的表达式的类型确实包含原型,则参数会像赋值一样隐式转换为相应参数的类型,将每个参数的类型视为其声明类型的非限定版本。函数原型声明器中的省略号表示法会导致参数类型转换在最后一个声明的参数之后停止。默认参数升级是在后续参数上执行的。

8 没有隐式执行其他转换;特别是,参数的数量和类型不会与不包含函数原型声明器的函数定义中的参数进行比较

参考

https://stackoverflow.com/questions/1255775/default-argument-promotions-in-c-function-calls

https://blog.csdn.net/astrotycoon/article/details/8284501


va_list系列的使用及问题
https://www.glj0.top/posts/608e8634/
作者
gong lj
发布于
2022年2月23日
更新于
2022年6月8日
许可协议