C/C++函數調用約定
在程式設計中,一個函數完整的執行需要經過編譯連結等多個過程,而在每個過程中編譯器都需要為程式提供不同的服務,那麼一個函數的調用執行到底需要幾個過程呢?下面我們先通過一個函數棧幀的建立看看。
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
int Add(int left, int right)
{
return left + right;
}
int main()
{
int a, b;
int ret = 0;
scanf("%d%d", &a, &b);
ret = Add(a, b);
system("system");
return 0;
}
在這個程式中,我們來通過Add()函數的内部彙編代碼來看看整個函數的執行過程:
下面我們用語言描述一下整個過程:
1. 為函數開辟空間
2. 初始化已開辟空間
3. 把函數參數壓棧
4. 執行函數
5. 處理函數傳回值
6. 對于第3步中壓棧的那些寄存器,恢複它們原來的值
7. 根據不同的調用約定,清除第1步中壓棧的參數,然後傳回,或者先傳回然後清除。
可以看到第6步是第3步的逆操作,而第7步是第1,2步的逆操作,在第3步中對函數的參數進行壓棧,那麼當參數個數多于一個時,編譯器會按照什麼順序把參數壓入棧的?而在第6、7步中又是怎麼把堆棧恢複原裝?這就引出了我們今天要讨論的題目——調用約定。
首先我們先來看看常用的幾種調用約定:
在C語言中有: __cdecl、__stdcall、__fastcall、naked、__pascal
在C++中有: __cdecl、__stdcall、__fastcall、naked、__pascal、_thiscall
VC 中預設調用是 __cdecl 方式,Windows API 使用 __stdcall 調用方式,在 DLL 導出函數中,為了跟Windows API 保持一緻,建議使用 __stdcall 方式。
在VC中,可以設定預設的調用約定,設定路徑為:Project à Properties à Configuration Properties à C/C++ à Advanced à call conversion
下面我們就來詳細介紹一下這六種調用約定:
1、__cdecl
__cdecl調用約定又稱為 C 調用約定,是 C/C++ 語言預設的調用約定。參數按照從右至左的方式入棧,函數本身不清理棧,此工作由調用者負責,傳回值在EAX中。由于由調用者清理棧,是以允許可變參數函數存在,如int sprintf(char* buffer,const char* format,...);。
2、__stdcall
__stdcall 很多時候被稱為 pascal 調用約定。pascal 語言是早期很常見的一種教學用計算機程式設計語言,其文法嚴謹。參數按照從右至左的方式入棧,函數自身清理堆棧,傳回值在EAX中。
3、__fastcall
顧名思義,__fastcall 的特點就是快,因為它通過 CPU 寄存器來傳遞參數。他用 eax 和 edx 傳送前兩個雙字(DWORD)或更小的參數,剩下的參數按照從右至左的方式入棧,函數自身清理堆棧,傳回值在 EAX 中。
4、naked
naked 是一個很少見的調用約定,一般不建議使用。編譯器不會給這種函數增加初始化和清理代碼,更特殊的是,你不能用return傳回傳回值,隻能用插入彙編傳回結果,此調用約定必須跟 __declspec 同時使用。例如定義一個求和程式,如__declspec(naked) int add(int a,int b);。
5、__pascal
這是 pascal 語言的調用約定,跟 __stdcall 一樣,參數按照從右至左的方式入棧,函數自身清理堆棧,傳回值在eax中。VC 中已經廢棄了這種調用方式,是以在寫 VC 程式時,建議使用 __stdcall 代替。
6、__thiscall
這是 C++ 語言特有的一種調用方式,用于類成員函數的調用約定。如果參數确定,this 指針存放于 ecx 寄存器,函數自身清理堆棧;如果參數不确定,this指針在所有參數入棧後再入棧,調用者清理棧。__thiscall 不是關鍵字,程式員不能使用。參數按照從右至左的方式入棧。