【文檔翻譯】__cdecl/__stdcall/__fastcall?解開神秘的調用約定!

来源:https://www.cnblogs.com/Code-For-What/archive/2023/12/02/17872401.html
-Advertisement-
Play Games

本文檔譯自 www.codeproject.com 的文章 "Calling Conventions Demystified",作者 Nemanja Trifunovic,原文參見此處 引言 - Introduction 在學習 Windows 編程的漫長、艱難而美妙的旅途中,你可能會對函數聲明前出 ...


本文檔譯自 www.codeproject.com 的文章 "Calling Conventions Demystified",作者 Nemanja Trifunovic,原文參見此處


引言 - Introduction

在學習 Windows 編程的漫長、艱難而美妙的旅途中,你可能會對函數聲明前出現的奇怪說明符感到好奇,比如 __cdecl__stdcall__fastcallWINAPI 等等。在閱讀過 MSDN 或其他參考資料之後,你可能知道了這些說明符是用來為函數指定一種叫“調用約定”的東西。在這篇文章中,我會使用 Visual C++ 來向你解釋不同的調用約定。我要強調的是,上面提到的說明符是微軟特有的,如果你想編寫可移植代碼,就不應該使用它們。

那麼,調用約定究竟是什麼呢?當我們調用函數時,通常會將參數傳遞給它,並獲得返回值。而調用約定就描述了參數是如何傳遞、值是如何從函數返回的。它還指定了函數名稱的修飾方式。不過,編寫優秀的 C/C++ 程式真的一定要瞭解調用約定嗎?並不是。但是,它可能有助於調試。此外,如果要把 C/C++ 與彙編代碼鏈接,那麼這也有幫助。

要理解本文,你需要具備彙編編程的一些非常基本的知識。

無論使用哪種調用約定,都會發生以下情況:

  1. 所有參數都被擴展到 4 位元組(除非特別說明,預設在 Win32 上),並放入記憶體的適當位置,這些位置通常在棧上。不過它們也可能被放在寄存器中,這便是通過調用約定指定的。
  2. 程式執行流會跳轉到被調用函數的地址。
  3. 在函數內部,寄存器 ESI、EDI、EBX 和 EBP 的值被保存在棧上。執行這些操作的代碼部分稱為 function prolog,通常由編譯器生成。
  4. 執行函數代碼,並將返回值放入 EAX 寄存器中。
  5. 寄存器 ESI、EDI、EBX 和 EBP 的值從棧中恢復。執行此操作的代碼段稱為 function epilog,與 function prolog 一樣,在大多數情況下,它由編譯器生成。
  6. 參數從棧中移除。此操作稱為清棧(stack cleanup),可以在被調用函數的內部執行,也可以由調用方執行,具體取決於所使用的調用約定。

作為調用約定的例子(不考慮 this),我們將使用一個簡單的函數:

int sumExample (int a, int b)
{
    return a + b;
}

對這個函數的調用看起來像這樣:

int c = sum (2, 3);

對於使用 __cdecl__stdcall__fastcall 的例子,我會把示例代碼編譯成 C 代碼。本文後面提到的函數名修飾用的是 C 的修飾方法。C++ 的名稱修飾方法超出了本文的討論範圍。


C 調用約定 - C calling convention (__cdecl)

這個約定是 C/C++ 的預設調用約定。如果項目被設置成使用其他的調用約定,我們也可以通過顯式聲明 __cdecl 來為某個函數指定:

int __cdecl sumExample (int a, int b);

__cdecl 調用約定的主要特點是:

  1. 參數將從右到左依次壓入棧中。
  2. 由調用者執行清棧。
  3. 函數名用下劃線字元 _ 作為首碼進行修飾。

現在,示例函數的調用看起來像這樣:

; // 參數從右到左依次壓入棧中
push        3    
push        2    

; // 調用函數
call        _sumExample 

; // 增加參數的總大小到 ESP 寄存器(向高位移動棧指針),以此來清理堆棧
add         esp,8 

; // 將 EAX 的返回值複製到局部變數 (int c)
mov         dword ptr [c],eax

被調用函數 sumExample 的內部如下所示:

; // function prolog
  push        ebp  
  mov         ebp,esp 
  sub         esp,0C0h 
  push        ebx  
  push        esi  
  push        edi  
  lea         edi,[ebp-0C0h] 
  mov         ecx,30h 
  mov         eax,0CCCCCCCCh 
  rep stos    dword ptr [edi] 
  
; //    return a + b;
  mov         eax,dword ptr [a] 
  add         eax,dword ptr [b] 

