C语言中可变参数函数实现原理浅析
1、C函数调用的栈结构
可变参数函数的实现与函数调用的栈结构密切相关,正常情况下C的函数参数入栈规则为__stdcall, 它是从右到左的,即函数中的最右边的参数最先入栈。例如,对于函数:
void fun(int a, int b, int c)
{
int d;
...
}
其栈结构为
0x1ffc-->d
0x2000-->a
0x2004-->b
0x2008-->c
对于任何编译器,每个栈单元的大小都是sizeof(int), 而函数的每个参数都至少要占一个栈单元大小,如函数 void fun1(char a, int b, double c, short d) 对一个32的系统其栈的结构就是
0x1ffc-->a (4字节)(为了字对齐)
0x2000-->b (4字节)
0x2004-->c (8字节)
0x200c-->d (4字节)
因此,函数的所有参数是存储在线性连续的栈空间中的,基于这种存储结构,这样就可以从可变参数函数中必须有的第一个普通参数来寻址后续的所有可变参数的类型及其值。
2. C语言通过几个宏来实现变参的寻址
根据函数调用的栈结构,标准C语言中,一般在stdarg.h头文件定义了下面的几个宏,用于实现变参的寻址及可变函数的设计,其中有可能不同的商业编译器的发行时实现的具体代码可能不一样,但是原理都是一样的。
//Linux 2.18内核
typedef char * va_list;
/*
Storage alignment properties -- 堆栈按机器字对齐
其中acpi_native_int是一个机器字,32位机的定义是:typedef u32 acpi_native_int
*/
#define _AUPBND (sizeof (acpi_native_int) - 1)
#define _ADNBND (sizeof (acpi_native_int) - 1)
/* Variable argument list macro definitions -- 变参函数内部实现需要用到的宏 */
#define _bnd(X, bnd) (((sizeof (X)) + (bnd)) & (~(bnd)))
#define va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))
#define va_arg(ap, T) (*(T *)(((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND))))
#define va_end(ap) (void) 0
在X86 32位机器中,以上这几个宏的用途主要是:
C语言传递参数是与__stdcall相同的,C语言传递参数时是用push指令从右到左将参数逐个压栈,因此C语言里通过栈指针来访问参数。虽然X86的push一次可以压2,4或8个字节入栈,C语言在压参数入栈时仍然是机器字的size为最小单位的,也就是说参数的地址都是字对齐的,这就是_bnd(X,bnd)存在的原因。汇编和C,编译出的X86函数一般在进入函数体后立即执行
push ebp
mov ebp, esp
这两条指令。首先把ebp入栈,然后将当前栈指针赋给ebp,以后访问栈里的参数都使用ebp作为基指针。
下面将对上面几个主要的宏进行分析:
#define _bnd(X, bnd) (((sizeof (X)) + (bnd)) & (~(bnd)))
计算类型为X的参数在栈中占据的字节数,是字对齐后的字节数。acpi_native_unit是一个机器字,32位机的定义是:typedef u32 acpi_native_uint;
显然,_AUPBND ,_ADNBND 的值是 4-1 == 3 == 0,按位取反( ~(bnd))就是0xfffffffc 。
因此,_bnd(X,bnd) 宏在32位机下是
( (sizeof(X) + 3)&0xfffffffc )
与&0xfffffffc相与后,最后两位是00,很明显,其作用是实现字对齐。
#define va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))
va_start(ap,A) ,