可變參數的函數的原理及其簡單模仿

可變參數的定義是類似這樣的:

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

}




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章