您的位置:首页 > 编程语言 > C语言/C++

可变参数函数实现

2007-03-15 14:59 393 查看
    网上看到有人问C下的printf函数怎样实现,觉得这个问题有点意思,于是找了下printf函数的源代码.
printf的声明如下:
    int __cdecl printf(const char *format, ...);
实现部分为先分析输出格式串,计算出后面参数的个数,接着依次输出后面的参数值到电脑终
端.我觉得printf函数之所以神秘,是因为我们基本上没有写过可变参数的函数,如果掌握了
可变参数的"秘密"之后,就与其它函数没有太大的区别了.
1.调用约定及堆栈图

在讲解可变参数之前,我们有必要了解参数是传递的.调用约定(calling conversion)说
的就是这个事情.每一种调用约定对应一种参数传递方式,各种调用约定的特性见下表:
 

调用约定
(Calling Conversion)
参数传递
(Argument Passing)
栈维护
(Stack Maintenance)
名称修饰
(Name Decoration)
备注(Notes)
__cdecl
从右到左
调用者清除栈参数.唯一允许可变参数的方式
函数名前加下划线,
如_Foo
C和C++默认的方式
__stdcall
从右到左
被调用者清除栈参数
函数名前加下划线,函数名后加@以及十进制表示的参数所占总字节数,如_Foo@12
几乎所有的系统函数都采用这种方式;VB内部函数也是这种方式
__fastcall
头两个DWORD参数通过ECX和EDX传递;剩余的从右到左传递.

调用者清除栈参数.
函数名前后都加@,并在后面跟十进制表示的参数所占字节数.
只在Intel cpu上才能够使用.Delphi编译器就采用这种方式.
This
右到左.参数this通过ECX寄存器传递.
调用者清除栈参数
None
在没有指定标准调用(__stdcall)的方式下C++的类方法调用就是这种情况.COM的方法都被声明为标准调用方式
Naked
从右到左
调用者清除栈参数.
None
VxD使用这种方式,或者你不想要prolog和epilog时采用.
 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

printf采用的就是__cdecl方式,在__cdecl下参数都是通过栈来传递,栈是一种后进先出的数
据结构,通常从高地址开始存放数据,函数栈结构有如下这个样子:
   
    [参数 n       ]
    ...
    [参数 2       ]
    [参数 1       ]
    [函数返回地址 ]
    [前基地址指针 ]
    [局部变量     ]
 

我们用一个例子来说明:
 
  void Foo(int a, int b)
    {
        DWORD MyArray[4];
        int Index;   
    }
   
    void main(void)
    {
        Foo(3, 4);
        int iCount = 1;
    }
 

 

当程序从main进入到Foo时,栈结构图如下
    [4                     ]    /*参数b的值*/
    [3                     ]    /*参数a的值*/
    [返回地址        ]    /*main中代码int iCount = 1;的地址*/
    [前基地址值    ]    /*ebp*/
    [MyArray[3]      ]
    [MyArray[2]      ]
    [MyArray[1]      ]
    [MyArray[0]      ]
    [Index              ]    /*Foo中的局部变量Index*/
 

由于栈中数据存放是从高到低的原则,如果我们知道参数a的地址为0x0012ff24,则参数b的地址为:
    &b = 0x0012ff24 + sizeof(a);
懂得了怎样通过一个参数地址得到另一个参数的地址,我们就已经具备了处理可变参数的能力了.
 

2.宏

    为了让处理可变参数的过程更直观、不易出错,我们通常都会看到可变参数的函数中对
如下几个宏的使用,宏及其定义如下(摘自VC6中的STDARG.H):
typedef char *  va_list;
 

#define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
#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 )
 

 

这几个宏应该算比较好理解,
    _INTSIZEOF(n)   计算n的字节大小,以int所占字节数作为对齐.
    va_start(ap,v)       让ap指向参数v的下一个参数.
    va_arg(ap,t)        得到ap所指向的值,并让ap指向下一个参数.
    va_end(ap)        让ap = 0.
   
3.例子

  一个例子可以让我们对这些宏有很好的掌握,下例来自MSDN
 

#include <malloc.h>
#include <stdio.h>
#include <string.h>
 

// crt_va.c
/* The program below illustrates passing a variable
 * number of arguments using the following macros:
 *      va_start            va_arg              va_end
 *      va_list             va_dcl (UNIX only)
 */
 

#include <stdio.h>
#include <stdarg.h>
int average( int first, ... );
 

int main( void )
{
       /* Call with 3 integers (-1 is used as terminator). */
       printf( "Average is: %d/n", average( 2, 3, 4, -1 ) );
      
       /* Call with 4 integers. */
       printf( "Average is: %d/n", average( 5, 7, 9, 11, -1 ) );
      
       /* Call with just -1 terminator. */
       printf( "Average is: %d/n", average( -1 ) );
}
 

/* Returns the average of a variable list of integers. */
int average( int first, ... )
{
       int count = 0, sum = 0, i = first;
       va_list marker;
      
       va_start( marker, first );     /* Initialize variable arguments. */
       while( i != -1 )
       {
              sum += i;
              count++;
              i = va_arg( marker, int);
       }
       va_end( marker );              /* Reset variable arguments.      */
       return( sum ? (sum / count) : 0 );
}
 

 

4.引用资源列表

    通过下面的书籍或文章可以找到相关的更多信息:
    http://www.codeproject.com/debug/cdbntsd2.asp     缓冲区溢出的原理和实践(Phrack) by Sinbad
<<Debugging Applications>> Chapter  by John Robbins
 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息