共用方式為


ARM64 例外狀況處理

ARM64 上的 Windows 會針對異步硬體產生的例外狀況和同步軟體產生的例外狀況,使用相同的結構化例外狀況處理機制。 語言專屬例外狀況處理常式使用語言協助程式函式,以 Windows 結構化例外狀況處理為基礎,進行建置。 本文件說明 ARM64 上的 Windows 例外狀況處理。 它說明 Microsoft ARM 組合器和 MSVC 編譯程式所產生的程式代碼所使用的語言協助程式。

目標和動機

例外狀況回溯數據慣例和此描述的目的是:

  • 提供足夠的描述,以便在所有情況下允許回溯,而不需要程式代碼探查。

    • 分析程式代碼需要將程式代碼分頁。 在某些情況下,它可防止回溯,因為它很有用(追蹤、取樣、偵錯)。

    • 分析程式代碼很複雜;編譯程式必須小心,只產生回溯程式可以譯碼的指示。

    • 如果使用回溯程式代碼無法完整描述回溯,則在某些情況下,它必須回復至指令譯碼。 指令譯碼會增加整體複雜度,最好避免。

  • 支援在中序和中表尾回溯。

    • 回溯在 Windows 中用於處理例外狀況以上。 程序代碼即使在初構或表結程式代碼序列中間時,也能精確地回溯。
  • 佔用最少的空間。

    • 回溯程式代碼不得匯總以大幅增加二進位大小。

    • 由於回溯程式代碼可能會鎖定在記憶體中,因此小使用量可確保每個載入的二進位檔的負擔降到最低。

假設

這些假設是在例外狀況處理描述中做出:

  • 初構和表結往往相互鏡像。 藉由利用這個常見的特性,描述回溯所需的元數據大小可以大幅減少。 在函式主體內,不論初構的作業是復原的,還是會以正向方式完成表結作業。 這兩個作業會產生相同的結果。

  • 函式通常會整體相對較小。 空間的數個優化依賴這個事實來達成最有效率的數據封裝。

  • epilog 中沒有條件式程序代碼。

  • 專用框架指標緩存器:如果 sp 儲存在初構中的另一個緩存器中,x29該緩存器在整個函式中仍維持不變。 這表示原始 sp 專案可以隨時復原。

  • sp除非 儲存在另一個緩存器中,否則堆棧指標的所有操作都會嚴格發生在 prolog 和 epilog 內。

  • 堆疊框架配置的組織方式如下一節所述。

ARM64 堆疊框架配置

Diagram that shows the stack frame layout for functions.

