Ošetření výjimek ARM64

Systém Windows v ARM64 používá stejný mechanismus strukturovaného zpracování výjimek pro asynchronní výjimky generované hardwarem a synchronní výjimky generované softwarem. Obslužné rutiny výjimek specifické pro jazyk jsou založené na strukturovaném zpracování výjimek systému Windows pomocí pomocných funkcí jazyka. Tento dokument popisuje zpracování výjimek ve Windows v ARM64. Ukazuje pomocné rutiny jazyka používané kódem vygenerovaným assemblerem Microsoft ARM a kompilátorem MSVC.

Cíle a motivace

Výjimky odvíjejí konvence dat a tento popis jsou určeny k:

  • Zadejte dostatečný popis, který umožňuje odvíjení bez sondování kódu ve všech případech.

    • Analýza kódu vyžaduje, aby kód byl stránkován. Za určitých okolností zabraňuje odvíjení, kdy je užitečné (trasování, vzorkování, ladění).

    • Analýza kódu je složitá; kompilátor musí být opatrný, aby vygeneroval pouze instrukce, které může dekódovat unwinder.

    • Pokud se odvíjení nedá plně popsat pomocí kódů odvíjení, pak se v některých případech musí vrátit k dekódování instrukcí. Dekódování instrukcí zvyšuje celkovou složitost a v ideálním případě byste se měli vyhnout.

  • Podpora odvíjení v mid-prologu a mid-epilogu.

    • Unwinding se používá ve Windows pro zpracování více než výjimek. Je důležité, aby kód mohl přesně odvíjet i v případě, že uprostřed sekvence kódu prologu nebo epilogu.
  • Zaberte minimální množství místa.

    • Kódy odvíjení nesmí agregovat, aby se binární velikost výrazně zvětšila.

    • Vzhledem k tomu, že kódy odvíjení budou pravděpodobně uzamčeny v paměti, malé nároky zajišťují minimální režii pro každý načtený binární soubor.

Předpoklady

Tyto předpoklady jsou provedeny v popisu zpracování výjimek:

  • Prology a epilogy se vzájemně zrcadlí. Díky využití této běžné vlastnosti může být velikost metadat potřebných k popisu odvíjení výrazně snížena. V těle funkce nezáleží na tom, jestli se operace prologu vrátí zpět, nebo se operace epilogu provádějí dopředu. Oba by měly mít identické výsledky.

  • Funkce mají tendenci k celku být relativně malé. Několik optimalizací prostoru závisí na této skutečnosti, aby bylo dosaženo nejúčinnějšího balení dat.

  • Vepich

  • Registr vyhrazeného ukazatele rámce: Pokud sp je uložen v jiném registru (x29) v prologu, zůstane tento registr nedotčený v celé funkci. Znamená to, že originál sp může být kdykoli obnoven.

  • sp Pokud není uložen v jiném registru, veškerá manipulace s ukazatelem zásobníku se provádí výhradně v rámci prologu a epilogu.

  • Rozložení rámečku zásobníku je uspořádané podle popisu v další části.

Rozložení rámečku zásobníku ARM64

Diagram that shows the stack frame layout for functions.

