X86 呼叫約定

本條目描述 x86 架構微處理器的呼叫約定。

呼叫約定描述了被呼叫程式碼的介面：

原子（標量）引數或複雜引數獨立部分的分配順序
引數是如何被傳遞的（放置在堆疊上，或是暫存器中，亦或兩者混合）
被呼叫者應儲存呼叫者的哪個暫存器
呼叫函式時如何為任務準備堆疊，以及任務完成如何恢復

這與程式語言中對於大小和格式的分配緊密相關。另一個密切相關的是名字修飾，這決定了程式碼中的符號名稱如何對映到連結器中的符號名。呼叫約定，型別表示和名稱修飾這三者的統稱，即是眾所周知的應用二進位制介面（ABI）。

不同編譯器在實現這些約定總是有細微的差別存在，所以在不同編譯器編譯出來的程式碼很難接合起來。另一方面，有些約定被當作一種 API 標準（如 stdcall），編譯器實現都較為一致。

歷史背景

在微型電腦出現之前，計算機廠商幾乎都會提供一份作業系統和為不同程式語言編寫的編譯器。平臺所使用的呼叫約定都是由廠商的軟體實現定義的。

在 Apple Ⅱ 出現之前的早期微機幾乎都是 “裸機”，少有一份 OS 或編譯器的，即是 IBM PC 也是如此。IBM PC 相容機的唯一的硬體標準是由 Intel 處理器 (8086, 80386) 定義的，並由 IBM 分發出去。硬體擴充套件和所有的軟體標準 (BIOS 呼叫約定) 都開放有市場競爭。

一群獨立的軟體公司提供了作業系統，不同語言的編譯器以及一些應用軟體。基於不同的需求，歷史實踐和開發人員的創造力，這些公司都使用了各自不同的呼叫約定，往往差異很大。

在 IBM 相容機市場洗牌後，微軟作業系統和程式設計工具 (有不同的呼叫約定) 佔據了統治地位，此時位於第二層次的公司如 Borland 和 Novell，以及開源專案如 GCC，都還各自維護自己的標準。互操作性的規定最終被硬體供應商和軟體產品所採納，簡化了選擇可行標準的問題。

呼叫者清理

在這些約定中，呼叫者自己清理堆疊上的實參（arguments），這樣就允許了可變引數列表的實現，如 printf 。

2.1 cdecl

cdecl（C declaration，即 C 宣告）是源起 C 語言的一種呼叫約定，也是 C 語言的事實上的標準。在 x86 架構上，其內容包括：

函式實參線上程棧上按照從右至左的順序依次壓棧。
函式結果儲存在暫存器 EAX/AX/AL 中
浮點型結果存放在暫存器 ST0 中
編譯後的函式名字首以一個下劃線字元（_）
呼叫者負責從執行緒棧中彈出實參（即清棧）
8 位元或者 16 位元長的整形實參提升為 32 位元長。
受到函式呼叫影響的暫存器（volatile registers）：EAX、ECX、EDX、ST0 – ST7、ES、GS
不受函式呼叫影響的暫存器：EBX、EBP、ESP、EDI、ESI、CS、DS
RET 指令從函式被呼叫者返回到呼叫者（實質上是讀取暫存器 EBP 所指的執行緒棧之處儲存的函式返回地址並載入到 IP 暫存器）

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 caller

caller:

pushl % ebp

movl % esp,% ebp

pushl $3

pushl $2

pushl $1

call callee

addl $12,% esp

addl $5,% eax

leave

ret

在函式返回後，呼叫的函式清理了堆疊。

在 cdecl 的理解上存在一些不同，尤其是在如何返回值的問題上。結果，x86 程式經過不同 OS 平臺的不同編譯器編譯後，會有不相容的情況，即使它們使用的都是 “cdecl” 規則並且不會使用系統呼叫。某些編譯器返回簡單的資料結構，長度大致佔用兩個暫存器，放在暫存器對 EAX:EDX 中；大點的結構和類物件需要異常處理器的一些特殊處理 (如一個定義的建構函式，解構函式或賦值)，存放在記憶體上。為了放置在記憶體上，呼叫者需要分配一些記憶體，並且讓一個指標指向這塊記憶體，這個指標就作為隱藏的第一個引數；被呼叫者使用這塊記憶體並返回指標 —— 返回時彈出隱藏的指標。