; // function epilog
  pop         edi  
  pop         esi  
  pop         ebx  
  mov         esp,ebp 
  pop         ebp  
  ret



標準調用約定 - Standard calling convention (__stdcall)

這個調用約定常常用在 Win32 API 的函數上。事實上,WINAPI 只是 __stdcall 的另一個名稱。

#define WINAPI __stdcall

同樣,可以為一個函數顯式指定標準調用約定:

int __stdcall sumExample (int a, int b);

我們也可以使用編譯器選項 /Gz 來給所有未顯式聲明約定的函數指定 __stdcall

__stdcall 調用約定的主要特點是:

  1. 參數將從右到左依次壓入棧中。
  2. 由被調用的函數執行清棧。
  3. 函數名通過添加下劃線 _@ 字元和所需的堆棧空間位元組數來修飾。

調用示例如下:

; // 參數從右到左依次壓入棧中
  push        3    
  push        2    
  
; // 調用函數
  call        _sumExample@8

; // 將 EAX 的返回值複製到局部變數 (int c)
  mov         dword ptr [c],eax

函數如下所示:

; // 此處是 function prolog (和 __cdecl 的例子一樣,略過)

; //    return a + b;
  mov         eax,dword ptr [a] 
  add         eax,dword ptr [b] 

; // 此處是 function epilog (和 __cdecl 的例子一樣,略過)

; // 清棧並返回控制流
  ret         8

因為棧由被調用的函數清理,所以通常 __stdcall 調用約定創建的可執行文件比 __cdecl 要小。因為在 __cdecl 中,必須為每個函數調用生成清棧的代碼。另一方面,參數數量可變的函數(如 printf())必須使用 __cdecl,因為只有調用者知道函數調用中的參數數量;所以,也只有調用方纔能執行清棧。


Fast 調用約定 - Fast calling convention (__fastcall)

__fastcall指出,只要有可能,參數就應該放在寄存器中,而不是棧中。這減少了函數調用的成本,因為使用寄存器的操作比使用堆棧的操作要快。

我們可以顯式聲明 __fastcall 來使用約定,如下所示:

int __fastcall sumExample (int a, int b);

我們也可以使用編譯器選項 /Gr 來給所有未顯式聲明約定的函數指定 __fastcall

__fastcall 的主要特點是:

  1. 需要 32 位大小(及以下)的前兩個函數參數被放入寄存器 ECX 和 EDX。其餘的從右向左壓入堆棧。
  2. 被調用的函數負責從堆棧中彈出參數。
  3. 函數名通過在開頭添加 @ 字元並附加 @ 和參數所需的位元組數(十進位)來修飾。

註意:Microsoft 保留在未來的編譯器版本中更改傳遞參數的寄存器的權利。

調用例子如下:

; // 將參數放入寄存器 EDX 和 ECX 中
  mov         edx,3 
  mov         ecx,2 
  
; // 調用函數
  call        @fastcallSum@8
  
; // 從寄存器 EAX 拷貝返回值到局部變數 (int c)  
  mov         dword ptr [c],eax

函數內部:

; // function prolog
  push        ebp  
  mov         ebp,esp 
  sub         esp,0D8h 
  push        ebx  
  push        esi  
  push        edi  
  push        ecx  
  lea         edi,[ebp-0D8h] 
  mov         ecx,36h 
  mov         eax,0CCCCCCCCh 
  rep stos    dword ptr [edi] 
  pop         ecx  
  mov         dword ptr [ebp-14h],edx 
  mov         dword ptr [ebp-8],ecx 
; // return a + b;
  mov         eax,dword ptr [a] 
  add         eax,dword ptr [b] 
;// function epilog  
  pop         edi  
  pop         esi  
  pop         ebx  
  mov         esp,ebp 
  pop         ebp  
  ret

這個調用約定究竟和 __cdecl__stdcall 相比有多快呢?你可以自己尋找答案。通過聲明不同的約定,再比較執行時間看看吧。我沒有發現 __fastcall 比其他調用約定更快,不過你可能會得出不同的結論。


Thiscall - Thiscall

Thiscall 是調用 C++ 類成員函數的預設調用約定(參數數量可變的除外)。

這種約定的主要特點是:

  1. 參數將從右到左依次壓入棧中。this被放在 ECX 寄存器中。
  2. 由被調用的函數執行清棧。

這個調用約定的例子有點不同。首先,代碼被編譯為 C++,而不是 C。其次,我們用一個帶有成員函數的結構體,而不是用自由函數。

