X86呼叫約定

本條目描述x86架構微處理器的呼叫約定。呼叫約定描述了被呼叫程式碼的介面：這與程式語言中對於大小和格式的分配緊密相關。另一個密切相關的是名字修飾，這決定了程式碼中的符號名稱如何對映到連結器中的符號名。呼叫約定，型別表示和名稱修飾這三者的統稱，即是眾所周知的應用二進位制介面。不同編譯器在實現這些約定總是有細微的差別存在，所以在不同編譯器編譯出來的程式碼很難接合起來。另一方面，有些約定被當作一種API標準，編譯器實現都較為一致。歷史背景.在微型電腦出現之前，計算機廠商幾乎都會提供一份作業系統和為不同程式語言編寫的編譯器。平臺所使用的呼叫約定都是由廠商的軟體實現定義的。在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架構上，其內容包括：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&amp;T 語法)：.globl callercaller:pushl %ebpmovl %esp,%ebppushl $3pushl $2pushl $1call calleeaddl $12,%espaddl $5,%eaxleaveret在函式返回後，呼叫的函式清理了堆疊。在cdecl的理解上存在一些不同，尤其是在如何返回值的問題上。結果，x86程式經過不同OS平臺的不同編譯器編譯後，會有不相容的情況，即使它們使用的都是“cdecl”規則並且不會使用系統呼叫。某些編譯器返回簡單的資料結構，長度大致佔用兩個暫存器，放在暫存器對EAX:EDX中；大點的結構和類物件需要異常處理器的一些特殊處理(如一個定義的建構函式，解構函式或賦值)，存放在記憶體上。為了放置在記憶體上，呼叫者需要分配一些記憶體，並且讓一個指標指向這塊記憶體，這個指標就作為隱藏的第一個引數；被呼叫者使用這塊記憶體並返回指標----返回時彈出隱藏的指標。在Linux/GCC，浮點數值透過x87偽棧被推入堆疊。像這樣：sub esp, 8 ; 給double值一點空間fld [ebp + x] ; 載入double值到浮點堆疊上fstp [esp] ; 推入堆疊call functadd esp, 8使用這種方法確保能以正確的格式推入堆疊。cdecl呼叫約定通常作為x86 C編譯器的預設呼叫規則，許多編譯器也提供了自動切換呼叫約定的選項。如果需要手動指定呼叫規則為cdecl，編譯器可能會支援如下語法：return_type _cdecl funct();其中_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位引數說明棧位元組數，用來在返回給呼叫者之前解堆疊。程式碼類似如下：ret 12pascal.基於Pascal語言的呼叫約定，引數從左至右入棧(與cdecl相反)。被呼叫者負責在返回前清理堆疊。此呼叫約定常見在如下16-bit 平臺的編譯器：OS/2 1.x，微軟Windows 3.x，以及Borland Delphi版本1.x。register.Borland fastcall的別名。stdcall.stdcall是由微軟建立的呼叫約定，是Windows API的標準呼叫約定。非微軟的編譯器並不總是支援該呼叫協議。GCC編譯器如下使用：int __attribute__((__stdcall__ )) func()stdcall是Pascal呼叫約定與cdecl呼叫約定的折衷：被呼叫者負責清理執行緒棧，引數從右往左入棧。其他各方面基本與cdecl相同。但是編譯後的函式名字尾以符號"@"，後跟傳遞的函式引數所佔的棧空間的位元組長度。暫存器EAX, ECX和EDX被指定在函式中使用，返回值放置在EAX中。stdcall對於微軟Win32 API和Open Watcom C++是標準。微軟的編譯工具規定：codice_1均是指此種呼叫約定。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)。WINAPI.WINAPI是平臺的預設呼叫約定。Windows作業系統上預設是StdCall；Windows CE上預設是Cdecl。Intel ABI.根據Intel ABI，EAX、EDX及ECX可以自由在過程或函式中使用，不需要保留。x86-64呼叫約定.x86-64呼叫約定得益於更多的暫存器可以用來傳參。而且，不相容的呼叫約定也更少了，不過還是有2種主流的規則。微軟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。System V AMD64 ABI.此約定主要在Solaris，GNU/Linux，FreeBSD和其他非微軟OS上使用。頭六個整型引數放在暫存器RDI, RSI, RDX, RCX, R8和R9上；同時XMM0到XMM7用來放置浮點變元。對於系統呼叫，R10用來替代RCX。同微軟x64約定一樣，其他額外的引數推入棧，返回值儲存在RAX中。與微軟不同的是，不需要提供影子空間。在函式入口，返回值與棧上第七個整型引數相鄰。