x64 呼叫慣例

本文描述了一個函式(呼叫者)用來呼叫另一個函式(被叫方)的標準流程與慣例,並以 x64 程式碼進行呼叫。

關於呼叫約定的更多資訊 __vectorcall ,請參見 __vectorcall。
關於__preserve_none呼叫約定的更多資訊,請參見__preserve_none。

呼叫慣例預設值

x64 應用程式二進位介面(ABI)預設採用四暫存器、快速呼叫的呼叫慣例。 呼叫堆疊上配置空間做為陰影存放區,供被呼叫者儲存這些緩存器。

函式調用的自變數與用於這些自變數的緩存器之間有嚴格的一對一對應。 任何不符合 8 個字節或不是 1、2、4 或 8 個字節的自變數,都必須以傳址方式傳遞。 單一自變數絕不會分散到多個緩存器。

x87 暫存器堆疊未使用。 它可能會被被呼叫者使用,但要將其視為在函式呼叫間會發生變動的不穩定狀態。 所有浮點運算都是使用16個 XMM 快取器來完成。

整數參數會透過暫存器 RCX、RDX、R8 和 R9 傳遞。 浮點數參數分別傳遞於 XMM0L、 XMM1L、 XMM2LXMM3L和 。 16 位元組的參數會以傳址方式傳遞。 參數傳遞會在參數傳遞中詳細說明。 這些暫存器,以及 RAX、R10、R11、XMM4 和 XMM5,都被視為易變,也就是說,它們可能會在被呼叫函式返回時遭到變更。 註冊使用詳盡記錄於 x64 寄存器使用 和 調用者/被調用者保存的寄存器中。

針對具有原型的函式,所有引數都會在傳遞之前轉換成呼叫端的預期類型。 呼叫端負責配置被呼叫者參數的空間。 呼叫端必須一律配置足夠的空間來儲存四個緩存器參數,即使被呼叫者不採用這麼多參數也一樣。 此慣例可簡化對非屬性 C 語言函式和 vararg C/C++ 函式的支援。 對於 vararg 或非屬性型別函式,任何浮點值都必須在對應的一般用途緩存器中重複。 前四個以外的任何參數都必須儲存在陰影存放區之後的堆疊上,再呼叫。 您可以在 Varargs 中找到 Vararg 函式詳細數據。 Unprototyped 函式信息詳述於 Unprototyped 函式中。

對齊方式

大部分的結構會對齊其自然對齊方式。 主要例外狀況是堆棧指標和 malloc 或 alloca 記憶體,其為16位元組,以協助效能。 必須手動完成 16 個字節以上的對齊。 由於 16 個字節是 XMM 作業的常見對齊大小,因此此值應該適用於大部分的程式代碼。 如需結構配置和對齊的詳細資訊,請參閱 x64 類型和記憶體配置。 如需堆疊配置的相關信息,請參閱 x64 堆疊使用量。

回溯性

葉子函數是指不會改變任何非揮發性暫存器的函數。 例如,非葉函數可能會透過呼叫函式來改變非揮發性 RSP。 或者,透過分配更多堆疊空間給本地變數來改變 RSP 。 在處理異常時,為了恢復非揮發性暫存器,非葉函式會以靜態資料標註。 數據描述如何在任意指令中正確回溯函式。 此數據會儲存為 pdata,或程式數據,接著會 參考 xdata 例外狀況處理數據。 xdata 包含回溯資訊,而且可以指向其他 pdata 或例外狀況處理程式函式。

Prolog 和 epilogs 受到高度限制,因此可以在 xdata 中正確描述它們。 除了分葉函式內,堆棧指標必須在不屬於 epilog 或 prolog 的任何程式代碼區域中保持 16 位元組對齊。 分葉函式只要模擬傳回即可解譯,因此不需要 pdata 和 xdata。 如需函式初構和表文的適當結構詳細資訊,請參閱 x64 初構和表結。 如需例外狀況處理的詳細資訊,以及 pdata 和 xdata 的例外狀況處理和回溯,請參閱 x64 例外狀況處理。

參數傳遞

根據預設,x64 呼叫慣例會將前四個自變數傳遞至緩存器中的函式。 用於這些自變數的緩存器取決於自變數的位置和類型。 剩餘的參數則依右至左順序在堆疊中傳遞。 呼叫者會保留所需的堆疊空間,並利用儲存或移動指令將這些參數寫入堆疊記憶體,並維持每個參數的 8 位元組對齊。