struct CSum
{
    int sum ( int a, int b) {return a+b;}
};

函數調用的彙編代碼如下所示:

push        3
push        2
lea         ecx,[sumObj]
call        ?sum@CSum@@QAEHHH@Z            ; CSum::sum
mov         dword ptr [s4],eax

函數內部如下所示:

push        ebp
mov         ebp,esp
sub         esp,0CCh
push        ebx
push        esi
push        edi
push        ecx
lea         edi,[ebp-0CCh]
mov         ecx,33h
mov         eax,0CCCCCCCCh
rep stos    dword ptr [edi]
pop         ecx
mov         dword ptr [ebp-8],ecx
mov         eax,dword ptr [a]
add         eax,dword ptr [b]
pop         edi
pop         esi
pop         ebx
mov         esp,ebp
pop         ebp
ret         8

如果我們有一個成員函數使用可變數量參數會發生什麼?在這種情況下,會使用 __cdeclthis 最後被壓入棧。


總結 - Conclusion

長話短說,我們總結調用約定之間的主要區別:

  • __cdecl 是 C 和 C++ 程式的預設調用約定。這種調用約定的優點是,它允許使用具有可變數量參數的函數。缺點是它會創建更大的可執行文件。
  • __stdcall 多用於 Win32 API 函數。它不允許函數具有可變數量的參數。
  • __fastcall 嘗試將參數放在寄存器中,而不是堆棧中,從而使函數調用更快。
  • Thscall 調用約定是不使用可變參數的 C++ 成員函數使用的預設調用約定。

在大多數情況下,這就是你需要瞭解的關於調用約定的全部內容。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 在我們開發一些複雜信息的時候,由於需要動態展示一些相關信息,因此我們需要考慮一些控制項內容的動態展示,可以通過動態構建控制項的方式進行顯示,如動態選項卡展示不同的信息,或者動態展示一個自定義控制項的內容等等,目的就是能夠減少一些硬編碼的處理方式,以及能夠靈活的展示數據。本篇隨筆通過實際案例介紹WPF應用開... ...
  • 痞子衡嵌入式半月刊: 第 86 期 這裡分享嵌入式領域有用有趣的項目/工具以及一些熱點新聞,農曆年分二十四節氣,希望在每個交節之日準時發佈一期。 本期刊是開源項目(GitHub: JayHeng/pzh-mcu-bi-weekly),歡迎提交 issue,投稿或推薦你知道的嵌入式那些事兒。 上期回顧 ...
  • 在src目錄下新建一個文件夾models,用來存放數據模型和操作資料庫的方法。 在models目錄下新建一個文件user.js,用來管理用戶信息相關的資料庫操作。 相關的數據模型和資料庫操作方法,最後通過module.exports暴露出去。 mongoose版本8.0.0 1-創建結構 const ...
  • 隨著移動互聯網的普及,越來越多的人開始學習和欣賞唐詩。不過,對於一些想要獲取指定詩歌ID的人來說,這似乎是一件有點困難的事情。好在《唐詩三百首》介面為我們提供了方便快捷的解決方法。下麵,就讓我們來介紹一下如何獲取指定詩歌ID的《唐詩三百首》介面。 數據源介紹: 數據示例下載 ↓ 《唐詩三百首》共選入 ...
  • 官網 Mongoose.js中文網 (mongoosejs.net) 基本使用 安裝 最新的是mongoose8.0.0版本,基於Promise,以前的版本是基於回調函數。 npm npm i mongoose yarn yarn add mongoose 使用 以mongoose8.0.0舉例: ...
  • 最近有個需求需要實現自定義首頁佈局,需要將屏幕按照 6 列 4 行進行等分成多個格子,然後將組件可拖拽對應格子進行渲染展示。 示例 對比一些已有的插件,發現想要實現產品的交互效果,沒有現成可用的。本身功能並不是太過複雜,於是決定自己基於 vue 手擼一個簡易的 Grid 拖拽佈局。 完整源碼在此,在 ...
  • 項目背景: vue 1.創建 backtop.vue 的回到頂部邏輯的組件 <template> <transition name="back-up-fade"> <div class="back-top" :style="{ bottom: bottom + 'px', right: right ...
  • 理解 async/await 的原理和使用方法是理解現代JavaScript非同步編程的關鍵。這裡我會提供一個詳細的實例,涵蓋原理、流程、使用方法以及一些註意事項。代碼註釋會儘量詳盡,確保你理解每個步驟。 實例:使用async/await進行非同步操作 <!DOCTYPE html> <html lan ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...