一、可变参数表介绍
c/c++语言具备一个不同于其他编程语言的的特性,即支持可变参数。
例如C库中的printf,scanf等函数,都支持输入数量不定的参数。例如:
printf("hello world"); ////< 1个参数
prinf("%d", a); ////< 2个参数
printf("%d, %d", a, b); ////< 3个参数
printf函数原型为 int printf(const char *format, …);
从printf的原型来看,其除了接受一个固定参数format以外,后面的参数使用…来表示。
在c/c++语言中,…表示可以接受不定数量的参数。二、可变参数表用法
在标准C/C++中,头文件中定义了如下三个宏:
void va_start ( va_list arg_ptr, prev_param ); /* ANSI version */
type va_arg ( va_list arg_ptr, type );
void va_end ( va_list arg_ptr );
-
va 就是variable argument(可变参数)的意思
-
arg_ptr 是指向可变参数表的指针
-
prev_param 则指可变参数表的前一个固定参数
-
type 为可变参数的类型
-
va_list 也是一个宏
其定义为typedef char * va_list 实质上是一char 型指针。
char 型指针的特点是++、--操作对其作用的结果是增1 和减1(因为sizeof(char)为1)。
与之不同的是int 等其它类型指针的++、--操作对其作用的结果是增sizeof(type)或减sizeof(type),而且sizeof(type)大于1。
通过使用va_start宏我们可以取得可变参数表的首指针,这个宏的定义为:
#define va_start ( ap, v ) ( ap = (va_list)&v + _INTSIZEOF(v) )
-
其作用为将最后那个固定参数的地址加上可变参数对其的偏移后赋值给ap,这样ap就是可变参数表的首地址。
_INTSIZEOF 宏定义为:
#define _INTSIZEOF(n) ((sizeof ( n ) + sizeof ( int ) – 1 ) & ~( sizeof( int ) – 1 ) )
宏定义va_arg原型为:
#define va_arg(list, mode) ((mode *)(list =\
(char *) ((((int)list + (__builtin_alignof(mode)<=4?3:7)) &\
(__builtin_alignof(mode)<=4?-4:-8))+sizeof(mode))))[-1]
-
其作用为指取出当前arg_ptr 所指的可变参数并将ap 指针指向下一可变参数。
va_end宏定义用来结束可变参数的获取,定义为:
#define va_end ( list )
- va_end ( list )实际上被定义为空,没有任何真实对应的代码,用于代码对称,与va_start对应;
-
可能发挥代码的“自注释”作用。所谓代码的“自注释”,指的是代码能自己注释自己。
三、可变参数表的简单使用
#include <stdlib.h>
#include <stdio.h>
#include <stdarg.h>
/**
* @brief 求n个数中的最大值
* @details
* @param[in] num 整数个数
* @param[out] ... 整数
* @retval 最大整数
* @par
*/
int max ( int num, ... ) {
int m = -0x7FFFFFFF; /* 32 系统中最小的整数 */
va_list ap;
va_start ( ap, num );
for ( int i= 0; i< num; i++ ) {
int t = va_arg (ap, int);
if ( t > m ) {
m = t;
}
}
va_end (ap);
return m;
}
int main ( int argc, char* argv[] ) {
int n = max ( 5, 5, 6 ,3 ,8 ,5); /* 求5 个整数中的最大值 */
cout << n;
return 0;
}
max(int num, …)中首先定义了可变参数表指针ap,而后通过va_start ( ap, num )取得了参数表首地址(赋给了ap),其后的for 循环则用来遍历可变参数表。
max函数相比于printf简单了许多,其原因如下:
-
max函数可变参数表的长度是已知的,通过num参数传入;
-
max函数可变参数表中参数的类型是已知的,都为int型;
-
printf 函数可变参数的个数不能轻易的得到,而可变参数的类 型也不是固定的,需由格式字符串进行识别(由%f、%d、%s 等确定)。
四、运行机制
反汇编是研究语法深层特性的终极良策,首先查看main函数中调用max函数时的反汇编:
1. 004010C8 push 5
2. 004010CA push 8
3. 004010CC push 3
4. 004010CE push 6
5. 004010D0 push 5
6. 004010D2 push 5
7. 004010D4 call @ILT+5(max) (0040100a)
- 第一步:将参数从右向左入栈(第1~6行)
-
第二步:调用call 指令进行跳转(第7行)
这两步包含了深刻的含义,它说明C/C++默认的调用方式为由调用者管理参数入栈的操作,且入栈的顺序为从右至左,这种调用方式称为_cdecl调用。
x86系统的入栈方向为从高地址到低地址,故第1至n个参数被放在了地址递增的堆栈内。在被调用函数内部,读取这些堆栈的内容就可获得各个参数的值,让我们反汇编到max函数的内部。
int max ( int num, ...) {
1. 00401020 push ebp
2. 00401021 mov ebp,esp
3. 00401023 sub esp,50h
4. 00401026 push ebx
5. 00401027 push esi
6. 00401028 push edi
7. 00401029 lea edi,[ebp-50h]
8. 0040102C mov ecx,14h
9. 00401031 mov eax,0CCCCCCCCh
10. 00401036 rep stos dword ptr [edi]
va_list ap;
int m = -0x7FFFFFFF; /* 32 系统中最小的整数 */
11. 00401038 mov dword ptr [ebp-8],80000001h
va_start ( ap, num );
12. 0040103F lea eax,[ebp+0Ch]
13. 00401042 mov dword ptr [ebp-4],eax
for ( int i= 0; i< num; i++ )
14. 00401045 mov dword ptr [ebp-0Ch],0
15. 0040104C jmp max+37h (00401057)
16. 0040104E mov ecx,dword ptr [ebp-0Ch]
17. 00401051 add ecx,1
18. 00401054 mov dword ptr [ebp-0Ch],ecx
19. 00401057 mov edx,dword ptr [ebp-0Ch]
20. 0040105A cmp edx,dword ptr [ebp+8]
21. 0040105D jge max+61h (00401081) {
int t= va_arg (ap, int);
22. 0040105F mov eax,dword ptr [ebp-4]
23. 00401062 add eax,4
24. 00401065 mov dword ptr [ebp-4],eax
25. 00401068 mov ecx,dword ptr [ebp-4]
26. 0040106B mov edx,dword ptr [ecx-4]
27. 0040106E mov dword ptr [t],edx
if ( t > m )
28. 00401071 mov eax,dword ptr [t]
29. 00401074 cmp eax,dword ptr [ebp-8]
30. 00401077 jle max+5Fh (0040107f)
m = t;
31. 00401079 mov ecx,dword ptr [t]
32. 0040107C mov dword ptr [ebp-8],ecx
}
33. 0040107F jmp max+2Eh (0040104e)
va_end (ap);
34. 00401081 mov dword ptr [ebp-4],0
return m;
35. 00401088 mov eax,dword ptr [ebp-8]
}
36. 0040108B pop edi
37. 0040108C pop esi
38. 0040108D pop ebx
39. 0040108E mov esp,ebp
40. 00401090 pop ebp
41. 00401091 ret
- 第1~10行进行执行函数内代码的准备工作,保存现场;
- 第2行对堆栈进行移动;
- 第3行则意味着max函数为其内部局部变量准备的堆栈空间为50h字节;
- 第11行表示把变量n 的内存空间安排在了函数内部局部栈底减8的位置(占用4个字节);
- 第12~13行非常关键,对应着va_start ( ap, num),这两行将第一个可变参数的地址赋值给了指针ap;
- 从第12行可以看出num 的地址为ebp+0Ch;
- 从第13行可以看出ap 被分配在函数内部局部栈底减4 的位置上(占用4 个字节);
- 第22~27行最为关键,对应着va_arg (ap, int);
- 第22~24行的作用为将ap 指向下一可变参数(可变参数的地址间隔为4 个字节,从add eax,4 可以看出);
- 第25~27行则取当前可变参数的值赋给变量t。这段反汇编很奇怪,它先移动可变参数指针,再在赋值指令里面回过头来取先前的参数值赋给t(从mov edx,dword ptr [ecx-4]语句可以看出);
- 第36~41行恢复现场和堆栈地址,执行函数返回操作。