位於最左側四個位置的整數值引數,會依由左至右的順序,分別傳遞至 RCX、RDX、R8 和 R9。 第五個及更高的參數會如先前所述,在堆疊上傳遞。 緩存器中的所有整數自變數都是靠右對齊的,因此被呼叫者可以忽略緩存器上層位,並只存取必要的緩存器部分。

前四個參數中的任何浮點與雙精度參數會依位置傳遞至 XMM0 - XMM3。 浮點數值只有在有 varargs 引數時,才會置於整數暫存器 RCX、RDX、R8 和 R9 中。 如需詳細資訊,請參閱 Varargs。 同樣地, XMM0 - XMM3 當對應的參數是整數或指標型態時,暫存器會被忽略。

__m128 類型、陣列和字串絕不會以即時值傳遞。 而是將指標傳遞至呼叫端所分配的記憶體。 大小為 8、16、32 或 64 位的結構和聯合以及 __m64 類型的傳遞方式,就像是相同位元大小的整數。 其他大小的結構或聯合體會以指向由呼叫端分配的記憶體的指標傳遞。 對於作為指標傳遞的這些匯總類型,包括 __m128,呼叫端配置的暫存記憶體必須對齊 16 字節。

不配置堆疊空間並且不呼叫其他函式的內建函式,有時會使用其他易失性暫存器來傳遞額外的暫存器參數。 編譯程式與內部函數實作之間的緊密系結可達成此優化。

被呼叫者負責視需要將寄存器參數轉儲到其備份空間。

下表摘要說明如何依左側的類型和位置傳遞參數:

參數類型 第五及更高 第四 第三 秒 最左邊
浮點數 堆疊 XMM3 XMM2 XMM1 XMM0
整數 堆疊 R9 R8 RDX RCX
匯總 (8、16、32 或 64 位) 和 __m64 堆疊 R9 R8 RDX RCX
其他匯總,作為指標 堆疊 R9 R8 RDX RCX
__m128,做為指標 堆疊 R9 R8 RDX RCX

引數傳遞範例 1 - 所有整數

func1(int a, int b, int c, int d, int e, int f);
// a in RCX, b in RDX, c in R8, d in R9, f then e passed on stack

傳遞引數範例二 - 全部為浮點數

func2(float a, double b, float c, double d, float e, float f);
// a in XMM0, b in XMM1, c in XMM2, d in XMM3, f then e passed on stack

參數傳遞範例 3 - 混合整數和浮點數

func3(int a, double b, int c, float d, int e, float f);
// a in RCX, b in XMM1, c in R8, d in XMM3, f then e passed on stack

傳遞參數範例 4 - __m64、__m128 和集合

func4(__m64 a, __m128 b, struct c, float d, __m128 e, __m128 f);
// a in RCX, ptr to b in RDX, ptr to c in R8, d in XMM3,
// ptr to f passed on stack, then ptr to e passed on stack

Varargs

如果參數是透過 varargs 傳遞(例如省略號自變數),則會套用一般緩存器參數傳遞慣例。 該慣例包括將第五個及之後的參數溢出至堆疊。 被呼叫者有責任輸出其地址已被使用的參數。 如果只有浮點值,整數緩存器和浮點緩存器都必須包含值,如果被呼叫者預期整數緩存器中的值。

Unprototyped 函式

對於未完全原型的函式,呼叫端會將整數值當做整數傳遞,並將浮點值當做雙精確度傳遞。 僅針對浮點值,整數暫存器和浮點暫存器都包含浮點值,以防被呼叫者預期值在整數暫存器中。

func1();
func2() {   // RCX = 2, RDX = XMM1 = 1.0, and R8 = 7
   func1(2, 1.0, 7);
}

傳回值

可放入 64 位元的純量回傳值(包括 __m64 類型)會透過 RAX 傳回。 非純量型別,包括浮點數、雙精確度浮點數,以及例如 __m128、__m128i 和 __m128d 的向量型別,會在 XMM0 中傳回。 在 或 RAX 中回傳XMM0的值中未使用的位元狀態是未定義的。

