可变参数函数的实现
可变参数函数的实现(kusamba@126.com)
本文通过简单的样例分析可变参数函数的实现
以下分析中的地址标示在不同的机器可能不一样。
先看一下可变参数的函数实例:
void Test1(int num, ...)
{
int sum = 0;
va_list argList;
va_start(argList, num);
for (int i = 0; i < num; i++)
{
sum += va_arg(argList, int);
}
va_end(argList);
printf("Sum is %d\n", sum);
}
//call function:
int main(int argc, char* argv[])
{
Test1(3, 1, 2, 3);
}
在Window平台(win32/Intel)通常的编程情况下, va_list, va_start, va_arg, va_end
定义如下(见<stdarg.h>):
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
typedef char * va_list;
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_end(ap) ( ap = (va_list)0 )
(1)说明:
_INTSIZEOF(n) : 以sizeof(int)为基本单位使用进一法计算变量n的size,所得结果再乘以
sizeof(int)
va_start(ap,v): 根据第一个参数的地址以及大小获取后一个参数的地址
va_arg(ap,t) : 根据所给的类型参数t获取当前参数的值,并将va_list的指针向后移动该
参数大小指向下一个参数
va_end(ap) : 将va_list指针设为0(无效),避免野指针
(2)函数调用方式:
C/C++默认的__cdcl调用方式:从右向左压栈
根据上面的知识我们就可以很轻松的解释Test1()多参数函数调用的秘密。
main()调用Test1(3, 1, 2, 3), 汇编如下:
89: Test1(3, 1, 2, 3);
0040DB88 push 3
0040DB8A push 2
0040DB8C push 1
0040DB8E push 3
0040DB90 call @ILT+55(Test1) (0040103c)
0040DB95 add esp,10h
函数压栈后,可能的堆栈分布如下:
0X100C 3
0X1008 2
0X1004 1
0X1000 3
在Test1(int num, ...)中
调用:
va_list argList;
va_start(argList, num);
得:argList ----> 0X1004
第一次调用:int var = va_arg(argList, int);
argList ----> 0X1008
var = 1
...
如此循环,从而获取所有的参数
在上述知识的前提下, 我们再来看如下例子:
void Test2(int& first, ...)
{
int sum = 0;
va_list argList;
va_start(argList, first);
for (int i = 0; i < first; i++)
{
sum += va_arg(argList, int);
}
va_end(argList);
printf("Sum is %d\n", sum);
}
在main()中调用:
{
int cnt = 3;
Test2(cnt, 1, 2, 3);
}
得出的结果竟然是:5445018 ????
问题出在什么地方?
我们参考:va_start(ap, v)宏的定义:
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
当参数中包含对某个变量的引用时,ap获得地址将不是参数在堆栈中的地址,而是被传入
的变量的地址,上例中为cnt的地址。这个时候ap已经成为野指针,极有可能会导致极为
严重的bug产生。
可以做如下测试:
void Test2_1(int& first, ...)
{
// 获取first在堆栈中的地址
int* pN1 = NULL;
__asm lea eax,first;
__asm mov pN1,eax;
int sum = 0;
va_list argList;
va_start(argList, first);
// 可以注意到argList的地址为传入函数的&cnt+4
for (int i = 0; i < first; i++)
{
sum += va_arg(argList, int);
}
va_end(argList);
printf("Sum is %d\n", sum);
}
在main()中调用:
// 注意cnt的地址 = 0x0012ff7c
Test2_1(cnt, 1, 2, 3);
察看变量的值,可以知道:
参数first在堆栈中的地址为:pN1 = 0x0012ff18
argList的地址为:0x0012ff80 = 0x0012ff18 + sizeof(first)
遇到这种情况可以自行实现va_start(ap, v),可以参考
ms-help://MS.MSDNQTR.2003FEB.2052/enu_kbvisualc/en-us/visualc/Q119394.htm
给出的一种实现
#define va_start(ap,v) {int var= _INTSIZEOF(v); \
__asm lea eax,v __asm add eax,var __asm mov ap,eax \
}
参考文档:
ms-
help://MS.MSDNQTR.2003FEB.2052/vclib/html/_crt_va_arg.2c_.va_end.2c_.va_start.htm
ms-help://MS.MSDNQTR.2003FEB.2052/enu_kbvisualc/en-us/visualc/Q119394.htm