動態的呼叫可變引數函式

2021-04-12 16:42:01 字數 2516 閱讀 8062

最近,碰到了乙個奇怪的問題:如何在函式中動態的呼叫可變引數函式。例如說,有某個可變引數函式:

void func1(int a, ...) 

現在給出乙個個數不定的動態陣列,把裡面的數值按順序的作為可變引數傳遞進 func1 函式中。

當然,如果允許改變 func1 的定義,那麼我相信每個人都可以輕鬆的完成這個任務,而且方法一定也是八仙過海,各顯神通。但是如果限定不可以更改 func1 的定義,那麼這個問題就麻煩了。在找遍手頭上所有 c/c++ 語法資料之後,才愕然的發現,這個問題在 c/c++ 語法的規範內,是無解的。那麼,為了解決這個問題,方法就只剩下乙個了:動用 c/c++ 的終極絕招:嵌入彙編**。

說起,嵌入彙編,很多人會覺得很深奧,很難懂。但是仔細分析一下,其實也不見得高不可攀。回到這個問題上,這個問題的根源在於:c/c++的語法中,函式呼叫時的壓棧、呼叫、清理堆疊和取得返回值必須在一條 c/c++ 語句中完成。所以,任何企圖用迴圈或者巨集展開後模擬迴圈的方法都不可能實現這個目的。所以,我們嵌入彙編的目的也就很明確了:就是自己用彙編實現一次函式呼叫過程。

c/c++的函式中,我們可以手工指定3種不同的呼叫約定:__stdcall、__cdecl、__fastcall。關於這幾種呼叫約定的異同,網上的文章已經是汗牛充棟,比比皆是了,在這裡就不再繼續重複,不過值得指出的是,只有 __cdecl 可以支援可變引數,所以,我們只需要實現這個呼叫約定的過程即可。

__cdecl的呼叫約定其實並不複雜,簡而言之,就是從右到左壓棧,呼叫者恢復(清理)堆疊。我們就可以先構造一段簡單的函式呼叫**看看:

void func3(int a, int b);

void func4(void)

}看上去並不複雜,引數壓棧的順序是從右到左,然後呼叫 call,最後再把esp的指標恢復為呼叫前的地方。這基本上就是編譯器在編譯「func3(1, 2);」語句時在後台偷偷實現的**。那麼對於我們需要實現的可變引數函式呼叫來說,壓棧和清理的工作,都顯得複雜一些,因為我們無法預知引數的個數,所以,壓棧的指令(push)將不會是固定的個數,而必須在乙個迴圈中實現。簡單的實現**如下:

int func5(int inum)

delete p;

}這段**比上面的func4要稍微複雜一點,但是也是很清晰明了的:迴圈壓棧的過程是 loop1 到 「jns loop1」之間的**,值得注意的是在呼叫函式 func1 之後,需要重新計算棧頂指標(esp)的位置,這時,需要重新從 inum 中讀取並計算,而且盡量不要用到 eax 和 edx 這兩個暫存器,因為它們可能會被用來傳遞 func1 的返回值。對於函式的返回值,如果返回的資料小於32位,那麼它將會被放入 eax 暫存器中,如果它的大小大於32位小於64位,那麼會用 edx 儲存高32位,eax 儲存低32位。其它的情況下,編譯器會在記憶體中開闢一塊新的記憶體存放返回值,然後在 eax 傳遞這塊記憶體的指標。

問題到這裡就應該結束了。但是,為了滿足一些不是 int 陣列型別作為引數的情況,我們需要對函式的引數壓棧方式進行了解:對於內建資料型別作引數壓棧時,用的是32位對齊的方式壓棧,不足32位的,補足32位(其實補的是什麼沒關係,反正在被呼叫函式中不會用到這些位,只是 push 指令要求32位而已)。大於32位的,則按照由高到低的順序依次壓棧。對於不可轉換為內建資料型別的變數壓棧,就會複雜很多,因為這涉及到這些物件的構造和析構的問題,在這裡就不詳細描述了。為了呼叫的方便,我把它簡單的封裝成了乙個類:

class cvararg

else

}void reset(void)

private:

std::vectorm_args;

};在這個類中,setarg 負責把需要傳入的各種型別的引數統一轉換為 int 陣列,存入臨時「堆疊」 m_args 中,在 run 函式中,利用 vector 存放位址連續的特性,結合上面 func5 給出的示例**,就可以完成這個呼叫。要注意兩點,第一:在 run 函式中,實際呼叫的目標函式的函式指標被定義為 void*,這時因為在彙編中,所有的指標都是一樣的,無須區分指標的型別。(其實所有 c/c++ 中的指標型別,都是給編譯器看的,只要能騙過編譯器,那麼用起來就百無禁忌了,不過要是出錯,也會是匪夷所思的);第二:在這個函式的結尾處沒有 return 語句,也沒有在呼叫 pfunc 函式後改變任何 eax、edx 的值,所以,run 函式的返回值,實際上等價於 pfunc 函式的返回值。(其實對於這個沒有「return」語句的函式,我也覺得有點奇怪:編譯器不但給編譯通過了,居然還乙個 warning 都沒有,不過也好,省得我還要給它偽造返回值)。

這個類的使用,也是很方便的:

cvararg va;

va.setarg(1);  //這裡需要按照正確順序呼叫 setarg

va.setarg(2);

va.run(func1); //如果需要返回值,用類似 t t = va.run(func1); 的語句呼叫

當然,這個類還有很大的優化和改進空間,特別是 setarg 函式,如果有興趣的話,可以繼續研究一下。不過在這裡還是要提醒各位:由於彙編**的可移植性不強,所以,如果需要照搬這裡的**使用的話,必須對這部分的**進行充分的測試,並盡可能的覆蓋所有可能使用到的情況,例如各種引數型別,各種呼叫/被呼叫函式等等。否則,出了問題,是很難查的。

php呼叫可變函式,PHP呼叫參數量可變的函式

所以我遇到了一些問題。我知道乙個解決方案,但它看起來不太乾淨,我想知道是否有更好的解決方案。我正在編寫乙個mysqli包裝器,用於執行準備好的語句。因為它是乙個包裝器,可以重用 動態 返回的列數取決於查詢,並且不是靜態的。我已經找到了乙個解決這個問題的方法,似乎每個人都在使用 call user f...

python可變引數呼叫函式問題

一直使用python實現一些想法,最近在使用python的過程中出現這樣乙個需求,定義了乙個函式,第乙個是普通引數,第二個是預設引數,後面還有可變引數,在最初學習python的時候,都知道非關鍵字可變引數和關鍵字可變引數兩種,呼叫的方式也非常多種多樣,這裡主要提出乙個比較隱含的問題,並將各種可能出現...

python可變引數呼叫函式問題

一直使用python實現一些想法,近期在使用python的過程 現這樣乙個需求,定義了乙個函式。第乙個是普通引數。第二個是預設引數,後面還有可變引數,在最初學習python的時候,都知道非keyword可變引數和keyword可變引數兩種,呼叫的方式或許多種多樣,這裡主要提出乙個比較隱含的問題。並將...