關於
本文涉及到代碼,演示環境為:win10 + VS2017 ,ubuntu+clang
clang版本:
參數入棧順序
順序
幾種常見的函數參數入棧順序,還有兩種就不介紹了(__clrcall、__thiscall)
順序 | 釋義 |
---|---|
__cdecl | 函數參數按照從右到左的順序入棧,並且由調用函數者把參數彈出棧以清理堆棧 |
__stdcall | 函數參數按照從右到左的順序入棧,被調用的函數在返回前清理傳送參數的棧,函數參數個數固定 |
__fastcall | 使用內部寄存器ECX,EDX傳遞前兩個DWORD 或者size更小的參數 |
__fastcall用的很少。 通常情況下: c/c++默認入棧方式:__cdel。Windows api使用的是__stdcall方式。
自定義參數入棧形式
可以自定義函數的入棧順序。 常用形式如下
函數返回值 入棧規則 函數名(參數類型 參數名);
一個例子
int __cdecl get_name_index(const std::string& str_name);
為什么要從右往左入棧?
- 結論:為了確定函數參數個數。大膽猜想:統一定長參數函數和不定長參數函數處理函數入棧順序。
- 有的函數參數個數是確定的,比如上面的函數,就一個參數,有的函數參數是不定長,學過C的都知道printf和scanf,當然也可以自定義變長參數,c++11引入了可變長參數。
- 定長參數的函數,當然,每個參數都有自己的地址,很容易就拿到了,but, 不定長參數呢,怎么知道每個參數的地址和壓入多少個參數? 比如printf和scanf函數,第一個參數就很特別,一個格式化的字符串。printf是怎么知道有多少個參數呢? 通過格式格式字符串個數確定。有多少個格式字符串,就需要多少個參數
注意: 棧是先進后出,俗稱后來居上。
- 顯然,定長函數參數就不需要分析了,每個參數都有自己的地址,能確定下來,從左往右 對比 從右往左沒有區別。
- 那不定長函數參數呢? 以 printf函數為例,
int a = 1;
int b = 2;
printf("%d, %d", a, b);
情況A: 從左往右。 那么上面的代碼,先入棧的是"%d, %d", 再是a, 最后是b。printf需要知道壓入了多少個參數,就需要檢查格式化字符串"%d, %d", 但是,這個參數被壓入了棧底。棧頂是b。肉眼當然知道壓入了多少個參數,編譯器需要通過判斷條件才能知道,判斷條件被壓入棧底,這時,編譯器就無法知道格式化字符串了,也就不知道參數個數了。
情況B:從右往左。 那么上面的代碼,先入棧的是b,再是a,最后是"%d, %d". printf先檢查第一個參數,棧頂是"%d, %d", 就可以知道壓入了多少個參數了。
參數計算順序
這個和編譯器有關。不同編譯器可能也相同。
一定要知道
Note: 期望輸出 和 參數計算順序 息息相關。
為什么? 代碼參數的計算順序決定了實際輸出,這與期望輸出是兩碼事。 而期望輸出是主觀預測輸出結果。
我將使用下面的測試代碼,觀察VS和clang兩種編譯器的參數計算順序。
int a = 2;
printf("%d, %d, %d", a, (a = (a + 2)), (a = (a + 3)));
win10 + VS2017
使用VS2017輸出下面的代碼,可知計算順序。可自己先計算下輸出結果是什么....
正確輸出: 7, 7, 7
分析(個人理解): printf("%d, %d, %d", a, (a = (a + 2)), (a = (a + 3)));
可以拆解為下面的代碼:
// printf("%d, %d, %d", a, (a = (a + 2)), (a = (a + 3)));
a = a + 3;
a = a + 2
a
printf函數先將表達式的結果計算出來,得到結算結果,再將計算結果壓入棧。先計算a=a+3;, 此時, a的值從2->5, 同理,a=a+2;后,a的值從5->7。當執行函數printf時,變成了
printf("%d, %d, %d", 7, 7, 7);
- 按照上面的分析方法,再來一個例子
int a = 2;
int b = 3;
printf("%d, %d, %d", a, a = (a+b), b = (b-a));
輸出結果
printf("%d, %d, %d", a, a = (a+b), b = (b-a));
// 等效下面的方式
1. b = b - a;// b = 3-2; b = 1
2. a = a +b; // a = 2 + 1; a = 3
3. printf("%d, %d, %d", 3, 3, 1);
ubuntu + clang
自己先計算結果,再看答案。
clang輸出結果
可見,clang的計算順序是從左至右,而VS的計算順序是從右至左。
為什么clang的輸出結果是這樣的? 大膽猜想(個人意見)先計算a, 將a單獨一個副本,再計算 a=a+2, 結果為4,再將a=a+2的結果4保存到另一個副本中,最后計算a=a+3=4+3 = 7,將(a=a+3)拷貝到下一個副本中。
結論
應該避免上述情況,即可實現同樣的表達式,可以實現在不同的平台得到同樣的結果。
踩坑
這點倒是深有體會,自己寫公共服務模塊時,就因為上面的情況出現了一些困擾。