可变参数函数实现

    网上看到有人问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

CC++默认的方式

__stdcall

从右到左

被调用者清除栈参数

函数名前加下划线,函数名后加@以及十进制表示的参数所占总字节数,_Foo@12

几乎所有的系统函数都采用这种方式;VB内部函数也是这种方式

__fastcall

头两个DWORD参数通过ECXEDX传递;剩余的从右到左传递.

调用者清除栈参数.

函数名前后都加@,并在后面跟十进制表示的参数所占字节数.

只在Intel cpu上才能够使用.Delphi编译器就采用这种方式.

This

右到左.参数this通过ECX寄存器传递.

调用者清除栈参数

None

在没有指定标准调用(__stdcall)的方式下C++的类方法调用就是这种情况.COM的方法都被声明为标准调用方式

Naked

从右到左

调用者清除栈参数.

None

VxD使用这种方式,或者你不想要prologepilog时采用.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 
















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

 

你可能感兴趣的:(可变参数函数实现)