U zřetězených funkcí fp rámce lze v závislosti na aspektech optimalizace uložit dvojici lr na libovolné pozici v oblasti místní proměnné. Cílem je maximalizovat počet místních hodnot, které lze dosáhnout jediným pokynem na základě ukazatele rámce (x29) nebo ukazatele zásobníku (sp). U alloca funkcí však musí být zřetězený a x29 musí odkazovat na konec zásobníku. Aby bylo možné zlepšit pokrytí režimu adresování párů registrů, jsou oblasti uložení nevolatilního registru umístěny v horní části zásobníku místní oblasti. Tady jsou příklady, které ilustrují několik nejúčinnějších sekvencí prologu. Kvůli přehlednosti a lepší lokalitě mezipaměti je pořadí ukládání volaných registrů ve všech kanonických prologech v "rostoucím" pořadí. #framesz níže představuje velikost celého zásobníku (s výjimkou alloca oblasti). #localsz a #outsz označte velikost místní oblasti (včetně oblasti uložení pro <x29, lr> dvojici) a velikost výstupního parametru.

  1. Zřetězený, #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. Zřetězený > , #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. Unchained, listové funkce (lr neuložené)

        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
    

    Všechna místní prostředí jsou přístupná na spzákladě . <x29,lr> odkazuje na předchozí rámec. Velikost rámce <= 512 je možné optimalizovat, sub sp, ... pokud je uložená oblast regů přesunuta do dolní části zásobníku. Nevýhodou je, že není konzistentní s jinými rozloženími výše. Uložené regy jsou součástí rozsahu pro párové regulární výrazy a před indexovaný režim adresování posunu.

  4. Nechainované, jiné než listové funkce (uloží lr se do uložené oblasti 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
    

    Nebo se sudým číslem uloženým v registrech 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
    

    Pouze x19 uloženo:

        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
    

    * Přidělení oblasti uložení regu není přeloženo do stp oblasti, protože předindexovaný reg-lr stp nemůže být reprezentován kódy odvíjení.

    Všechna místní prostředí jsou přístupná na spzákladě . <x29> odkazuje na předchozí rámec.

  5. Zřetězený, #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
    

    Ve srovnání s prvním příkladem prologu výše má tento příklad výhodu: všechny pokyny k uložení registru jsou připravené ke spuštění po pouze jedné instrukci přidělení zásobníku. To znamená, že neexistuje žádná antizávislosti na sp tom, že brání paralelismu na úrovni instrukce.

  6. Zřetězení, velikost > rámce 512 (volitelné pro funkce bez 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
    

    Pro účely x29 optimalizace je možné umístit na libovolnou pozici v místní oblasti, aby se zajistilo lepší pokrytí pro "reg-pair" a pre-/post-indexed offset adresní režim. Místní hodnoty pod ukazateli rámce lze přistupovat na spzákladě .

  7. Zřetězený, velikost > rámu 4K, s nebo bez 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>
    

Informace o zpracování výjimek ARM64

.pdata Záznamy

Záznamy .pdata jsou uspořádané pole položek s pevnou délkou, které popisují každou funkci manipulace se zásobníkem v binárním souboru PE. Fráze "manipulace se zásobníkem" je významná: funkce typu list, které nevyžadují žádné místní úložiště a nevyžadují žádné .pdata místní úložiště, a nevyžadují záznam. Tyto záznamy by měly být explicitně vynechány, aby se ušetřilo místo. Odvíjení jedné z těchto funkcí může získat zpáteční adresu přímo od lr volajícího.

Každý .pdata záznam pro ARM64 má délku 8 bajtů. Obecný formát každého záznamu umístí 32bitovou RVA funkce začínat v prvním slově, za kterým následuje druhé slovo, které obsahuje ukazatel na blok s proměnnou délkou .xdata , nebo zabalené slovo popisující kanonický sekvenci odvíjení funkce.

.pdata record layout.

Pole jsou následující:

  • Funkce Start RVA je 32bitová hodnota RVA začátku funkce.

  • Příznak je 2bitové pole, které označuje, jak interpretovat zbývající 30 bitů druhého .pdata slova. Pokud je příznak 0, zbývající bity tvoří RVA informace o výjimce (se dvěma nejnižšími bity implicitně 0). Pokud je příznak nenulový, zbývající bity tvoří strukturu Packed Unwind Data .

  • Informace o výjimce RVA je adresa struktury informací o výjimce proměnné délky uložená .xdata v části. Tato data musí být zarovnaná se 4 bajty.

  • Zabalená data Unwind jsou komprimovaný popis operací potřebných k odvíjení funkce za předpokladu, že jde o kanonický formulář. V tomto případě se nevyžaduje žádný .xdata záznam.

.xdata Záznamy

Pokud zabalený formát unwind není dostatečný k popisu odvíjení funkce, je nutné vytvořit záznam o proměnné délce .xdata . Adresa tohoto záznamu je uložena ve druhém slově záznamu .pdata . .xdata Formát je sada slov s zabalenou proměnnou délkou:

.xdata record layout.

Tato data jsou rozdělená do čtyř částí:

  1. Záhlaví 1 nebo 2 slov popisující celkovou velikost struktury a poskytnutí klíčových dat funkce. Druhé slovo je přítomné pouze v případě, že jsou pole Počet epilogu i Slova kódu nastavena na hodnotu 0. Záhlaví obsahuje tato bitová pole:

    a. Délka funkce je 18bitové pole. Označuje celkovou délku funkce v bajtech rozdělenou 4. Pokud je funkce větší než 1M, musí se k popisu funkce použít více .pdata záznamů a .xdata záznamy. Další informace najdete v části Velké funkce .

    b. Vers je 2bitové pole. Popisuje verzi zbývajícího .xdatasouboru . V současné době je definována pouze verze 0, takže hodnoty 1–3 nejsou povolené.

    c. X je 1bitové pole. Označuje přítomnost (1) nebo nepřítomnost (0) údajů o výjimce.

    d. E je 1bitové pole. Označuje, že informace popisující jeden epilog se zabalí do hlavičky (1) a nevyžadují více slov oboru později (0).

    e. Epilog Count je 5bitové pole, které má dva významy v závislosti na stavu bitu E :

    1. Pokud je E 0, určuje počet celkového počtu rozsahů epilogu popsaných v části 2. Pokud ve funkci existuje více než 31 oborů, musí být pole Code Words nastaveno na hodnotu 0, aby bylo možné určit, že je požadováno slovo rozšíření.

    2. Pokud je E 1, pak toto pole určuje index prvního odvinutí kódu, který popisuje jeden a jediný epilog.

    f. Code Words je 5bitové pole, které určuje počet 32bitových slov potřebných k zahrnutí všech kódů odvíjení v oddílu 3. Pokud je požadováno více než 31 slov (tj. 124 kódů odvíjení), musí být toto pole 0, aby bylo možné označit, že je požadováno slovo přípony.

    g. Rozšířené počty epilogů a rozšířená slova kódu jsou 16bitová a 8bitová pole. Poskytují více místa pro kódování neobvykle velkého počtu epilogů nebo neobvykle velkého počtu odvíjecích slov kódu. Slovo rozšíření, které obsahuje tato pole, je přítomné pouze v případě, že pole Epilog Count (Počet epilogu) a Code Words (Slova kódu) v prvním záhlaví jsou 0.

  2. Pokud počet epilogů není nulový, zobrazí se seznam informací o oborech epilogu, který je zabalený na slovo, přijde za záhlavím a volitelnou rozšířenou hlavičkou. Ukládají se v pořadí zvýšení počátečního posunu. Každý obor obsahuje následující bity:

    a. Počáteční posun epilogu je 18bitové pole, které má posun v bajtech dělené 4, epilogu vzhledem ke začátku funkce.

    b. Res je 4bitové pole vyhrazené pro budoucí rozšíření. Jeho hodnota musí být 0.

    c. Počáteční index epilogu je 10bitové pole (více než 2 bity než rozšířená slova kódu). Označuje bajtový index prvního odvíjení kódu, který popisuje tento epilog.

  3. Jakmile seznam oborů epilogu přijde na pole bajtů, které obsahují odvíjecí kódy, popsané podrobně v další části. Toto pole je vycpané na konci k nejbližší hranici celého slova. Kódy unwind jsou zapsány do tohoto pole. Začínají tím, co je nejblíže k těle funkce, a pohybují se směrem k okrajům funkce. Bajty pro každý odvinutý kód jsou uloženy ve velkém pořadí, takže nejvýznamnější bajt se načte jako první, který identifikuje operaci a délku zbytku kódu.

  4. Nakonec po odvinutí bajtů kódu, pokud byl bit X v hlavičce nastaven na hodnotu 1, přijde informace obslužné rutiny výjimky. Skládá se z jediné obslužné rutiny výjimky RVA , která poskytuje adresu samotné obslužné rutiny výjimky. Následuje okamžitě množství dat vyžadovaných obslužnou rutinou výjimky s proměnlivou délkou.

Záznam .xdata je navržený tak, aby bylo možné načíst prvních 8 bajtů a použít je k výpočtu plné velikosti záznamu, minus délku dat výjimky s proměnlivou velikostí, která následuje. Následující fragment kódu vypočítá velikost záznamu:

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;
}

I když prolog a každý epilog má svůj vlastní index do kódů odvíjení, tabulka se mezi nimi sdílí. Je to zcela možné (a ne úplně neobvyklé), že můžou všechny sdílet stejné kódy. (Příklad viz příklad 2 v části Příklady oddílu.) Pro tento případ by se měly optimalizovat zejména zapisovače kompilátoru. Je to proto, že největší index, který lze zadat, je 255, což omezuje celkový počet kódů odvíjení pro konkrétní funkci.

Unwind codes

Pole odvíjecích kódů je fond sekvencí, které přesně popisují, jak vrátit zpět účinky prologu. Ukládají se ve stejném pořadí, v jakém musí být operace vráceny. Kódy unwind lze považovat za malou instrukční sadu kódovanou jako řetězec bajtů. Po dokončení provádění se návratová adresa volající funkce nachází v lr registru. A všechny nevolatelní registry se obnoví na jejich hodnoty v době, kdy byla funkce volána.

Pokud by výjimky byly zaručeny pouze někdy v těle funkce, a nikdy v prologu nebo v žádném epilogu, bude nutné pouze jednu sekvenci. Model odvíjení windows však vyžaduje, aby se kód mohl odvíjet od částečně spuštěného prologu nebo epilogu. Pro splnění tohoto požadavku byly kódy unwind pečlivě navrženy, aby jednoznačně mapovaly 1:1 na každý relevantní opcode v prologu a epilogu. Tento návrh má několik důsledků:

  • Když spočítáte počet kódů odvíjení, je možné vypočítat délku prologu a epilogu.

  • Když spočítáte počet instrukcí po začátku oboru epilogu, je možné přeskočit ekvivalentní počet odvíjených kódů. Zbytek sekvence můžeme provést, abychom dokončili částečně spuštěné odvíjení provedené epilogem.

  • Když spočítáte počet instrukcí před koncem prologu, je možné přeskočit ekvivalentní počet odvíjených kódů. Zbytek sekvence můžeme spustit a vrátit zpět pouze ty části prologu, které dokončily provádění.

Kódy unwind jsou kódovány podle následující tabulky. Všechny kódy odvíjení jsou jeden/dvojitý bajt s výjimkou těch, které přidělují obrovský zásobník (alloc_l). Celkem je 22 kódů odvíjení. Každý odvíjecí kód mapuje přesně jednu instrukci v prologu/epilogu, aby bylo možné uvolnit částečně spuštěné prology a epilogy.

Unwind code Bity a interpretace
alloc_s 000xxxxx: přidělit malý zásobník s velikostí < 512 (2^5 * 16).
save_r19r20_x 001zzzzzzz: save <x19,x20> pair at [sp-#Z*8]!, pre-indexed offset >= -248
save_fplr 01zzzzzz: save <x29,lr> pair at [sp+#Z*8], offset <= 504.
save_fplr_x 10zzzzzz: save <x29,lr> pair at [sp-(#Z+1)*8]!, pre-indexed offset >= -512
alloc_m 11000xxx'xxxxxxxx: přidělení velkého zásobníku s velikostí < 32 K (2^11 * 16).
save_regp 110010xx'xxzzzzzz: save x(19+#X) pair at [sp+#Z*8], offset <= 504
save_regp_x 110011xx'xxzzzzzz: save pair x(19+#X) at [sp-(#Z+1)*8]!, pre-indexed offset >= -512
save_reg 110100xx'xxzzzzzz: save reg x(19+#X) at [sp+#Z*8], offset <= 504
save_reg_x 1101010x'xxxzzzzz: save reg x(19+#X) at [sp-(#Z+1)*8]!, pre-indexed offset >= -256
save_lrpair 1101011x'xxzzzzzz: save pair <x(19+2*#X),lr> at [sp+#Z*8], offset <= 504
save_fregp 1101100x'xxzzzzzz: save pair d(8+#X) at [sp+#Z*8], offset <= 504
save_fregp_x 1101101x'xxzzzzzz: save pair d(8+#X) at [sp-(#Z+1)*8]!, pre-indexed offset >= -512
save_freg 1101110x'xxzzzzzz: save reg d(8+#X) at [sp+#Z*8], offset <= 504
save_freg_x 11011110'xxxzzzzz: save reg d(8+#X) at [sp-(#Z+1)*8]!, pre-indexed offset >= -256
alloc_l 11100000'xxxxxxxx'xxxxxxxx'xxxxxxxx:přidělení velkého zásobníku s velikostí < 256M (2^24 * 16)
set_fp 11100001: nastavení x29 pomocí mov x29,sp
add_fp 11100010'xxxxxxxx: nastaveno x29 pomocí add x29,sp,#x*8
nop 11100011: Nevyžaduje se žádná operace odvíjení.
end 11100100: konec odvíjeného kódu. Implikuje ret to v epilogu.
end_c 11100101: konec odvíjeného kódu v aktuálním zřetězeným oboru.
save_next 11100110: Uložte další nevolatilní dvojici Int nebo FP register.
11100111: rezervováno
11101xxx: Vyhrazeno pro vlastní případy zásobníku níže generované pouze pro rutiny asm
11101000: Vlastní zásobník pro MSFT_OP_TRAP_FRAME
11101001: Vlastní zásobník pro MSFT_OP_MACHINE_FRAME
11101010: Vlastní zásobník pro MSFT_OP_CONTEXT
11101011: Vlastní zásobník pro MSFT_OP_EC_CONTEXT
11101100: Vlastní zásobník pro MSFT_OP_CLEAR_UNWOUND_TO_CALL
11101101: rezervováno
11101110: rezervováno
11101111: rezervováno
11110xxx: rezervováno
11111000'yyy: reserved
11111001'y'y: reserved
11111010'y'y'y: reserved
11111011'y'y'y'y: reserved
pac_sign_lr 11111100: Podepište zpáteční adresu pomocí lrpacibsp
11111101: rezervováno
11111110: rezervováno
11111111: rezervováno

V pokynech s velkými hodnotami, které pokrývají více bajtů, jsou nejdůležitější bity uloženy jako první. Tento návrh umožňuje najít celkovou velikost v bajtech odvíjeného kódu vyhledáním pouze prvního bajtu kódu. Vzhledem k tomu, že každý kód odvíjení je přesně namapován na instrukce v prologu nebo epilogu, můžete vypočítat velikost prologu nebo epilogu. Projděte si posloupnost od začátku do konce a pomocí vyhledávací tabulky nebo podobného zařízení určete délku odpovídajícího operačního kódu.

Adresování posunu po indexu není v prologu povolené. Všechny rozsahy posunu (#Z) odpovídají kódování stp/str adres s výjimkou save_r19r20_xpřípadů, kdy 248 je dostačující pro všechny oblasti úspory (10 Int registrů + 8 FP registrů + 8 vstupních registrů).

save_nextmusí dodržovat uložení páru int nebo FP volatile register: , , , save_fregp_x, , save_r19r20_xnebo jiné save_next. save_fregpsave_regp_xsave_regp Uloží další dvojici registrů v dalším 16 bajtovém slotu v "rostoucím" pořadí. A save_next odkazuje na první pár registrace FP, když následuje save-next , který označuje poslední dvojici Int register.

Vzhledem k tomu, že velikosti pravidelných návratových a přeskakovaných instrukcí jsou stejné, není nutné oddělit odvíjecí end kód ve scénářích tail-call.

end_c je navržený tak, aby zpracovával nesouvislé fragmenty funkcí pro účely optimalizace. Za end_c koncem odvíjecích kódů v aktuálním rozsahu musí následovat další řada odvíjecích kódů končících skutečným endkódem . Kódy odvíjení mezi end_c a end představují operace prologu v nadřazené oblasti ("fantomový" prolog). Další podrobnosti a příklady jsou popsány v následující části.

Zabalená data odvíjení

Pro funkce, jejichž prology a epilogy se řídí kanonickým formulářem popsaným níže, lze použít zabalená data unwind. Eliminuje potřebu záznamu .xdata zcela a výrazně snižuje náklady na poskytování odvíjecích dat. Kanonické prology a epilogy jsou navrženy tak, aby splňovaly běžné požadavky jednoduché funkce: Jedna, která nevyžaduje obslužnou rutinu výjimky a která provede operace nastavení a odstranění ve standardním pořadí.

Formát záznamu .pdata s zabalenými daty unwind vypadá takto:

.pdata record with packed unwind data.

Pole jsou následující:

  • Funkce Start RVA je 32bitová hodnota RVA začátku funkce.
  • Příznak je 2bitové pole, jak je popsáno výše, s následujícími významy:
    • 00 = zabalená unwindová data se nepoužívají; zbývající bity ukazují na .xdata záznam
    • 01 = zabalená unwind data používaná s jedním prologem a epilogem na začátku a na konci oboru
    • 10 = zabalená unwind data použitá pro kód bez jakéhokoli prologu a epilogu. Užitečné pro popis oddělených segmentů funkcí
    • 11 = rezervováno.
  • Délka funkce je 11bitové pole poskytující délku celé funkce v bajtech děleno 4. Pokud je funkce větší než 8 tisíc, je nutné místo toho použít úplný .xdata záznam.
  • Velikost rámce je 9bitové pole označující počet bajtů zásobníku, který je přidělen pro tuto funkci, dělený číslem 16. Funkce, které přidělují více než (8k-16) bajtů zásobníku, musí používat úplný .xdata záznam. Zahrnuje oblast místních proměnných, oblast odchozích parametrů, oblast volaného int a oblast FP a domovskou oblast parametrů. Vyloučí oblast dynamického přidělování.
  • CR je 2bitový příznak označující, jestli funkce obsahuje další pokyny k nastavení řetězu rámu a návratového propojení:
    • 00 = nechainovaná funkce, <x29,lr> pár není uložen v zásobníku
    • 01 = unchained function, <lr> is saved in stack
    • 10 = zřetězený funkce se podepsanou pacibsp zpáteční adresou
    • 11 = zřetězená funkce, instrukce páru úložiště/načtení se používá v prologu/epilogu. <x29,lr>
  • H je 1bitový příznak označující, jestli funkce ukládá celočíselné registry (x0-x7) jejich uložením na začátku funkce. (0 = nemá domácí registry, 1 = registry domů).
  • RegI je 4bitové pole označující počet nevolatilních registrů INT (x19-x28) uložených v kanonickém umístění zásobníku.
  • RegF je 3bitové pole označující počet nevolatilních registrů FP (d8-d15) uložených v kanonickém umístění zásobníku. (RegF=0: neuloží se žádný registr FP; RegF>0: Registru RegF+1 FP se uloží. Zabalená data odvíjení se nedají použít pro funkci, která ukládá jenom jeden registr FP.

Kanonické prology, které spadají do kategorií 1, 2 (bez oblasti výstupních parametrů), 3 a 4 v oddílu výše mohou být reprezentovány zabaleným unwind formátem. Epilogy pro kanonické funkce se řídí podobným formulářem, s výjimkou H nemá žádný vliv, set_fp instrukce se vynechá a pořadí kroků a pokyny v každém kroku jsou v epilogu obrácené. Algoritmus pro zabalení .xdata se řídí následujícími kroky, které jsou podrobně popsané v následující tabulce:

Krok 0: Předem vypočítá velikost každé oblasti.

Krok 1: Podepište zpáteční adresu.

Krok 2: Uložení volaných registrů volaných volaných

Krok 3: Tento krok je specifický pro typ 4 v počátečních částech. lr je uložen na konci oblasti Int.

Krok 4: Uložení volaných registrů FP

Krok 5: Uložení vstupních argumentů v oblasti domovského parametru

Krok 6: Přidělení zbývajícího zásobníku, včetně místní oblasti, <x29,lr> páru a oblasti výstupních parametrů 6a odpovídá kanonickému typu 1. 6b a 6c jsou určené pro kanonický typ 2. 6d a 6e jsou pro typ 3 i typ 4.

Krok # Hodnoty příznaku # of instructions Opcode Unwind code
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
0 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

* Pokud CR == 01 a RegI je liché číslo, krok 2 a poslední save_rep v kroku 1 se sloučí do jednoho save_regp.

** Pokud regI == CR == 0 a RegF != 0, stp první pro plovoucí desetinnou čárku provede předběžné zvýšení.

V epilogu se nenachází žádná instrukce odpovídající mov x29,sp . Zabalená data unwind nelze použít, pokud funkce vyžaduje obnovení sp z x29.

Uvolnění částečných prologů a epilogů

V nejběžnějších situacích odvíjení dojde k výjimce nebo volání v těle funkce, mimo prolog a všechny epilogy. V těchto situacích je odvíjení jednoduché: unwinder jednoduše provede kódy v poli unwind. Začíná indexem 0 a pokračuje, dokud end se nezjistí opcode.

V případě, že při provádění prologu nebo epilogu dojde k výjimce nebo přerušení, je obtížnější se správně uvolnit. V těchto situacích je rám zásobníku vytvořen pouze částečně. Problémem je přesně určit, co bylo provedeno, aby se správně vrátil zpět.

Například tuto posloupnost prologu a epilogu:

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

Vedle každého opcode je vhodný odvíjecí kód popisující tuto operaci. Můžete vidět, jak série odvíjecí kódy prolog je přesný zrcadlový obraz odvíjecí kódy pro epilog (bez počítání konečné instrukce epilogu). Je to běžná situace: Proto vždy předpokládáme, že kódy prologu prowind jsou uložené v opačném pořadí než pořadí provádění prologu.

Takže pro prolog i epilog jsme zůstali se společnou sadou kódů odvíjení:

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

Případ epilogu je jednoduchý, protože je v normálním pořadí. Počínaje posunem 0 v epilogu (který začíná na posunu 0x100 ve funkci), očekáváme, že se spustí úplná sekvence odvíjení, protože ještě nebylo provedeno žádné vyčištění. Pokud zjistíme, že máme jednu instrukci (na posunu 2 v epilogu), můžeme úspěšně odvinout přeskočením prvního odvinutí kódu. Tuto situaci můžeme generalizovat a předpokládat mapování 1:1 mezi opcodes a unwind kódy. Potom, abychom mohli začít odvíjení od instrukce n v epilogu, měli bychom přeskočit první n odvíjecí kódy a začít spouštět odsud.

Ukázalo se, že prolog funguje podobná logika, s výjimkou obrácené. Pokud začneme odvíjení od posunu 0 v prologu, chceme provést nic. Pokud odpovítáme od posunu 2, což je jedna instrukce, chceme začít spouštět sekvenci odvíjení jednoho odvíjecího kódu od konce. (Nezapomeňte, že kódy jsou uloženy v obráceném pořadí.) A tady můžeme také generalizovat: pokud začneme odvíjení z instrukce n v prologu, měli bychom začít spouštět n unwind kódy od konce seznamu kódů.

Kódy prologu a epilogu se vždy neshodují přesně, proto může být nutné pole unwind obsahovat několik sekvencí kódů. K určení posunu místa, kde začít zpracovávat kódy, použijte následující logiku:

  1. Pokud se odvíjeje od těla funkce, začněte spouštět odvíjecí kódy v indexu 0 a pokračujte, dokud nenasáhnete end opcode.

  2. Pokud se odvíjeje od epilogu, použijte počáteční index specifický pro epilog, který je součástí oboru epilogu jako výchozí bod. Vypočítá, kolik bajtů je příslušný počítač od začátku epilogu. Pak pokračujte vpřed přes kódy odvíjení a přeskočte kódy odvíjení, dokud se nezapočítávají všechny již provedené pokyny. Pak spusťte spuštění od tohoto okamžiku.

  3. Pokud se odvíjeje z prologu, použijte jako výchozí bod index 0. Vypočítá délku kódu prologu ze sekvence a pak vypočítá, kolik bajtů je příslušný počítač od konce prologu. Pak pokračujte vpřed přes kódy unwind a přeskočte kódy odvíjení, dokud nebudou zohledněny všechny dosud provedené pokyny. Pak spusťte spuštění od tohoto okamžiku.

Tato pravidla znamenají, že kódy prologu odvíjení musí být vždy první v poli. A také kódy používané k odvíjení v obecném případě odvíjení z těla. Všechny sekvence kódu specifické pro epilog by měly následovat hned po.

Fragmenty funkcí

Pro účely optimalizace kódu a další důvody může být vhodnější rozdělit funkci na oddělené fragmenty (označované také jako oblasti). Při rozdělení vyžaduje každý výsledný fragment funkce vlastní samostatný .pdata (a případně .xdata) záznam.

U každého odděleného sekundárního fragmentu, který má svůj vlastní prolog, se očekává, že se v jeho prologu neprovede žádná úprava zásobníku. Veškerý prostor zásobníku vyžadovaný sekundární oblastí musí být předem přidělen nadřazenou oblastí (nebo označovaný jako hostitelská oblast). Tato preallocation udržuje manipulaci s ukazateli zásobníku výhradně v původním prologu funkce.

Typickým případem fragmentů funkcí je oddělení kódu, kde kompilátor může přesunout oblast kódu z hostitelské funkce. Existují tři neobvyklé případy, které by mohly mít za následek oddělení kódu.

Příklad

  • (oblast 1: začátek)

        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
        ...
    
  • (oblast 1: konec)

  • (oblast 3: začátek)

        ...
    
  • (oblast 3: konec)

  • (oblast 2: začátek)

        ...
        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
    
  • (oblast 2: konec)

  1. Pouze prolog (oblast 1: všechny epilogy jsou v oddělených oblastech):

    Musí být popsán pouze prolog. Tento prolog nemůže být reprezentován v kompaktním .pdata formátu. V plném .xdata případě to může být reprezentováno nastavením Epilog Count = 0. Viz oblast 1 v příkladu výše.

    Odvíjení kódů: set_fp, save_regp 0,240, save_fplr_x_256, end.

  2. Pouze epilogy (oblast 2: Prolog je v hostitelské oblasti)

    Předpokládá se, že ovládací prvek času přeskočí do této oblasti, všechny kódy prologu byly provedeny. Částečné odvíjení může probíhat v epilogech stejným způsobem jako v normální funkci. Tento typ oblasti nemůže být reprezentován kompaktním .pdata. V úplném .xdata záznamu lze kódovat pomocí "fantomového" prologu, který je závorkou páru end_c kódu a end odvíjejený. Úvodní end_c hodnota značí, že velikost prologu je nula. Počáteční index epilogu jednoho epilogu odkazuje na set_fp.

    Unwind code for region 2: end_c, set_fp, save_regp 0,240, , save_fplr_x_256, . end

  3. Žádné prology ani epilogy (oblast 3: prology a všechny epilogy jsou v jiných fragmentech):

    Kompaktní .pdata formát lze použít pomocí nastavení Příznak = 10. S úplným .xdata záznamem, Epilog Count = 1. Unwind kód je stejný jako kód pro oblast 2 výše, ale Epilog Start Index také odkazuje na end_c. V této oblasti kódu nikdy nedojde k částečnému odvíjení.

Dalším složitějším případem fragmentů funkcí je "obtékání zmenšení". Kompilátor se může rozhodnout zpozdit ukládání některých volaných registrů, dokud se neschová položka funkce.

  • (oblast 1: začátek)

        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
        ...
    
  • (oblast 2: začátek)

        stp     x21,x22,[sp,#224]       // save_regp 2, 224
        ...
        ldp     x21,x22,[sp,#224]       // save_regp 2, 224
    
  • (oblast 2: konec)

        ...
        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
    
  • (oblast 1: konec)

V prologu oblasti 1 je prostor zásobníku předem přidělený. Uvidíte, že oblast 2 bude mít stejný odvíjecí kód, i když se přesune mimo svou hostitelskou funkci.

Oblast 1: set_fp, save_regp 0,240, save_fplr_x_256, end. Epilog Start Index odkazuje jako set_fp obvykle.

Oblast 2: save_regp 2, 224, end_c, set_fp, save_regp 0,240, save_fplr_x_256, , end. Epilog Start Index odkazuje na první odvíjení kódu save_regp 2, 224.

Velké funkce

Fragmenty lze použít k popisu funkcí větších než limit 1M uložený bitovými poli v .xdata hlavičce. Aby bylo možné popsat neobvykle velkou funkci, je potřeba ji rozdělit na fragmenty menší než 1 M. Každý fragment by měl být upraven tak, aby nerozdělil epilog na více kusů.

Pouze první fragment funkce bude obsahovat prolog; všechny ostatní fragmenty jsou označené jako bez prologu. V závislosti na počtu výskytů epilogů může každý fragment obsahovat nula nebo více epilogů. Mějte na paměti, že každý rozsah epilogu v fragmentu určuje počáteční posun vzhledem ke začátku fragmentu, nikoli začátek funkce.

Pokud fragment nemá žádný prolog a žádný epilog, stále vyžaduje vlastní .pdata (a případně .xdata) záznam, který popisuje, jak se odvinout z těla funkce.

Příklady

Příklad 1: Zřetězený rámec, kompaktní tvar

|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]

Příklad 2: Rámeček zřetězený, plný formulář se zrcadlem 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

Počáteční index epilogu [0] odkazuje na stejnou sekvenci kódu prologu.

Příklad 3: Variadic unchained – funkce

|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

Počáteční index epilogu [4] odkazuje na střed kódu prologu odvíjení (částečně znovu použít pole unwind).

Viz také

Přehled konvencí ARM64 ABI
Zpracování výjimek ARM