網(wǎng)站首頁 編程語言 正文
?函數(shù)的調(diào)用約定其實(shí)比較簡(jiǎn)單,并不復(fù)雜,但很多人對(duì)這一塊內(nèi)容不太了解,甚至連工作幾年的朋友也不太清楚。最近有朋友想了解這一塊的內(nèi)容,所以今天我們就來講一下C/C++函數(shù)調(diào)用約定相關(guān)的內(nèi)容。
1、概述
常見的函數(shù)調(diào)用約定有__cdecl C調(diào)用、__stdcall標(biāo)準(zhǔn)調(diào)用、__fastcall快速調(diào)用以及__pascal調(diào)用:
這些調(diào)用是開發(fā)語言中的關(guān)鍵字,放置在函數(shù)前,用來指定函數(shù)的調(diào)用約定,比如:
BOOL __stdcall InitSDK();
如上所示,調(diào)用約定關(guān)鍵字一般放于返回值類型與函數(shù)之間。而函數(shù)返回值類型前面一般放置函數(shù)的導(dǎo)入導(dǎo)出聲明:(dll庫的函數(shù)接口有導(dǎo)入導(dǎo)出之分)
// 定義導(dǎo)出導(dǎo)入SDK_DLL_API宏
#ifdef DLL_EXPORTS
#define SDK_DLL_API _declspec(dllexport)
#else
#define SDK_DLL_API _declspec(dllimport)
#endif
// 將導(dǎo)出導(dǎo)入SDK_DLL_API宏放到返回值類型之前
SDK_DLL_API BOOL __stdcall InitSDK();
函數(shù)的調(diào)用約定主要決定三方面的內(nèi)容:
1)函數(shù)參數(shù)的入棧順序
函數(shù)調(diào)用時(shí)主調(diào)函數(shù)的參數(shù)是通過棧傳遞給被調(diào)用函數(shù)的。從匯編上看的比較清晰,在call函數(shù)之前,會(huì)將參數(shù)的值壓到棧上,比如:
如果函數(shù)有多個(gè)參數(shù),則會(huì)有兩種入棧方式,一種是從右到左依次入棧,一種是從左到右依次入棧,這是函數(shù)調(diào)用約定決定的。
2)參數(shù)棧空間由誰來釋放
函數(shù)調(diào)用完成后傳遞給被調(diào)用函數(shù)的參數(shù)的占用的棧空間是需要釋放掉的,專業(yè)術(shù)語叫“平棧”,清理掉參數(shù)的棧空間才能做到棧平衡。參數(shù)占用的棧空間到底是誰來清理,也是函數(shù)調(diào)用約定決定的。編譯器在編譯鏈接生成匯編代碼時(shí),就生成好了清理參數(shù)棧空間的匯編代碼。
3)編譯時(shí)的函數(shù)名稱改編
不同的調(diào)用約定下編譯生成的函數(shù)名稱格式可能是不同的。C++之所以支持函數(shù)重載(源代碼中,函數(shù)名稱相同,函數(shù)參數(shù)不同),就是因?yàn)镃++編譯器會(huì)對(duì)函數(shù)名稱進(jìn)行改編,改編后的名稱中包含參數(shù)類型進(jìn)而能區(qū)分出重載的函數(shù)。
2、常見的調(diào)用約定說明
常見的函數(shù)調(diào)用約定有__cdecl C調(diào)用、__stdcall標(biāo)準(zhǔn)調(diào)用、__fastcall快速調(diào)用以及__pascal調(diào)用。C/C++ 中主要使用__cdecl C調(diào)用、__stdcall標(biāo)準(zhǔn)調(diào)用、__fastcall快速調(diào)用三種。__pascal 是用于 Pascal / Delphi 編程語言的調(diào)用規(guī)則,C/C++ 中也可以使用這種調(diào)用規(guī)則,但該調(diào)用約定已經(jīng)被C++廢棄,不提倡使用了。
下面我們來看看這幾種調(diào)用約定的異同點(diǎn),見下面的表格:
2.1、__cdecl C調(diào)用
它是C/C++函數(shù)默認(rèn)的調(diào)用規(guī)范,C/C++運(yùn)行時(shí)庫中的函數(shù)基本都是__cdecl調(diào)用。在該調(diào)用約定下,參數(shù)從右向左依次壓入棧中,由主調(diào)函數(shù)負(fù)責(zé)清理參數(shù)的棧空間。該調(diào)用約定適用于支持可變參數(shù)的函數(shù),因?yàn)橹挥兄髡{(diào)函數(shù)才知道給該種函數(shù)傳遞了多少個(gè)參數(shù),才知道應(yīng)該清理多少棧空間。比如支持可變參數(shù)的C函數(shù)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標(biāo)準(zhǔn)調(diào)用
它是Windows系統(tǒng)提供的系統(tǒng)API函數(shù)的調(diào)用約定,比如API函數(shù)GetWindowText的聲明如下:
WINUSERAPI
int
WINAPI
GetWindowTextW(
_In_ HWND hWnd,
_Out_writes_(nMaxCount) LPWSTR lpString,
_In_ int nMaxCount);
其中,WINAPI宏就是__stdcall標(biāo)準(zhǔn)調(diào)用,即:
#define WINAPI __stdcall
同時(shí)__stdcall也是很多提供給第三方使用的SDK庫的API接口的調(diào)用約定。在該調(diào)用約定下,參數(shù)從右向左依次壓入棧中,由被調(diào)用函數(shù)負(fù)責(zé)清理?xiàng)?臻g。如果函數(shù)是可變參的,函數(shù)的調(diào)用約定會(huì)自動(dòng)轉(zhuǎn)化為__cdecl調(diào)用。
2.3、__fastcall快速調(diào)用
該調(diào)用約定之所以被稱作為快速調(diào)用,因?yàn)橛胁糠謪?shù)可以通過寄存器直接傳遞,效率比較高。對(duì)于內(nèi)存大小小于等于4字節(jié)的參數(shù),直接使用ECX和EDX寄存器傳遞,剩余的參數(shù)則依次從右到左壓入棧中通過棧傳遞,參數(shù)傳遞占用的棧空間由被調(diào)用函數(shù)清理。
2.4、__thiscall調(diào)用
__thiscall是C++中的非靜態(tài)類成員函數(shù)的默認(rèn)調(diào)用約定。該調(diào)用約定也用到了寄存器傳參,在調(diào)用C++類的非靜態(tài)成員函數(shù)時(shí)會(huì)傳入當(dāng)前類對(duì)象的地址,該地址通過ECX寄存器來傳遞的。在該調(diào)用約定下,函數(shù)的參數(shù)按照從右到左的順序入棧,被調(diào)用的函數(shù)在返回前清理參數(shù)的棧空間。
3、調(diào)用約定不一致導(dǎo)致的軟件異常問題
以前我們將C++開發(fā)的SDK庫提供給第三方廠商做二次開發(fā),第三方客戶使用的是C#語言,即C#開發(fā)的程序去調(diào)用C++開發(fā)的SDK庫,當(dāng)時(shí)因?yàn)镾DK頭文件中聲明的回調(diào)函數(shù)沒有指定調(diào)用約定,導(dǎo)致程序出現(xiàn)異常崩潰的問題。
我們C++開發(fā)的SDK提供了設(shè)置消息回調(diào)的API接口,并給出了回調(diào)函數(shù)的聲明,如下:
/* 函數(shù)功能:用于消息回發(fā)的回調(diào)函數(shù)指針(服務(wù)器主動(dòng)推送的消息通過該回調(diào)函數(shù)推給上層)
參數(shù):DWORD dwMsgId:消息id
const unsigned char* pMsgBuf:消息中攜帶的數(shù)據(jù)buffer,buffer中的具體內(nèi)容取決于消息id,參看消息id的頭文件
DWORD dwMsgBufLen:消息中攜帶的數(shù)據(jù)buffer長度
返回值:void
*/
typedef void (*PMsgCallBackFunc)( DWORD dwMsgId, const unsigned char* pMsgBuf, DWORD dwMsgBufLen );
設(shè)置回調(diào)函數(shù)的接口如下:
// 設(shè)置業(yè)務(wù)消息回調(diào)接口
SDK_DLL_API void __stdcall SetMsgCallBack( IN PMsgCallBackFunc pMsgCallBackFunc );
回調(diào)函數(shù)的實(shí)現(xiàn)在上層的C#程序中,回調(diào)函數(shù)的調(diào)用在C++實(shí)現(xiàn)的SDK中,因?yàn)榛卣{(diào)函數(shù)PMsgCallBackFunc在聲明時(shí)沒有指定函數(shù)調(diào)用約定,在C#程序中默認(rèn)是__stdcall標(biāo)準(zhǔn)約定,所以在C#中編譯時(shí)回調(diào)函數(shù)內(nèi)部會(huì)清理?xiàng)?臻g。而回調(diào)函數(shù)是在C++ SDK中調(diào)用的,在SDK編譯時(shí)默認(rèn)是__cdecl調(diào)用,會(huì)在調(diào)用回調(diào)函數(shù)處的主調(diào)函數(shù)中釋放棧空間,這樣導(dǎo)致回調(diào)函數(shù)調(diào)用后,主調(diào)函數(shù)會(huì)釋放一次棧空間,回調(diào)函數(shù)內(nèi)部會(huì)釋放一次棧空間,所以多釋放了一次參數(shù)棧空間,導(dǎo)致了棧不平衡,導(dǎo)致程序運(yùn)行出異常。
考慮跨語言調(diào)用的場(chǎng)景,SDK要提供標(biāo)準(zhǔn)的C接口。在SDK的頭文件中,SDK導(dǎo)出接口要指定調(diào)用約定,回調(diào)函數(shù)的聲明也要指定調(diào)用約定。
4、與調(diào)用約定相關(guān)的工程配置選項(xiàng)及/RTC編譯選項(xiàng)
在Visual Studio創(chuàng)建的C++工程中,在沒明確指定函數(shù)調(diào)用約定時(shí),默認(rèn)使用的都是__cdecl調(diào)用,我們可以在工程屬性配置中看到:
對(duì)于C++工程,我們一般不需要修改默認(rèn)的調(diào)用約定。如果要指定dll庫導(dǎo)出接口的調(diào)用約定,我們也不需要修改工程配置,只需要在導(dǎo)出接口的頭文件的函數(shù)聲明處指定調(diào)用約定就可以了。
有人可能會(huì)說,工程屬性配置中使用了默認(rèn)的__cdecl調(diào)用,我們又在頭文件中將接口指定為__stdcall標(biāo)準(zhǔn)調(diào)用,會(huì)不會(huì)有沖突?到底以哪個(gè)為準(zhǔn)呢?沒有沖突的,編譯時(shí)是優(yōu)先以接口聲明處指定的調(diào)用約定為準(zhǔn)的。
在Debug下/RTC運(yùn)行時(shí)檢測(cè)編譯選項(xiàng)是默認(rèn)開啟的,/RTC運(yùn)行時(shí)檢測(cè)在函數(shù)調(diào)用完成后會(huì)去檢測(cè)棧是否平衡,關(guān)于這一點(diǎn)的說明如下:(MSDN上對(duì)/RTC編譯選項(xiàng)的說明)?
如果沒有釋放參數(shù)的棧空間或者參數(shù)棧空間多釋放了一次,都能檢測(cè)出來。如果檢測(cè)到,會(huì)彈出如下的提示:
原文鏈接:https://blog.csdn.net/chenlycly/article/details/125354572
相關(guān)推薦
- 2022-04-01 CentOS安裝Docker的方法_docker
- 2022-05-25 如何把自己寫的jar包打進(jìn)本地maven倉庫呢(也是springboot項(xiàng)目怎么打成SDK)
- 2022-05-26 flutter底部彈出BottomSheet詳解_Android
- 2022-12-13 bat文件與Vbs文件之間的常用操作(獲取用戶輸入,執(zhí)行VBS文件)_DOS/BAT
- 2023-07-03 什么是懶加載,如何實(shí)現(xiàn)圖片或列表懶加載?
- 2022-06-04 Python數(shù)據(jù)處理的三個(gè)實(shí)用技巧分享_python
- 2023-04-02 GoLang調(diào)用鏈可視化go-callvis使用介紹_Golang
- 2022-10-25 Python繪制loss曲線和準(zhǔn)確率曲線實(shí)例代碼_python
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運(yùn)行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲(chǔ)小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運(yùn)算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯(cuò)誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實(shí)現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡(jiǎn)單動(dòng)態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對(duì)象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊(duì)列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支