在 Linux/GCC，浮點數值透過 x87 偽棧被推入堆疊。像這樣：

sub esp, 8 ; 給 double 值一點空間

fld ebp + x ; 載入 double 值到浮點堆疊上

fstp esp ; 推入堆疊

call funct

add esp, 8

使用這種方法確保能以正確的格式推入堆疊。

cdecl 呼叫約定通常作為 x86 C 編譯器的預設呼叫規則，許多編譯器也提供了自動切換呼叫約定的選項。如果需要手動指定呼叫規則為 cdecl，編譯器可能會支援如下語法：

returntype cdecl funct ;

其中_cdecl 修飾符需要在函式原型中給出，在函式宣告中會覆蓋掉其他的設定。

2.2 syscall

與 cdecl 類似，引數被從右到左推入堆疊中。EAX, ECX 和 EDX 不會保留值。引數列表的大小被放置在 AL 暫存器中。

syscall 是 32 位 OS/2 API 的標準。

2.3 optlink

引數也是從右到左被推入堆疊。從最左邊開始的三個字元變元會被放置在 EAX, EDX 和 ECX 中，最多四個浮點變元會被傳入 ST (0) 到 ST (3) 中 —— 雖然這四個引數的空間也會在引數列表的棧上保留。函式的返回值在 EAX 或 ST (0) 中。保留的暫存器有 EBP, EBX, ESI 和 EDI。

optlink 在 IBM VisualAge 編譯器中被使用。

被呼叫者清理

如果被呼叫者要清理棧上的引數，需要在編譯階段知道棧上有多少位元組要處理。因此，此類的呼叫約定並不能兼容於可變引數列表，如 printf 。然而，這種呼叫約定也許會更有效率，因為需要解堆疊的程式碼不要在每次呼叫時都生成一遍。

使用此規則的函式容易在 asm 程式碼被認出，因為它們會在返回前解堆疊。x86 ret 指令允許一個可選的 16 位引數說明棧位元組數，用來在返回給呼叫者之前解堆疊。程式碼類似如下：

ret 12

3.1 pascal

基於 Pascal 語言的呼叫約定，引數從左至右入棧 (與 cdecl 相反)。被呼叫者負責在返回前清理堆疊。

此呼叫約定常見在如下 16-bit 平臺的編譯器：OS/2 1.x，微軟 Windows 3.x，以及 Borland Delphi 版本 1.x。

3.2 register

Borland fastcall 的別名。

3.3 stdcall

stdcall 是由微軟建立的呼叫約定，是 Windows API 的標準呼叫約定。非微軟的編譯器並不總是支援該呼叫協議。GCC 編譯器如下使用：

int attribute((stdcall)) func

stdcall 是 Pascal 呼叫約定與 cdecl 呼叫約定的折衷：被呼叫者負責清理執行緒棧，引數從右往左入棧。其他各方面基本與 cdecl 相同。但是編譯後的函式名字首以下劃線（_），其後綴以符號 @及其引數所佔的棧空間的位元組長度。暫存器 EAX、ECX 和 EDX 被指定在函式中使用，返回值放置在 EAX 中。stdcall 對於微軟 Win32 API 和 Open Watcom C++ 是標準。

微軟的編譯工具規定：PASCAL、WINAPI、APIENTRY、FORTRAN、CALLBACK、STDCALL、far pascal、fortran、stdcall 均是指此種呼叫約定。

3.4 fastcall

此約定還未被標準化，不同編譯器的實現也不一致。

3.4.1 Microsoft/GCC fastcall

Microsoft 或 GCC 的fastcall 約定 (也即msfastcall) 把第一個 (從左至右) 不超過 32 位元的引數透過暫存器 ECX/CX/CL 傳遞，第二個不超過 32 位元的引數透過暫存器 EDX/DX/DL，其他引數按照自右到左順序壓棧傳遞。

3.4.2 Borland fastcall