使用者定義的類型可透過全域函式和靜態成員函式的值傳回。 要以 中的 RAX值回傳使用者定義的型別,其長度必須為 1、2、4、8、16、32 或 64 位元。 它也必須沒有使用者定義的建構函式、解構函式或複製指派運算元。 它不能有私有或受保護的非靜態資料成員,也不能有參考類型的非靜態資料成員。 它不能有基類或虛擬函式。 而且,它只能有也符合這些需求的數據成員。 此定義本質上與 C++03 POD 類型相同。 由於 C++11 標準的定義已改變,我們不建議使用此 std::is_pod 方法進行此測試。 否則,呼叫者必須為回傳值分配記憶體,並將指標作為第一個參數傳遞。 其餘的參數接著會向右移動一個位置。 相同的指標必須由被呼叫者 RAX回傳。

這些範例展示有指定之宣告的函式如何傳遞參數和傳回值:

傳回值 1 - 64 位結果的範例

__int64 func1(int a, float b, int c, int d, int e);
// Caller passes a in RCX, b in XMM1, c in R8, d in R9, e passed on stack,
// callee returns __int64 result in RAX.

傳回值 2 - 128 位結果的範例

__m128 func2(float a, double b, int c, __m64 d);
// Caller passes a in XMM0, b in XMM1, c in R8, d in R9,
// callee returns __m128 result in XMM0.

傳回值 3 的範例 - 依指標的使用者類型結果

struct Struct1 {
   int j, k, l;    // Struct1 exceeds 64 bits.
};
Struct1 func3(int a, double b, int c, float d);
// Caller allocates memory for Struct1 returned and passes pointer in RCX,
// a in RDX, b in XMM2, c in R9, d passed on the stack;
// callee returns pointer to Struct1 result in RAX.

傳回值範例 4 - 透過值傳遞的使用者類型結果

struct Struct2 {
   int j, k;    // Struct2 fits in 64 bits, and meets requirements for return by value.
};
Struct2 func4(int a, double b, int c, float d);
// Caller passes a in RCX, b in XMM1, c in R8, and d in XMM3;
// callee returns Struct2 result by value in RAX.

呼叫端/被呼叫者已儲存緩存器

x64 ABI 將暫存器 RAX、RCX、RDX、R8、R9、R10、R11 和 XMM0-XMM5 視為易變。 如果存在,YMM0-YMM15和ZMM0-ZMM15的上部部分也具有揮發性。 在AVX512VL上, ZMM、、 YMM以及 XMM 暫存器16-31也是揮發性的。 當支援 AMX 時,TMM tile 暫存器是易失的。 請考慮在函數調用時揮發性暫存器已被破壞,除非安全性可通過分析證明,例如整合程序優化。

x64 ABI 將暫存器 RBX、RBP、RDI、RSI、RSP、R12、R13、R14、R15,以及 XMM6-XMM15 視為非揮發性暫存器。 它們必須由使用它們的函式保存和恢復。

當有 APX 支援時,暫存器 R16-R29 是可變性的。 R30 且 R31 為非揮發性。

函式指標

函式指標僅僅是指向相應函式標籤的指標。 函式指標沒有目錄 (TOC) 需求。

向舊版程式代碼提供浮點支援

MMX 與浮點堆疊暫存器(MM0-MM7/ST0-ST7)會在上下文切換器間被保留。 這些緩存器沒有明確的呼叫慣例。 在核心模式程式代碼中,絕對禁止使用這些暫存器。

FPCSR

暫存器狀態也包含 x87 FPU 控制字。 呼叫慣例會指定此緩存器為非揮發性。

x87 FPU 控制字寄存器會在程式執行開始時使用下列標準值來設定:

註冊[bits] 設定
FPCSR\[0:6] 例外會遮蔽所有 1(所有例外都被遮蔽)
FPCSR\[7] 保留 - 0
FPCSR\[8:9] 精密控制 - 10B (雙精度)
FPCSR\[10:11] 四捨五入控制 - 0 (四捨五入到最接近)
FPCSR\[12] 無限控制件 - 0 (未使用 )

被叫者若修改其中 FPCSR 任何欄位,必須先還原該欄位,才能返回呼叫者。 此外,已修改上述任何欄位的呼叫端,必須在叫用被呼叫者之前將其還原為標準值,除非經協定,被呼叫者預期已修改的值。

關於控制旗標非揮發性的規則有兩個例外:

  • 在文件所述用途為修改非揮發性 FPCSR 旗標的函式中。

  • 當可以證明違反這些規則後程式的行為與未違反規則的程式相同時,例如,通過整體程式分析來證明其正確性。

