C++中的函數調用約定(調用慣例)主要針對三個問題:
1、參數傳遞的方式(是否采用寄存器傳遞參數、采用哪個寄存器傳遞參數、參數壓桟的順序等);
參數的傳遞方式,最常見的是通過棧傳遞。函數的調用方將參數壓入棧中,函數自己再從棧中將參數取出。
對於有多個參數的函數,調用慣例要規定函數調用方將參數壓棧的順序,是從左往右還是從右往左。有些調用慣例還允許使用寄存器傳遞參數。
2、函數調用結束后的棧指針由誰恢復(被調用的函數恢復還是調用者恢復);
棧的維護方式:在函數將參數壓棧之后,函數體 會被調用,此后需要將被壓入的參數全部彈出,以使得棧在函數調用前后保持一致。這個彈出工作可以由函數的調用方來完成,也可以由函數本身完成。
3、函數編譯后的名稱;
名稱修飾策略,為了鏈接的時候對調用慣例進行區分,調用慣例要對函數本身的名字進行修飾。不同的調用慣例有不同的名字修飾策略。
對實例代碼有幾點說明(使用的平台為vs2012+intel x86架構)
1、棧頂指針即為esp;
2、int型占32字節內存;
3、桟頂為小地址端,棧底為大地址端,因此出棧需要增大esp;
下面對C++中見到的stdcall、cdecl、fastcall和thiscall做簡要說明。
1、stdcall
stdcall是standard call的縮寫,也被稱為pascal調用約定,因為pascal使用的函數調用約定就是stdcall。
使用stdcall的函數聲明方式為:int __stdcall function(int a,int b)
stdcall的調用約定意味着:
1)采用桟傳遞全部參數,參數從右向左壓入棧;
2)被調用函數負責恢復棧頂指針 ;
3) 函數名自動加前導的下划線,后面是函數名,之后緊跟一個@符號,其后緊跟着參數的尺寸,例如_function@4;
下面給出實例:
- int _stdcall funb(int p,int q) //聲明為stdcall方式
- {
- return p-q;
- }
- e=funb(3,4);
- 012C42F7 push 4 //參數q入棧
- 012C42F9 push 3 //參數p入棧
- 012C42FB call funb (012C1244h) //調用函數
- 012C4300 mov dword ptr [e],eax //調用者沒有處理esp
函數編譯后的匯編代碼為:
- int _stdcall funb(int p,int q)
- {
- 012C3D80 push ebp
- 012C3D81 mov ebp,esp //將esp保存入ebp中
- 012C3D83 sub esp,0C0h
- 012C3D89 push ebx
- 012C3D8A push esi
- 012C3D8B push edi
- 012C3D8C lea edi,[ebp-0C0h]
- 012C3D92 mov ecx,30h
- 012C3D97 mov eax,0CCCCCCCCh
- 012C3D9C rep stos dword ptr es:[edi]
- return p-q;
- 012C3D9E mov eax,dword ptr [p]
- 012C3DA1 sub eax,dword ptr [q]
- }
- 012C3DA4 pop edi
- 012C3DA5 pop esi
- 012C3DA6 pop ebx
- 012C3DA7 mov esp,ebp
- 012C3DA9 pop ebp
- 012C3DAA ret 8 //注意此處,用被調函數負責恢復esp
以上面函數為例,參數q首先被壓棧,然后是參數p(參數從右向左入棧),然后利用call調用函數,
而在編譯時,這個函數的名字被翻譯成_funb@8,其中8代表參數為8個字節(2個int型變量)。
另外,stdcall可以用於類成員函數的調用,這種情況下唯一的不同就是,所有參數從右向左依次入棧后,this指針會最后一個入棧。下面給出示例。
- class A
- {
- public:
- A(int a)
- {
- this->val=a;
- }
- int _stdcall fun(int par) //類成員函數采用stdcall
- {
- return val-par;
- }
- private:
- int val;
- };
函數調用代碼如下:
- A t(3);
- int d,e,f,g;
- g=t.fun(4);
函數調用代碼編譯后為:
- g=t.fun(4);
- 00DB4317 push 4 //參數4入棧
- 00DB4319 lea eax,[t]
- 00DB431C push eax //this指針入棧,下面會驗證eax內容即為A的對象的地址
- 00DB431D call A::fun (0DB1447h)
- 00DB4322 mov dword ptr [g],eax
編譯后的代碼為:
- int _stdcall fun(int par)
- {
- 00DB3CF0 push ebp
- 00DB3CF1 mov ebp,esp
- 00DB3CF3 sub esp,0C0h
- 00DB3CF9 push ebx
- 00DB3CFA push esi
- 00DB3CFB push edi
- 00DB3CFC lea edi,[ebp-0C0h]
- 00DB3D02 mov ecx,30h
- 00DB3D07 mov eax,0CCCCCCCCh
- 00DB3D0C rep stos dword ptr es:[edi]
- return val-par;
- 00DB3D0E mov eax,dword ptr [this]
- 00DB3D11 mov eax,dword ptr [eax]
- 00DB3D13 sub eax,dword ptr [par]
- }
- 00DB3D16 pop edi
- 00DB3D17 pop esi
- 00DB3D18 pop ebx
- 00DB3D19 mov esp,ebp
- 00DB3D1B pop ebp
- 00DB3D1C ret 8 //由被調用函數負責恢復棧頂指針,由於參數為int型變量(4字節)和一個指針(32為,4字節),共8字節
下面驗證入棧時eax中的內容為A對象的地址。
入棧時eax內容如下,為0x0035F808。
找到內存中0x0035F808的內容,為3,。
再看main函數中實例化對象的代碼。
可見,this指針正是通過eax入棧。
由此可見,用於類成員函數時,唯一的不同就是在參數入棧完畢后,this指針會最后一個入棧。
2、cdecl
cdecl是C Declaration的縮寫,又稱為C調用約定,是C語言缺省的調用約定,采用這種方式調用的函數的聲明是:
int function (int a ,int b) //不加修飾就是采用默認的C調用約定
int _cdecl function(int a,int b) //明確指出采用C調用約定
cdecl調用方式規定:
1、采用桟傳遞參數,參數從右向左依次入棧;
2、由調用者負責恢復棧頂指針;
3、在函數名前加上一個下划線前綴,格式為_function;
要注意的是,調用參數個數可變的函數只能采用這種方式(如printf)。
下面給出實例。
- int _cdecl funa(int p,int q) //采用cdecl方式
- {
- return p-q;
- }
調用處的代碼編譯為:
- d=funa(3,4);
- 012C42E8 push 4
- 012C42EA push 3
- 012C42EC call funa (012C1064h) //調用funca
- 012C42F1 add esp,8 //調用者恢復棧頂指針esp
- 012C42F4 mov dword ptr [d],eax //返回值傳遞給變量d
函數編譯后的代碼為:
- int _cdecl funa(int p,int q)
- {
- 012C3D40 push ebp
- 012C3D41 mov ebp,esp
- 012C3D43 sub esp,0C0h
- 012C3D49 push ebx
- 012C3D4A push esi
- 012C3D4B push edi
- 012C3D4C lea edi,[ebp-0C0h]
- 012C3D52 mov ecx,30h
- 012C3D57 mov eax,0CCCCCCCCh
- 012C3D5C rep stos dword ptr es:[edi]
- return p-q;
- 012C3D5E mov eax,dword ptr [p]
- 012C3D61 sub eax,dword ptr [q]
- }
- 012C3D64 pop edi
- 012C3D65 pop esi
- 012C3D66 pop ebx
- 012C3D67 mov esp,ebp
- 012C3D69 pop ebp
- 012C3D6A ret //注意此處,被調函數沒有恢復esp
因此,stdcall與cdecl的區別就是誰負責恢復棧頂指針和編譯后函數的名稱問題。
cedcal同樣可以用於類成員函數的調用。此時,cdedl與stdcall的區別在於由誰恢復棧頂指針。
類定義如下:
- class A
- {
- public:
- A(int a)
- {
- this->val=a;
- }
- int _cdecl fun(int par) //采用cedcl方式
- {
- return val-par;
- }
- private:
- int val;
- };
調用代碼編譯如下:
- g=t.fun(4)
- 013D4317 push 4
- 013D4319 lea eax,[t]
- 013D431C push eax //先入棧參數4,后入棧this指針
- 013D431D call A::fun (013D144Ch)
- 013D4322 add esp,8 //由調用者恢復棧頂指針
- 013D4325 mov dword ptr [g],eax
3、fastcall
采用fasecall的函數聲明方式為:
int __fastcall function(int a,int b)
fastcall調用約定意味着:
1、函數的第一個和第二個(從左向右)32字節參數(或者尺寸更小的)通過ecx和edx傳遞,其他參數通過桟傳遞。從第三個參數(如果有的話)開始從右向左的順序壓棧;
2、被調用函數恢復棧頂指針;
3、在函數名之前加上"@",在函數名后面也加上“@”和參數字節數,例如@function@8;
示例代碼如下:
- int __fastcall func(int p,int q,int r) //采用fastcall
- {
- return p-q-r;
- }
調用代碼如下:
- f=func(3,4,5);
- 00E74303 push 5 //第三個參數r壓桟
- 00E74305 mov edx,4 //p q通過ecx和edx傳遞
- 00E7430A mov ecx,3
- 00E7430F call func (0E71442h)
- 00E74314 mov dword ptr [f],eax //調用者不負責恢復棧頂指針esp
函數編譯后的代碼如下:
- int __fastcall func(int p,int q,int r)
- {
- 00E73DC0 push ebp
- 00E73DC1 mov ebp,esp
- 00E73DC3 sub esp,0D8h
- 00E73DC9 push ebx
- 00E73DCA push esi
- 00E73DCB push edi
- 00E73DCC push ecx
- 00E73DCD lea edi,[ebp-0D8h]
- 00E73DD3 mov ecx,36h
- 00E73DD8 mov eax,0CCCCCCCCh
- 00E73DDD rep stos dword ptr es:[edi]
- 00E73DDF pop ecx
- 00E73DE0 mov dword ptr [q],edx
- 00E73DE3 mov dword ptr [p],ecx
- return p-q-r;
- 00E73DE6 mov eax,dword ptr [p]
- 00E73DE9 sub eax,dword ptr [q]
- 00E73DEC sub eax,dword ptr [r]
- }
- 00E73DEF pop edi
- 00E73DF0 pop esi
- 00E73DF1 pop ebx
- 00E73DF2 mov esp,ebp
- 00E73DF4 pop ebp
- }
- 00E73DF5 ret 4 //恢復棧頂指針,由於只有一個參數r被壓桟,因此esp+4即可
可以看到,fasecall利用寄存器ecx與edx傳遞參數,避免了訪存帶來的開銷。適合少量參數提高效率的場合。
4、thiscall
thiscall是唯一一個不能明確指明的函數修飾,因為thiscall只能用於C++類成員函數的調用,同時thiscall也是C++成員函數缺省的調用約定。由於成員函數調用還有一個this指針,因此必須特殊處理。
thiscall意味着:
1、采用桟傳遞參數,參數從右向左入棧。如果參數個數確定,this指針通過ecx傳遞給被調用者;如果參數個數不確定,this指針
在所有參數壓棧后被壓入堆棧;
2、對參數個數不定的,調用者清理堆棧,否則由被調函數清理堆棧