從左至右，傳入三個引數至 EAX, EDX 和 ECX 中。剩下的引數推入棧，也是從左至右。

在 32 位編譯器 Embarcadero Delphi 中，這是預設呼叫約定，在編譯器中以 register 形式為人知。

在 i386 上的某些版本 Linux 也使用了此約定。

呼叫者或被呼叫者清理

4.1 thiscall

在呼叫 C++ 非靜態成員函式時使用此約定。基於所使用的編譯器和函式是否使用可變引數，有兩個主流版本的 thiscall。

對於 GCC 編譯器，thiscall 幾乎與 cdecl 等同：呼叫者清理堆疊，引數從右到左傳遞。差別在於 this 指標，thiscall 會在最後把 this 指標推入棧中，即相當於在函式原型中是隱式的左數第一個引數。

在微軟 Visual C++ 編譯器中，this 指標透過 ECX 暫存器傳遞，其餘同 cdecl 約定。當函式使用可變引數，此時呼叫者負責清理堆疊 (參考 cdecl)。thiscall 約定只在微軟 Visual C++ 2005 及其之後的版本被顯式指定。其他編譯器中，thiscall 並不是一個關鍵字 (反彙編器如 IDA 使用thiscall)。

WINAPI

WINAPI 是平臺的預設呼叫約定。Windows 作業系統上預設是 StdCall；Windows CE 上預設是 Cdecl。

Intel ABI

根據 Intel ABI，EAX、EDX 及 ECX 可以自由在過程或函式中使用，不需要保留。

x86-64 呼叫約定

x86-64 呼叫約定得益於更多的暫存器可以用來傳參。而且，不相容的呼叫約定也更少了，不過還是有 2 種主流的規則。

7.1 微軟 x86-64 呼叫約定

在 Windows x64 環境下編譯程式碼時，只有一種呼叫約定，也就是說 32 位下的各種約定在 64 位下統一成一種了。

微軟 x64 呼叫約定使用 RCX、RDX、R8、R9 四個暫存器用於儲存函式呼叫時的 4 個引數 (從左到右)，使用 XMM0、XMM1、XMM2、XMM3 來傳遞浮點變數。其他的引數直接入棧 (從右至左)。整型返回值放置在 RAX 中，浮點返回值在 XMM0 中。少於 64 位的引數並沒有做零擴充套件，此時高位充斥著垃圾。

在微軟 x64 呼叫約定中，呼叫者的一個職責是在呼叫函式之前 (無論實際的傳參使用多大空間)，在棧上的函式返回地址之上（靠近棧頂）分配一個 32 位元組的 “影子空間”；並且在呼叫結束後從棧上彈掉此空間。影子空間是用來給 RCX, RDX, R8 和 R9 提供儲存值的空間，即使是對於少於四個引數的函式也要分配這 32 個位元組。

例如， 一個函式擁有 5 個整型引數，第一個到第四個放在暫存器中，第五個就被推到影子空間之外的棧頂。當函式被呼叫，此棧用來組成返回值 —— 影子空間 32 位 + 第五個引數。

在 x86-64 體系下，Visual Studio 2008 在 XMM6 和 XMM7 中 (同樣的有 XMM8 到 XMM15) 儲存浮點數。結果對於使用者寫的組合語言例程，必須儲存 XMM6 和 XMM7 (x86 不用儲存這兩個暫存器)，這也就是說，在 x86 和 x86-64 之間移植彙編例程時，需要注意在函式呼叫之前 / 之後，要儲存 / 恢復 XMM6 和 XMM7。

7.2 System V AMD64 ABI

此約定主要在 Solaris，GNU/Linux，FreeBSD 和其他非微軟 OS 上使用。頭六個整型引數放在暫存器 RDI, RSI, RDX, RCX, R8 和 R9 上；同時 XMM0 到 XMM7 用來放置浮點變元。對於系統呼叫，R10 用來替代 RCX。同微軟 x64 約定一樣，其他額外的引數推入棧，返回值儲存在 RAX 中。

與微軟不同的是，不需要提供影子空間。在函式入口，返回值與棧上第七個整型引數相鄰。