可变参数的函数的原理及其简单模仿

来源:互联网 发布:服装设计的网络课程 编辑:程序博客网 时间:2024/04/30 01:28

可变参数的定义是类似这样的:

void _cdecl myfun(char * fmt, ...){

...

}

这里的fmt主要是为了能够识别后面到底有几个参数及其类型的,否则编译器是无法判断函数参数个数的。

由于参数的个数可变,所以也只有c调用风格的函数可以实现它,因为只有c调用风格的函数,参数的传递是由调用者负责的,而stdcall是由函数自身负责的。win32 api都是stdcall的。要想实现可变参数,那么,必须有办法取到第一个参数,并可以预知参数类型,从而通过递增或者递减地址,来逐个提取参数。

上述的递增递减的不同,取决于cpu。比如,如果cpu 的指令push是导致esp的栈指针指向内存的低端地址还是高端地址。我的机器push会导致esp的值变小,也就是低端生长。

加入我们有个函数f调用了myfun函数,那么大致是这样的

void f(){

  int a1;

  int a2;

   myfun("abd", 'a', 4);

}

则函数调用出的反汇编情况,大致是:

push 4

push 'a'的ascii码

push "abd"的内存地址

call myfun

add esp , 12         平衡一下栈操作,如果是stdcall就不会有这句了,相应的栈平衡会在函数myfun的内部的末尾处理,因为stdcall可以根据函数声明判断应该pop多少来平衡栈。

所以,看到这里,其实我们已经可以思考一下,也就知道该如何实现可变参数提取了,只要取得myfunc中的fmt参数地址(也就是push "abd"中压入的地址),然后把地址的值增加,就可以依次取出 第二个参数地址。。。

比如myfunc(char * fmt, ...){

    char * last_para_address = reinterpret<char*>(&fmt);//

   last_para_address += 4;//这里就是存放“abd”指针的那个参数的地址

   last_para_address += 4;//这就是存放‘a'的那个参数的地址

   。。。。。

所以,我们可以这样实现

 void _cdecl myprintf(char * fmt, ...){
    char * vara_address = reinterpret_cast<char*>(&fmt);
    #define getArgByType(t) ((sizeof(t) + sizeof(int) - 1)/sizeof(int)*sizeof(int)) //push,pop每次都是按照一个Int类型大小操作的,所以,比如char类型参数,入栈时也是压入int大小字节
    
    char * local1 = NULL;
    char * local2 = NULL;
    int direction = 0;
    if((&local1) > (&local2)){//判断栈的增长方向,我的机器local1是先分配的变量,栈向着低端生长,所以&local1>&local2
        direction = 1;//此时,由最后一个参数fmt向着内存高端递增,便可以获取声明中第二个,第三个。。。参数了
    }else{
        direction = -1;
    }
    vara_address += getArgByType(fmt) * direction;
    while(*fmt != '\0'){
        switch(*fmt){
            case 'c':
                cout<<"char "<<*reinterpret_cast<char*>(vara_address)<<endl;//fmt的作用就是判断每个参数类型,从而可以知道该移动多长的字节才能取到下一个参数地址
                vara_address += getArgByType(char);
                break;
            case 'd':
                cout<<"int "<<*reinterpret_cast<int*>(vara_address)<<endl;
                vara_address += getArgByType(int);
                break;
            default:
                vara_address += getArgByType(int);//这里就是给default随便做个处理,无关大雅
                break;
    }
    ++fmt;
}

}

这应该就是类似printf的实现机制了吧。

另外说明一点,获取cpu的大端小端可以使用类似如下代码:

union type{

char c[sizeof(int)];

int   i;

};

type t ;

t.c[0] = 0x1;

t.c[1] = 0x2;

t.c[2] = 0x3;

t.c[3] = 0x4;

if(t.i == 0x04030201){//小端,就是通常我们的机器上的情况

}else{//大端 t.i == 0x01020304

}




原创粉丝点击