儘管被視為非揮發性,但沒有靜態的解轉描述子說明它被儲存在哪裡,以及應該從哪裡恢復。 會修改 FPCSR 的具例外安全性的程式碼,在堆疊回捲時,應使用例外終結機制(例如 C++ 解構函式或 __finally 子句)來明確將其還原。

MXCSR

登錄狀態也包含 MXCSR。 呼叫慣例會將這個緩存器分成揮發性部分和非揮發性部分。 揮發性部分由六個狀態旗標組成,而 MXCSR\[0:5]寄存器 MXCSR\[6:15]其餘部分,則被視為非揮發性。

非volatile 部分會在程式執行開始時設定為下列標準值:

註冊[bits] 設定
MXCSR\[6] 反正規數為零 - 0
MXCSR\[7:12] 例外會遮蔽所有 1(所有例外都被遮蔽)
MXCSR\[13:14] 四捨五入控制 - 0 (四捨五入到最接近)
MXCSR\[15] 將遮罩的下溢情況歸零 - 0 (關閉)

任何修改 MXCSR 內非揮發性欄位的被呼叫端,都必須在返回其呼叫端之前先還原這些欄位。 此外,已修改上述任何欄位的呼叫端,必須在叫用被呼叫者之前將其還原為標準值,除非經協定,被呼叫者預期已修改的值。

關於控制旗標非揮發性的規則有兩個例外:

  • 在文件中記載指定函式的用途為修改非揮發性 MXCSR 旗標的函式中。

  • 當可以證明違反這些規則後程式的行為與未違反規則的程式相同時,例如,通過整體程式分析來證明其正確性。

除非函式文件中有明確說明,否則不要對 MXCSR 暫存器的易變部分狀態在跨越函式邊界時做任何假設。

儘管 MXCSR 部分被視為非揮發性,但沒有靜態的解轉描述子說明它被儲存在哪裡以及應該從哪裡還原。 會修改 MXCSR 的非揮發性部分的例外安全程式碼,在堆疊展開時,應使用例外終結器(例如 C++ 的解構函式或 __finally 子句)來明確還原它。

setjmp/longjmp

當你包含setjmpex.h或setjmp.h時,所有對setjmp或longjmp的呼叫會導致解構,並引發解構函式和__finally的呼叫。 此行為與 x86 不同,後者 包含 setjmp.h 會導致 __finally 子句與解構子未被呼叫。

呼叫 setjmp 會保留目前的堆疊指標、非揮發性暫存器以及 MXCSR 暫存器。 對 longjmp 的呼叫會返回到最近的 setjmp 呼叫位置,並將堆疊指標、非揮發性暫存器和 MXCSR 暫存器重設為最近一次 setjmp 呼叫所保存的狀態。

若支援 APX,則在函式中,從呼叫 setjmp 的時刻起,到發出最終導致呼叫 longjmp 的那次呼叫為止,R30 和 R31 都不應被修改。 這項限制是因為 R30 和 R31 不會作為 jmp_buf 的一部分儲存——此結構定義無法更改。 相反地,它們會透過解開器來恢復。 以下範例說明資料還原方式的差異如何影響此限制:

jmp_buf jmpbuffer;

void function_a() {
    ...

    int val = setjmp(jmpbuffer);  // At this time R30 is 10

    ...

    if (val == 0) {
        function_b();  // At this time R30 is 20
    }

    ...
}

void function_b() {
    ...

    longjmp(jmpbuffer, 1);

    ...
}

在這個例子中,R30 的值會從呼叫 setjmp 的時點變化到呼叫 function_b 的時點。 在 function_b 中,longjmp 會回溯堆疊,直到到達呼叫 setjmp 的函式(在此例中為 function_a)。 對於 的 R30 還原值將是 20 (被呼叫的點 function_b 的值),而非 10 (被呼叫的值 setjmp )。 這表示當 setjmp 因 longjmp 而第二次返回時,R30 的值會被設為 20 而不是 10,這是不正確的。 這就是為什麼編譯器必須確保 R30 並 R31 保持從被呼叫的點 setjmp 到函式中最後一個位置的一致性,這最終可能導致 longjmp 被呼叫。

由於longjmp可以從例外篩選器(而不只是子常式)中呼叫,這實際上表示,從呼叫setjmp的那一刻起直到函式其餘部分,R30和R31都應保持不變。

另請參閱