阅读:2780回复:4
C语言变参函数解析
1 函数声明
首先,要实现类似printf()的变参函数,函数的最后一个参数要用 ... 表示,如 int log(char * arg1, ...) 这样编译器才能知道这个函数是变参函数。这个参数与变参函数的内部实现完全没有关系,只是让编译器在编译调用此类函数的语句时不计较参数多少老老实实地把全部参数压栈而不报错,当然...之前至少要有一个普通的参数,这是由实现手段限制的。 2 函数实现 C语言通过几个宏实现变参的寻址。下面是linux2.18内核源码里这几个宏的定义,相信符合C89,C99标准的C语言基本都是这样定义的。 typedef char *va_list; /* Storage alignment properties -- 堆栈按机器字对齐 */ #define _AUPBND (sizeof (acpi_native_uint) - 1) #define _ADNBND (sizeof (acpi_native_uint) - 1) /* Variable argument list macro definitions -- 变参函数内部实现需要用到的宏 */ #define _bnd(X, bnd) (((sizeof (X)) + (bnd)) & (~(bnd))) #define va_arg(ap, T) (*(T *)(((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND)))) #define va_end(ap) (void) 0 #define va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND)))) 下面以x86 32位机为例分析这几个宏的用途 要理解这几个宏需要对C语言如何传递参数有一定了解。与PASCAL相反,与stdcall 相同,C语言传递参数时是用push指令从右到左将参数逐个压栈,因此C语言里通过栈指针来访问参数。虽然X86的push一次可以压2,4或8个字节入栈,C语言在压参数入栈时仍然是机器字的size为最小单位的,也就是说参数的地址都是字对齐的,这就是_bnd(X,bnd)存在的原因。另外补充一点常识,不管是汇编还是C,编译出的X86函数一般在进入函数体后立即执行 push ebp mov ebp, esp 这两条指令。首先把ebp入栈,然后将当前栈指针赋给ebp,以后访问栈里的参数都使用ebp作为基指针。 一一解释这几个宏的作用。 _bnd(X,bnd) ,计算类型为X的参数在栈中占据的字节数,当然是字对齐后的字节数了。acpi_native_unit是一个机器字,32位机的定义是:typedef u32 acpi_native_uint; 显然,_AUPBND ,_ADNBND 的值是 4-1 == 3 == 0x00000003 ,按位取反( ~(bnd))就是0xfffffffc 。 因此,_bnd(X,bnd) 宏在32位机下就是 ( (sizeof(X) + 3)&0xfffffffc ) 很明显,其作用是--倘若sizeof(X)不是4的整数倍,去余加4。 _bnd(sizeof(char),3) == 4 _bnd(sizeof(struct size7struct),3) == 8 va_start(ap,A) ,初始化参数指针ap,将函数参数A右边第一个参数的地址赋给ap。 A必须是一个参数的指针,所以此种类型函数至少要有一个普通的参数啊。像下面的例子函数,就是将第二个参数的指针赋给ap。 va_arg(ap,T) ,获得ap指向参数的值,并使ap指向下一个参数,T用来指明当前参数类型。 注意((ap) += (_bnd (T, _AUPBND))) 是被一对括号括起来的,然后才减去(_bnd (T, _ADNBND), 而_AUPBND和_ADNBND是相等的。所以取得的值是ap当前指向的参数值,但是先给ap加了当前参数在字对齐后所占的字节数,使其指向了下一个参数。 va_end(ap), 作用是美观。 3 总结 先用一个 ... 参数声明函数是变参函数,接下来在函数内部以va_start(ap,A)宏初始化参数指针,然后就可以用va_arg(ap,类型)从左到右逐个获取参数值了 分析到此处算是一清二白了,下面给一个例子 int log(char * fmt,...) { va_list ap; int d; char c, *p, *s; va_start(ap, fmt); while (*fmt) switch(*fmt++) { case 's': /* string */ s = va_arg(ap, char *); printf("string %s\n", s); break; case 'd': /* int */ d = va_arg(ap, int); printf("int %d\n", d); break; case 'c': /* char */ c = va_arg(ap, char); printf("char %c\n", c); break; } va_end(ap); } |
|
沙发#
发布于:2007-01-11 13:09
分析的蛮好!
如果结合一个汇编的例子来说的就更加直观了。 |
|
|
板凳#
发布于:2007-01-11 17:43
今天看了一下反汇编的代码
vc做的很智能,虽然也用类似的宏,但编出的指令像手写的,看不出来用了什么宏。 vc宏 : #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 ) vc编出的log函数反汇编代码 .text:00401000 arg_4 = dword ptr 8 .text:00401000 arg_8 = dword ptr 0Ch .text:00401000 arg_C = byte ptr 10h .text:00401000 .text:00401000 mov eax, [esp+arg_4] .text:00401004 push eax .text:00401005 push offset unk_407038 .text:0040100A call sub_401060 .text:0040100F mov ecx, [esp+8+arg_8] .text:00401013 push ecx .text:00401014 push offset unk_407034 .text:00401019 call sub_401060 .text:0040101E movsx edx, [esp+10h+arg_C] .text:00401023 push edx .text:00401024 push offset unk_407030 .text:00401029 call sub_401060 .text:0040102E add esp, 18h .text:00401031 xor eax, eax .text:00401033 retn .text:00401033 sub_401000 endp gcc编译器逊色点,能看得出用了这套宏,而且,假如后面传入的参数不是字对齐,编译时有警告 log.c:27: warning: `char' is promoted to `int' when passed through `...' log.c:27: warning: (so you should pass `int' not `char' to `va_arg') 然后内部实现时这个参数编译不出,直接以int $0x5代替 |
|
地板#
发布于:2007-01-11 18:24
引用第2楼boly81于2007-01-11 17:43发表的“”: 大概的原理都差不多。咱把原理搞清楚了。具体用的时候再看看文档,应该是no problem的。 呵呵 |
|
|
地下室#
发布于:2007-04-09 19:18
很受用,谢下楼主了,
|
|
|