stdarg.h解析
ranpanf
stdarg.h是ANSI C 的标准头文件。它对可变参数的函数(vararg function)提供了支持。什么是可变参数的函数呢?举个例子:printf 与scanf就是。
stdarg.h对vararg function支持的关键是定义了几个非常有用的宏。注:下面说道的程序运行栈不是很精确,应该是进程的运行栈或进程的执行栈。
1.typedef char *va_list;
注解:本质是一个指向程序运行栈中某一个地址的指针(以后提到的栈都为程序运行栈)
(((sizeof(type) + sizeof(long) - 1) / sizeof(long)) */ sizeof(long))
注解:计算某种数据类型的参数在栈中占有的空间。/是连接宏定义中的两行:如
#define sum(a,b)((a)+(b))
等效于:
#define sum(a,b)((/
a)+(b))
在IA32(32位机器程序或汇编程序)的程序中,PUSH和POP指令的操作数是4Bytes(DWORD)。如:char变量为一个字节,但入栈时需要一个DWORD。一个
sizeof为5Bytes的结构变量需要2个DWORD。
3.#define va_start(ap, last) /
((ap) = (va_list)&(last) + __va_size(last))
注解:让直指指向第一个可变参数的首地址。
4. #define va_arg(ap, type) /
(*(type *)((ap) += __va_size(type), (ap) -/ __va_size(type)))
注解:这是我见过用逗号操作数最巧妙地例子。
5. #define va_end(ap) ((void)0)
注解:出现一个空值 标准库里将指向可变参数的指针重置为NULL。
为了详细解析,我先说一下几个要点。
1. 函数参数的传递机制:这里不是指值传递和引用传递。而是以从汇编(机器)语言程序员看参数传递。
我们知道有三种传递方式:寄存器传值,存储器传值,堆栈传值。C支持那种参数传值方式呢?C程序员怎么选择函数参数传递方式呢?在C语言的标准引入了一个概念:调用规则(calling convention),调用规则有:_cdecl,_stdcall,_fastcall,_thiscall(仅C++支持)等,请大家参阅MSDN获得详情。_cdecl 默认C/C++的调用规则,_stdcall win32API的调用规则,_fastcall一般不用,_thiscall C++的类成员函数的调用规则。详细情况见下表:
表1:函数调用规则详解
调用规则 | 清栈 | 是否支持vararg function | 参数传递 |
_cdecl | 调用者(caller) | 支持 | |
_stdcall | 被调用者(callee) | 不 | 函数参数从右向左入栈 |
_thiscall | 被调用者(callee) | 不 | 函数参数从右向左入栈,最后压入默认参数this |
_fastcall | 被调用者(callee) | 不 | 前两个参数存入寄存器ECX和EDX,剩余参数从右向左入栈 |
函数的调用过程:用户函数的调用是由程序运行栈来管理的,
标准库函数和系统调用一般也会用到栈,还有共享代码段(dll),陷入(软中断)等概念,我就不详细说了。现在只要知道用户函数是由程序运行栈管理的即可,栈的功能体现在三点:1.用栈保存函数的返回地址,2.用栈传递函数参数,3.在栈中创建局部变量。这里有个概念叫栈帧,它是指函数在调用时占用的栈中一部分连续的空间。函数的嵌套反映在栈中就是调用函数的栈帧的地址高于被调用者的栈帧的地址并顺序存放(假设栈是从高向低增长的)。函数的返回会伴随着他的栈帧的销毁,这也是为什么局部变量会在作用域之外没法应用。上面的表中有一列为“清栈”,想必你读到此处,它的含义你明白了:调用者清栈,是指被调用的函数参数保存在它调用者的栈帧中;而被调用者清栈,是指被调用的函数参数保存在它自己的栈帧中,通常用RET n指令返回。
现在我解析一段小程序:
1. #include <stdio.h>
2. #include <stdarg.h>
3. int average( int first, ... )
4. {
5. int count = 0, sum = 0, i = first;
7. va_start( marker, first ); /* Initialize variable arguments. */
8. while( i != -1 )
9. {
10. count++;
11. sum += i;
13. }
14. va_end( marker ); /* Reset variable arguments.
15. return( sum ? (sum / count) : 0 );
16. }
17. int main(){
18. int a=20;
19. printf("Average(%d,%d)=%d/n",a,400,average(a,400,-1));
20. return printf("Average(%d,%d,%d)=%d/n",100,20,400,average(100,20,400,-1));
21. }
执行结果:
其中的average函数就是vararg function
当执行到18行是:程序运行栈状态如图1所示:
地址(address) | 内容(value) |
0xFFFF 0000 | 00 |
0xFFFE FFFF | 00 |
0xFFFE FFFE | 00 |
0xFFFE FFFD | 03 |
|
|
栈底 |
main()的栈帧 |
ESP |
当前main栈帧中保存了局部变量a的值(小端编码)3 |
图1.执行到18行时的栈状态
|
当执行到19行时,程序运行栈的状态如图2.所示:
图2.执行到19行是的栈状态
地址(address) | 内容(value) |
0xFFFF 0000 | 00 |
0xFFFE FFFF | 00 |
0xFFFE FFFE | 00 |
14 | |
0xFFFF FFFC | FF |
0xFFFF FFFB | FF |
0xFFFF FFFA | FF |
0xFFFF FFF9 | FF |
0xFFFF FFF8 | 00 |
0xFFFF FFF7 | 00 |
0xFFFF FFF6 | 01 |
0xFFFF FFF5 | 90 |
0xFFFF FFF3 | 00 |
00 | |
00 | |
0xFFFF FFF1 | 02 |
... ... | ... ... |
0xFFFF FFED | ... ... |
... ... | ... ... |
0xFFFF FFE9 | 00 |
... ... | ... ... |
0xFFFF FFE5 | 00 |
0xFFFF FFE4 | 00 |
0xFFFF FFE3 | 00 |
0xFFFF FFE2 | 00 |
0xFFFF FFE1 | 14 |
|
|
栈底 |
变量a(20) |
average的参数-1 |
average的参数400
|
average的参数a(20)
|
average返回地址 |
局部变量count(0) |
局部变量sum(0) |
局部变量i(20) |
从0xFFFF 0000到0xFFFE FFF1 为main函数的调用栈帧。在main函数的
栈帧中,被调用函数average的参数从右到左入栈(0xFFFE FFFC 到0xFFFE FFF1).
既然被掉函数的参数位于调用函数的栈帧中,因此有main函数来清栈。参数从右向左依次入栈,由着两点可知:调用规则是_cdecl.
第6步“va_list marker;”:定义了一个指向一个字节的指针maker。
第7步“va_start(maker,first);” :使得指针maker指向第一个可变参数(400,即栈中的0xFFFE FFF5存储单元)这是怎么做到的呢?其实很简单!只要知道紧接着average第一个变参数之前的非可变参数(也就是最后一个非可变参数)在栈中的地址,再加上减去该参数在栈中的存储大小就可以获得第一可变个参数的首地址.在该列中average的最后一个非可变参数a的地址为0xFFFE FFF1,a在栈中的大小为4Bytes,显然0xFFFE FFF1+4=0xFFFE FFF5.第一个非可变参数的地址可由取地址运算符&取得,在它在栈中的大小可使用sizeof取得。试看两宏:
#define __va_size(type) /
(((sizeof(type) + sizeof(long) - 1) / sizeof(long)) */ sizeof(long))
该宏计算type在栈中的存储大小, 因为可以用sizeof得到type的存储大小,所以type可以是变量也可以是类型。请注意数据类型的存储大小与在栈中的存储大小的区别,如char 的存储大小为Byte ,而它的栈中存储大小为4Bytes。这也是为什么不直接用sizeof的原因。
#define va_start(ap, last) /
((ap) = (va_list)&(last) + __va_size(last))
该宏让ap指针指向函数的第一个可变参数的首地址。&(last)为函数的最后一个非可变参数的地址,__va_size(last)是它在栈中的存储大小,两者相加便得第一个可变参数的首地址。
第12步“i = va_arg( marker, int);”:取函数当前可变参数,并且让指针指向下一个
可变参数,该宏的定义如下:
#define va_arg(ap, type) /
(*(type *)((ap) += __va_size(type), (ap) - __va_size(type)))
这里用到了逗号运算符“,”。逗号运算符的定义是:expr1,expr2
先计算expr1,在计算expr2,整个表达式的结果为expr2的值。
显然逗号运算符的左侧操作数:(ap) += __va_size(type)是将修改指针移向下一个可变参数,但不会作为整个表达式的结果;而右侧操作数(ap) - __va_size(type)的不会修该指针的值,但会将恢复到指针被修改之前结果并将其作为整个表达式的结果。该结果为char型的变量的地址值,经过强制类型转化和取指针指向的值,终于得到了type类型的可变参数。
几点需要注意:
1. 有可变参数的函数必须拥有非可变参数,并且非可变参数必须位于可变参数的前面。
2. 程序员必须自己控制可变参数的类型以及可变参数的数目。