網站首頁 編程語言 正文
?函數的調用約定其實比較簡單,并不復雜,但很多人對這一塊內容不太了解,甚至連工作幾年的朋友也不太清楚。最近有朋友想了解這一塊的內容,所以今天我們就來講一下C/C++函數調用約定相關的內容。
1、概述
常見的函數調用約定有__cdecl C調用、__stdcall標準調用、__fastcall快速調用以及__pascal調用:
這些調用是開發語言中的關鍵字,放置在函數前,用來指定函數的調用約定,比如:
BOOL __stdcall InitSDK();
如上所示,調用約定關鍵字一般放于返回值類型與函數之間。而函數返回值類型前面一般放置函數的導入導出聲明:(dll庫的函數接口有導入導出之分)
// 定義導出導入SDK_DLL_API宏
#ifdef DLL_EXPORTS
#define SDK_DLL_API _declspec(dllexport)
#else
#define SDK_DLL_API _declspec(dllimport)
#endif
// 將導出導入SDK_DLL_API宏放到返回值類型之前
SDK_DLL_API BOOL __stdcall InitSDK();
函數的調用約定主要決定三方面的內容:
1)函數參數的入棧順序
函數調用時主調函數的參數是通過棧傳遞給被調用函數的。從匯編上看的比較清晰,在call函數之前,會將參數的值壓到棧上,比如:
如果函數有多個參數,則會有兩種入棧方式,一種是從右到左依次入棧,一種是從左到右依次入棧,這是函數調用約定決定的。
2)參數棧空間由誰來釋放
函數調用完成后傳遞給被調用函數的參數的占用的棧空間是需要釋放掉的,專業術語叫“平棧”,清理掉參數的棧空間才能做到棧平衡。參數占用的棧空間到底是誰來清理,也是函數調用約定決定的。編譯器在編譯鏈接生成匯編代碼時,就生成好了清理參數棧空間的匯編代碼。
3)編譯時的函數名稱改編
不同的調用約定下編譯生成的函數名稱格式可能是不同的。C++之所以支持函數重載(源代碼中,函數名稱相同,函數參數不同),就是因為C++編譯器會對函數名稱進行改編,改編后的名稱中包含參數類型進而能區分出重載的函數。
2、常見的調用約定說明
常見的函數調用約定有__cdecl C調用、__stdcall標準調用、__fastcall快速調用以及__pascal調用。C/C++ 中主要使用__cdecl C調用、__stdcall標準調用、__fastcall快速調用三種。__pascal 是用于 Pascal / Delphi 編程語言的調用規則,C/C++ 中也可以使用這種調用規則,但該調用約定已經被C++廢棄,不提倡使用了。
下面我們來看看這幾種調用約定的異同點,見下面的表格:
2.1、__cdecl C調用
它是C/C++函數默認的調用規范,C/C++運行時庫中的函數基本都是__cdecl調用。在該調用約定下,參數從右向左依次壓入棧中,由主調函數負責清理參數的棧空間。該調用約定適用于支持可變參數的函數,因為只有主調函數才知道給該種函數傳遞了多少個參數,才知道應該清理多少棧空間。比如支持可變參數的C函數printf:
int __cdecl printf ( const char *format, ... )
{
va_list arglist;
int buffing;
int retval;
_VALIDATE_RETURN( (format != NULL), EINVAL, -1);
va_start(arglist, format);
_lock_str2(1, stdout);
__try {
buffing = _stbuf(stdout);
retval = _output_l(stdout,format,NULL,arglist);
_ftbuf(buffing, stdout);
}
__finally {
_unlock_str2(1, stdout);
}
return(retval);
}
2.2、__stdcall標準調用
它是Windows系統提供的系統API函數的調用約定,比如API函數GetWindowText的聲明如下:
WINUSERAPI
int
WINAPI
GetWindowTextW(
_In_ HWND hWnd,
_Out_writes_(nMaxCount) LPWSTR lpString,
_In_ int nMaxCount);
其中,WINAPI宏就是__stdcall標準調用,即:
#define WINAPI __stdcall
同時__stdcall也是很多提供給第三方使用的SDK庫的API接口的調用約定。在該調用約定下,參數從右向左依次壓入棧中,由被調用函數負責清理棧空間。如果函數是可變參的,函數的調用約定會自動轉化為__cdecl調用。
2.3、__fastcall快速調用
該調用約定之所以被稱作為快速調用,因為有部分參數可以通過寄存器直接傳遞,效率比較高。對于內存大小小于等于4字節的參數,直接使用ECX和EDX寄存器傳遞,剩余的參數則依次從右到左壓入棧中通過棧傳遞,參數傳遞占用的棧空間由被調用函數清理。
2.4、__thiscall調用
__thiscall是C++中的非靜態類成員函數的默認調用約定。該調用約定也用到了寄存器傳參,在調用C++類的非靜態成員函數時會傳入當前類對象的地址,該地址通過ECX寄存器來傳遞的。在該調用約定下,函數的參數按照從右到左的順序入棧,被調用的函數在返回前清理參數的棧空間。
3、調用約定不一致導致的軟件異常問題
以前我們將C++開發的SDK庫提供給第三方廠商做二次開發,第三方客戶使用的是C#語言,即C#開發的程序去調用C++開發的SDK庫,當時因為SDK頭文件中聲明的回調函數沒有指定調用約定,導致程序出現異常崩潰的問題。
我們C++開發的SDK提供了設置消息回調的API接口,并給出了回調函數的聲明,如下:
/* 函數功能:用于消息回發的回調函數指針(服務器主動推送的消息通過該回調函數推給上層)
參數:DWORD dwMsgId:消息id
const unsigned char* pMsgBuf:消息中攜帶的數據buffer,buffer中的具體內容取決于消息id,參看消息id的頭文件
DWORD dwMsgBufLen:消息中攜帶的數據buffer長度
返回值:void
*/
typedef void (*PMsgCallBackFunc)( DWORD dwMsgId, const unsigned char* pMsgBuf, DWORD dwMsgBufLen );
設置回調函數的接口如下:
// 設置業務消息回調接口
SDK_DLL_API void __stdcall SetMsgCallBack( IN PMsgCallBackFunc pMsgCallBackFunc );
回調函數的實現在上層的C#程序中,回調函數的調用在C++實現的SDK中,因為回調函數PMsgCallBackFunc在聲明時沒有指定函數調用約定,在C#程序中默認是__stdcall標準約定,所以在C#中編譯時回調函數內部會清理棧空間。而回調函數是在C++ SDK中調用的,在SDK編譯時默認是__cdecl調用,會在調用回調函數處的主調函數中釋放棧空間,這樣導致回調函數調用后,主調函數會釋放一次棧空間,回調函數內部會釋放一次棧空間,所以多釋放了一次參數棧空間,導致了棧不平衡,導致程序運行出異常。
考慮跨語言調用的場景,SDK要提供標準的C接口。在SDK的頭文件中,SDK導出接口要指定調用約定,回調函數的聲明也要指定調用約定。
4、與調用約定相關的工程配置選項及/RTC編譯選項
在Visual Studio創建的C++工程中,在沒明確指定函數調用約定時,默認使用的都是__cdecl調用,我們可以在工程屬性配置中看到:
對于C++工程,我們一般不需要修改默認的調用約定。如果要指定dll庫導出接口的調用約定,我們也不需要修改工程配置,只需要在導出接口的頭文件的函數聲明處指定調用約定就可以了。
有人可能會說,工程屬性配置中使用了默認的__cdecl調用,我們又在頭文件中將接口指定為__stdcall標準調用,會不會有沖突?到底以哪個為準呢?沒有沖突的,編譯時是優先以接口聲明處指定的調用約定為準的。
在Debug下/RTC運行時檢測編譯選項是默認開啟的,/RTC運行時檢測在函數調用完成后會去檢測棧是否平衡,關于這一點的說明如下:(MSDN上對/RTC編譯選項的說明)?
如果沒有釋放參數的棧空間或者參數棧空間多釋放了一次,都能檢測出來。如果檢測到,會彈出如下的提示:
原文鏈接:https://blog.csdn.net/chenlycly/article/details/125354572
相關推薦
- 2023-10-26 void 0 有什么意義?undefined竟然是可變的?
- 2024-07-13 根據腳手架archetype快速構建spring boot/cloud項目
- 2022-10-18 C#模擬實現鼠標自動點擊與消息發送功能_C#教程
- 2022-11-17 Go語言學習教程之指針的示例詳解_Golang
- 2022-04-16 pytorch?collate_fn的基礎與應用教程_python
- 2022-08-05 Python?如何給圖像分類(圖像識別模型構建)_python
- 2022-08-20 windows系統安裝配置nginx環境_nginx
- 2022-07-09 Docker網絡介紹
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支