ARM64 ABI 慣例概觀
在 64 位模式的 ARM 處理器上編譯並執行時,Windows 的基本應用程式二進位介面 (ABI)大部分都遵循 ARM 的標準 AArch64 EABI。 本文會重點說明 EABI 中所記載專案的一些重要假設和變更。 如需 32 位 ABI 的相關信息,請參閱 ARM ABI 慣例概觀。 如需標準 ARM EABI 的詳細資訊,請參閱 ARM 架構 的應用程式二進位介面 (ABI) (外部連結)。
定義
隨著 64 位支援的引進,ARM 已定義數個詞彙:
- AArch32 – ARM 所定義的舊版 32 位指令集架構(ISA),包括 Thumb 模式執行。
- AArch64 – ARM 所定義的新 64 位指令集架構 (ISA)。
- ARMv7 – 第 7 代 ARM 硬體的規格,其中只包含 AArch32 的支援。 此版本的 ARM 硬體是支援 ARM 的第一個 Windows 版本。
- ARMv8 – 第 8 代 ARM 硬體的規格,其中包含 AArch32 和 AArch64 的支援。
Windows 也會使用這些詞彙:
- ARM – 是指 32 位 ARM 架構(AArch32),有時稱為 WoA(ARM 上的 Windows)。
- ARM32 – 與上述 ARM 相同;為了清楚起見,本檔使用。
- ARM64 – 是指 64 位 ARM 架構 (AArch64)。 沒有像 WoA64 這樣的事情。
最後,參考數據類型時,會參考ARM中的下列定義:
- Short-Vector – 在 SIMD 中直接表示的數據類型,向量為 8 個字節或 16 個字節的元素。 它對齊其大小,可以是8個字節或16個字節,其中每個元素可以是1、2、4或8個字節。
- HFA (同質浮點匯總) – 具有 2 到 4 個相同浮點成員、浮點數或雙精度浮點數的數據類型。
- HVA (同質短向量匯總) – 具有 2 到 4 個相同 Short-Vector 成員的數據類型。
基本需求
ARM64 版本的 Windows 預先假設它隨時都在 ARMv8 或更新版本的架構上執行。 浮點數和 NEON 支援都假設存在於硬體中。
ARMv8 規格描述 AArch32 和 AArch64 的新選擇性密碼編譯和 CRC 協助程式作業碼。 他們的支援目前是選擇性的,但建議使用。 若要利用這些 opcode,應用程式應該先進行運行時間檢查,以檢查其是否存在。
位元組序
如同 ARM32 版本的 Windows,在 ARM64 Windows 上會以小端模式執行。 若在 AArch64 中支援核心模式,切換結束度很難達成,因此更容易強制執行。
對齊方式
在ARM64上執行的Windows可讓CPU硬體以透明方式處理未對齊的存取。 在 AArch32 的改進中,這項支援現在也適用於所有整數存取(包括多字存取),以及浮點存取。
不過,仍必須一律對齊未快取(裝置)記憶體的存取權。 如果程式代碼可能從未快取的記憶體讀取或寫入錯誤數據,則必須確定所有存取都一致。
局部變數的預設版面設定對齊方式:
以位元組為單位的大小 | 以位元組為單位的對齊方式 |
---|---|
1 | 7 |
2 | 2 |
3, 4 | 4 |
> 4 | 8 |
全域和靜態的預設版面配置對齊方式:
以位元組為單位的大小 | 以位元組為單位的對齊方式 |
---|---|
1 | 1 |
2 - 7 | 4 |
8 - 63 | 8 |
>= 64 | 16 |
整數快取器
AArch64 架構支援 32 個整數快取器:
註冊 | 揮發性 | 角色 |
---|---|---|
x0-x8 | 動態 | 參數/結果臨時緩存器 |
x9-x15 | 動態 | 臨時快取器 |
x16-x17 | 動態 | 程式內呼叫臨時緩存器 |
x18 | N/A | 保留的平台快取器:在核心模式中,指向目前處理器的 KPCR;在使用者模式中,指向TEB |
x19-x28 | 靜態 | 臨時快取器 |
x29/fp | 靜態 | 框架指標 |
x30/lr | 兩者 | 鏈接快存器:被呼叫端函式必須保留它以傳回它,但呼叫端的值將會遺失。 |
每個緩存器可以存取為完整的 64 位值(透過 x0-x30)或 32 位值(透過 w0-w30)。 32 位作業會將其結果零延伸至 64 位。
如需參數緩存器使用的詳細資訊,請參閱參數傳遞一節。
不同於 AArch32,程式計數器 (PC) 和堆疊指標 (SP) 不會編制索引緩存器。 其存取方式有限。 另請注意,沒有 x31 快取器。 該編碼用於特殊用途。
框架指標 (x29) 與 ETW 和其他服務所使用的快速堆疊步行相容。 它必須指向堆疊上先前的 {x29, x30} 組。
浮點/SIMD 快取器
AArch64 架構也支援 32 個浮點/SIMD 快取器,摘要如下:
註冊 | 揮發性 | 角色 |
---|---|---|
v0-v7 | 動態 | 參數/結果臨時緩存器 |
v8-v15 | 兩者 | 低64位為非揮發性。 高64位是 Volatile。 |
v16-v31 | 動態 | 臨時快取器 |
每個緩存器都可以以完整的 128 位值存取(透過 v0-v31 或 q0-q31)。 它可存取為 64 位值(透過 d0-d31)、32 位值(透過 s0-s31)、16 位值(透過 h0-h31)或 8 位值(透過 b0-b31)。 存取小於128位的存取只會存取完整128位緩存器中的較低位。 除非另有指定,否則它們會讓其餘位維持不變。 (AArch64 與 AArch32 不同,其中較小的緩存器被包裝在較大的緩存器之上。
浮點控制器 (FPCR) 在其內的各種位欄位上具有特定需求:
Bits | 意義 | 揮發性 | 角色 |
---|---|---|---|
26 | AHP | 非揮發性 | 替代半精確度控件。 |
25 | DN | 非揮發性 | 預設 NaN 模式控制件。 |
24 | FZ | 靜態 | 排清到零模式控制件。 |
23-22 | RMode | 靜態 | 四捨五入模式控制件。 |
15,12-8 | IDE/IXE/etc | 非揮發性 | 例外狀況陷阱啟用位,必須一律為 0。 |
系統快取器
如同 AArch32,AArch64 規格提供三個系統控制的「線程標識碼」緩存器:
註冊 | 角色 |
---|---|
TPIDR_EL0 | 已保留。 |
TPIDRRO_EL0 | 包含目前處理器的 CPU 號碼。 |
TPIDR_EL1 | 指向目前處理器的 KPCR 結構。 |
浮點例外狀況
AArch64 系統上支援 IEEE 浮點例外狀況是選擇性的。 對於具有硬體浮點例外狀況的處理器變體,Windows 核心會以無訊息方式攔截例外狀況,並在 FPCR 快取器中隱含停用這些例外狀況。 這個陷阱可確保跨處理器變體的正規化行為。 否則,在沒有例外狀況支持的情況下,在平臺上開發的程式代碼可能會在支援的平臺上執行時,發現自己會採取非預期的例外狀況。
參數傳遞
針對非變異函式,Windows ABI 會遵循 ARM 為參數傳遞所指定的規則。 這些規則會直接從 AArch64 架構的程式調用標準中摘錄:
階段 A – 初始化
這個階段剛好完成一次,再開始處理自變數。
下一個一般用途緩存器號碼 (NGRN) 設定為零。
下一個 SIMD 和浮點緩存器號碼 (NSRN) 設定為零。
下一個堆疊自變數位址 (NSAA) 會設定為目前的堆疊指標值 (SP)。
階段 B – 自變數的預先填補和延伸
針對清單中的每個自變數,會套用下列清單中的第一個比對規則。 如果沒有符合規則,則會使用未修改自變數。
如果自變數類型是複合類型,其大小不能由呼叫端和被呼叫者靜態決定,自變數會複製到記憶體,而自變數會由複本的指標取代。 (C/C++中沒有這類類型,但它們存在於其他語言或語言延伸模組中)。
如果自變數類型是 HFA 或 HVA,則會使用未修改的自變數。
如果自變數類型是大於 16 個字節的複合類型,則會將自變數複製到呼叫端配置的記憶體,而自變數會由複本的指標取代。
如果自變數類型是複合類型,則自變數的大小會四捨五入為最接近的8個字節倍數。
階段 C – 將自變數指派給緩存器和堆疊
針對清單中的每個自變數,會接著套用下列規則,直到已配置自變數為止。 將自變數指派給緩存器時,緩存器中的任何未使用位都有未指定的值。 如果自變數指派給堆疊位置,則任何未使用的填補位元組都有未指定的值。
如果自變數是半精確度、單精度、雙精度浮點數或短向量類型,且 NSRN 小於 8,則會將自變數配置給緩存器 v[NSRN] 最小有效位。 NSRN 會遞增一個。 自變數現在已配置。
如果自變數是 HFA 或 HVA,而且有足夠的未配置 SIMD 和浮點緩存器(NSRN + 成員數目 ≤ 8),則自變數會配置給 SIMD 和浮點緩存器,每一個 HFA 或 HVA 成員一個緩存器。 NSRN 會以所使用的緩存器數目遞增。 自變數現在已配置。
如果自變數是 HFA 或 HVA,則 NSRN 會設定為 8,而自變數的大小會四捨五入為最接近的 8 個字節倍數。
如果自變數是 HFA、HVA、四精確度浮點數或短向量類型,則 NSAA 會四捨五入為 8 的較大值或自變數類型的自然對齊。
如果自變數是半精確度或單精度浮點類型,則自變數的大小會設定為8個字節。 效果就像自變數已複製到64位緩存器中最小有效位,而其餘位則填入未指定值。
如果自變數是 HFA、HVA、半精確度、單精度、雙精度浮點數或短向量類型,則會將自變數複製到調整後的 NSAA 記憶體中。 NSAA 會增加引數的大小。 自變數現在已配置。
如果自變數是整數或指標類型,自變數的大小會小於或等於 8 個字節,而 NGRN 小於 8,自變數會複製到 x[NGRN] 中最小有效位。 NGRN 會遞增一個。 自變數現在已配置。
如果自變數的對齊方式為 16,則 NGRN 會四捨五入為下一個偶數。
如果自變數是整數類型,則自變數的大小等於 16,而 NGRN 小於 7,自變數會複製到 x[NGRN] 和 x[NGRN+1]。 x[NGRN] 應包含自變數記憶體表示法的較低尋址雙字。 NGRN 會遞增兩個。 自變數現在已配置。
如果自變數是複合類型,且自變數的雙字大小不超過 8 減 NGRN,則自變數會複製到連續的一般用途緩存器,從 x[NGRN 開始]。 自變數會傳遞,就像它已從雙字對齊位址載入緩存器一樣,並具有從記憶體載入連續緩存器的適當 LDR 指令序列。 此標準未指定緩存器中任何未使用部分的內容。 NGRN 會以所使用的緩存器數目遞增。 自變數現在已配置。
NGRN 設定為8。
NSAA 會四捨五入為8或自變數類型的自然對齊。
如果自變數是複合類型,則會將自變數複製到調整后的NSAA記憶體。 NSAA 會增加引數的大小。 自變數現在已配置。
如果自變數的大小小於8個字節,則自變數的大小會設定為8個字節。 效果就好像自變數已複製到64位緩存器中最小有效位,而其餘位則填入未指定的值。
自變數會複製到調整后的NSAA記憶體。 NSAA 會增加引數的大小。 自變數現在已配置。
Addendum:Variadic 函式
採用可變自變數數目的函式會以不同於上述方式處理,如下所示:
所有複合都會一樣處理;不特別處理 HFA 或 HVA。
不會使用 SIMD 和浮點緩存器。
實際上,將自變數配置至虛構堆疊的規則 C.12–C.15 與下列規則相同,其中前 64 個字節的堆疊會載入 x0-x7,而任何剩餘的堆疊自變數通常會放置。
傳回值
整數值會在 x0 中傳回。
浮點值會視情況在 s0、d0 或 v0 中傳回。
如果下列所有保留項目,類型會被視為 HFA 或 HVA:
- 它不是空的,
- 它沒有任何非簡單的預設或複製建構函式、解構函式或指派運算元,
- 其所有成員具有相同的 HFA 或 HVA 類型,或是符合其他成員 HFA 或 HVA 類型的 float、double 或 neon 類型。
在 s0-s3、d0-d3 或 v0-v3 中傳回具有四個或更少專案的 HVA 值。
傳回的型別會根據是否有特定屬性,以及函式是否為非靜態成員函式,以不同的方式處理。 具有所有這些屬性的類型,
- 它們是由 C++14 標準定義所匯總,也就是說,他們沒有使用者提供的建構函式、沒有私人或受保護的非靜態數據成員、沒有基類,也沒有虛擬函式,以及
- 他們有一個簡單的複製指派運算元,以及
- 他們有一個微不足道的解構函式,
和 是由非成員函式或靜態成員函式傳回,請使用下列傳回樣式:
- 在 s0-s3、d0-d3 或 v0-v3 中會傳回具有四個或更少元素的 HFA 類型。
- 小於或等於8個字節的類型會在 x0 中傳回。
- 小於或等於 16 個字節的類型會在 x0 和 x1 中傳回,其中 x0 包含低階 8 個字節。
- 對於其他匯總類型,呼叫端應保留足夠大小和對齊方式的記憶體區塊,以保存結果。 記憶體區塊的位址應該以 x8 中函式的額外自變數的形式傳遞。 被呼叫者可以在子程式執行期間的任何時間點修改結果記憶體區塊。 呼叫端不需要保留儲存在 x8 中的值。
所有其他類型都會使用此慣例:
- 呼叫端應保留足夠大小和對齊方式的記憶體區塊,以保存結果。 記憶體區塊的位址應當做 x0 中函式的額外自變數傳遞,如果 x0 中傳遞$this則為 x1。 被呼叫者可以在子程式執行期間的任何時間點修改結果記憶體區塊。 被呼叫者會傳回 x0 中記憶體區塊的位址。
Stack
在 ARM 提出的 ABI 之後,堆疊必須隨時保持 16 位元組對齊。 AArch64 包含硬體功能,可在 SP 未對齊 16 位元組且完成 SP 相對負載或存放區時產生堆疊對齊錯誤。 Windows 會隨時啟用此功能執行。
配置 4k 或更多堆疊值的函式,必須確保每個頁面在最後一頁之前依序觸控。 此動作可確保 Windows 用來展開堆疊的防護頁面無法「跳躍」任何程序代碼。 一般而言,觸控是由 __chkstk
協助程式所完成,其具有自定義呼叫慣例,其會通過 x15 中除以 16 的總堆棧配置。
紅色區域
目前堆疊指標正下方的16位元組區域會保留供分析和動態修補案例使用。 此區域允許插入仔細產生的程式代碼,以將兩個緩存器儲存在 [sp, #-16] ,並暫時將其用於任意用途。 Windows 核心保證在使用者和核心模式中都採用例外狀況或中斷時,不會覆寫這 16 個字節。
核心堆疊
Windows 中的預設核心模式堆疊是六頁(24k)。 請特別注意核心模式中具有大型堆疊緩衝區的函式。 一個不及時的中斷可能會有很少的頭部,並建立堆棧恐慌錯誤檢查。
堆疊行走
Windows 中的程式代碼會使用啟用框架指標來編譯 ,以啟用快速堆疊行走。 一般而言,x29 (fp) 會指向鏈結中的下一個連結,也就是 {fp, lr} 組,表示堆棧上上一個框架的指標和傳回位址。 鼓勵第三方程式代碼啟用框架指標,以允許改善的分析與追蹤。
例外狀況回溯
例外狀況處理期間的回溯會透過使用回溯程式代碼來協助。 回溯程式代碼是儲存在可執行檔之 .xdata 區段中的位元組序列。 它們以抽象的方式描述序言和結尾的作業,以便復原函式序言的效果,以準備備份至呼叫端的堆疊框架。 如需回溯程式代碼的詳細資訊,請參閱 ARM64 例外狀況處理。
ARM EABI 也會指定使用回溯程式代碼的例外狀況回溯模型。 不過,所呈現的規格不足以在 Windows 中回溯,這必須處理電腦位於函式序言或結尾的案例。
動態產生的程式代碼應該透過 RtlAddFunctionTable
和相關聯的函式來描述動態函式數據表,讓產生的程式代碼可以參與例外狀況處理。
迴圈計數器
所有 ARMv8 CPU 都必須支援循環計數器緩存器,這是 Windows 設定在任何例外狀況層級可讀取的 64 位緩存器,包括使用者模式。 您可以使用元件程式代碼中的 MSR opcode,或 _ReadStatusReg
C/C++ 程式代碼中的內建,透過特殊PMCCNTR_EL0緩存器來存取它。
這裡的循環計數器是真正的迴圈計數器,而不是時鐘。 計數頻率會隨著處理器頻率而有所不同。 如果您覺得必須知道迴圈計數器的頻率,就不應該使用循環計數器。 相反地,您要測量時鐘時間,您應該使用 QueryPerformanceCounter
。