針對框架鏈結函式, fplr 配對可以儲存在局部變數區域中的任何位置,視優化考慮而定。 目標是根據框架指標 () 或堆疊指標 (x29sp) ,將單一指令所能達到的局部變數數目最大化。 不過,針對 alloca 函式,它必須鏈結,而且 x29 必須指向堆棧底部。 為了允許更好的快取器配對尋址模式涵蓋範圍,非揮發緩存器儲存區域會放在本機區域堆疊的頂端。 以下是說明數個最有效率的初構序列的範例。 為了清楚明瞭和更好的快取位置,將所有標準初構中儲存被呼叫者儲存緩存器的順序是「成長」的順序。 #framesz 下方代表整個堆疊的大小(不包括 alloca 區域)。 #localsz#outsz 分別表示區域大小(包括配對的 <x29, lr> 儲存區域)和傳出參數大小。

  1. 鏈結,#localsz <= 512

        stp    x19,x20,[sp,#-96]!        // pre-indexed, save in 1st FP/INT pair
        stp    d8,d9,[sp,#16]            // save in FP regs (optional)
        stp    x0,x1,[sp,#32]            // home params (optional)
        stp    x2,x3,[sp,#48]
        stp    x4,x5,[sp,#64]
        stp    x6,x7,[sp,#82]
        stp    x29,lr,[sp,#-localsz]!   // save <x29,lr> at bottom of local area
        mov    x29,sp                   // x29 points to bottom of local
        sub    sp,sp,#outsz             // (optional for #outsz != 0)
    
  2. 鏈結,#localsz > 512

        stp    x19,x20,[sp,#-96]!        // pre-indexed, save in 1st FP/INT pair
        stp    d8,d9,[sp,#16]            // save in FP regs (optional)
        stp    x0,x1,[sp,#32]            // home params (optional)
        stp    x2,x3,[sp,#48]
        stp    x4,x5,[sp,#64]
        stp    x6,x7,[sp,#82]
        sub    sp,sp,#(localsz+outsz)   // allocate remaining frame
        stp    x29,lr,[sp,#outsz]       // save <x29,lr> at bottom of local area
        add    x29,sp,#outsz            // setup x29 points to bottom of local area
    
  3. 未連結,分葉函式 (lr 未儲存)

        stp    x19,x20,[sp,#-80]!       // pre-indexed, save in 1st FP/INT reg-pair
        stp    x21,x22,[sp,#16]
        str    x23,[sp,#32]
        stp    d8,d9,[sp,#40]           // save FP regs (optional)
        stp    d10,d11,[sp,#56]
        sub    sp,sp,#(framesz-80)      // allocate the remaining local area
    

    所有局部變數都會根據 來 sp存取。 <x29,lr> 指向上一個框架。 針對畫面大小 <= 512,如果儲存的 regs 儲存區域移至堆棧底部, sub sp, ... 則可以將 優化。 缺點是,它與上述其他版面配置不一致。 而且,已儲存的 regs 會參與配對 regs 和預先和後置索引位移尋址模式的範圍。

  4. 未連結的非分葉函式(儲存 lr 在 Int 儲存區域中)

        stp    x19,x20,[sp,#-80]!       // pre-indexed, save in 1st FP/INT reg-pair
        stp    x21,x22,[sp,#16]         // ...
        stp    x23,lr,[sp,#32]          // save last Int reg and lr
        stp    d8,d9,[sp,#48]           // save FP reg-pair (optional)
        stp    d10,d11,[sp,#64]         // ...
        sub    sp,sp,#(framesz-80)      // allocate the remaining local area
    

    或者,使用偶數儲存的 Int 快取器,

        stp    x19,x20,[sp,#-80]!       // pre-indexed, save in 1st FP/INT reg-pair
        stp    x21,x22,[sp,#16]         // ...
        str    lr,[sp,#32]              // save lr
        stp    d8,d9,[sp,#40]           // save FP reg-pair (optional)
        stp    d10,d11,[sp,#56]         // ...
        sub    sp,sp,#(framesz-80)      // allocate the remaining local area
    

    x19 儲存:

        sub    sp,sp,#16                // reg save area allocation*
        stp    x19,lr,[sp]              // save x19, lr
        sub    sp,sp,#(framesz-16)      // allocate the remaining local area
    

    * reg 儲存區域配置不會折疊到 , stp 因為無法以回溯程式代碼來表示預先編製索引的 reg-lr stp

    所有局部變數都會根據 來 sp存取。 <x29> 指向上一個框架。

  5. Chained, #framesz <= 512, #outsz = 0

        stp    x29,lr,[sp,#-framesz]!       // pre-indexed, save <x29,lr>
        mov    x29,sp                       // x29 points to bottom of stack
        stp    x19,x20,[sp,#(framesz-32)]   // save INT pair
        stp    d8,d9,[sp,#(framesz-16)]     // save FP pair
    

    相較於上述第一個初構範例,此範例有一個優點:所有緩存器儲存指令都準備好在一個堆棧配置指令之後執行。 這表示沒有防止指令層級平行處理原則的 sp 反相依性。

  6. 鏈結,框架大小 > 512 (選擇性的函式沒有 alloca

        stp    x29,lr,[sp,#-80]!            // pre-indexed, save <x29,lr>
        stp    x19,x20,[sp,#16]             // save in INT regs
        stp    x21,x22,[sp,#32]             // ...
        stp    d8,d9,[sp,#48]               // save in FP regs
        stp    d10,d11,[sp,#64]
        mov    x29,sp                       // x29 points to top of local area
        sub    sp,sp,#(framesz-80)          // allocate the remaining local area
    

    為了達到優化目的, x29 可以在本機的任何位置放置,為「reg-pair」和預先/後編製索引的位移尋址模式提供更佳的涵蓋範圍。 您可以根據 sp存取框架指標下方的局部變數。

  7. 鏈結,框架大小 > 4K,含或不含alloca(),

        stp    x29,lr,[sp,#-80]!            // pre-indexed, save <x29,lr>
        stp    x19,x20,[sp,#16]             // save in INT regs
        stp    x21,x22,[sp,#32]             // ...
        stp    d8,d9,[sp,#48]               // save in FP regs
        stp    d10,d11,[sp,#64]
        mov    x29,sp                       // x29 points to top of local area
        mov    x15,#(framesz/16)
        bl     __chkstk
        sub    sp,sp,x15,lsl#4              // allocate remaining frame
                                            // end of prolog
        ...
        sub    sp,sp,#alloca                // more alloca() in body
        ...
                                            // beginning of epilog
        mov    sp,x29                       // sp points to top of local area
        ldp    d10,d11,[sp,#64]
        ...
        ldp    x29,lr,[sp],#80              // post-indexed, reload <x29,lr>
    

ARM64 例外狀況處理資訊

.pdata 記錄

這些 .pdata 記錄是固定長度專案的已排序數位,可描述PE二進位中的每個堆疊操作函式。 「堆疊操作」一詞很重要:不需要任何本機記憶體的分葉函式,且不需要儲存/還原非揮發性緩存器,不需要 .pdata 記錄。 應明確省略這些記錄以節省空間。 從其中一個函式回溯,可以直接從 lr 取得傳回位址,以移至呼叫端。

ARM64 的每個 .pdata 記錄長度為8個字節。 每個記錄的一般格式會將函式的 32 位 RVA 放在第一個單字中,後面接著第二個字,其中包含可變長度 .xdata 區塊的指標,或描述標準函式回溯序列的封裝字組。

.pdata record layout.

欄位如下所示:

  • 函式啟動 RVA 是函式開頭的 32 位 RVA。

  • Flag 是 2 位字段,指出如何解譯第二 .pdata 個字的其餘 30 位。 如果 Flag 為 0,則其餘位會形成例外狀況資訊 RVA(隱含兩個最低位 0)。 如果 Flag 不是零,則其餘位會形成 封裝回溯數據 結構。

  • 例外狀況資訊 RVA 是儲存在 .xdata 區段中之可變長度例外狀況資訊結構的位址。 此資料必須是對齊的 4 位元組。

  • 封裝回溯數據 是從函式回溯所需的作業壓縮描述,假設是標準形式。 在此情況下,不需要 .xdata 記錄。

.xdata 記錄

當封裝的回溯格式不足以描述函式的回溯時,必須建立可變長度 .xdata 的記錄。 此記錄的位址會儲存在記錄的第二個字中 .pdata 。 的格式 .xdata 是一組包裝的可變長度字組:

.xdata record layout.

此資料分成四個區段:

  1. 1 字或 2 字標頭,描述結構的整體大小並提供索引鍵函式數據。 只有當 Epilog Count 和 Code Words 欄位都設定為 0 時,才會顯示第二個單字 標頭具有下欄位欄位欄位:

    a. 函數長度 是18位欄位元。 它會以位元組為單位表示函式的總長度,除以4。 如果函式大於 1M,則必須使用多個 .pdata.xdata 記錄來描述函式。 如需詳細資訊,請參閱 大型函式 一節。

    b. Vers 是 2 位欄位元。 其描述其餘 .xdata的版本。 目前只會定義第0版,因此不允許1-3的值。

    c. X 是1位欄位欄位。 它表示例外狀況數據是否存在 (1) 或不存在 (0)。

    d. E 是1位欄位欄位。 它表示描述單一表結的資訊會封裝到標頭 (1), 而不是稍後需要更多範圍文字 (0)。

    e. Epilog Count 是一個 5 位字段,其意義有兩種,視 E 位的狀態而定:

    1. 如果 E 為 0,它會指定第 2 節中所述的 epilog 範圍總數計數。 如果函式中有超過 31 個範圍存在,則必須將 [ 程式代碼字 ] 欄位設定為 0,表示需要擴充字。

    2. 如果 E 為 1,則此字段會指定第一個回溯程式代碼的索引,其中描述一個且只描述一個結尾。

    f. Code Words 是 5 位位元段,指定包含第 3 節中所有回溯程式代碼所需的 32 位字數。 如果需要超過 31 個單字(也就是 124 個回溯程式代碼),則此欄位必須為 0,表示需要延伸字。

    .g 擴充的 Epilog CountExtended Code Words 分別是 16 位和 8 位字段。 它們提供更多空間來編碼異常大量的表結,或異常大量的回溯程序代碼字。 只有當第一個標題字中的 Epilog Count 和 Code Words 欄位都為 0 時,才會顯示包含這些字段的擴充字

  2. 如果 epilog 計數不是零,則包含一個到單字的 epilog 範圍相關信息清單,會在標頭和選擇性的擴充標頭之後。 它們會以增加起始位移的順序儲存。 每個範圍都包含下欄位:

    a. Epilog Start Offset 是一個 18 位位元段,其位移 以位元組為單位,除以 4,相對於函式的開頭。

    b. Res 是保留供未來擴充的 4 位欄位元。 其值必須為 0。

    c. Epilog Start Index 是一個 10 位字段(比 擴充字組多 2 個位)。 它會指出描述此表結之第一個回溯程式代碼的位元組索引。

  3. 表結範圍清單之後,會有包含回溯程式代碼的位元組陣列,稍後一節會詳細說明。 此陣列在最近完整字組界面的結尾處填補。 回溯程式代碼會寫入此陣列。 它們會從最接近函式主體的函式開始,並移至函式的邊緣。 每個回溯程式代碼的位元組會以大端順序儲存,因此會先擷取最重要的位元組,以識別作業和其餘程式代碼的長度。

  4. 最後,在回溯程式代碼位元組之後,如果 標頭中的 X 位設定為 1,則會傳回例外狀況處理程式資訊。 它是由單 一例外狀況處理程式 RVA 所組成,可提供例外狀況處理程式本身的位址。 緊接著例外狀況處理程式所需的可變長度數據量。

記錄 .xdata 的設計目的是要擷取前8個字節,並使用它們來計算記錄的完整大小,減去後續可變大小的例外狀況數據長度。 下列代碼段會計算記錄大小:

ULONG ComputeXdataSize(PULONG Xdata)
{
    ULONG Size;
    ULONG EpilogScopes;
    ULONG UnwindWords;

    if ((Xdata[0] >> 22) != 0) {
        Size = 4;
        EpilogScopes = (Xdata[0] >> 22) & 0x1f;
        UnwindWords = (Xdata[0] >> 27) & 0x1f;
    } else {
        Size = 8;
        EpilogScopes = Xdata[1] & 0xffff;
        UnwindWords = (Xdata[1] >> 16) & 0xff;
    }

    if (!(Xdata[0] & (1 << 21))) {
        Size += 4 * EpilogScopes;
    }

    Size += 4 * UnwindWords;

    if (Xdata[0] & (1 << 20)) {
        Size += 4;  // Exception handler RVA
    }

    return Size;
}

雖然初構和每個表結都有自己的索引至回溯程序代碼,但數據表會在兩者之間共用。 他們完全有可能(而且並不完全罕見)可以共用相同的程序代碼。 (如需範例,請參閱 中的 範例 2範例 區段。)編譯程式寫入器應特別針對此案例進行優化。 這是因為可以指定的最大索引是 255,這會限制特定函式的回溯程式代碼總數。

回溯程序代碼

回溯程式代碼的陣列是序列集區,其描述如何復原初構的效果。 它們會以相同的順序儲存作業需要復原。 回溯程式代碼可視為小型指令集,編碼為位元組位元串。 執行完成時,呼叫函式的傳回位址位於緩存器中 lr 。 而且,所有非揮發性緩存器都會在呼叫函式時還原到其值。

如果保證例外狀況只會發生在函式主體內,而且絕不在初構或任何表結內發生,則只需要單一序列。 不過,Windows 回溯模型要求程式代碼可以從部分執行的初構或表文中回溯。 為了符合這項需求,回溯程式代碼已經過精心設計,因此它們明確地將 1:1 對應至 prolog 和 epilog 中的每個相關 opcode。 此設計有數個含意:

  • 藉由計算回溯程式代碼的數目,就可以計算初構和表結的長度。

  • 藉由計算超過結尾範圍開頭的指示數目,就可以略過對等的回溯程式代碼數目。 我們可以執行序列的其餘部分,以完成表觀所完成的部分執行回溯。

  • 藉由計算初構結尾之前的指示數目,就可以略過對等的回溯程式代碼數目。 我們可以執行序列的其餘部分,只復原已完成執行的初構部分。

回溯程式代碼會根據下表進行編碼。 所有回溯程式代碼都是單一/雙位元組,但配置大型堆疊的回溯程序代碼除外。alloc_l 總共有 22 個回溯程式代碼。 每個回溯程式代碼會對應至 prolog/epilog 中的一個指令,以允許回溯部分執行的初構和表文。

回溯程序代碼 位和解譯
alloc_s 000xxxxx:配置大小 < 為 512 的小型堆疊(2^5 * 16)。
save_r19r20_x 001zzzzz:儲存 <x19,x20> 組位於 [sp-#Z*8]!,預先編製索引的位移 >= -248
save_fplr 01zzzzzz:位於 的儲存<x29,lr>組位移 <= 504。[sp+#Z*8]
save_fplr_x 10zzzzzz:儲存 <x29,lr> 組位於 [sp-(#Z+1)*8]!,預先編製索引的位移 >= -512
alloc_m 11000xxx'xxxxxxxx:配置大小 < 為 32K 的大型堆棧(2^11 * 16)。
save_regp 110010xx'xxzzzzzz:位於 [sp+#Z*8]的儲存x(19+#X)組,offset <= 504
save_regp_x 110011xx'xxzzzzzz:儲存組 x(19+#X) 位於 [sp-(#Z+1)*8]!,預先編製索引的位移 >= -512
save_reg 110100xx'xxzzzzzz: save reg x(19+#X) at [sp+#Z*8], offset <= 504
save_reg_x 1101010x'xxxzzzzz:儲存 reg x(19+#X) at [sp-(#Z+1)*8]!,預先編製索引的位移 >= -256
save_lrpair 1101011x'xxzzzzzz:位於 [sp+#Z*8]的儲存組<x(19+2*#X),lr>,offset <= 504
save_fregp 1101100x'xxzzzzzzzz:位於 [sp+#Z*8]的儲存組d(8+#X),offset <= 504
save_fregp_x 1101101x'xxzzzzzz:位於 [sp-(#Z+1)*8]!的儲存組d(8+#X),預先編製索引的位移 >= -512
save_freg 1101110x'xxzzzzzz: save reg d(8+#X) at [sp+#Z*8], offset <= 504
save_freg_x 11011110'xxxzzzzz:將 reg d(8+#X) 儲存在 [sp-(#Z+1)*8]!,預先編製索引的位移 >= -256
alloc_l 11100000'xxxxxxxx'xxxxxxxx'xxxxxxxx:配置大小 < 為 256M 的大型堆棧(2^24 * 16)
set_fp 11100001:使用 設定x29mov x29,sp
add_fp 11100010『xxxxxxxx:使用 設定x29add x29,sp,#x*8
nop 11100011:不需要回溯作業。
end 11100100:回溯程式代碼的結尾。 ret意指在表結中。
end_c 11100101:目前鏈結範圍內回溯程式代碼的結尾。
save_next 11100110:儲存下一個非揮發性 Int 或 FP 快取器組。
11100111:保留
11101xxx:僅針對 asm 例程產生的自定義堆棧案例保留
11101000:自定義堆疊 MSFT_OP_TRAP_FRAME
11101001:自定義堆疊 MSFT_OP_MACHINE_FRAME
11101010:自定義堆疊 MSFT_OP_CONTEXT
11101011:自定義堆疊 MSFT_OP_EC_CONTEXT
11101100:自定義堆疊 MSFT_OP_CLEAR_UNWOUND_TO_CALL
11101101:保留
11101110:保留
11101111:保留
11110xxx:保留
11111000'yyyyyyy : reserved
11111001'y: reserved
11111010'y:reserved
11111011'y:reserved
pac_sign_lr 11111100:使用 簽署傳 lr 回位址 pacibsp
11111101:保留
11111110:保留
11111111:保留

在涵蓋多個字節的大型值的指示中,會先儲存最重要的位。 此設計可讓您只查閱程序代碼的第一個字節,以位元組為單位尋找回溯程式代碼的總大小。 由於每個回溯程序代碼都完全對應至初構或表文中的指令,因此您可以計算初構或表觀的大小。 從序列開始到結尾,並使用查閱表格或類似的裝置來判斷對應 opcode 的長度。

在初構中不允許後續編製索引的位移尋址。 所有位移範圍 (#Z) 都符合尋址的 stp/str 編碼,但 除外,其中 save_r19r20_x248 個就足以用於所有儲存區域 (10 Int 快取器 + 8 個 FP 快取器 + 8 個輸入快取器)。

save_next必須遵循 Int 或 FP 揮發性快取器組的儲存:save_regp、、save_regp_xsave_fregpsave_fregp_xsave_r19r20_x或另一個 save_next。 它會以「成長」順序,將下一個緩存器組儲存在接下來的 16 位元組位置。 save_next是指第一個 FP 快取器組,當它遵循save-next代表最後一個 Int 快取器組的 之後。

由於一般傳回和跳躍指令的大小相同,因此不需要在尾端呼叫案例中分隔 end 回溯程序代碼。

end_c 設計用來處理非連續函式片段以進行優化。 end_c,表示目前範圍中的回溯程式代碼結尾,後面必須接著另一系列以實際 end結尾的回溯程序代碼。 和 end 之間的end_c回溯程式代碼代表父區域中的初構作業(「虛設」初構)。 下一節將說明更多詳細數據和範例。

封裝回溯數據

對於其初構和表結遵循以下標準格式的函式,可以使用包裝的回溯數據。 它完全 .xdata 不需要記錄,並大幅降低提供回溯數據的成本。 標準初構和表結的設計目的是要符合簡單函式的常見需求:不需要例外狀況處理程式,並以標準順序執行其設定和卸除作業。

包含已封裝回溯資料的記錄格式 .pdata 如下所示:

.pdata record with packed unwind data.

欄位如下所示:

  • 函式啟動 RVA 是函式開頭的 32 位 RVA。
  • 旗標 是如上所述的 2 位字段,具有下列意義:
    • 00 = 未使用包裝回溯數據;剩餘位指向 .xdata 記錄
    • 01 = 包裝回溯數據,用於範圍開頭和結尾的單一初構和表文
    • 10 = 封裝回溯數據,用於程式代碼,而不需要任何初構和表結。 適用於描述分隔函式區段
    • 11 = 保留。
  • 函式長度 是一個11位位位元段,提供以位元組為單位的整個函式長度,除以4。 如果函式大於 8k,則必須改用完整 .xdata 記錄。
  • 框架大小 是9位位元段,指出配置給此函式的堆疊位元組數目,除以16。 配置大於 (8k-16) 個字節堆疊的函式必須使用完整 .xdata 記錄。 其中包含局部變數區域、傳出參數區域、已呼叫端儲存的 Int 和 FP 區域,以及主參數區域。 它會排除動態配置區域。
  • CR 是 2 位旗標,指出函式是否包含額外的指示來設定框架鏈結和傳回連結:
    • 00 = 未鏈結函式, <x29,lr> 配對不會儲存在堆疊中
    • 01 = 未鏈結函式, <lr> 會儲存在堆疊中
    • 10 = 具有帶正負號傳回位址的 pacibsp 鏈結函式
    • 11 = 鏈結函式,儲存/載入組指令用於 prolog/epilog <x29,lr>
  • H 是 1 位旗標,指出函式是否在函式的開頭儲存整數參數緩存器 (x0-x7)。 (0 = 不住家登記,1 = 房屋登記)。
  • RegI 是 4 位字段,指出儲存在標準堆棧位置的非揮發性 INT 快取器數目(x19-x28)。
  • RegF 是一個 3 位字段,表示儲存在標準堆棧位置的非揮發性 FP 緩存器數目(d8-d15)。 (RegF=0: 未儲存 FP 快取器;RegF>0:儲存 RegF+1 FP 快取器。 封裝回溯數據無法用於只儲存一個 FP 快取器的功能。

屬於類別 1、2(不含傳出參數區域)、上一節中的 3 和 4 的正式初構,可以透過包裝的回溯格式來表示。 標準函式的表結會遵循類似的形式,但 H 沒有作用、 set_fp 省略指令,而且每個步驟中的步驟順序和指示都會在表結中反轉。 封裝 .xdata 的演算法會遵循下列步驟,如下表所述:

步驟 0:預先計算每個區域的大小。

步驟 1:簽署傳回位址。

步驟 2:儲存 Int 被呼叫者儲存的緩存器。

步驟 3:此步驟適用於早期章節中的類型 4。 lr 儲存在 Int 區域結尾。

步驟 4:儲存 FP 被呼叫者儲存的緩存器。

步驟 5:將輸入自變數儲存在主參數區域中。

步驟 6:配置剩餘堆疊,包括本機區域、 <x29,lr> 配對和傳出參數區域。 6a 對應至標準類型 1。 6b 和 6c 適用於標準類型 2。 6d 和 6e 適用於類型 3 和類型 4。

步# 旗標值 # of instructions OpCode 回溯程序代碼
0 #intsz = RegI * 8;
if (CR==01) #intsz += 8; // lr
#fpsz = RegF * 8;
if(RegF) #fpsz += 8;
#savsz=((#intsz+#fpsz+8*8*H)+0xf)&~0xf)
#locsz = #famsz - #savsz
1 CR == 10 1 pacibsp pac_sign_lr
2 0 <RegI<= 10 RegI / 2 +
RegI % 2
stp x19,x20,[sp,#savsz]!
stp x21,x22,[sp,#16]
...
save_regp_x
save_regp
...
3 CR == 01* 1 str lr,[sp,#(intsz-8)]* save_reg
4 0 <RegF<= 7 RegF + 1) / 2 +
RegF + 1) % 2)
stp d8,d9,[sp,#intsz]**
stp d10,d11,[sp,#(intsz+16)]
...
str d(8+RegF),[sp,#(intsz+fpsz-8)]
save_fregp
...
save_freg
5 H == 1 4 stp x0,x1,[sp,#(intsz+fpsz)]
stp x2,x3,[sp,#(intsz+fpsz+16)]
stp x4,x5,[sp,#(intsz+fpsz+32)]
stp x6,x7,[sp,#(intsz+fpsz+48)]
nop
nop
nop
nop
6a CR == 10 || CR == 11) &&
#locsz<= 512
2 stp x29,lr,[sp,#-locsz]!
mov x29,sp***
save_fplr_x
set_fp
6b CR == 10 || CR == 11) &&
512 <#locsz<= 4080
3 sub sp,sp,#locsz
stp x29,lr,[sp,0]
add x29,sp,0
alloc_m
save_fplr
set_fp
6c CR == 10 || CR == 11) &&
#locsz> 4080
4 sub sp,sp,4080
sub sp,sp,#(locsz-4080)
stp x29,lr,[sp,0]
add x29,sp,0
alloc_m
alloc_s/alloc_m
save_fplr
set_fp
6d CR == 00 || CR == 01) &&
#locsz<= 4080
1 sub sp,sp,#locsz alloc_s/alloc_m
6e CR == 00 || CR == 01) &&
#locsz> 4080
2 sub sp,sp,4080
sub sp,sp,#(locsz-4080)
alloc_m
alloc_s/alloc_m

* 如果 CR == 01 且 RegI 是奇數,則步驟 2 和步驟 1 中的最後 save_rep 一個會合併成一個 save_regp

** 如果 RegI == CR == 0,而 RegF != 0,浮點的第一個 stp 會執行預先產生。

附表中沒有對應至 mov x29,sp 的指令。 如果函式需要從 x29還原sp,就無法使用封裝回溯數據。

回溯部分初構和表結

在最常見的回溯情況下,例外狀況或呼叫會在函式主體中發生,遠離初構和所有表結。 在這些情況下,回溯很簡單:回溯器只會在回溯數位中執行程序代碼。 它會從索引 0 開始,並繼續進行,直到 end 偵測到 opcode 為止。

在執行初構或表結時發生例外狀況或中斷的情況,更難正確回溯。 在這些情況下,只會部分建構堆疊框架。 問題在於判斷已完成的確切作業,以正確復原它。

例如,採用此初構和表結序列:

0000:    stp    x29,lr,[sp,#-256]!          // save_fplr_x  256 (pre-indexed store)
0004:    stp    d8,d9,[sp,#224]             // save_fregp 0, 224
0008:    stp    x19,x20,[sp,#240]           // save_regp 0, 240
000c:    mov    x29,sp                      // set_fp
         ...
0100:    mov    sp,x29                      // set_fp
0104:    ldp    x19,x20,[sp,#240]           // save_regp 0, 240
0108:    ldp    d8,d9,[sp,224]              // save_fregp 0, 224
010c:    ldp    x29,lr,[sp],#256            // save_fplr_x  256 (post-indexed load)
0110:    ret    lr                          // end

每個 opcode 旁邊是描述這項作業的適當回溯程式代碼。 您可以看到 prolog 的一系列回溯程式代碼是如何為 epilog 回溯程式代碼的確切鏡像影像(不計算 epilog 的最終指令)。 這是常見的情況:這就是為什麼我們一律假設 prolog 的回溯程式代碼會以反向順序儲存在初構的執行順序中。

因此,對於初構和表結,我們留下一組常見的回溯程序代碼:

set_fp、、 save_regp 0,240save_fregp,0,224save_fplr_x_256end

表結案例很簡單,因為它以正常順序排列。 從表結內的位移 0 開始(從函式中的位移0x100開始),我們預期會執行完整的回溯序列,因為尚未完成清除。 如果我們發現自己在 中找到了一個指示(在表結中的位移 2),我們可以略過第一個回溯程式代碼來成功回溯。 我們可以將這種情況一般化,並假設 opcode 與回溯程式代碼之間的 1:1 對應。 然後,若要從 epilog 中的指示 n 開始回溯,我們應該略過前 n 個回溯程式代碼,然後從該處開始執行。

事實證明,類似的邏輯適用於初構,但反向除外。 如果我們從初構中的位移0開始回溯,我們想要不執行任何動作。 如果我們從位移 2 回回溯,也就是 中的一個指令,則我們想要從結尾開始執行回溯序列一個回溯程序代碼。 (請記住,程式代碼會以反向順序儲存。此外,我們也可以一般化:如果我們從初構中的指示 n 開始回溯,我們應該從程式代碼清單結尾開始執行 n 回溯程序代碼。

Prolog 和 epilog 程式代碼不一定完全相符,這就是為什麼回溯數位列可能需要包含數個程式代碼序列的原因。 若要判斷開始處理程式代碼的位置位移,請使用下列邏輯:

  1. 如果從函式主體內回溯,請從索引 0 開始執行回溯程式代碼,並繼續直到達到 end opcode 為止。

  2. 如果從表結內部回溯,請使用表結範圍所提供的 epilog 特定起始索引作為起點。 計算有問題的計算機從結尾算起多少個字節。 然後透過回溯程式代碼向前推進,略過回溯程序代碼,直到已執行的所有指令都考慮為止。 然後從該時間點開始執行。

  3. 如果從初構內回溯,請使用索引 0 作為起點。 計算序列中的初構程式代碼長度,然後計算有關計算機從初構結尾的位元元組數目。 然後透過回溯程式代碼向前推進,略過回溯程序代碼,直到所有尚未執行的指令都算在內。 然後從該時間點開始執行。

這些規則表示初構的回溯程序代碼一律必須是陣列中的第一個。 此外,它們也是用來在一般情況下從主體內回溯的程序代碼。 任何表結特定的程式代碼序列都應該緊接在後面。

函式片段

基於程式代碼優化的目的和其他原因,最好將函式分割成分隔的片段(也稱為 區域)。 分割時,每個產生的函式片段都需要它自己的個別 .pdata 記錄(且可能 .xdata) 記錄。

對於每個具有自己初構的個別次要片段,預期其初構中不會進行堆疊調整。 次要區域所需的所有堆疊空間都必須由其父區域預先配置(或稱為主機區域)。 這個預先配置會將堆疊指標操作嚴格保留在函式的原始初構中。

函式片段的典型案例是「程式代碼分隔」,編譯程式可能會將程式代碼區域移出其主機函式。 有三個不尋常的案例可能是因為程式代碼分離所導致。

範例

  • (區域 1:開始)

        stp     x29,lr,[sp,#-256]!      // save_fplr_x  256 (pre-indexed store)
        stp     x19,x20,[sp,#240]       // save_regp 0, 240
        mov     x29,sp                  // set_fp
        ...
    
  • (區域 1: 結束)

  • (區域 3:開始)

        ...
    
  • (區域 3:結束)

  • (區域 2:開始)

        ...
        mov     sp,x29                  // set_fp
        ldp     x19,x20,[sp,#240]       // save_regp 0, 240
        ldp     x29,lr,[sp],#256        // save_fplr_x  256 (post-indexed load)
        ret     lr                      // end
    
  • (區域 2:結束)

  1. 僅限初構 (區域 1:所有表結都位於不同的區域):

    只需要描述初構。 此初構無法以精簡 .pdata 格式表示。 在完整 .xdata 案例中,可以藉由設定 Epilog Count = 0 來表示。 請參閱上述範例中的區域 1。

    回溯程式代碼:set_fp、、save_regp 0,240save_fplr_x_256end

  2. 僅限 Epilogs (region 2: prolog is in host region)

    假設當控件跳入此區域時,所有初構代碼都已執行。 部分回溯可能發生在表結中的方式與一般函式相同。 這種類型的區域無法以 compact .pdata表示。 在完整 .xdata 記錄中,它可以使用「虛設」初構進行編碼, end_c 並以和 end 回溯程式代碼組括住。 end_c前置表示初構的大小為零。 單一表結的 Epilog 開始索引會指向 set_fp

    區域 2 的回溯程式代碼:end_c、、、set_fpsave_regp 0,240save_fplr_x_256end

  3. 沒有初構或表結(區域 3:初構和所有表結位於其他片段中):

    壓縮 .pdata 格式可以透過設定 Flag = 10 來套用。 使用完整 .xdata 記錄時,Epilog Count = 1。 回溯程式代碼與上述區域 2 的程式代碼相同,但 Epilog 開始索引也指向 end_c。 部分回溯永遠不會在此程式代碼區域中發生。

函式片段的另一個更複雜的案例是「壓縮包裝」。編譯程式可以選擇延遲儲存某些被呼叫端儲存緩存器,直到函式專案初構之外為止。

  • (區域 1:開始)

        stp     x29,lr,[sp,#-256]!      // save_fplr_x  256 (pre-indexed store)
        stp     x19,x20,[sp,#240]       // save_regp 0, 240
        mov     x29,sp                  // set_fp
        ...
    
  • (區域 2:開始)

        stp     x21,x22,[sp,#224]       // save_regp 2, 224
        ...
        ldp     x21,x22,[sp,#224]       // save_regp 2, 224
    
  • (區域 2:結束)

        ...
        mov     sp,x29                  // set_fp
        ldp     x19,x20,[sp,#240]       // save_regp 0, 240
        ldp     x29,lr,[sp],#256        // save_fplr_x  256 (post-indexed load)
        ret     lr                      // end
    
  • (區域 1: 結束)

在區域 1 的初構中,會預先配置堆棧空間。 您可以看到區域 2 會有相同的回溯程式代碼,即使它已移出其主機函式也一樣。

區域 1:set_fp、、、save_regp 0,240save_fplr_x_256end Epilog Start Index 會像往常一樣指向 set_fp

區域 2:save_regp 2, 224、、、end_cset_fpsave_regp 0,240、、save_fplr_x_256end Epilog Start Index 指向第一個回溯程式代碼 save_regp 2, 224

大型函式

片段可用來描述大於標頭中位欄位字段所加之 1M 限制的 .xdata 函式。 若要描述類似這樣的異常大型函式,它必須分成小於 1M 的片段。 應該調整每個片段,使其不會將表結分割成多個片段。

只有函式的第一個片段才會包含初構;所有其他片段都會標示為沒有初構。 根據存在的表結數目而定,每個片段可能包含零個或多個表結。 請記住,片段中的每個表結範圍都會指定其相對於片段開頭的起始位移,而不是函式的開頭。

如果片段沒有初構,也沒有表結,它仍然需要自己的 .pdata (可能 .xdata)記錄,以描述如何從函式主體內回溯。

範例

範例 1:框架鏈結、精簡格式

|Foo|     PROC
|$LN19|
    str     x19,[sp,#-0x10]!        // save_reg_x
    sub     sp,sp,#0x810            // alloc_m
    stp     fp,lr,[sp]              // save_fplr
    mov     fp,sp                   // set_fp
                                    // end of prolog
    ...

|$pdata$Foo|
    DCD     imagerel     |$LN19|
    DCD     0x416101ed
    ;Flags[SingleProEpi] functionLength[492] RegF[0] RegI[1] H[0] frameChainReturn[Chained] frameSize[2080]

範例 2:包含鏡像 Prolog 和 Epilog 的框架鏈結完整表單

|Bar|     PROC
|$LN19|
    stp     x19,x20,[sp,#-0x10]!    // save_regp_x
    stp     fp,lr,[sp,#-0x90]!      // save_fplr_x
    mov     fp,sp                   // set_fp
                                    // end of prolog
    ...
                                    // begin of epilog, a mirror sequence of Prolog
    mov     sp,fp
    ldp     fp,lr,[sp],#0x90
    ldp     x19,x20,[sp],#0x10
    ret     lr

|$pdata$Bar|
    DCD     imagerel     |$LN19|
    DCD     imagerel     |$unwind$cse2|
|$unwind$Bar|
    DCD     0x1040003d
    DCD     0x1000038
    DCD     0xe42291e1
    DCD     0xe42291e1
    ;Code Words[2], Epilog Count[1], E[0], X[0], Function Length[6660]
    ;Epilog Start Index[0], Epilog Start Offset[56]
    ;set_fp
    ;save_fplr_x
    ;save_r19r20_x
    ;end

Epilog Start Index [0] 指向相同的 Prolog 回溯程式代碼序列。

範例 3:Variadic 未鏈結函式

|Delegate| PROC
|$LN4|
    sub     sp,sp,#0x50
    stp     x19,lr,[sp]
    stp     x0,x1,[sp,#0x10]        // save incoming register to home area
    stp     x2,x3,[sp,#0x20]        // ...
    stp     x4,x5,[sp,#0x30]
    stp     x6,x7,[sp,#0x40]        // end of prolog
    ...
    ldp     x19,lr,[sp]             // beginning of epilog
    add     sp,sp,#0x50
    ret     lr

    AREA    |.pdata|, PDATA
|$pdata$Delegate|
    DCD     imagerel |$LN4|
    DCD     imagerel |$unwind$Delegate|

    AREA    |.xdata|, DATA
|$unwind$Delegate|
    DCD     0x18400012
    DCD     0x200000f
    DCD     0xe3e3e3e3
    DCD     0xe40500d6
    DCD     0xe40500d6
    ;Code Words[3], Epilog Count[1], E[0], X[0], Function Length[18]
    ;Epilog Start Index[4], Epilog Start Offset[15]
    ;nop        // nop for saving in home area
    ;nop        // ditto
    ;nop        // ditto
    ;nop        // ditto
    ;save_lrpair
    ;alloc_s
    ;end

Epilog Start Index [4] 指向 Prolog 回溯程式代碼中間(部分重複使用回溯陣列)。

另請參閱

ARM64 ABI 慣例概觀
ARM 例外狀況處理