ARM 例外狀況處理
ARM 上的 Windows 會針對異步硬體產生的例外狀況和同步軟體產生的例外狀況,使用相同的結構化例外狀況處理機制。 語言專屬例外狀況處理常式使用語言協助程式函式,以 Windows 結構化例外狀況處理為基礎,進行建置。 本文件說明 WINDOWS on ARM 中的例外狀況處理,以及Microsoft ARM 組合器和 MSVC 編譯程式所產生的語言協助程式。
ARM 例外狀況處理
ARM 上的 Windows 會使用回溯程式代碼來控制結構化例外狀況處理 (SEH) 期間的堆疊回溯。 回溯程式代碼是儲存在 .xdata
可執行檔映像區段中的位元組序列。 這些程式代碼會以抽象的方式描述函式序言和結尾程式代碼的作業。 處理程式會在函式回溯至呼叫端的堆疊框架時,使用它們來復原函式序言的效果。
ARM EABI (內嵌的應用程式二進位介面) 會指定使用回溯程式代碼的例外狀況回溯模型。 模型不足以在 Windows 中進行 SEH 回溯。 它必須處理處理器位於函式序言或結尾處的異步案例。 Windows 亦會將回溯控制分為函式級別回溯和語言專屬範圍回溯,其在 ARM EABI 中統一。 基於上述原因,Windows on ARM 會指定回溯資料和程序的更多詳細資料。
假設
Windows on ARM 的可執行映像檔使用可攜式執行檔 (PE) 格式。 如需詳細資訊,請參閱 PE格式。 例外狀況處理資訊會儲存在 .pdata
映像的 和 .xdata
區段中。
例外狀況處理機制會對遵循適用於 Windows on ARM 的 ABI 之程式碼,進行下列假設:
當函式主體內發生例外狀況時,處理程式可以復原序言的作業,或以向前方式執行結尾的作業。 這兩個作業會產生相同的結果。
序言和結尾通常會彼此鏡像。 此功能可用來減少描述回溯所需的元數據大小。
函式通常相對來說會較小。 數個優化依賴此觀察來有效率地封裝數據。
如果在結尾上放置條件,則它會同等地套用至結尾中的每一個指令。
如果序言將堆棧指標 (SP) 儲存在另一個緩存器中,該緩存器在整個函式中必須保持不變,因此可以隨時復原原始 SP。
除非 SP 儲存在其他暫存器,否則對它的所有操作都必須嚴格發生在序言和結尾中。
若要回溯任何堆疊框架,需要下列作業:
以 4 個位元組的增量,調整 r13 (SP)。
彈出一個或多個整數暫存器。
彈出一個或多個 VFP (虛擬浮點數) 暫存器。
將任意暫存器值複製到 r13 (SP)。
使用較小的後置遞減作業,從堆疊載入 SP。
剖析數個定義良好框架類型中的一個。
.pdata
記錄
.pdata
PE 格式影像中的記錄是固定長度專案的已排序數位,可描述每個堆疊操作函式。 分葉函式(不會呼叫其他函式的函式) .pdata
不需要記錄,因為它們不會操作堆疊。 (即它們不需要任何區域儲存區,也無需儲存或還原靜態暫存器)。 您可以從 區段省略這些函式的 .pdata
記錄,以節省空間。 上述其中一個函式的回溯作業可以將連結暫存器 (LR) 的傳回位址,複製到程式計數器 (PC),以向上移至呼叫者。
ARM 的每個 .pdata
記錄長度為8個字節。 記錄的一般格式會將函式的相對虛擬位址 (RVA) 放在前 32 位單字中,後面接著第二個字,其中包含可變長度 .xdata
區塊的指標,或描述標準函式回溯序列的包裝字組,如下表所示:
字組位移 | Bits | 目的 |
---|---|---|
0 | 0-31 | Function Start RVA 是函式開頭的 32 位 RVA。 如果函式包含捲動方塊程式碼,則必須設定此位址的低位元。 |
1 | 0-1 | Flag 是 2 位欄位,表示如何解譯第二 .pdata 個字的其餘 30 位。 如果 Flag 為 0,則其餘位會形成 例外狀況資訊 RVA (含低兩個位隱含 0)。 如果 Flag 不是零,則其餘位會形成 封裝回溯數據 結構。 |
1 | 2-31 | 例外狀況資訊 RVA 或 封裝回溯數據。 例外狀況資訊 RVA 是儲存在 .xdata 區段中之可變長度例外狀況資訊結構的位址。 此資料必須是對齊的 4 位元組。封裝回溯數據 是從函式回溯所需的作業壓縮描述,假設是標準形式。 在此情況下,不需要 .xdata 記錄。 |
封裝的回溯資料
對於如下所述之序言和結尾遵循標準格式的函式而言,可以使用封裝的回溯資料。 它不需要 .xdata
記錄,並大幅減少提供回溯數據所需的空間。 標準序言和結尾的設計目的是要符合不需要例外狀況處理程式之簡單函式的常見需求,並以標準順序執行其設定和卸除作業。
下表顯示已封裝回溯資料的記錄格式 .pdata
:
字組位移 | Bits | 目的 |
---|---|---|
0 | 0-31 | Function Start RVA 是函式開頭的 32 位 RVA。 如果函式包含捲動方塊程式碼,則必須設定此位址的低位元。 |
1 | 0-1 | Flag 是具有下列意義的 2 位欄位:- 00 = 未使用包裝回溯數據;剩餘位會指向 .xdata 記錄。- 01 = 封裝回溯數據。 - 10 = 封裝回溯數據,其中假設函式沒有序言。 這在描述與函式開頭不連續的函式片段時非常有用。 - 11 = 保留。 |
1 | 2-12 | Function Length 是一個11位位位元組,提供以位元組除以2的位元組為單位的整個函式長度。 如果函式大於 4K 個字節,則必須改用完整 .xdata 記錄。 |
1 | 13-14 | Ret 是 2 位欄位欄位,表示函式傳回的方式:- 00 = 透過 pop {pc} 傳回 ( L 在此案例中,旗標位必須設定為 1)。- 01 = 使用 16 位分支傳回。 - 10 = 使用 32 位分支傳回。 - 11 = 完全沒有結尾。 這在描述可能僅包含序言而結尾在別處的不連續函式片段非常有用。 |
1 | 15 | H 是一個 1 位旗標,指出整數參數緩存器函式 「homes」 是否在函式開頭推送它們,並在傳回之前解除分配堆疊的 16 個字節。 (0 = 不住家登記,1 = 房屋登記。 |
1 | 16-18 | Reg 是 3 位欄位,表示上次儲存的非揮發性緩存器索引。 R 如果位為 0,則只會儲存整數快取器,並假設位於 r4-rN 的範圍內,其中 N 等於 4 + Reg 。 R 如果位為 1,則只會儲存浮點快取器,並假設在 d8-dN 的範圍內,其中 N 等於 8 + Reg 。 = 1 和 Reg = 7 的特殊組合R 表示不會儲存任何快取器。 |
1 | 19 | R 是1位旗標,指出儲存的非揮發性緩存器是整數緩存器 (0) 或浮點緩存器 (1)。 如果 R 設定為 1,且 Reg 字段設定為 7,則不會推送任何非揮發性緩存器。 |
1 | 20 | L 是1位旗標,指出函式是否儲存/還原LR,以及欄位所 Reg 指示的其他快取器。 (0 = 不儲存/還原,1 = 儲存/還原。) |
1 | 21 | C 是1位旗標,指出函式是否包含額外的指示,以設定快速堆疊行走的框架鏈結 (1) 或否 (0)。 如果設定此位元,則會隱含地將 r11 加入所儲存整數靜態暫存器的清單。 (如果使用 旗標, C 請參閱下面的限制。 |
1 | 22-31 | Stack Adjust 是10位位位元段,表示配置給此函式的堆疊位元組數目,除以4。 不過,僅可以對 0x000-0x3F3 之間的值直接進行編碼。 配置超過 4044 個字節堆疊的函式必須使用完整 .xdata 記錄。 Stack Adjust 如果欄位0x3F4或更大,則低4位具有特殊意義:- 位 0-1 表示堆疊調整的字數 (1-4) 減 1。 - 如果序言將此調整結合至其推送作業,位 2 會設定為 1。 - 如果結尾將此調整結合至其快顯作業,位 3 會設定為 1。 |
由於上述編碼中可能存在冗餘,所以適用下列限制:
C
如果旗標設定為 1:L
旗標也必須設定為 1,因為框架鏈結需要 r11 和 LR。r11 不得包含在 所
Reg
描述的緩存器集中。 也就是說,如果推送 r4-r11,Reg
則應該只描述 r4-r10,因為C
旗標表示 r11。
Ret
如果欄位設定為 0,則L
旗標必須設定為 1。
違反這些限制會導致不支援的序列。
基於下列討論的目的,兩個虛擬旗標衍生自 Stack Adjust
:
PF
或 「序言折疊」表示已Stack Adjust
設定0x3F4或更大且位 2。EF
或 「結尾折疊」表示已Stack Adjust
設定0x3F4或更大且位 3。
標準函式的序言可能具有多達 5 個指令 (請注意,3a 和 3b 互斥):
指示 | 下列情況中會假設 Opcode 存在: | 大小 | OpCode | 回溯程式碼 |
---|---|---|---|---|
1 | H ==1 |
16 | push {r0-r3} |
04 |
2 | C ==1 或 ==1 或 L R ==0 或 PF ==1 |
16/32 | push {registers} |
80-BF/D0-DF/EC-ED |
3a | C ==1 and (R ==1 and PF ==0) |
16 | mov r11,sp |
FB |
3b | C ==1 and (R ==0 或 PF ==1) |
32 | add r11,sp,#xx |
FC |
4 | R ==1 和 Reg != 7 |
32 | vpush {d8-dE} |
E0-E7 |
5 | Stack Adjust != 0 和 PF ==0 |
16/32 | sub sp,sp,#xx |
00-7F/E8-EB |
如果位設定為 1, H
則指令 1 一律存在。
若要設定框架鏈結,如果 C
設定位,指示 3a 或 3b 就會存在。 如果除了 r11 和 LR 之外未推入任何暫存器,則它為 16 位元 mov
;否則,其為 32 位元 add
。
如果未指定非摺疊調整,則指令 5 為明確堆疊調整。
指令 2 和 4 基於是否需要推入而設定。 下表摘要說明根據、L
、 R
和 PF
字段儲存C
的緩存器。 在所有情況下, N
等於 Reg
+ 4、 E
等於 Reg
+ 8,且 S
等於 (~Stack Adjust
) 和 3。
C | L | R | PF | 推入的整數暫存器 | 推入的 VFP 暫存器 |
---|---|---|---|---|---|
0 | 0 | 0 | 0 | r4 - r*N * |
none |
0 | 0 | 0 | 1 | r*S * - r*N * |
none |
0 | 0 | 1 | 0 | none | d8 - d*E * |
0 | 0 | 1 | 1 | r*S * - r3 |
d8 - d*E * |
0 | 1 | 0 | 0 | r4 - r**N 、LR |
none |
0 | 1 | 0 | 1 | r*S * - r*N *、LR |
none |
0 | 1 | 1 | 0 | LR | d8 - d*E * |
0 | 1 | 1 | 1 | r*S * - r3、LR |
d8 - d*E * |
1 | 0 | 0 | 0 | (編碼無效) | (編碼無效) |
1 | 0 | 0 | 1 | (編碼無效) | (編碼無效) |
1 | 0 | 1 | 0 | (編碼無效) | (編碼無效) |
1 | 0 | 1 | 1 | (編碼無效) | (編碼無效) |
1 | 1 | 0 | 0 | r4 - r*N 、r11、LR |
none |
1 | 1 | 0 | 1 | r*S * - r*N 、r11、LR |
none |
1 | 1 | 1 | 0 | r11、LR | d8 - d*E * |
1 | 1 | 1 | 1 | r*S * - r3、r11、LR |
d8 - d*E * |
標準函式的結尾遵循類似格式,但方向相反且具有其他選項。 結尾可能長達 5 個指令,且其格式由序言的格式嚴格指定。
指示 | 下列情況中會假設 Opcode 存在: | 大小 | OpCode |
---|---|---|---|
6 | Stack Adjust !=0 和 EF ==0 |
16/32 | add sp,sp,#xx |
7 | R ==1 和 Reg !=7 |
32 | vpop {d8-dE} |
8 | C ==1 或 (==1 and (L H ==0 或 Ret !=0)) 或 R ==0 或 EF ==1 |
16/32 | pop {registers} |
9a | H ==1 and (L ==0 或 Ret !=0) |
16 | add sp,sp,#0x10 |
9b | H ==1 和 L ==1 和 Ret ==0 |
32 | ldr pc,[sp],#0x14 |
10a | Ret ==1 |
16 | bx reg |
10b | Ret ==2 |
32 | b address |
如果指定非摺疊調整,則指令 6 為明確堆疊調整。 由於 PF
與無關 EF
,因此可以有沒有指令 6 的指令 5,反之亦然。
指令 7 和 8 使用與序言相同的邏輯來判斷要從堆疊還原的緩存器,但有這三個變更:第一,用來取代 PF
;第二,EF
如果 Ret
= 0 和 H
= 0,則 LR 會取代為緩存器清單中的 PC,結尾會立即結束;第三個,如果 Ret
= 0 和 H
= 1, 然後,LR 會從緩存器清單中省略,然後由指令 9b 快顯。
如果 H
已設定,則指示 9a 或 9b 存在。 當 為非零時 Ret
,會使用指令 9a,這也表示 10a 或 10b 的存在。 如果 L=1,則會在指令 8 中彈出 LR。 當 為 1 且Ret
為零時L
,會使用指令 9b,表示結尾的早期結束,並同時傳回和調整堆棧。
如果結尾尚未結束,則指示 10a 或 10b 存在,以根據的值 Ret
指出 16 位或 32 位分支。
.xdata
記錄
當封裝的回溯格式不足以描述函式的回溯時,必須建立可變長度 .xdata
的記錄。 此記錄的位址會儲存在記錄的第二個字中 .pdata
。 的格式 .xdata
是包含四個區段的包裝可變長度字組:
描述結構整體大小的
.xdata
1 或 2 字標頭,並提供索引鍵函式數據。 只有當 [結尾計數] 和 [程序代碼字] 字段都設定為 0 時,才會顯示第二個字。 下表詳細說明這些欄位:Word Bits 目的 0 0-17 Function Length
是18位位元段,表示以位元組為單位的函式總長度,除以2。 如果函式大於 512 KB,則必須使用多個.pdata
和.xdata
記錄來描述函式。 如需詳細資料,請參閱本文件中的<大型函式>一節。0 18-19 Vers 是描述其餘 .xdata
版本的 2 位欄位。 目前僅定義版本 0,保留值 1-3。0 20 X 是 1 位欄位元,表示例外狀況數據是否存在 (1) 或不存在 (0)。 0 21 E
是一個 1 位字段,表示描述單一結尾的資訊會封裝到標頭中(1),而不是稍後需要額外的範圍文字(0)。0 22 F 是 1 位欄位元,表示此記錄描述函式片段 (1) 或完整函式 (0)。 片段表示沒有序言,而且應該忽略所有序言處理。 0 23-27 結尾計數 是一個5位字段,其意義有兩個,視位的狀態 E
而定:
- 如果E
為 0,此字段是第 2 節所述的結尾範圍總數的計數。 如果函式中有超過 31 個範圍存在,則此欄位和 [ 程式代碼字 ] 欄位都必須設定為 0,表示需要擴充字。
- 如果E
為 1,此字段會指定描述唯一結尾之第一個回溯程式代碼的索引。0 28-31 Code Words 是 4 位位元元段,指定包含第 4 節中所有回溯程式代碼所需的 32 位字數。 如果超過 63 個回溯程式代碼位元組需要超過 15 個字,此字段和 Epilogue Count 字段都必須設定為 0,表示需要擴充字。 1 0-15 擴充結尾計數 是16位字段,可提供更多空間來編碼異常大量的結尾。 只有當第一個頁首字中的 [結尾計數] 和 [程序代碼字] 欄位都設定為 0 時,才會顯示包含此欄位的擴展名。 1 16-23 擴充程式代碼字 是一個8位字段,可提供更多空間來編碼異常大量的回溯字組。 只有當第一個頁首字中的 [結尾計數] 和 [程序代碼字] 欄位都設定為 0 時,才會顯示包含此欄位的擴展名。 1 24-31 已保留 例外狀況數據之後(如果
E
標頭中的位設定為 0)是結尾範圍的相關信息清單,這些範圍會封裝一個到一個字,並儲存在增加起始位移的順序。 每一個範圍包含下列欄位:Bits 目的 0-17 結尾開始位移 是一個 18 位欄位,描述結尾的位移,以位元組除以 2,相對於函式的開頭。 18-19 Res 是保留供未來擴充的 2 位欄位元。 其值必須為 0。 20-23 Condition 是 4 位字段,可提供執行結尾的條件。 對於無條件的結尾,它應該設定為 0xE,指出「永遠」。 (結尾必須完全是有條件的或完全是無條件的,而在 Thumb-2 模式下,結尾以 IT opcode 後的第一個指令開始)。 24-31 結尾開始索引 是8位欄位元段,表示描述此結尾之第一個回溯程式碼的位元組索引。 在結尾範圍清單之後是包含回溯程式碼的位元組陣列,在本文章的<回溯程式碼>一節有詳細描述。 此陣列在最近完整字組界面的結尾處填補。 位元組以 Little-Endian 順序儲存,因此可在 Little-Endian 模式下直接擷取。
如果標頭中的 X 欄位是 1,回溯程式代碼位元組後面接著例外狀況處理程式資訊。 這包含一個 例外狀況處理程式 RVA ,其中包含例外狀況處理程式的位址,後面緊接著例外狀況處理程式所需的 (可變長度) 數據量。
記錄 .xdata
的設計目的是要擷取前8個字節,並計算記錄的完整大小,而不包括後續變數大小例外狀況數據的長度。 此程式碼片段會計算記錄大小:
ULONG ComputeXdataSize(PULONG Xdata)
{
ULONG Size;
ULONG EpilogueScopes;
ULONG UnwindWords;
if ((Xdata[0] >> 23) != 0) {
Size = 4;
EpilogueScopes = (Xdata[0] >> 23) & 0x1f;
UnwindWords = (Xdata[0] >> 28) & 0x0f;
} else {
Size = 8;
EpilogueScopes = Xdata[1] & 0xffff;
UnwindWords = (Xdata[1] >> 16) & 0xff;
}
if (!(Xdata[0] & (1 << 21))) {
Size += 4 * EpilogueScopes;
}
Size += 4 * UnwindWords;
if (Xdata[0] & (1 << 20)) {
Size += 4; // Exception handler RVA
}
return Size;
}
雖然序言和每個結尾都有回溯程式碼的索引,但數據表會在兩者之間共用。 它們都可以共用相同的回溯程序代碼並不罕見。 建議編譯器撰寫者針對此情況進行最佳化,因為可以指定的最大索引為 255,這會限制特定函式可能具有的回溯程式碼總數。
回溯程式碼
回溯程式碼陣列是一組指令序列,用於確切描述如何以作業必須復原的順序,來復原序言的影響。 回溯程式碼是迷你指令集,編碼為位元組的字串。 當執行完成時,向呼叫函式傳回的位址位於 LR 暫存器中,而所有靜態暫存器會還原為呼叫該函式時的值。
如果已保證例外狀況僅在函式主體中發生,而不會在序言或結尾中發生,則只需要一個回溯序列即可。 不過,Windows 回溯模型需要能夠從部分執行的序言或結尾回溯。 為了符合此需求,已仔細對回溯程式碼進行設計,讓其與序言和結尾中的每一個相關 opcode,具有明確的一對一對應。 這有下列幾個含意:
可以透過計算回溯程式碼的數目,來計算序言和結尾的長度。 即使使用可變長度 Thumb-2 指令,也可以這樣做,因為 16 位元和 32 位元作業碼具有不同的對應。
透過計算經過結尾範圍開頭的指令數目,可以跳過相同數目的回溯程式碼,並執行系列的其餘部分,以完成結尾所執行之部分執行的回溯。
透過計算序言結尾之前的指令數目,可以跳過相同數目的回溯程式碼,並執行序列的其餘部分,以僅復原序言中已完成執行的那些部分。
下表顯示從回溯程式碼到作業碼的對應。 最常見的程式碼只有一個位元組,需要兩個、三個甚至四個位元組的程式碼較不常見。 每一個程式碼都從最重要的位元組到最不重要的位元組進行儲存。 回溯程式碼結構與 ARM EABI 中所述的編碼不同,因為這些回溯程式碼設計用來與序言和結尾中的 opcode 進行一對一對應,以容許回溯部分執行的序言和結尾。
位元組 1 | 位元組 2 | 位元組 3 | 位元組 4 | Opsize | 說明 |
---|---|---|---|---|---|
00-7F | 16 | add sp,sp,#X 其中 X 是 (Code & 0x7F) * 4 |
|||
80-BF | 00-FF | 32 | pop {r0-r12, lr} 如果 Code & 0x2000 和 r0-r12 中的對應位是在 Code & 0x1FFF 中設定對應的位,則會彈出 LR |
||
C0-CF | 16 | mov sp,rX 其中 X 是 Code & 0x0F |
|||
D0-D7 | 16 | pop {r4-rX,lr} 其中 X 是 (Code & 0x03) + 4,如果 Code & 0x04 |
|||
D8-DF | 32 | pop {r4-rX,lr} 其中 X 是 (Code & 0x03) + 8,如果 Code & 0x04 |
|||
E0-E7 | 32 | vpop {d8-dX} 其中 X 是 (Code & 0x07) + 8 |
|||
E8-EB | 00-FF | 32 | addw sp,sp,#X 其中 X 是 (Code & 0x03FF) * 4 |
||
EC-ED | 00-FF | 16 | pop {r0-r7,lr} 如果 Code & 0x0100 和 r0-r7 中的對應位是在 Code 和 0x00FF 中設定對應的位,則會彈出 LR |
||
EE | 00-0F | 16 | Microsoft 特定 | ||
EE | 10-FF | 16 | 可用的 | ||
EF | 00-0F | 32 | ldr lr,[sp],#X 其中 X 是 (Code & 0x000F) * 4 |
||
EF | 10-FF | 32 | 可用的 | ||
F0-F4 | - | 可用的 | |||
F5 | 00-FF | 32 | vpop {dS-dE} 其中 S 是 (Code & 0x00F0) >> 4,E 是 Code & 0x000F |
||
F6 | 00-FF | 32 | vpop {dS-dE} 其中 S 是 (Code & 0x00F0) >> 4) + 16, E 是 (Code & 0x000F) + 16 |
||
F7 | 00-FF | 00-FF | 16 | add sp,sp,#X 其中 X 是 (Code & 0x00FFFF) * 4 |
|
F8 | 00-FF | 00-FF | 00-FF | 16 | add sp,sp,#X 其中 X 是 (Code & 0x00FFFFFF) * 4 |
F9 | 00-FF | 00-FF | 32 | add sp,sp,#X 其中 X 是 (Code & 0x00FFFF) * 4 |
|
FA | 00-FF | 00-FF | 00-FF | 32 | add sp,sp,#X 其中 X 是 (Code & 0x00FFFFFF) * 4 |
FB | 16 | nop (16 位元) | |||
FC | 32 | nop (32 位元) | |||
FD | 16 | end + 16 位元 nop (結尾中) | |||
FE | 32 | end + 32 位元 nop (結尾中) | |||
FF | - | end |
這會顯示回溯程式代碼程式代碼中每個位元組的十六進位值範圍,以及opcode大小Opsize和對應的原始指令解譯。 空儲存格表示較短的回溯程式碼。 如果指令具有涵蓋多位元組的大型值,則會最先儲存最重要的位元。 [ Opsize] 字段會顯示與每個 Thumb-2 作業相關聯的隱含 opcode 大小。 表格中具有不同編碼的明顯重複項目用於區分不同的作業碼大小。
設計回溯程式碼,以便程式碼的第一個位元組告知程式碼的總大小 (以位元組為單位),以及指令資料流中對應的 opcode 大小。 若要計算序言或結尾的大小,從序列的開頭到結尾查核回溯程式碼,並使用查閱資料表或類似方法,來判定對應 opcode 的長度。
回溯程式碼 0xFD 和 0xFE 等同於一般結束程式碼 0xFF,但在結尾中,會負責處理額外的一個 nop作業碼,16 位元或 32 位元。 對於序言,程式碼 0xFD、0xFE 及 0xFF 完全相等。 這說明一般結尾結尾結尾 bx lr
或 b <tailcall-target>
,其沒有對等的序言指示。 這會增加回溯序列可以在序言和結尾之間共用的機會。
在許多情況下,應該可以對序言和所有結尾使用相同的回溯程式碼集。 不過,若要處理部分執行序言和結尾的回溯,您可能需要具有順序或行為不同的多個回溯程式碼序列。 這就是為什麼每一個結尾對回溯陣列都有自己的索引,以顯示開始執行的位置。
回溯部分序言和結尾
最常見的回溯情況是在遠離序言和所有結尾的函式主體中發生例外狀況。 在此情況下,回溯器會執行回溯陣列中從索引 0 開始的程式碼,然後繼續直到偵測到結尾 opcode。
當序言或結尾執行時,如果發生例外狀況,則僅會部分地建構堆疊框架,且回溯器必須確切判定發生了什麼,以便正確進行復原。
例如,假設如下序言和結尾序列:
0000: push {r0-r3} ; 0x04
0002: push {r4-r9, lr} ; 0xdd
0006: mov r7, sp ; 0xc7
...
0140: mov sp, r7 ; 0xc7
0142: pop {r4-r9, lr} ; 0xdd
0146: add sp, sp, #16 ; 0x04
0148: bx lr
在每一個作業碼旁邊,是用於描述此作業的適當回溯程式碼。 序言的回溯程式碼序列是結尾之回溯程式碼的鏡像,不包括最終的指令。 此案例很常見,而且原文的回溯程序代碼一律假設會以反向順序儲存序言的執行順序。 以下為我們提供常見的回溯程式碼集:
0xc7, 0xdd, 0x04, 0xfd
0xFD 程式碼是序列結束的特殊程式碼,表示結尾比序言長一個 16 位元的指令。 這可更好地共用回溯程式碼。
在本範例中,如果序言和結尾之間的函式主體執行時發生例外狀況,則回溯會從結尾開始,即在結尾程式碼的位移 0 處開始。 這對應於範例中的位移 0x140。 回溯器會執行完整回溯序列,因為沒有發生任何清除。 如果是在結尾程式碼開頭之後的一個指令發生例外狀況,則回溯器可以跳過第一個回溯程式碼而成功回溯。 假設 opcode 與回溯程式代碼之間的一對一對應,如果從結尾中的指示 n 回溯,回溯器應該略過前 n 個回溯程序代碼。
對於序言,會以相反的方式執行相似的邏輯。 如果從序言中的位移 0 開始回溯,則無需執行任何動作。 如果從序言中的一個指令開始回溯,則回溯序列應該從距離結尾一個回溯程式碼處開始,因為序言回溯程式碼以相反的順序儲存。 在一般情況下,如果從序言中的指示 n 回溯,回溯應該從程式代碼清單結尾的 n 回溯程式代碼開始執行。
序言和結尾回溯程序代碼不一定完全相符。 在該情況下,回溯程式碼陣列可能需要包含數個程式碼序列。 若要判定開始處理程式碼的位移,請使用下列邏輯:
如果從函式主體內開始回溯,則從索引 0 開始執行回溯程式碼,然後繼續直到到達結束作業碼。
如果從結尾內開始回溯,請使用結尾範圍提供的結尾專屬開始索引。 計算 PC 距離結尾開頭多少個位元組。 向前跳過整個回溯程式碼,直到處理所有已執行的指令為止。 執行在該位置開始的回溯序列。
如果從序言內開始回溯,請從回溯程式碼的索引 0 開始。 計算序列中序言程式碼的長度,然後計算 PC 距離序言結尾多少個位元組。 向前跳過整個回溯程式碼,直到處理所有未執行的指令為止。 執行在該位置開始的回溯序列。
序言的回溯程式碼必須一律為陣列中的第一個程式碼。 它們也是用來在一般情況下從主體內回溯的程序代碼。 任何結尾專屬程式碼序列應該緊接在序言程式碼序列之後。
函式片段
對於程式碼最佳化,將函式分割成不連續的部分可能更有用。 完成此作業時,每個函式片段都需要自己的個別 .pdata
記錄,而且可能需要 .xdata
記錄。
假設函式序言在函式的開頭且無法分割,此時函式片段會有四種情況:
只有序言;所有結尾都在其他片段中。
序言和一或多個結尾;其他片段中的結尾更多。
無序言或結尾;序言及一個或多個結尾在其他片段中。
僅結尾;序言和其他片段中可能更多的結尾。
在第一種情況中,只有序言是必須描述的。 這可以透過一般描述序言,並指定 Ret
3 的值來表示沒有結尾,以精簡.pdata
形式完成。 在完整 .xdata
格式中,您可以如往常在索引 0 提供序言回溯程式代碼,並指定結尾計數 0 來完成。
第二種情況就像正常的函式。 如果片段中只有一個結尾,而且位於片段結尾,則可以使用精簡 .pdata
的記錄。 否則,必須使用完整 .xdata
記錄。 請記住,針對結尾開頭指定的位移相對於片段的開頭,而不是函式的原始開頭位置。
第三個和第四個案例分別是第一個和第二個案例的變異,但不包含序言。 在這些情況下,假設在結尾開始之前有程序代碼,而且它被視為函式主體的一部分,通常透過復原序言的效果來解除復原。 因此,上述情況必須以虛擬序言編碼,其會描述如何從主體內部進行回溯,但當判定是否在片段開頭執行部分回溯時,它會被視為 0 長度。 或者,透過使用與結尾相同的回溯程式碼,也可能描述此虛擬序言,因為這些程式碼假定執行相等的作業。
第三個和第四個案例中,將壓縮.pdata
記錄的欄位設定為 2,或將標頭中的 .xdata
F 旗標設定Flag
為 1 來指定虛擬序言。 任何一種情況,都會忽略對部分序言回溯的檢查,且所有非結尾回溯都會被視為完整的。
大型函式
片段可用來描述大於標頭中位欄位字段所加之 512 KB 限制的 .xdata
函式。 若要描述較大的函式,只要將其分成小於 512 KB 的片段即可。 應該調整每個片段,使其不會將結尾分割成多個片段。
只有函式的第一個片段包含序言。 所有其他片段都標示為沒有序言。 視結尾的數目而定,每一個片段可能包含零個或多個結尾。 請記住,片段中的每一個結尾範圍都會指定其相對於片段開頭,而不是函式開頭的開始位移。
如果片段沒有序言,也沒有結尾,它仍然需要自己的 .pdata
記錄,並可能 .xdata
-- 記錄來描述如何從函式主體內回溯。
壓縮包裝
函式片段更複雜的特殊案例稱為 壓縮包裝。 這是延遲快取器從函式開頭儲存到函式稍後的技術。 它會針對不需要儲存註冊的簡單案例進行優化。 此案例有兩個部分:有一個外部區域會配置堆棧空間,但會儲存最少的緩存器集,以及儲存和還原其他緩存器的內部區域。
ShrinkWrappedFunction
push {r4, lr} ; A: save minimal non-volatiles
sub sp, sp, #0x100 ; A: allocate all stack space up front
... ; A:
add r0, sp, #0xE4 ; A: prepare to do the inner save
stm r0, {r5-r11} ; A: save remaining non-volatiles
... ; B:
add r0, sp, #0xE4 ; B: prepare to do the inner restore
ldm r0, {r5-r11} ; B: restore remaining non-volatiles
... ; C:
pop {r4, pc} ; C:
壓縮包裝函式通常預期會在一般序言中預先配置額外緩存器的空間,然後使用 或 stm
而非push
儲存緩存器str
。 此動作會保留函式原始序言中的所有堆疊指標操作。
範例壓縮包裝函式必須分成三個區域,這些區域在批註中標示為 A
、 B
和 C
。 第一個 A
區域涵蓋函式的開頭,到其他非揮發性儲存的結尾。 .pdata
必須建構 或 .xdata
記錄,才能將這個片段描述為具有序言,而且沒有結尾。
中間 B
區域會取得自己的 .pdata
或 .xdata
記錄,描述沒有序言和結尾的片段。 不過,此區域的回溯程式碼必須仍存在,因為其會被視為函式主體。 這些程式代碼必須描述複合序言,代表儲存在區域 A
序言中的原始緩存器,以及輸入區域 B
之前儲存的額外緩存器,就好像它們是由一連串作業所產生一樣。
區域 B
快取器儲存無法視為「內部序言」,因為針對區域 B
描述的複合序言必須同時描述區域 A
序言和儲存的其他緩存器。 如果片段 B
有序言,回溯程序代碼也會暗示該序言的大小,而且無法以一對一的方式對應複合序言與只儲存其他緩存器的 opcode。
額外的緩存器儲存必須視為區域的 A
一部分,因為在完成之前,複合序言不會準確地描述堆棧的狀態。
最後 C
一個區域會取得自己的 .pdata
或 .xdata
記錄,描述沒有序言但有結尾的片段。
如果在進入區域 B
之前完成堆疊操作,替代方法也可以運作為一個指令:
ShrinkWrappedFunction
push {r4, lr} ; A: save minimal non-volatile registers
sub sp, sp, #0xE0 ; A: allocate minimal stack space up front
... ; A:
push {r4-r9} ; A: save remaining non-volatiles
... ; B:
pop {r4-r9} ; B: restore remaining non-volatiles
... ; C:
pop {r4, pc} ; C: restore non-volatile registers
關鍵見解是在每個指令界限上,堆疊與區域的回溯程序代碼完全一致。 如果回溯會在此範例中的內部推送之前發生,則會將其視為區域 A
的一部分。 只有地區 A
序言被解開。 如果回溯會在內部推送之後發生,則會將其視為區域 B
的一部分,其沒有序言。 不過,它具有回溯程序代碼,可描述來自區域 A
的內部推送和原始序言。 內部快顯的類似邏輯保留。
編碼最佳化
回溯程式代碼的豐富性,以及能夠利用精簡和擴充的數據形式,提供許多機會來優化編碼,以進一步減少空間。 透過積極使用這些技術,可以使用回溯程式代碼來描述函式和片段的凈額外負荷可以最小化。
最重要的優化概念:不要將序言和結尾界限混淆,以便從編譯程序的觀點使用邏輯序言和結尾界限進行回溯。 回溯界限可以壓縮,變得更緊密,以提高效率。 例如,序言可能包含在堆疊設定之後執行驗證檢查的程序代碼。 但是,一旦所有堆疊操作都完成,就不需要編碼進一步的作業,以及從回溯序言中移除的以外的任何作業。
這一相同規則同樣適用於函式長度。 如果有數據(例如常值集區)在函式中結尾之後,則不應該包含在函式長度中。 藉由將函式縮小為函式的一部分程式代碼,結尾的機率就大得多,而且可以使用精簡 .pdata
的記錄。
在序言中,一旦堆棧指標儲存至另一個緩存器,通常不需要記錄任何進一步的 Opcode。 若要回溯函式,完成的第一件事是從儲存的緩存器復原SP。 進一步的作業對回溯沒有任何影響。
單一指令結尾完全不需要編碼為範圍或回溯程序代碼。 如果執行指令之前進行回溯,則可以放心地假設它是來自函式主體內的 。 只要執行序言回溯程序代碼就已足夠。 在執行單一指令之後進行回溯時,則根據定義,它會在另一個區域中進行。
多指令結尾不必編碼結尾的第一個指令,原因與上一點相同:如果在該指令執行之前進行回溯,完整序言回溯就已足夠。 如果回溯會在該指令之後進行,則只需要考慮後續的作業。
回溯程式代碼重複使用應該是積極的。 每個結尾範圍的索引會指定回溯程式代碼陣列中任意起點的點。 它不必指向上一個序列的開頭;它可以指向中間。 最好的方法是產生回溯程式代碼序列。 然後,掃描序列已編碼集區中的確切位元組比對。 使用任何完美的比對作為重複使用的起點。
忽略單一指令結尾之後,如果沒有剩餘的結尾,請考慮使用一個精簡 .pdata
的形式;如果沒有結尾,它就變得更有可能。
範例
在下列範例中,映像檔基礎位於 0x00400000。
範例 1:分葉函式,無區域變數
Prologue:
004535F8: B430 push {r4-r5}
Epilogue:
00453656: BC30 pop {r4-r5}
00453658: 4770 bx lr
.pdata
(固定,2 個字):
字組 0
Function Start RVA
= 0x000535F8 (= 0x004535F8-0x00400000)
字組 1
Flag
= 1,表示正式序言和結尾格式Function Length
= 0x31 (= 0x62/2)Ret
= 1,表示 16 位分支傳回H
= 0,表示參數未住家R
= 0 和Reg
= 1,表示 r4-r5 的推送/快顯L
= 0,表示沒有 LR 儲存/還原C
= 0,表示沒有框架鏈結Stack Adjust
= 0,表示沒有堆疊調整
範例 2:具有區域配置的巢狀函式
Prologue:
004533AC: B5F0 push {r4-r7, lr}
004533AE: B083 sub sp, sp, #0xC
Epilogue:
00453412: B003 add sp, sp, #0xC
00453414: BDF0 pop {r4-r7, pc}
.pdata
(固定,2 個字):
字組 0
Function Start RVA
= 0x000533AC (= 0x004533AC -0x00400000)
字組 1
Flag
= 1,表示正式序言和結尾格式Function Length
= 0x35 (= 0x6A/2)Ret
= 0,表示快顯 {pc} 傳回H
= 0,表示參數未住家R
= 0 和Reg
= 3,表示 r4-r7 的推送/popL
= 1,表示已儲存/還原 LRC
= 0,表示沒有框架鏈結Stack Adjust
= 3 (= 0x0C/4)
範例 3:巢狀 Variadic 函式
Prologue:
00453988: B40F push {r0-r3}
0045398A: B570 push {r4-r6, lr}
Epilogue:
004539D4: E8BD 4070 pop {r4-r6}
004539D8: F85D FB14 ldr pc, [sp], #0x14
.pdata
(固定,2 個字):
字組 0
Function Start RVA
= 0x00053988 (= 0x00453988-0x00400000)
字組 1
Flag
= 1,表示正式序言和結尾格式Function Length
= 0x2A (= 0x54/2)Ret
= 0,表示快顯 {pc}-style 傳回 (在此案例中為ldr pc,[sp],#0x14
傳回)H
= 1,表示參數已主場R
= 0 和Reg
= 2,表示 r4-r6 的推送/popL
= 1,表示已儲存/還原 LRC
= 0,表示沒有框架鏈結Stack Adjust
= 0,表示沒有堆疊調整
範例 4:具有多個結尾的函式
Prologue:
004592F4: E92D 47F0 stmdb sp!, {r4-r10, lr}
004592F8: B086 sub sp, sp, #0x18
Epilogues:
00459316: B006 add sp, sp, #0x18
00459318: E8BD 87F0 ldm sp!, {r4-r10, pc}
...
0045943E: B006 add sp, sp, #0x18
00459440: E8BD 87F0 ldm sp!, {r4-r10, pc}
...
004595D4: B006 add sp, sp, #0x18
004595D6: E8BD 87F0 ldm sp!, {r4-r10, pc}
...
00459606: B006 add sp, sp, #0x18
00459608: E8BD 87F0 ldm sp!, {r4-r10, pc}
...
00459636: F028 FF0F bl KeBugCheckEx ; end of function
.pdata
(固定,2 個字):
字組 0
Function Start RVA
= 0x000592F4 (= 0x004592F4-0x00400000)
字組 1
Flag
= 0,表示.xdata
記錄存在(多個結尾需要).xdata
address - 0x00400000
.xdata
(變數,6 個字):
字組 0
Function Length
= 0x0001A3 (= 0x000346/2)Vers
= 0,表示 的第一個版本.xdata
X
= 0,表示沒有例外狀況數據E
= 0,表示結尾範圍的清單F
= 0,表示完整的函式描述,包括序言Epilogue Count
= 0x04,表示 4 個總結尾範圍Code Words
= 0x01,表示一個 32 位的回溯程式代碼字
字組 1-4,描述 4 個位置的 4 個結尾範圍。 每一個範圍都具有一組常用的回溯程式碼,與序言共用,位於位移 0x00 處,且無條件,指定條件 0x0E (一律)。
回溯程式碼,在字組 5 處開始:(在序言/結尾之間共用)
回溯程式代碼 0 = 0x06:sp += (6 << 2)
回溯程式碼 1 = 0xDE:pop {r4-r10, lr}
回溯程式碼 2 = 0xFF:end
範例 5:具有動態堆疊和內部結尾的函式
Prologue:
00485A20: B40F push {r0-r3}
00485A22: E92D 41F0 stmdb sp!, {r4-r8, lr}
00485A26: 466E mov r6, sp
00485A28: 0934 lsrs r4, r6, #4
00485A2A: 0124 lsls r4, r4, #4
00485A2C: 46A5 mov sp, r4
00485A2E: F2AD 2D90 subw sp, sp, #0x290
Epilogue:
00485BAC: 46B5 mov sp, r6
00485BAE: E8BD 41F0 ldm sp!, {r4-r8, lr}
00485BB2: B004 add sp, sp, #0x10
00485BB4: 4770 bx lr
...
00485E2A: F7FF BE7D b #0x485B28 ; end of function
.pdata
(固定,2 個字):
字組 0
Function Start RVA
= 0x00085A20 (= 0x00485A20-0x00400000)
字組 1
Flag
= 0,表示.xdata
記錄存在(需要多個結尾).xdata
address - 0x00400000
.xdata
(變數,3 個字):
字組 0
Function Length
= 0x0001A3 (= 0x000346/2)Vers
= 0,表示 的第一個版本.xdata
X
= 0,表示沒有例外狀況數據E
= 0,表示結尾範圍的清單F
= 0,表示完整的函式描述,包括序言Epilogue Count
= 0x001,表示總結尾範圍 1Code Words
= 0x01,表示一個 32 位的回溯程式代碼字
字組 1:位移 0xC6 (= 0x18C/2) 處的結尾範圍,從 0x00 處的回溯程式碼索引開始,且具有條件 0x0E (一律)
回溯程式碼,在字組 2 處開始:(在序言/結尾之間共用)
回溯程式碼 0 = 0xC6:sp = r6
回溯程式碼 1 = 0xDC:pop {r4-r8, lr}
回溯程序代碼 2 = 0x04:sp += (4 << 2)
回溯程式碼 3 = 0xFD:end,對於結尾,計數為 16 位元指令
範例 6:具有例外狀況處理常式的函式
Prologue:
00488C1C: 0059 A7ED dc.w 0x0059A7ED
00488C20: 005A 8ED0 dc.w 0x005A8ED0
FunctionStart:
00488C24: B590 push {r4, r7, lr}
00488C26: B085 sub sp, sp, #0x14
00488C28: 466F mov r7, sp
Epilogue:
00488C6C: 46BD mov sp, r7
00488C6E: B005 add sp, sp, #0x14
00488C70: BD90 pop {r4, r7, pc}
.pdata
(固定,2 個字):
字組 0
Function Start RVA
= 0x00088C24 (= 0x00488C24-0x00400000)
字組 1
Flag
= 0,表示.xdata
記錄存在(需要多個結尾).xdata
address - 0x00400000
.xdata
(變數,5 個字):
字組 0
Function Length
=0x000027 (= 0x00004E/2)Vers
= 0,表示 的第一個版本.xdata
X
= 1,表示例外狀況數據存在E
= 1,表示單一結尾F
= 0,表示完整的函式描述,包括序言Epilogue Count
= 0x00,表示結尾回溯程式代碼從位移開始0x00Code Words
= 0x02,表示回溯程式代碼的兩個 32 位字
回溯程式碼,在字組 1 處開始:
回溯程式碼 0 = 0xC7:sp = r7
回溯程序代碼 1 = 0x05:sp += (5 << 2)
回溯程式碼 2 = 0xED/0x90:pop {r4, r7, lr}
回溯程式碼 4 = 0xFF:end
Word 3 指定例外狀況處理程式 = 0x0019A7ED (= 0x0059A7ED - 0x00400000)
字組 4 及以上為內嵌例外狀況資料
範例 7:Funclet
Function:
00488C72: B500 push {lr}
00488C74: B081 sub sp, sp, #4
00488C76: 3F20 subs r7, #0x20
00488C78: F117 0308 adds r3, r7, #8
00488C7C: 1D3A adds r2, r7, #4
00488C7E: 1C39 adds r1, r7, #0
00488C80: F7FF FFAC bl target
00488C84: B001 add sp, sp, #4
00488C86: BD00 pop {pc}
.pdata
(固定,2 個字):
字組 0
Function Start RVA
= 0x00088C72 (= 0x00488C72-0x00400000)
字組 1
Flag
= 1,表示正式序言和結尾格式Function Length
= 0x0B (= 0x16/2)Ret
= 0,表示快顯 {pc} 傳回H
= 0,表示參數未住家R
= 0 和Reg
= 7,表示未儲存/還原任何緩存器L
= 1,表示已儲存/還原 LRC
= 0,表示沒有框架鏈結Stack Adjust
= 1,表示 1 × 4 個字節堆疊調整