是由 callee (被呼叫者) 自己負責,而不是 caller (呼叫者),參數推入堆疊的次序是
由右至左,很顯而易見的是 stdcall 會產生體積較小的程式碼,
因為你的主程式不用自己平衡堆疊
來看看另外一種重要的呼叫慣例 __cdecl
要說 stdcall 與 cdecl 哪一個重要,其實都很重要,沒有先後,
像 Linux系統下就通通全部都是 cdecl,這種特性本身也體現了 Linux
追求作為一個 "很純 C" 極致的作業系統,假如用 Linux 的話,基本上不太用
煩惱呼叫慣例的問題。
cdecl 呼叫慣例也存在很久了,正如其名,"C 呼叫慣例"
它的參數傳遞規則也是很簡單,與 stdcall 相同,由右至左,它的函式
編譯後的真名也很簡單,看一下例子就懂
假如有一條函式 int __cdecl MyAdd(int, int) 則
真名為 _MyAdd,沒錯,就是原來名稱前面加底線。
__cdecl 這個 keyword 可以省略,因為一般 C語言環境
通通預設都是這種呼叫慣例,除非修改環境設定。
可是 cdecl 與 stdcall 除了真名不同外,有一點也不同,這是最重要的特性,
這種呼叫慣例的堆疊平衡是由 caller 處理,callee 是不處理堆疊平衡的問題。
看程式碼就懂
假如呼叫了 使用 cdecl 慣例的 MyAdd,產生的組合語言會變成
push 2 ;推入立即數 2
push 1 ;推入立即數 1
call _MyAdd ;推入返回位址,在呼叫 _MyAdd
add esp,8 ;由caller自己平衡堆疊頂指標 esp
mov dword ptr [ebp-4],eax ; eax 在傳給區域變數
可以看到 因為有兩個參數,所以 caller 平衡時要將 esp 在加 8。
在來看看 參數傳入 MyAdd 後,MyAdd 怎麼接收它們
int MyAdd(int a,int b)
... 省略 Prologue 跟 stdcall 一模一樣
=========================================
return a + b;
mov eax,dword ptr [ebp+8]
add eax,dword ptr [ebp+0Ch]
=========================================
... 省略部分 Epilogue 跟 stdcall 一模一樣
ret ; 沒有特別指定堆疊平衡數值,
; 所以最後只從堆疊彈出返回位址,然後 esp<==esp+4
; 根據返回位址跳躍。
因為堆疊平衡由caller 負責,所以就可以實現不定參數個數的函式,例如 printf,
這類函式只能使用 cdecl。
簡單的說,就是把 ret n 拆開成兩個部分
ret n ; 本來在 callee 內
變成
ret ; 現在只有 ret 在 callee 內
add esp, n ; 將 esp 加 n 的動作由 caller 負責
在知道原理之後,也許會有個疑問,那要怎麼自己寫這類不定參數個數的函式呢
其實解了上面的原理就很簡單了,顯然要先取得 不定參數個數 倒底有多少個參數
其次就是每個參數實際存放在 堆疊裡面的位址。
因為我們知道不管是 stdcall 或 cdecl,剛進去函式內部時,當執行完Prologue時,
[esp] 必然指向 ebp 而前一個堆疊元素就是 返回位址 [esp+4] 而
所以 [esp + 8] 就是 函式的第一個參數值,用內插組語可以很輕易取得。
但是在 C 語言中,這類函式最好至少要給出第一個參數,
後面使用不定參數會比較方便,例如
int MyAdd(int first, ... ); // 至少要給出第一個參數
這是為了 C 語言實作不定參數上能夠較容易實現,因為只要有第一項參數
就可以用 C 語言的 "address of" operator 取得第一項參數的位址,而 VC 中
也使用了 Macro 方便取得每一個參數值,否則得要自己操作 [ebp+n]取得不定參數。
一個可變參數的加法函式
int MyAdd(int first, ...)
{
int sum = 0, i = first;
va_list marker;
va_start( marker, first ); /*Initialize variable arguments.*/
while( i != -1 )
{
sum += i;
i = va_arg( marker, int);
}
va_end(marker); /*Reset variable arguments.*/
return sum;
}
假如是 int MyAdd( ... )這種形式,那你得要自己處理頭位址問題
int MyAdd3( ... )
{
int *first;
int sum = 0, i;
va_list marker;
__asm{
push eax ; save eax
// mov eax, ebp ; 因為參數頭位址必然是 ebp+8
// add eax, 8 ; eax = ebp + 8
// 取得參數頭位址,在加上偏移值,可以用 lea 指令合併
lea eax, [ebp+8]; lea 只取 ebp+8 而不會取 [ebp+8]
mov first, eax ; first = eax
pop eax ; restore eax
}
va_start( marker, *first ); /*Initialize variable arguments.*/
i = *first; // 我們研究一下 指標變數的原始面貌
/* mov eax,dword ptr [ebp-4]; 其實 first:=[ebp-4]
mov ecx,dword ptr [eax] ; 內容本身是位址,[eax]才是內容
mov dword ptr [ebp-0Ch],ecx; i:=[ebp-0ch] */
while( i != -1 )
{
sum += i;
i = va_arg( marker, int);
}
va_end(marker); /*Reset variable arguments.*/
return sum;
}
指標變數用組合語言觀點看,其實就是雙指標了,32-bit 整數位址
作為 stack [ebp-4] 的內容,取出內容後在指一次,取得實際內容。
在內插組合語言內先取得第一個參數位址放入 first,在利用C語言的指標運算
*first 取得內容,才把第一個數值傳遞給 變數 i。
讓我們看看這些 va_ 開頭的 Macro
#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 )
很明顯,va_start 的目的就是要取得參數陣列的頭位址,為了要保證指標指向的大小為
byte,因此進行了強迫型態轉換,其中 va_list 只是 char* 的化身,加 _INTSIZEOF(v),
就可以指向變參數陣列的頭位址,_INTSIZEOF 巨集很有趣,這種運算會剛剛好使得
記憶體對齊指定型態的邊界,這裡要對齊的邊界型態是 int,為何需要如此?假如你有
看懂翻譯成組合語言的表達,應該也發現了 push 在 32-bit 系統是一次 4-byte在推入,所以
一定要保證剛剛好一次存取一整個堆疊元素 4-byte 大小,因為有些型態翻譯為組合語言
是會變成兩次 push 來表達,假如第一個參數型態變成 double 那組合語言會變成 push 兩次
4-byte 的整數值,邊界沒對齊的話,整個堆疊區就崩潰了。
至於 va_arg(apt, t) 就沒甚麼神奇了,你在組合語言內可以用 [ebp+n] 不斷偏移取得所有參數值
在 C 語言也是用 += 運算子 不斷偏移資料,Macro 參數 t 是指 type,指標偏移要根據資料
型態的實際大小來移動,在這裡是 int,最後 va_end 只是用完後,指標歸零而已。
那麼在不定參數中,假如並非所以型態都是 int,你也許可能會有一些型態要視為 double
這樣的話,得自己在函式中,利用指標重新解讀資料的意義。
例如 你可能已經 取得所有變參數陣列的資料,並且儲存在一個 Array 中,類似於
int Buf[] = {0x00000000,0x3ff00000};
這兩個 int 可能在你的編碼中,真正的意義是 double,因此
你需要用指標重新解讀資料的意義。
double*pf64Buf = (double*)(Buf);
當你用 printf 印出*pf64Buf的內容,就會發現原來這就是浮點數1.0
完
後記:怎麼講個 cdecl 會不小心講到一些指標的知識,因為它們是一連貫的知識
只有當你這些東西完全了解,你才會知道甚麼叫 cdecl,尤其是變參數的特性。
fastcall 還沒開講 不過其實它不是很重要,只是推入的參數有幾個放在暫存器
用來加快參數傳遞速度而已,Part 3在來講。
還有,Du 快把QT文教學PO上來 \/. (我要一次上手文)
你們好酷喔!可以這樣三個人共筆。^^
回覆刪除哈哈 謝謝你 不過我不太會建構網頁
回覆刪除所以就從最簡單的開始了
while( i != -1 )
回覆刪除{
sum += i;
i = va_arg( marker, int);
}
請問這里while 迴圈跳脫的條件為什麼是 i == -1 呢?