X86調用約定

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

調用約定描述了被調用代碼的接口：

- 原子（標量）參數或複雜參數獨立部分的分配順序
- 參數是如何被傳遞的（放置在堆棧上，或是寄存器中，亦或兩者混合）
- 被調用者應保存調用者的哪個寄存器
- 調用函數時如何為任務準備堆棧，以及任務完成如何恢復

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

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

## 歷史背景

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

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

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

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

## 調用者清理

在這些約定中，調用者自己清理堆棧上的實參（arguments），這樣就允許了可變參數列表的實現，如`printf()`。

### 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版本開始，調用函數時，堆棧上的數據必須以16位元組對齊(之前的版本只需要4位元組對齊即可)。

考慮下面的C代碼片段：

在x86上，會產生如下彙編代碼（Intel語法）：

caller:

```
   ; 制作新的调用帧
   ; (某些编译器可能转而产生一个'enter'指令)
   push    ebp       ; 保存旧的调用帧
   mov     ebp, esp  ; 初始化新的调用帧
   ; 压入调用实际参数，采用反序
   ; (某些编译器可能从栈指针减去所需空间。
   ; 接着直接填写每个实际参数)
   ; sub esp, 12     : 'enter'指令也做此事
   ; mov [ebp-4], 3  : 或者 mov [esp+8], 3
   ; mov [ebp-8], 2  : 或者 mov [esp+4], 2
   ; mov [ebp-12], 1 : 或者 mov [esp], 1
   push    3
   push    2
   push    1
   call    callee    ; 调用子例程'callee'
   add     esp, 12   ; 从栈帧移除调用实际参数，4字节对齐
   add     eax, 5    ; 修改子例程结果
                     ; (eax是被调用者'callee'的返回值。
                     ; 所以不需要将其移入一个局部变量)
   ; 复原旧的调用帧
   ; (某些编译器可能转而产生一个'leave'指令)
                     ; 多数调用约定都指使ebp由被调用者保存。
                     ; 就是说，它在调用了被调用者后保持原样。
                     ; 这里的'caller'和'callee'都遵守约定
   mov     esp, ebp  ; 释放局部变量的空间
   pop     ebp       ; 复原旧的调用帧
   ret               ; 返回
```

在函數返回後，調用的函數清理了堆棧。在cdecl的理解上存在一些不同，尤其是在如何返回值的問題上。結果，x86程序經過不同OS平台的不同編譯器編譯後，會有不兼容的情況，即使它們使用的都是「cdecl」規則並且不會使用系統調用。

某些編譯器返回簡單的數據結構，長度大致占用兩個寄存器，放在寄存器對EAX:EDX中；大點的結構和類對象需要異常處理器的一些特殊處理(如一個定義的構造函數，析構函數或賦值)，存放在記憶體上。為了放置在記憶體上，調用者需要分配一些記憶體，並且讓一個指針指向這塊記憶體，這個指針就作為隱藏的第一個參數；被調用者使用這塊記憶體並返回指針——返回時彈出隱藏的指針。

在Linux/GCC中，將浮點數值推入調用棧採用了x87浮點裝載指令fld和存儲並彈出指令fstp。下面是傳遞一個雙精度浮點值參數的子例程調用例子：

```nasm
	sub	    esp, 8
	fld	    QWORD PTR double_value
	lea	    esp, [esp-8]
	fstp	QWORD PTR [esp]
	call	callee
	add	    esp, 16
```

這裡採用了16位元組堆棧對齊，使用這種迂迴方法確保能以正確的格式推入堆棧。

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

其中`__cdecl`修飾符需要在函數原型中給出，在函數聲明中會覆蓋掉其他的設置。

### syscall

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

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

### optlink

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

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

## 被調用者清理

如果被調用者要清理棧上的參數，需要在編譯階段知道棧上有多少字節要處理。因此，此類的調用約定並不能兼容於可變參數列表，如`printf()`。然而，這種調用約定也許會更有效率，因為需要解堆棧的代碼不要在每次調用時都生成一遍。

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

