《C语言中可变参数函数实现原理浅谈》由会员分享,可在线阅读,更多相关《C语言中可变参数函数实现原理浅谈(9页珍藏版)》请在金锄头文库上搜索。
1、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的系统其栈
2、的结构就是 0x1ffc-a (4字节)(为了字对齐) 0x2000-b (4字节) 0x2004-c (8字节)0x200c-d (4字节)因此,函数的所有参数是存储在线性连续的栈空间中的,基于这种存储结构,这样就可以从可变参数函数中必须有的第一个普通参数来寻址后续的所有可变参数的类型及其值。2. C语言通过几个宏来实现变参的寻址根据函数调用的栈结构,标准C语言中,一般在stdarg.h头文件定义了下面的几个宏,用于实现变参的寻址及可变函数的设计,其中有可能不同的商业编译器的发行时实现的具体代码可能不一样,但是原理都是一样的。/Linux 2.18内核typedef char * va_li
3、st;/* 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) + (
4、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个字节入
5、栈,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
6、u32 acpi_native_uint; 显然,_AUPBND ,_ADNBND 的值是 4-1 = 3 = 0x00000003 ,按位取反( (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) ,初始化参数指针ap,将函数参数A右边第一个参数的地址
7、赋给ap。 A必须是一个参数的指针,所以此种类型函数至少要有一个普通的参数。如下图所示:高地址|-| |函数返回地址 | |-| | | |-| |第n个参数(第一个可变参数) | |-|-va_start后ap指向 |第n-1个参数(最后一个固定参数)| 低地址|-|- &A#define va_arg(ap, T) (*(T *)(ap) += (_bnd (T, _AUPBND) - (_bnd (T,_ADNBND)获得ap指向参数的值,并使ap指向下一个参数,T用来指明当前参数类型。由于_AUPBND和_ADNBND是相等的,所以取得的值是ap当前指向的参数值,但是先给ap加了当前参
8、数在字对齐后所占的字节数,使其指向了下一个参数。如下图所示:高地址|-| |函数返回地址 | |-| |.| |-|-va_arg后ap指向 |第n个参数(第一个可变参数) | |-|-va_start后ap指向 |第n-1个参数(最后一个固定参数)| 低地址|-|- &A #define va_end(ap) (void) 0用于删除指针ap,同时,这样做编译器不会为va_end()产生代码;置零后也防止了悬浮指针的存在。相当于ap=NULL.。小结:因此,根据stdarg.h头文件所定义的宏,可以总结出实现一个可变函数设计时所需要的步骤或者说算法:(1)在程序中将依次用到以下这些宏: vo
9、id va_start( va_list ap ,A); type va_arg( va_list ap, T ); void va_end( va_list ap ); va在这里是variable-argument(可变参数)的意思。(2)函数里首先定义一个va_list型的变量,这里是ap,这个变量是存储参数地址的指针。因为得到参数的地址之后,再结合参数的类型,才能得到参数的值。(3)然后用va_start宏初始化(2)中定义的变量ap,这个宏的第二个参数是可变参数列表的前一个参数,即最后一个固定参数(普通参数)。(4)然后依次用va_arg宏使ap返回可变参数的地址,得到这个地址之后,
10、结合参数的类型,就可以得到参数的值。(5)设定结束条件。由于被调的函数在调用时一般是不知道可变参数的正确数目的,程序员必须自己在代码中指明结束条件。3、C中常见的可变参数的函数:printf(),scanf()(1)printf()函数使用以printf()函数为典型进行分析,经常使用printf()函数的形式有如下这些:int intData =2011;float fData = 88.8;char *pStr = Hello world;printf(print as itselfn);printf(intData =%d, fData =%.3f, pStr=%sn, intData,
11、fData, pStr);等等从以上printf()的使用情况来看,不难发现一个规律,就是无论其随后的可变参数有多少个,printf()的第一个参数总是一个字符串。正是这第一个参数,使得它可以确认后面还有有多少个参数尾随。而尾随的每个参数占用的栈空间大小又是通过第一个格式字符串%确定的。(2)printf()实现代码分析/start.cstatic char sprint_buf1024;int printf(char *fmt, .)va_list args;int n;va_start(args, fmt);n = vsprintf(sprint_buf, fmt, args);va_en
12、d(args);write(stdout, sprint_buf, n);return n;/unistd.hstatic inline long write(int fd, const char *buf, off_t count)return sys_write(fd, buf, count);分析:从上面的代码来看,printf似乎并不复杂,并且遵循在文中2部分所小结的设计可变参数函数的步骤或算法:它通过一个宏va_start把所有的可变参数放到了由args指向的一块内存中,然后再调用vsprintf. 真正的参数个数以及格式的确定是在vsprintf可以确定的。由于vsprintf的代
13、码比较复杂,也不是这里要讨论的重点,所以下面就不再列出。重点是va_start(ap, A)宏的实现,它对定位从参数A后面的参数有重大的指导意义。现在把 #define va_start(ap, A) (void) (ap) = (char *) &(A) + (_bnd (A,_AUPBND) 的含义解释一下如下: va_start(ap, A) char *ap = (char *)(&A) + sizeof(A) /*A类型大小地址对齐*/ 在printf的va_start(args, fmt)中,fmt的类型为char *, 因此对于一个32位系统 sizeof(char *) = 4, 如果int大小也是32,则va_start(args, fmt);相当于 char *args = (char *)(&fmt) + 4; 此时args的值正好为fmt后第一个参数的地址。对于如下的可变参数函数 void fun(double d,.) va_list args; int n; va_start(args, d); 则 va_start(args, d);相当于