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