### pascal

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

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

### register

Borland fastcall的別名。

### stdcall

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

```c
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`均是指此種調用約定。

WINAPI是平台的預設調用約定。Windows作業系統上默認是StdCall；Windows CE上默認是Cdecl。

### fastcall

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

#### Microsoft/GCC fastcall

Microsoft或GCC的__fastcall約定(也即__msfastcall)把第一個(從左至右)不超過32比特的參數通過寄存器ECX/CX/CL傳遞，第二個不超過32比特的參數通過寄存器EDX/DX/DL，其他參數按照自右到左順序壓棧傳遞。

#### Borland fastcall

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

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

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

## 調用者或被調用者清理

### thiscall

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

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

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

## 寄存器保持

調用約定的另一部份是要保證哪些寄存器在子例程調用之後保持原來的值。這種行為叫做寄存器保持（preservation）。

### 調用者保存的寄存器

依據大多數編譯器所遵循的Intel二進位兼容標準，EAX、EDX和ECX是在一個過程或函數內可以自由使用而不需保存的。

正如其名字所暗示的，這些通用寄存器通常持有臨時而易失（volatile）的資訊，故而可以被任何子例程覆寫。因此，將每個這種寄存器壓入堆棧是調用者的責任，如果想要在子例程調用之後復原它們的值的話。

### 被調用者保存的寄存器

其它寄存器被用來持有長存而不易失（non-volatile）的值，也就是應當跨越調用而被保持。

換句話說，在調用者發起過程調用的時候，它可以預期這些寄存器在被調用者返回之後保持原來的值。因此，在開始時保存（壓入堆棧）和在返回調用者之前復原（相應的彈出堆棧）這些寄存器是被調用者的責任。與前面的情況一樣，這種做法只應當在被調用者變更了的寄存器上進行。

## x86-64調用約定

x86-64調用約定得益於更多的寄存器可以用來傳遞參數值，而且不兼容的調用約定也更少了。但是由於所用於64-位數據模型的不同而有2種主流的規則。

數據模型｜short｜long｜longlong｜指針，size_t｜樣例作業系統

LLP64｜16｜32｜32｜64｜64｜Microsoft Windows（x86-64、IA-64和ARM64）使用Visual C++；和MinGW

LP64｜16｜32｜64｜64｜64｜多數Unix和類Unix系統，比如Solaris、Linux、BSD、macOS。Windows在使用Cygwin時；z/OS

### 微軟x64調用約定

在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。

### System V AMD64 ABI

此約定主要在Solaris，GNU/Linux，FreeBSD和其他非微軟OS上使用。頭六個整型參數放在寄存器RDI, RSI, RDX, RCX, R8和R9上；同時XMM0到XMM7用來放置浮點變元。對於系統調用，R10用來替代RCX。同微軟x64約定一樣，其他額外的參數推入棧，返回值保存在RAX中。與微軟不同的是，不需要提供影子空間。在函數入口，返回值與棧上第七個整型參數相鄰。

實際參數類型｜寄存器

整數/指針實際參數1–6｜RDI, RSI, RDX, RCX, R8, R9

浮點實際參數1–8｜XMM0 – XMM7

餘下實際參數｜堆棧

靜態連結指針｜R10

前面的例子在Linux上將編譯為：

## x86調用約定列表

下面是x86調用約定的一個列表。這些約定主要意圖用於C/C++編譯器（特別是下面的64-位部份），因此很大程度上是特殊案例。其他語言可以在其實現中使用其他格式和約定。

架構｜名字｜作業系統，編譯器｜參數傳遞｜堆棧清理｜註解

寄存器｜堆棧次序

8086｜cdecl｜RTL (C)｜調用者

Pascal｜LTR (Pascal)｜被調用者

