boly81
驱动小牛
驱动小牛
  • 注册日期2004-06-25
  • 最后登录2012-06-08
  • 粉丝0
  • 关注0
  • 积分490分
  • 威望73点
  • 贡献值0点
  • 好评度49点
  • 原创分0分
  • 专家分0分
阅读:2780回复:4

C语言变参函数解析

楼主#
更多 发布于:2007-01-11 10:49
  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);
}
pilixuanke
驱动中牛
驱动中牛
  • 注册日期2005-10-31
  • 最后登录2016-01-09
  • 粉丝0
  • 关注0
  • 积分1018分
  • 威望626点
  • 贡献值0点
  • 好评度512点
  • 原创分0分
  • 专家分0分
沙发#
发布于:2007-01-11 13:09
分析的蛮好!

如果结合一个汇编的例子来说的就更加直观了。
向底层开发进军!!!
boly81
驱动小牛
驱动小牛
  • 注册日期2004-06-25
  • 最后登录2012-06-08
  • 粉丝0
  • 关注0
  • 积分490分
  • 威望73点
  • 贡献值0点
  • 好评度49点
  • 原创分0分
  • 专家分0分
板凳#
发布于: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代替
pilixuanke
驱动中牛
驱动中牛
  • 注册日期2005-10-31
  • 最后登录2016-01-09
  • 粉丝0
  • 关注0
  • 积分1018分
  • 威望626点
  • 贡献值0点
  • 好评度512点
  • 原创分0分
  • 专家分0分
地板#
发布于:2007-01-11 18:24
引用第2楼boly812007-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)) )
.......


大概的原理都差不多。咱把原理搞清楚了。具体用的时候再看看文档,应该是no problem的。 呵呵
向底层开发进军!!!
aqiuzaizai
驱动牛犊
驱动牛犊
  • 注册日期2007-02-02
  • 最后登录2008-04-02
  • 粉丝0
  • 关注0
  • 积分180分
  • 威望69点
  • 贡献值0点
  • 好评度68点
  • 原创分0分
  • 专家分0分
地下室#
发布于:2007-04-09 19:18
很受用,谢下楼主了,
竹密何妨流水过 山高岂碍野云飞
游客

返回顶部