分类目录

展开|收起

看你喜欢

(1) (1) (42) (1) (1) (1) (16) (2) (1) (1) (4) (1) (2) (7) (4) (1) (1) (1) (1) (3) (1) (5) (1) (1) (1) (1) (1) (2) (1) (4) (4) (3) (1) (1) (2) (1) (37) (2) (1) (5) (3) (1) (4) (1) (1) (11) (3) (1) (9) (3) (1) (23) (2) (1) (2) (1) (1) (1) (1)

最新精华

函数调用汇编分析【4】- 典型问题分析2(可变参数函数的实现原理)

代码中经常会看到va_start、va_arg以及va_end这样奇怪的东东,查看定义原来都是宏定义,许多人不知道这个是干什么的,其实这个是用于实现可变参数的函数。有时候我们自己也需要写一个可变参数的函数来方便使用。理解可变参数的实现原理的核心是理解函数参数的入栈方式,如果你理解了本文前面的内容,实际你就基本掌握了可变参数的函数原理,可能你自己还不知道,呵呵。以下内容copy网上的一篇介绍“C中的可变参数研究”(原作者和出处一时不记得了,请原作者见谅,并致谢,当初是看这篇文章才明白的),我基本没修改,因为它描述得已经很好了,看完你就明白了。

标题:C中的可变参数研究

(1)何谓可变参数

int   printf(const   char*   format,   ...);

这是使用过C语言的人所再熟悉不过的printf函数原型,它的参数中就有固定参数format和可变参数(用”…”表示)。而我们又可以用各种方式来调用printf,如:

printf("%d",value);
printf("%s",str);
printf("the   number   is   %d   ,string   is:%s",   value,   str);

(2)实现原理

C语言用宏来处理这些可变参数。这些宏看起来很复杂,其实原理挺简单,就是根据参数入栈的特点从最靠近第一个可变参数的固定参数开始,依次获取每个可变参数的地址。下面我们来分析这些宏。在VC中的stdarg.h头文件中,针对不同平台有不同的宏定义,我们选取X86平台下的宏定义:

typedef   char   *va_list;

/*把va_list被定义成char*,这是因为在我们目前所用的PC机上,字符指针类型可以用来存储内存单元地址。而在有的机器上va_list是被定义成void*的*/

#define   _INTSIZEOF(n)   ((sizeof(n) + sizeof(int)- 1)& ~(sizeof(int)   -   1) )

/*_INTSIZEOF(n)宏是为了考虑那些内存地址需要对齐的系统,从宏的名字来应该是跟sizeof(int)对齐。一般的sizeof(int)=4,也就是参数在内存中的地址都为4的倍数。比如,如果sizeof(n)在1-4之间,那么_INTSIZEOF(n)=4;如果sizeof(n)在5-8之间,那么_INTSIZEOF(n)=8。*/

#define   va_start(ap,v)(ap = (va_list)&v + _INTSIZEOF(v) )

/*va_start的定义为&v+_INTSIZEOF(v) ,这里&v是最后一个固定参数的起始地址,再加上其实际占用大小后,就得到了第一个可变参数的起始内存地址。所以我们运行va_start(ap, v)以后,ap指向第一个可变参数在的内存地址*/

#define   va_arg(ap,t) (*(t*)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
/*这个宏做了两个事情,
①用用户输入的类型名对参数地址进行强制类型转换,得到用户所需要的值
②计算出本参数的实际大小,将指针调到本参数的结尾,也就是下一个参数的首地址,以便后续处理。*/
#define   va_end(ap)   (ap =  (va_list)0)

/*x86平台定义为ap=(char*)0;使ap不再指向堆栈,而是跟NULL一样。有些直接定义为((void*)0),这样编译器不会为va_end产生代码,例如gcc在linux的x86平台就是这样定义的。在这里大家要注意一个问题:由于参数的地址用于va_start宏,所以参数不能声明为寄存器变量或作为函数或数组类型。*/

以下再用图来表示:

在VC等绝大多数C编译器中,默认情况下,参数进栈的顺序是由右向左的,因此,参数进栈以后的内存模型如下图所示:最后一个固定参数的地址位于第一个可变参数之下,并且是连续存储的。

variant-para

(3)printf 研究

下面是一个简单的printf函数的实现:

#include   "stdio.h"
#include   "stdlib.h"
void   myprintf(char*   fmt,   ...)  // 一个简单的类似于printf的实现,参数必须都是int类型
{
  char*   pArg=NULL;   //等价于原来的va_list
  char   c;

  pArg   =   (char*)   &fmt;   // 注意不要写成p = fmt !!因为这里要对
  //参数取址,而不是取值
  pArg   +=   sizeof(fmt);   // 等价于原来的va_start

  do
  {
  c   =*fmt;
  if   (c   !=   '%')
  {
  putchar(c);   // 照原样输出字符
  }
  else
  {
  //按格式字符输出数据
  switch(*++fmt)
  {
  case   'd':
  printf("%d",*((int*)pArg));
  break;
  case   'x':
  printf("%#x",*((int*)pArg));
  break;
  default:
  break;
  }
  pArg   +=   sizeof(int);   // 等价于原来的va_arg
  }
  ++fmt;
  }while   (*fmt   !=   '\0');
  pArg   =   NULL;   // 等价于va_end
  return;
}

int   main(int   argc,   char*   argv[])
{
  int   i   =   1234;
  int   j   =   5678;

  myprintf("the   first   test:i=%d",i,j);
  myprintf("the   secend   test:i=%d;   %x;j=%d;",i,0xabcd,j);
  system("pause");
  return   0;
}

在intel+win2k+vc6的机器执行结果如下:

the first test:i=1234
the secend test:i=1234; 0xabcd;j=5678;

(4)应用

求最大值:

#include   "stdio.h"
#include   "stdlib.h"
int max(int n, int num, ...)
  {
  va_list   x;//说明变量x
  va_start(x,num);//x被初始化为指向num后的第一个参数
  int   m=num;
  for(int   i=1;i   {
  //将变量x所指向的int类型的值赋给y,同时使x指向下一个参数
  int   y=va_arg(x,int);
  if(y>m)m=y;
  }
  va_end(x);//清除变量x
  return   m;
}

main()
{
  printf("%d,%d",max(3,5,56),max(6,0,4,32,45,533));
}

看懂了以上内容,你就能看懂代码里的va_start、va_arg以及va_end是什么意思了。

(注:函数调用汇编分析系列文章暂时就写到这里,还有一些内容如pagefault时的函数调用链回溯,动态补丁也和这个相关,待后续整理)

  打分:5.0/5 (共3人投票)
(浏览总计: 89 次)
Add Comment Register



发表回复

  

  

  

您可以使用这些HTML标签

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>