__cdecl、__stdcall、__fastcall 与 __pascal 浅析
- X86調用約定 calling convention:https://www.cnblogs.com/shangdawei/p/3323252.html
- __cdecl、__stdcall、__fastcall 與 __pascal 淺析:https://www.cnblogs.com/yenyuloong/p/9626658.html
- 王爽 匯編語言第三版 第9章 轉移指令的原理:https://blog.csdn.net/freeking101/article/details/100581181
- 調用約定(Calling Conventions) (__cdecl、__stdcall、__fastcall) C++函數名修飾 ( Name Mangling ):http://www.3scard.com/index.php?m=blog&f=view&id=10
- 深入體會__cdecl與__stdcall:https://www.cnblogs.com/sober/archive/2009/09/01/1558178.html
- __stdcall 詳解:https://www.cnblogs.com/songfeixiang/p/3733661.html
- 用od分析_cdecl和_stdcall調用慣例的差異:https://blog.csdn.net/tch3430493902/article/details/101366850
- X86調用約定:https://zh.wikipedia.org/wiki/X86調用約定
? ? ? ? 在C語言中,假設我們有這樣的一個函數:int function(int a,int b);調用時只要用result = function(1,2)這樣的方式就可以使用這個函數。但是,當高級語言被編譯成計算機可以識別的機器碼時,有一個問題就凸現出來:在CPU中,計算機沒有辦法知道一個函數調用需要多少個、什么樣的參數,也沒有硬件可以保存這些參數。也就是說,計算機不知道怎么給這個函數傳遞參數,傳遞參數的工作必須由函數調用者和函數本身來協調。為此,計算機提供了一種被稱為棧的數據結構來支持參數傳遞。
? ? ? ? 棧是一種先進后出的數據結構,棧有一個存儲區、一個棧頂指針。棧頂指針指向堆棧中第一個可用的數據項(被稱為棧頂)。用戶可以在棧頂上方向棧中加入數據,這個操作被稱為壓棧(Push),壓棧以后,棧頂自動變成新加入數據項的位置,棧頂指針也隨之修改。用戶也可以從堆棧中取走棧頂,稱為彈出棧(pop),彈出棧后,棧頂下的一個元素變成棧頂,棧頂指針隨之修改。函數調用時,調用者依次把參數壓棧,然后調用函數,函數被調用以后,在堆棧中取得數據,并進行計算。函數計算結束以后,或者調用者、或者函數本身修改棧,使堆棧恢復原裝。
? ? ? ? 在參數傳遞中,有兩個很重要的問題必須得到明確說明:當參數個數多于一個時,按照什么順序把參數壓入堆棧函數調用后,由誰來把堆棧恢復原裝。在高級語言中,通過函數調用約定來說明這兩個問題。常見的調用約定有:__stdcall,__cdecl,__fastcall,__thiscall,naked call
extern "C" 的含義 和 __stdcall/__cdecl的區別
extern "C" 和 __stdcall、__cdecl 這兩個概念都是C和C++語言混用時需要關注的。extern "C"是代碼段的修飾, 既可以單獨對函數進行修飾也可以放在代碼片段前對整段代碼進行修飾;是告知編譯器接下來的代碼中所有的函數名要以C語言的方式進行解析;_stdcall和_cdecl則是對函數名進行修飾,告知編譯器函數名應該按何種方式進行解析。
為什么要加extern C
工程中經常會遇到C和C++混合編程的情況,有時是C++的工程中需要使用C語言編寫的庫,有時是C的工程需要使用C++;但如果不加任何修飾,直接調用的話,就會遇到鏈接問題,提示找不到函數名稱。
這是由于C++在編譯時候,會將函數名做一些修飾,在函數名前加上函數名的長度,在函數名后面加上參數類型。鏈接的時候也使用相同的策略。C++這么做是由于C++語言支持函數重載,在函數名相同參數不同的幾個函數也可以共存。C語言不支持重載,所以編譯和鏈接時對函數名不會加修飾。加extern ”C“就是為了讓編譯器以C語言的函數名處理方式來編譯。
使用場景主要有兩種:
1.C++ 的模塊調用 C語言寫的庫
C語言 test.h、test.c 創建的動態庫libtest.so
#### test.h文件如下
#include "stdio.h"void test_add(int a, int b);#### test.c文件如下
#include "test.h"void test_add(int a, int b) {int num = a + b;printf("%d + %d = %d\n", a, b, num); }#### 編譯命令
gcc test.c -fPIC -shared -o libtest.so#### C++ 的文件 caller_cplusplus.cpp 調用
#ifdef __cplusplus extern "C" { #endif #ifdef __cplusplus #include "test.h" } #endif int main() {test_add(3, 4);return 0; }#### 編譯命令
g++ -o callerCpp caller_cplusplus.cpp -L. -ltest2. C++頭文件聲明接口函數?
在C中引用C++語言中的函數和變量時,C++的頭文件需添加extern "C",但是在C語言中不能直接引用聲明了extern "C"的該頭文件,應該僅將C文件中將C++中定義的extern "C"函數聲明為extern類型。
//C++頭文件 cppExample.h
#ifndef CPP_EXAMPLE_H #define CPP_EXAMPLE_Hextern "C" int add( int x, int y );#endif//C++實現文件 cppExample.cpp
#include "cppExample.h"int add( int x, int y ) {return x + y; }/* C實現文件 cFile.c*/
/* 這樣會編譯出錯:#include "cExample.h" */
上面介紹的是extern "C"的含義和使用方法,主要是跨C和C++的庫調用時需要注意的地方。
CRT鏈接選項(C運行時庫的鏈接選擇):一個原則,調用者和庫的CRT鏈接選項要相同,盡量都使用/MD選項 (多線程動態鏈接)。
__cdecl/__stdcall 是規定了函數名應該如何修飾,以及參數壓棧方式,棧由誰清理
函數名修飾規則:
- __stdcall :約定在輸出函數名前加上一個下劃線前綴,后面加上一個“@”符號和其參數的字節數,格式為_functionname@number(number即參數棧長度)
- __cdecl :約定僅在輸出函數名前加上一個下劃線前綴,格式為_functionname
參數棧的管理:兩種方式定義的參數傳遞方式都是從右至左壓,區別在于清理棧的角色不同。
- __stdcall:是被調用函數清理(即函數自己清理)
- __cdecl:是調用者清理;
所以__stdcall修飾函數名時加上參數棧的大小可以讓函數自己進行棧的清理;對于可變參數的函數,無法在函數定義時確認參數長度,只能讓調用者去清理參數棧,所以對于可變參數的函數應該用__cdecl修飾(可變參數的函數exp:printf)
調用約定(或者 調用協議) __cdecl、__stdcall 和 __fastcall 的區別
https://blog.csdn.net/a3192048/article/details/82084374
__stdcall 和 __cdecl 的區別淺析(?C?代碼?和?生成對應匯編代碼分析?):https://blog.csdn.net/qinrenzhi/article/details/94403385
函數的?調用約定,顧名思義就是對函數調用的一個約束和規定(規范),描述了函數參數是怎么傳遞和由誰清除堆棧的。它決定以下內容:
- (1) 函數參數的壓棧順序。
- (2) 由調用者還是被調用者把參數彈出棧。
- (3) 以及產生函數修飾名的方法。
- 1)? __stdcall 的全稱是 standard call。是 C++ 的標準調用方式。函數所有參數從右到左依次入棧,如果是調用類成員的話,最后一個入棧的是this指針。函數返回時使用 retn?X?指令,其中 X?為調整堆棧的字節數。這些堆棧中的參數由被調用的函數在返回后清除,使用的 指令是 retn X,X 表示參數占用的字節數,CPU在 ret 之后自動彈出 X 個字節的堆棧空間,稱為自動清棧,這種方式 叫做自動清棧 (?被調用的函數在返回前清理傳送參數的內存棧?)。即 函數在編譯的時候就必須確定參數個數,并且調用者必須嚴格的控制參數的生成,不能多,不能少,否則返回后會出錯。
- 2)? __cdecl 的全稱是 C Declaration(declaration,聲明),即 C語言默認的函數調用方式。函數參數的入棧順序為從右到左依次入棧。函數返回時作用 ret 指令。被調用的函數支持 可變參數,即?被調用函數不會要求調用者傳遞多少參數,調用者傳遞過多或者過少的參數,甚至完全不同的參數都不會產生編譯階段的錯誤。調用者根據調用時傳入參數的個數,手動平衡堆棧,即?這些參數由調用者清除,稱為手動清棧。
- 3)? __fastcall 是編譯器指定的快速調用方式。由于大多數的函數參數個數很少,使用堆棧傳遞比較費時。因此 __fastcall 通常規定將前兩個(或若干個)參數由寄存器傳遞,其余參數還是通過堆棧傳遞。不同編譯器編譯的程序規定的寄存器不同。返回方式和 __stdcall 相當。(實際上,它用 ECX 和 EDX 傳送前兩個雙字(DWORD)或更小的參數,剩下的參數仍舊自右向左壓棧傳送,被調用的函數在返回前清理傳送參數的內存棧)。
- 4)? __thiscall 是為了解決類成員調用中 this指針傳遞而規定的。_thiscall 要求把 this 指針放在特定寄存器中,該寄存器由編譯器決定。VC 使用 ecx,Borland 的 C++ 編譯器使用 eax。返回方式 和 __stdcall 相當。
- 5)? nakedcall 采用 1 - 4??的調用約定時,如果必要的話,進入函數時編譯器會產生代碼來保存ESI,EDI,EBX,EBP寄存器,退出函數時則產生代碼恢復這些寄存器的內容。naked call不產生這樣的代碼。naked call不是類型修飾符,故必須和_declspec 共同使用。
注意?:__fastcall 和? __thiscall? 涉及的寄存器由編譯器決定,因此不能用作跨編譯器的接口。所以 Windows 上的 COM 對象接口都定義為 __stdcall 調用方式。
用 __stdcall 定義的函數在結束時由該函數自己負責把參數彈出堆棧,windows?API 都是采用 __stdcall 壓棧/調用方式。
用 __cdecl 定義的函數在結束時不管參數,而由調用函數負責把參數彈出堆棧,c 的庫函數都是采用 __cdecl 壓棧/調用方式,在c 程序中定義的函數默認都是 __cdecl 方式。
關鍵字??__stdcall、__cdecl 和 __fastcall 可以直接加在要輸出的函數前,也可以在編譯環境的Setting...\C/C++??\Code??Generation項選擇。當加在輸出函數前的關鍵字 與 編譯環境中的選擇不同時,直接加在輸出函數前的關鍵字有效。它們對應的命令行參數分別為/Gz、/Gd 和 /Gr。缺省狀態為 /Gd,即__cdecl?。????
VC++ 對函數的省缺聲明是? "__cedcl" ,將只能被 C/C++ 調用 。
示例代碼:
#include<stdio.h> #include<iostream>// 使用 __stdcall void __stdcall output_1(int x, int y) {printf("%d %d", x, y); }// VC++對函數的省缺聲明是 "__cedcl", 將只能被C/C++調用. void output_2(int x, int y) {printf("%d %d", x, y); }void test_1() {__asm{mov eax, 10mov ebx, 20push eaxpush ebxcall output_1}printf("\n"); }void test_2() {__asm{mov eax, 10mov ebx, 20push eaxpush ebxcall output_2add esp, 8; 調用者負責平衡堆棧}printf("\n"); }int main(int argc, char* argv[]) {test_1();test_2();return 0; }運行結果:
示例代碼:
#define _AFXDLL // 如果不定義這,需要改成單線程版本#include <afx.h> // CString 頭文件 #include <afxwin.h> // AfxMessageBox的頭文件TCHAR appname[] = TEXT("API Test");void main() {int a = 5; //變量a _asm{mov eax, a; // 將變量a的值放入寄存器eax add eax, eax; // 相當于a=a+a mov a, eax; // 將 a+a 的結果賦給a }//查看結果,注意a的初值為5 CString rst;rst.Format(_T("a=%d"), a);AfxMessageBox(rst); }使用規則 和 設置方法
1、修飾名?( Decoration name ),即編譯之后的函數名。"C" 或者 "C++" 函數 在 內部(編譯和鏈接)通過 修飾名 識別。修飾名是編譯器 在 編譯 函數定義 或者 原型 時生成的 字符串。有些情況下使用函數的修飾名是必要的,如在模塊定義文件里頭指定輸出? "C++" 重載函數、構造函數、析構函數,又如在匯編代碼里調用 "C" 或 "C++" 函數等。修飾名由函數名、類名、調用約定、返回類型、參數等共同決定。???
2、函數名修飾約定 隨 編譯種類 和 調用約定 的不同而不同,下面分別說明。? ??
C語言編譯器?的?函數名修飾約定規則:
__stdcall 調用約定在輸出函數名前加上一個下劃線前綴,后面加上一個 "@" 符號和其參數的字節數,格式為: _functionname@number,例如: function(int a, int b),其修飾名為:_function@8__cdecl 調用約定僅在輸出函數名前加上一個下劃線前綴,格式為: _functionname。__fastcall 調用約定在輸出函數名前加上一個 "@" 符號,后面也是一個 "@" 符號 和 其參數的字節數,格式為: @functionname@number。C++語言編譯器?的?函數名修飾約定規則:????
__stdcall調用約定: 1、以 "?" 標識函數名的開始,后跟函數名; 2、函數名后面以“@@YG”標識參數表的開始,后跟參數表; 3、參數表以代號表示: X--void, D--char, E--unsigned char, F--short, H--int, I--unsigned int, J--long, K--unsigned long, M--float, N--double, _N--bool, .... PA--表示指針,后面的代號表明指針類型,如果相同類型的指針連續出現,以 "0" 代替,一個 "0" 代表一次重復; 4、參數表的第一項為該函數的返回值類型,其后依次為參數的數據類型,指針標識在其所指數據類型前; 5、參數表后以 "@Z" 標識整個名字的結束,如果該函數無參數,則以 "Z" 標識結束。 其格式為 ?functionname@@YG*****@Z 或 ?functionname@@YG*XZ , 例如 int Test1(char *var1, unsigned long) ----- ?Test1@@YGHPADK@Z void Test2() ----- ?Test2@@YGXXZ__cdecl調用約定: 規則同上面的_stdcall調用約定,只是參數表的開始標識由上面的“@@YG”變為“@@YA”。 __fastcall調用約定: 規則同上面的_stdcall調用約定,只是參數表的開始標識由上面的“@@YG”變為“@@YI”。__stdcall 和 __cdecl 相同點 和 不同點
相同點:參數入棧順序相同:從右到左
不同點:
支持可變參數的函數必須定義為__cdecl,如:
int printf(char *fmt, ...);
在 windef.h 中對 __stdcall 和 __cdecl 的定義
#define CALLBACK __stdcall #define WINAPI __stdcall #define WINAPIV __cdecl #define APIENTRY WINAPI #define APIPRIVATE __stdcall #define PASCAL __stdcall #define cdecl _cdecl #ifndef CDECL #define CDECL _cdecl #endif特別說明
- 1.? 在默認情況下,采用 __cdecl 方式,因此可以省略.
- 2.? WINAPI 一般用于修飾動態鏈接庫中導出函數
- 3.? CALLBACK 僅用于修飾回調函數?
便于更好理解, 看下面例子(函數調用的過程以匯編代碼表示):?
//函數定義 void cdecl fun1(int x,int y); void stdcall fun2(int x,int y); void pascal fun3(int x,int y); // 對應調用函數時的匯編代碼 **************************************** void cdecl fun1(int x,int y); fun1(x,y); 調用 fun1 的匯編代碼 push y push x call fun1 add sp,sizeof(x)+sizeof(y) ;跳過參數區(x,y) fun1 的匯編代碼: fun1 proc push bp mov bp,sp …… … pop bp ret ;返回,但不跳過參數區 fun1 endp **************************************** void stdcall fun2(int x,int y); fun2(x,y); 調用 fun2 的匯編代碼 push y push x call fun2 fun2 的匯編代碼: fun2 proc push bp mov bp,sp …… … pop bp ret sizeof(x)+sizeof(y) ;返回并跳過參數區(x,y) fun2 endp ***************************************** void pascal fun3(int x,int y); fun3(x,y); 調用 fun3 的匯編代碼 push x push y call fun3 fun3 的匯編代碼: fun3 proc push bp mov bp,sp …… … pop bp ret sizeof(x)+sizeof(y) ;返回并跳過參數區(x,y) fun3 endp_stdcall 與 _cdecl 兩者之間的區別:
??????WINDOWS 的函數調用時需要用到棧(STACK,一種先入后出的存儲結構)。當函數調用完成后,棧需要清除,這里就是問題的關鍵,如何清除??
??????如果我們的函數使用了 _cdecl,那么棧的清除工作是由調用者,用 COM 的術語來講就是客戶來完成的。這樣帶來了一個棘手的問題,不同的編譯器產生棧的方式不盡相同,那么調用者能否正常的完成清除工作呢?答案是不能。
??????如果使用 __stdcall,上面的問題就解決了,函數自己解決清除工作。所以,在跨(開發)平臺的調用中,我們都使用__stdcall( 雖然有時是以 WINAPI 的樣子出現 )。
??????那么為什么還需要_cdecl 呢 ?當我們遇到這樣的函數如 fprintf() 它的參數是可變的,不定長的,被調用者事先無法知道參數的長度,事后的清除工作也無法正常的進行,因此,這種情況我們只能使用_cdecl。
??????到這里我們有一個結論:如果程序中沒有涉及可變參數,最好使用__stdcall 關鍵字
cdecl
cdecl( C declaration,即C聲明 ) 是源起C語言的一種調用約定,也是C語言的事實上的標準。在x86架構上,其內容包括:
? ? ? ? Visual C++ 規定 函數返回值如果是POD值且長度如果不超過32比特,用寄存器EAX傳遞;長度在33-64比特范圍內,用寄存器EAX:EDX傳遞;長度超過64比特或者非POD值,則調用者為函數返回值預先分配一個空間,把該空間的地址作為隱式參數傳遞給被調函數。
? ? ? ? GCC的函數返回值都是由調用者分配空間,并把該空間的地址作為隱式參數傳遞給被調函數,而不使用寄存器EAX。GCC自4.5版本開始,調用函數時,堆棧上的數據必須以16B對齊(之前的版本只需要4B對齊即可)。
考慮下面的C代碼片段:
int callee(int, int, int);int caller(void){register int ret;ret = callee(1, 2, 3);ret += 5;return ret;}在x86上, 會產生如下匯編代碼(AT&T 語法):
.globl callercaller:pushl %ebpmovl %esp,%ebppushl $3pushl $2pushl $1call calleeaddl $12,%espaddl $5,%eaxleaveret在函數返回后,調用的函數清理了堆棧。 在cdecl的理解上存在一些不同,尤其是在如何返回值的問題上。結果,x86程序經過不同OS平臺的不同編譯器編譯后,會有不兼容的情況,即使它們使用的都是“cdecl”規則并且不會使用系統調用。某些編譯器返回簡單的數據結構,長度大致占用兩個寄存器,放在寄存器對EAX:EDX中;大點的結構和類對象需要異常處理器的一些特殊處理(如一個定義的構造函數,析構函數或賦值),存放在內存上。為了放置在內存上,調用者需要分配一些內存,并且讓一個指針指向這塊內存,這個指針就作為隱藏的第一個參數;被調用者使用這塊內存并返回指針----返回時彈出隱藏的指針。 在Linux/GCC,浮點數值通過x87偽棧被推入堆棧。像這樣:
sub esp, 8 ; 給double值一點空間fld [ebp + x] ; 加載double值到浮點堆棧上fstp [esp] ; 推入堆棧call functadd esp, 8使用這種方法確保能以正確的格式推入堆棧。 cdecl調用約定通常作為x86 C編譯器的默認調用規則,許多編譯器也提供了自動切換調用約定的選項。如果需要手動指定調用規則為cdecl,編譯器可能會支持如下語法:
return_type _cdecl funct();其中_cdecl修飾符需要在函數原型中給出,在函數聲明中會覆蓋掉其他的設置。
call 指令與 retn 指令
??? 首先我們得了解 CALL 和 RETN 指令的作用,才能更好地理解調用規則,這也是先決條件。
??? 實際上,CALL 指令就是先將下一條指令的 EIP 壓棧,然后 JMP 跳轉到對應的函數的首地址,當執行完函數體后,通過 RETN 指令從堆棧中彈出 EIP,程序就可以繼續執行 CALL 的下一條指令。
__cdecl 與 __stdcall 調用規則
??? C/C++ 中不同的函數調用規則會生成不同的機器代碼,產生不同的微觀效果,接下來讓我們一起來淺析四種調用規則的原理和它們各自的異同。首先我們通過一段 C 語言代碼來引導我們的淺析過程。
這里我們編寫了三個函數,它們的功能都是返回兩個參數的相加結果,只是每個函數都有不一樣的調用規則。
??? 我們使用 printf 函數主要是為了在 OllyDBG 中能夠快速下斷點,以確定后邊調用三個函數的位置,便于分析。在這里我給每個函數都用了內聯的 NOP 指令來分隔開,圖中也用紅框標明,這樣可以便于區分每個函數的調用過程。通過一些簡單的步驟,我們用 OllyDBG 查看了編譯后代碼的“真面目”。代碼中有 4 個 CALL,第一個是 printf,我們不關心這個。后面三個分別是具有 __cdecl,__stdcall,__fastcall 調用規則的函數 CALL(這里我已經做了注釋)。
??? 在這里為了循序漸進,我們先介紹 __cdecl 與 __stdcall 調用規則,后面我們會接著淺析 __fastcall 調用規則。
??? 首先,我們得明白一個教條(其實也是自己概括的),那就是 —— 調用規則的區別產生其實就是由于調用者與被調用者之間的“責任分配”問題。
??? 代碼段中的第 2 個就是 __cdecl 調用規則的 CALL。__cdecl 是 C/C++、MFC 默認的調用規則。我們可以看到,在執行 CALL 之前,程序會將參數按照從右到左的方式壓棧,這里是兩個整型參數,每壓棧一個 ESP 都會減 4,這樣下來 ESP 會減少 8,然后 CALL 這個函數。常規地,我們可以看到,這個 CALL 里面參數的處理和通常情況下一致,先將 EBP 壓棧保存現場,然后使 EBP 重合于 ESP,再通過 EBP + 偏移地址來取得兩個參數值,賦值再累加到 EAX 中,EAX 將作為返回值給調用者使用,還原 EBP 現場,調用 RETN 返回到調用者。最后,使得 ESP 加 8。哎!這剛好和開頭對稱嘛!為了堆棧平衡,ESP 最終又被拉回到了 CALL 之前的位置。我們暫且可以小結一下,實際上?在 __cdecl 調用規則中,需要調用者來負責清棧操作(由調用者將 ESP 拉高以維持堆棧平衡)。
??? 代碼段中的第 3 個是 __stdcall 調用規則的 CALL。__stdcall 調用規則在 Win32 API 函數中用的比較多。跟 __cdecl 一樣,在執行 CALL 之前,程序會先將參數從右到左依次壓棧,我們跟進 CALL 里面,可以看到以下的反匯編代碼,我們很容易發現,除了最后一條指令,其他的指令與 __cdecl 調用規則是基本一樣的。最后一條指令是“RETN 0x8”,這是什么意思呢?實際上呢,就相當于先執行“ADD ESP, 0x8”再執行“POP EIP” 。換言之,就是將 ESP 加 8,然后正常 RETN 返回到調用者。
??? 我們不難發現,__stdcall 調用規則使得被調用者來執行清棧操作(由被調用者函數自身將 ESP 拉高以維持堆棧平衡),這也是 __stdcall 與 __cdecl 調用規則的最根本的區別。
??? __cdecl 偏向于把責任分配給調用者,動腦筋想想,我們的程序在 CALL __cdecl 調用規則的函數之前,把參數從右到左依次壓棧,CALL 返回后,剩下的清棧操作都交給調用者處理,調用者負責拉高 ESP。再回來想想 __stdcall,在 CALL 中將調用者的 EBP 壓棧以保存現場,然后使 EBP 對齊于 ESP,然后通過 EBP + 偏移地址取得參數,并且經過加法得到 EAX 返回值,從堆棧彈出 EBP 恢復現場,但是最后不一樣的地方,程序將執行 “RETN 0x8” 將 ESP 拉回之前的 ESP + 8 的位置,換言之,被調用者將負責清棧操作。這就是之前所謂的“責任分配”的區別。
__fastcall 調用規則
??? 不難揣測 fastcall 的英文意思貌似是“快速調用”,這一點與它的調用規則息息相關,它的快速是有原因的,讓我們繼續來看看之前那張反匯編的截圖,代碼段中的第 4 個就是 __fastcall 調用規則的 CALL。進 CALL 前,出乎意料地,程序將兩個參數從右到左分別傳給了 EDX,ECX 寄存器,講到這里,學過計算機系統相關知識的人很容易理解為什么這叫“快速調用”了,寄存器比內存快很多很多倍,可以認為傳參給寄存器,要比在內存中更快得多,效率更高。
??? 由于參數是直接傳遞給了寄存器,堆棧并未發生改變,在 CALL 中,EBP 壓棧,EBP 和 ESP 對齊之后,ESP 減 8,這個操作有點像對局部變量分配堆棧空間(這里有我之前一篇博客,對局部變量的存放規則做了淺析),然后程序將 EDX,ECX 分別賦值給 EBP – 8 與 EBP – 4 這兩個地址,這個過程相當于用寄存器給局部變量賦值,接下來運算結果將保存在 EAX 中,ESP 歸位,EBP 恢復現場,最后 RETN 返回調用者領空。
??? 本例只傳送了兩個整數型參數。其實呢,對于 __fastcall 調用規則,左邊開始的兩個不大于4字節(int)的參數分別放在ECX和EDX寄存器,其余的參數仍舊自右向左壓棧傳送。并且,__fastcall 調用規則使得被調用者負責清理棧的操作(由被調用者函數自身將 ESP 拉高以維持堆棧平衡),這一點和 __stdcall 一樣。
?
__pascal 調用規則
??? __pascal 是用于 Pascal / Delphi 編程語言的調用規則,C/C++ 中也可以使用這種調用規則。簡單地說,__pascal 調用規則與 __stdcall 不同的地方就是壓棧順序恰恰相反,前面講到的三種調用規則的壓棧順序都是從右到左依次入棧,__pascal 則是從左到右依次入棧。并且,被調用者(函數自身)將自行完成清棧操作,這和 __stdcall,__fastcall 一樣。由于比較簡單,我就沒有做出示例。
小結
??? 做個表格來小結一下,很直觀就能看出這四種調用規則的異同:
| 調用規則 | 入棧順序 | 清棧責任 |
| __cdecl | 從右到左 | 調用者 |
| __stdcall | 從右到左 | 被調用者 |
| __fastcall | 從右到左(先 EDX、ECX,再到堆棧) | 被調用者 |
| __pascal | 從左到右 | 被調用者 |
通過分析反匯編還原 C 語言 if…else 結構
From:https://www.cnblogs.com/yenyuloong/p/9629749.html
讓我們從反匯編的角度去分析并還原 C 語言的 if … else 結構,首先我們不看源代碼,我們用 OllyDBG 載入 PE 文件,定位到 main 函數領空,如下圖所示。
以下的 C 語言代碼段。
函數調用方式 __stdcall 與 __cdecl
????在程序執行過程中發生函數調用時,系統會作如下操作:首先把參數壓入堆棧,然后把ip寄存器的值壓入堆棧(作為函數的返回地址),然后把堆棧指針(ESP)的值賦給EBP并把EBP壓入堆棧,最后為本地變量留出一定空間,即把ESP減去一定的值。
舉個例子說明一下:
在 VC 中建一個 Win32?Console?Application的空項目,輸入以下代碼:
在 main 函數的下一行設置斷點,編譯,調試。按 Alt +?F9、?A?切換到匯編窗口,可以看到
源代碼對應匯編代碼:
3 int test(int a, int b) //test函數的定義 4 {007416E0 push ebp //ebp入棧007416E1 mov ebp,esp //esp-->ebp007416E3 sub esp,0C0h 007416E9 push ebx 007416EA push esi 007416EB push edi 007416EC lea edi,[ebp-0C0h] 007416F2 mov ecx,30h 007416F7 mov eax,0CCCCCCCCh 007416FC rep stos dword ptr es:[edi] 007416FE mov ecx,offset _8A1C4DC9_C++_console@cpp (074C003h) 00741703 call @__CheckForDebuggerJustMyCode@4 (07411FEh) 5 return a + b;00741708 mov eax,dword ptr [a] 0074170B add eax,dword ptr [b] 6 }0074170E pop edi 0074170F pop esi 00741710 pop ebx 00741711 add esp,0C0h 00741717 cmp ebp,esp 00741719 call __RTC_CheckEsp (0741208h) 0074171E mov esp,ebp 00741720 pop ebp 00741721 ret ............7 8 void main(int argc, char** argv) 9 {00741750 push ebp 00741751 mov ebp,esp 00741753 sub esp,0C0h 00741759 push ebx 0074175A push esi 0074175B push edi 0074175C lea edi,[ebp-0C0h] 00741762 mov ecx,30h 00741767 mov eax,0CCCCCCCCh 0074176C rep stos dword ptr es:[edi] 0074176E mov ecx,offset _8A1C4DC9_C++_console@cpp (074C003h) 00741773 call @__CheckForDebuggerJustMyCode@4 (07411FEh) 10 test(10, 20);00741778 push 14h //將參數壓入堆棧,14h轉換成十進制是200074177A push 0Ah //將參數壓入堆棧,0Ah轉換成十進制是100074177C call test (0741230h) //調用test函數00741781 add esp,8 //恢復堆棧,這是__cdecl的工作方式 11 }00741784 xor eax,eax 00741786 pop edi 00741787 pop esi 00741788 pop ebx 00741789 add esp,0C0h 0074178F cmp ebp,esp 00741791 call __RTC_CheckEsp (0741208h) 00741796 mov esp,ebp 00741798 pop ebp 00741799 ret現在把上面的?C?語言代碼第一行改為:int?__stdcall?test(int?a,int?b)
?
對應匯編代碼:
int __stdcall test(int a, int b) { 00A716E0 push ebp 00A716E1 mov ebp,esp 00A716E3 sub esp,0C0h 00A716E9 push ebx 00A716EA push esi 00A716EB push edi 00A716EC lea edi,[ebp-0C0h] 00A716F2 mov ecx,30h 00A716F7 mov eax,0CCCCCCCCh 00A716FC rep stos dword ptr es:[edi] 00A716FE mov ecx,offset _8A1C4DC9_C++_console@cpp (0A7C003h) 00A71703 call @__CheckForDebuggerJustMyCode@4 (0A711FEh) return a + b; 00A71708 mov eax,dword ptr [a] 00A7170B add eax,dword ptr [b] } 00A7170E pop edi 00A7170F pop esi 00A71710 pop ebx 00A71711 add esp,0C0h 00A71717 cmp ebp,esp 00A71719 call __RTC_CheckEsp (0A71208h) 00A7171E mov esp,ebp 00A71720 pop ebp 00A71721 ret 8 //與__cdecl的不同之處,由被調用函數自己恢復堆棧void main(int argc, char** argv) { 00A71750 push ebp 00A71751 mov ebp,esp 00A71753 sub esp,0C0h 00A71759 push ebx 00A7175A push esi 00A7175B push edi 00A7175C lea edi,[ebp-0C0h] 00A71762 mov ecx,30h 00A71767 mov eax,0CCCCCCCCh 00A7176C rep stos dword ptr es:[edi] 00A7176E mov ecx,offset _8A1C4DC9_C++_console@cpp (0A7C003h) 00A71773 call @__CheckForDebuggerJustMyCode@4 (0A711FEh) test(10, 20); 00A71778 push 14h 00A7177A push 0Ah 00A7177C call test (0A7137Ah) //調用過程不負責恢復堆棧 } 00A71781 xor eax,eax 00A71783 pop edi 00A71784 pop esi 00A71785 pop ebx 00A71786 add esp,0C0h 00A7178C cmp ebp,esp 00A7178E call __RTC_CheckEsp (0A71208h) 00A71793 mov esp,ebp 00A71795 pop ebp 00A71796 ret總結
以上是生活随笔為你收集整理的__cdecl、__stdcall、__fastcall 与 __pascal 浅析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《底层逻辑》--思维导图
- 下一篇: 网络爬虫干货总结!