fastcall (非成員)｜Microsoft｜AX, DX, BX｜LTR (Pascal)｜被調用者｜返回指針於BX。

fastcall (成員函數)｜Microsoft｜AX, DX｜LTR (Pascal)｜被調用者｜this在堆棧低端地址。返回指針於AX。

fastcall｜Turbo C｜AX, DX, BX｜LTR (Pascal)｜被調用者｜this在堆棧低端地址。返回指針在堆棧高端地址。

watcall｜Watcom C/C++｜AX, DX, BX, CX｜RTL (C)｜被調用者｜返回指針於SI。

IA-32｜cdecl｜類Unix(GCC)｜RTL (C)｜調用者｜在返回結構/類的時候，調用代碼分配空間並通過在堆棧上的一個隱藏參數傳遞到這個空間的一個指針。被調用函數向這個地址寫返回值。堆棧對齊在16-字節邊界，由於一次GCC變更。

cdecl｜Microsoft｜RTL (C)｜調用者｜在返回結構/類的時候，、平坦舊數據 (POD)返回值小於等於32位的返回於EAX寄存器，、POD返回值33–64位大小的通過EAX:EDX寄存器返回。、非POD返回值或大於64-位的值，調用代碼分配空間並通過在堆棧上的一個隱藏參數傳遞到這個空間的一個指針。被調用函數向這個地址寫返回值。堆棧對齊在4-字節邊界。

stdcall｜Microsoft｜RTL (C)｜被調用者｜也被GCC支持。

fastcall｜Microsoft｜ECX, EDX｜RTL (C)｜被調用者｜如果不是成員函數就返回指針在堆棧上。也被GCC支持。

register｜Delphi, Free Pascal, Linux｜EAX, EDX, ECX｜LTR (Pascal)｜被調用者

thiscall｜Windows (Microsoft Visual C++)｜ECX｜RTL (C)｜被調用者｜預設用於成員函數。

vectorcall｜Windows (Microsoft Visual C++)｜ECX, EDX, XYMM0–5｜RTL (C)｜被調用者｜擴展自fastcall。也被ICC和Clang支持。

watcall｜Watcom C/C++｜EAX, EDX, EBX, ECX｜RTL (C)｜被調用者｜返回指針於ESI。

x86-64｜Microsoft x64調用約定｜Windows (Microsoft Visual C++, GCC, Intel C++ Compiler, Delphi), UEFI｜RCX/XMM0, RDX/XMM1, R8/XMM2, R9/XMM3｜RTL (C)｜調用者｜堆棧對齊在16位元組邊界。32位元組影子空間在堆棧上。指定的8個寄存器只能用於參數1到4。對於C++類，隱藏的this參數是第一個參數，因此傳遞於RCX。

vectorcall｜Windows (Microsoft Visual C++, Clang, ICC)｜RCX/XYMM0, RDX/XYMM1, R8/XYMM2, R9/XYMM3 + XYMM4–5｜RTL (C)｜調用者｜擴展自MS x64。

System V AMD64 ABI｜Solaris, Linux, BSD, macOS, OpenVMS (GCC, Intel C++ Compiler, Clang, Delphi)｜RDI, RSI, RDX, RCX, R8, R9, XYZMM0–7｜RTL (C)｜調用者｜堆棧對齊在16位元組邊界。128位元組紅色區域在棧幀之下。內核接口使用RDI、RSI、RDX、R10、R8和R9。在C++中，this是第一個參數。

regcall｜Windows, Linux (Clang, ICC)｜RAX, RCX, RDX, RDI, RSI, R8, R9, R10, R11, R12, R14, R15 + XYZMM0-15 + R10 在Linux之上｜RTL (C)｜調用者｜儘可能多使用寄存器來傳遞參數和返回值。

注釋：

- GCC、clang和較小程度上的ICC能夠在任何平台上使用任何調用約定，要如此可以使用特性比如ms_abi、sysv_abi和fastcall。