Porozumění Arm64EC ABI a assemblerovému kódu

Arm64EC ("Emulation Compatible") je nové binární rozhraní aplikace (ABI) pro vytváření aplikací pro Windows 11 na Arm. Přehled Arm64EC a informace o tom, jak začít vytvářet aplikace Win32 jako Arm64EC, najdete v tématu Použití Arm64EC k vytváření aplikací pro Windows 11 na zařízeních Arm.

Tento článek poskytuje podrobný pohled na Arm64EC ABI s dostatečnými informacemi pro vývojáře aplikací k psaní a ladění kódu zkompilovaného na Arm64EC, včetně ladění nízké úrovně/assembleru a psaní assemblerového kódu, který cílí na Arm64EC ABI.

Návrh Arm64EC

Arm64EC poskytuje nativní funkce a výkon a současně poskytuje transparentní a přímou interoperabilitu s kódem x64 běžícím pod emulací.

Arm64EC ve větší míře přidává vlastnosti k modelu Classic Arm64 ABI. Klasické ABI se velmi málo změnilo, ale arm64EC ABI přidaly části, které umožňují interoperabilitu x64.

V tomto dokumentu se původní standard Arm64 ABI označuje jako "Classic ABI". Tento termín se vyhne nejednoznačnosti, která je podstatou přetížených termínů, jako je "Native". Arm64EC je zcela nativní jako původní ABI.

Arm64EC vs. Arm64 Classic ABI

Následující seznam ukazuje, kde Arm64EC se liší od modelu Arm64 Classic ABI.

Tyto rozdíly jsou malé změny, když se podíváme na to, jak mnoho definuje celý ABI.

Mapování registru a blokované registry

Pokud chcete povolit interoperabilitu na úrovni typu s kódem x64, kód Arm64EC se zkompiluje se stejnými definicemi architektury preprocesoru jako kód x64.

Jinými slovy, _M_AMD64 a _AMD64_ jsou definovány. Jedním z typů ovlivněných tímto pravidlem CONTEXT je struktura. Struktura CONTEXT definuje stav procesoru v daném bodě. Používá se pro věci, jako jsou rozhraní API Exception Handling a GetThreadContext. Existující kód x64 očekává, že kontext procesoru bude reprezentován jako struktura x64 CONTEXT , jinak řečeno, struktura definovaná CONTEXT během kompilace x64.

Tuto strukturu musíte použít k reprezentaci kontextu procesoru při provádění kódu x64 a arm64EC kódu. Stávající kód nerozumí nové koncepci, jako je například sada registru procesoru, která se mění z funkce na funkci. Pokud používáte strukturu x64 CONTEXT k reprezentaci stavů provádění Arm64, v podstatě mapujete Arm64 registry do registrů x64.

Toto mapování také znamená, že nemůžete použít žádné registry Arm64, které se nevejdou do x64 CONTEXT. Jejich hodnoty můžou být ztraceny, kdykoli operace používá CONTEXT (a některé operace můžou být asynchronní a neočekávané, například garbage collection v běhovém prostředí spravovaného jazyka nebo APC).

Záhlaví Windows v sadě SDK představují pravidla mapování mezi Arm64EC a x64 registry ve struktuře ARM64EC_NT_CONTEXT. Tato struktura je v podstatě sjednocení CONTEXT struktury, přesně tak, jak je definováno pro x64, ale s dodatečným překrytím registru Arm64.

Například RCX mapuje na X0, RDX na X1, RSP na SP, RIP na PC a tak dále. Rejstříky x13, , x14, x23x24, x28a v16 prostřednictvím v31 nemají žádnou reprezentaci, a proto nelze použít v Arm64EC.

Toto omezení využití registru je prvním rozdílem mezi arm64 Classic a EC ABI.

Kontrola hovorů

Kontroloři funkcí byli součástí Windows od doby, kdy byla s Windows 8.1 zavedena ochrana řízení toku (CFG). Ověřovače volání jsou sanitizátory adres pro ukazatele funkcí (v době, kdy se tyto nástroje ještě nenazývaly sanitizátory adres). Pokaždé, když zkompilujete kód s možností /guard:cf, kompilátor vygeneruje další volání funkce kontroly těsně před každým nepřímým voláním nebo přeskočením. Systém Windows poskytuje samotnou funkci kontroly. CFG provádí kontrolu platnosti vůči známým správným cílům volání. Mezi binární soubory kompilované /guard:cf také patří tyto informace.

Tento příklad ukazuje použití kontroly volání v modelu Classic Arm64:

mov     x15, <target>
adrp    x16, __guard_check_icall_fptr
ldr     x16, [x16, __guard_check_icall_fptr]
blr     x16                                     ; check target function
blr     x15                                     ; call function

V případě CFG kontrola volání jednoduše potvrdí, pokud je cíl platný, nebo proces rychle selže, pokud cíl není platný. Kontroly hovorů mají vlastní konvence volání. Převezmou ukazatel funkce v registru, který není používán normální konvencí volání, a zachovávají všechny registry používané běžnou konvencí volání. Tímto způsobem nezpůsobují únik registrů kolem sebe.

Kontroly hovorů jsou volitelné ve všech ostatních rozhraních ABI systému Windows, ale povinné v Arm64EC. Kontrola volání v Arm64EC shromažďuje úlohu ověření architektury volané funkce. Ověří, zda je volání jinou funkcí EC ("Emulation Compatible") nebo funkce x64, která musí být provedena pod emulací. V mnoha případech je to možné ověřit pouze za běhu.

Kontroly volání Arm64EC vycházejí z existujících kontrolních prvků Arm64, ale mají mírně odlišné vlastní konvence volání. Přebírají další parametr a mohou upravit registr obsahující cílovou adresu. Pokud je například cílem kód pro x64, musí být řízení nejprve přeneseno na logiku emulační struktury.

Ve Arm64EC by se použila stejná kontrola volání:

mov     x11, <target>
adrp    x9, __os_arm64x_check_icall_cfg
ldr     x9, [x9, __os_arm64x_check_icall_cfg] 
adrp    x10, <name of the exit thunk>
add     x10, x10, <name of the exit thunk>
blr     x9                                      ; check target function
blr     x11                                     ; call function

Mezi drobné rozdíly oproti Klasickému Arm64 patří:

  • Název symbolu pro kontrolu volání se liší.
  • Cílová adresa se zadává x11 místo x15.
  • Cílová adresa (x11) je [in, out] místo [in].
  • K dispozici je dodatečný parametr, zajištěný prostřednictvím x10, který se nazývá „Exit Thunk“.

Exit Thunk je malý funkční blok, který transformuje parametry funkce z volací konvence Arm64EC na x64.

Kontrola volání Arm64EC je umístěna pomocí jiného symbolu než toho, který se používá pro ostatní ABI ve Windows. Na modelu Classic Arm64 ABI je symbol kontroly volání __guard_check_icall_fptr. Tento symbol bude v Arm64EC, ale je k dispozici pro staticky propojený kód x64, nikoli samotný kód Arm64EC. Kód Arm64EC bude používat buď __os_arm64x_check_icall nebo __os_arm64x_check_icall_cfg.

V Arm64EC nejsou kontroly volání volitelné. CFG je však stále nepovinný, podobně jako u ostatních ABI. CFG může být v době kompilace zakázána nebo může existovat legitimní důvod, proč nekontrolovat CFG, i když je povolen cfG (např. ukazatel funkce se nikdy nenachází v paměti RW). U nepřímého volání s kontrolou CFG by se měla použít kontrola __os_arm64x_check_icall_cfg. Pokud je CFG zakázaná nebo nepotřebná, __os_arm64x_check_icall měli byste místo toho použít.

Níže je souhrnná tabulka použití nástroje pro kontrolu volání v modelu Classic Arm64, x64 a Arm64EC a upozorňuje na skutečnost, že binární soubor Arm64EC může mít dvě možnosti v závislosti na architektuře kódu.

Binární Code Nechráněné nepřímé volání Nepřímé volání chráněné CFG
x64 x64 žádná kontrola hovorů __guard_check_icall_fptr nebo __guard_dispatch_icall_fptr
Arm64 Classic Arm64 žádná kontrola hovorů __guard_check_icall_fptr
Arm64EC x64 žádná kontrola hovorů __guard_check_icall_fptr nebo __guard_dispatch_icall_fptr
Arm64EC __os_arm64x_check_icall __os_arm64x_check_icall_cfg

Mít kód s povoleným CFG (tj. kód s odkazem na kontrolní moduly volání CFG) nezávisle na ABI neznamená ochranu CFG za běhu. Binární soubory chráněné CFG můžou běžet na nižší úrovni v systémech, které nepodporují CFG: Nástroj pro kontrolu volání se inicializuje pomocí pomocného no-op v době kompilace. Proces může mít také zakázané CFG na základě konfigurace. Pokud je CFG zakázaná (nebo podpora OS není k dispozici) u předchozích ABI, OS jednoduše neaktualizuje kontrolu volání při načtení binárního souboru. Pokud je ochrana CFG v Arm64EC zakázaná, operační systém nastaví __os_arm64x_check_icall_cfg stejnou hodnotu jako __os_arm64x_check_icall, což bude i nadále poskytovat potřebnou kontrolu cílové architektury ve všech případech, ale ne ochranu CFG.

Stejně jako u CFG v Classic Arm64 musí volání cílové funkce (x11) okamžitě následovat za voláním kontroloru volání. Adresa kontroly volání musí být umístěna v nestálém registru a ani adresa cílové funkce by se nikdy neměla kopírovat do jiného registru nebo přetékat do paměti.

Kontroly zásobníků

__chkstk kompilátor automaticky používá pokaždé, když funkce přidělí oblast na zásobníku větší než stránka. Aby se zabránilo přeskočení stránky ochrany zásobníku, která chrání konec zásobníku, __chkstk se volá, aby se zajistilo, že jsou všechny stránky v přidělené oblasti proložené.

__chkstk je obvykle volána z prologu funkce. Z tohoto důvodu a pro optimální generování kódu používá vlastní konvenci volání.

To znamená, že kód x64 a Arm64EC potřebují své vlastní, odlišné, __chkstk funkce, protože vstupní a výstupní thunky předpokládají standardní konvence volání.

x64 a Arm64EC sdílejí stejný obor názvů symbolů, takže nemohou být dvě funkce pojmenované __chkstk. Aby bylo možné přizpůsobit kompatibilitu s již existujícím kódem x64, název __chkstk bude přiřazen ke kontrole zásobníku x64. Kód Arm64EC použije __chkstk_arm64ec místo toho.

Konvence vlastního volání pro __chkstk_arm64ec je stejná jako pro Classic Arm64 __chkstk: x15 udává velikost přidělení v bajtech, dělenou 16. Zachovají se všechny nevolatelní registry i všechny volatelní registry, které jsou součástí standardní konvence volání.

Vše uvedené výše platí __chkstk stejně pro __security_check_cookie a jeho arm64EC protějšk: __security_check_cookie_arm64ec.

Variadické konvence volání

Arm64EC se řídí klasickými konvencemi volání Arm64 ABI s výjimkou variadických funkcí (označovaných také jako varargs nebo funkce s klíčovým slovem parametru se třemi tečkami (. .).

Pro variadický konkrétní případ se arm64EC řídí volací konvencí velmi podobnou x64 variadic, pouze s několika rozdíly. Následující seznam ukazuje hlavní pravidla pro Arm64EC variadic:

  • K předávání parametrů se používají pouze první čtyři registry: x0, x1, x2, x3. Zbývající parametry přetéknou do zásobníku. Toto pravidlo se přesně řídí konvencí volání x64 a liší se od Arm64 Classic, kde se používají registry x0x7.
  • Parametry s plovoucí desetinnou čárkou a SIMD předané pomocí registru používají obecný registr, nikoli SIMD. Toto pravidlo se podobá Modelu Arm64 Classic a liší se od modelu x64, kde se parametry FP/SIMD předávají v registru pro obecné účely i v registru SIMD. Například pro funkci f1(int, …) volanou jako f1(int, double), na x64, druhý parametr je přiřazen k oběma RDX a XMM1. U Arm64EC je druhý parametr přiřazen pouze x1.
  • Při předávání struktur podle hodnoty prostřednictvím registru platí pravidla velikosti x64: Struktury s velikostmi přesně 1, 2, 4 a 8 bajtů se načítají přímo do registru pro obecné účely. Struktury s jinými velikostmi přetéknou do zásobníku a ukazatel na přelité umístění je přiřazen registru. Toto pravidlo v podstatě převádí přístup podle hodnoty na přístup odkazem na základní úrovni. Na modelu Classic Arm64 ABI se struktury libovolné velikosti až 16 bajtů přiřazují přímo k registrům pro obecné účely.
  • Registr x4 načte ukazatel na první parametr předaný prostřednictvím zásobníku (pátý parametr). Toto pravidlo nezahrnuje struktury přeteklé kvůli omezením velikosti popsaným dříve.
  • Registr x5 načte velikost všech parametrů předaných zásobníkem (velikost všech parametrů počínaje pátým). Toto pravidlo neobsahuje struktury předané hodnotou, protože omezení velikosti uvedená dříve.

V následujícím příkladu pt_nova_function přebírá parametry v jiné než variadické podobě, takže se řídí konvencí volání Classic Arm64. Potom volá pt_va_function se stejnými parametry, ale jako variadické volání.

struct three_char {
    char a;
    char b;
    char c;
};

void
pt_va_function (
    double f,
    ...
);

void
pt_nova_function (
    double f,
    struct three_char tc,
    __int64 ull1,
    __int64 ull2,
    __int64 ull3
)
{
    pt_va_function(f, tc, ull1, ull2, ull3);
}

pt_nova_function přebírá pět parametrů, které přiřadí podle pravidel konvence volání Classic Arm64:

  • "f" je dvojitá. Přiřazuje se k d0.
  • "tc" je struktura s velikostí 3 bajtů. Přiřazuje se k x0.
  • ull1 je celé číslo 8 bajtů. Přiřazuje se k x1.
  • ull2 je celé číslo 8 bajtů. Přiřazuje se k x2.
  • ull3 je celé číslo 8 bajtů. Přiřazuje se k x3.

pt_va_function je variadická funkce, takže se řídí pravidly Arm64EC variadic popsaných dříve:

  • "f" je dvojitá. Přiřazuje se k x0.
  • "tc" je struktura s velikostí 3 bajtů. Přetéká do zásobníku a jeho umístění se načte do x1.
  • ull1 je celé číslo 8 bajtů. Přiřazuje se k x2.
  • ull2 je celé číslo 8 bajtů. Přiřazuje se k x3.
  • ull3 je celé číslo 8 bajtů. Přiřadí se přímo ke zásobníku.
  • x4 načte umístění ull3 v zásobníku.
  • x5 načte velikost ull3.

Následující příklad ukazuje možný výstup kompilace pro pt_nova_function, který znázorňuje rozdíly přiřazení parametrů popsané dříve.

stp         fp,lr,[sp,#-0x30]!
mov         fp,sp
sub         sp,sp,#0x10

str         x3,[sp]          ; Spill 5th parameter
mov         x3,x2            ; 4th parameter to x3 (from x2)
mov         x2,x1            ; 3rd parameter to x2 (from x1)
str         w0,[sp,#0x20]    ; Spill 2nd parameter
add         x1,sp,#0x20      ; Address of 2nd parameter to x1
fmov        x0,d0            ; 1st parameter to x0 (from d0)
mov         x4,sp            ; Address of the 1st in-stack parameter to x4
mov         x5,#8            ; Size of the in-stack parameter area

bl          pt_va_function

add         sp,sp,#0x10
ldp         fp,lr,[sp],#0x30
ret

Doplňky ABI

Pokud chcete dosáhnout transparentní interoperability s kódem x64, proveďte mnoho doplňků klasického modelu Arm64 ABI. Tyto doplňky zpracovávají rozdíly v konvencích volání mezi Arm64EC a x64.

Následující seznam obsahuje tyto doplňky:

Vstupní a výstupní thunky

Vstupní a výstupní překlenovací rutiny překládají konvenci volání Arm64EC (většinou stejné jako klasické Arm64) do konvence volání x64 a obráceně.

Běžnou chybnou představou je, že konvence volání můžete převést pomocí jednoho pravidla použitého u všech podpisů funkcí. Realitou je, že konvence volání mají pravidla přiřazení parametrů. Tato pravidla závisí na typu parametru a liší se mezi jednotlivými ABI. Důsledkem je, že překlad mezi ABI je specifický pro každou signaturu funkce a liší se podle typu každého parametru.

Zvažte následující funkci:

int fJ(int a, int b, int c, int d);

Přiřazení parametru probíhá takto:

  • Arm64: a -> x0, b -> x1, c -> x2, d -> x3
  • x64: a -> RCX, b -> RDX, c -> R8, d -> r9
  • Arm64 –> překlad x64: x0 –> RCX, x1 –> RDX, x2 –> R8, x3 –> R9

Teď zvažte jinou funkci:

int fK(int a, double b, int c, double d);

Přiřazení parametru probíhá takto:

  • Arm64: a -> x0, b -> d0, c -> x1, d -> d1
  • x64: a -> RCX, b -> XMM1, c -> R8, d -> XMM3
  • Překlad Arm64 –> x64: x0 –> RCX, d0 –> XMM1, x1 –> R8, d1 –> XMM3

Tyto příklady ukazují, že přiřazení a překlad parametrů se liší podle typu, ale také závisí na typech předchozích parametrů v seznamu. Tento detail je ilustrován třetím parametrem. V obou funkcích je inttyp parametru , ale výsledný překlad se liší.

Z tohoto důvodu existují vstupní a výstupní thunky a jsou speciálně přizpůsobené podle signatury každé jednotlivé funkce.

Oba typy thunků jsou funkce. Emulátor automaticky vyvolá vstupní bloky, když funkce x64 volají funkce Arm64EC (spuštění Enters Arm64EC). Při volání funkcí Arm64EC do funkcí x64, kontrolní volby automaticky vyvolávají funkce ukončení (ukončení z Arm64EC).

Při kompilaci kódu Arm64EC kompilátor vygeneruje vstupní blok pro každou funkci Arm64EC odpovídající jeho podpisu. Kompilátor také vygeneruje výstupní thunk pro každou funkci, kterou volá funkce Arm64EC.

Podívejte se na následující příklad:

struct SC {
    char a;
    char b;
    char c;
};

int fB(int a, double b, int i1, int i2, int i3);

int fC(int a, struct SC c, int i1, int i2, int i3);

int fA(int a, double b, struct SC c, int i1, int i2, int i3) {
    return fB(a, b, i1, i2, i3) + fC(a, c, i1, i2, i3);
}

Při kompilaci předchozího kódu, který cílí na Arm64EC, kompilátor vygeneruje:

  • Kód pro fA.
  • Vstupní funkce pro fA
  • Ukončit thunk pro fB
  • Ukončit blok pro fC

Kompilátor vygeneruje fA vstupní zástupce v případě, že fA je volán z kódu x64. Kompilátor vygeneruje ukončovací bloky pro fB a fC pro případ fB a fC jsou kódem x64.

Kompilátor může několikrát vygenerovat stejný exit thunk, protože je generuje na místě volání, nikoli v samotné funkci. Tato duplicita může vést k velkému množství nadbytečných thunks. Aby se zabránilo této duplicitě, kompilátor použije triviální pravidla optimalizace, aby se zajistilo, že se do konečného binárního souboru zahrnou jenom požadované bloky.

Například v binárním souboru, kde funkce A Arm64EC volá funkci BArm64EC , B není exportována a její adresa není nikdy známa mimo A. Je bezpečné odstranit výstupní 'thunk' z A do B, spolu se vstupním 'thunkem' pro B. Je také bezpečné aliasovat všechny výstupní a vstupní bloky, které obsahují stejný kód, i když byly generovány pro různé funkce.

Ukončovací bloky

Pomocí ukázkových funkcí fA, fBa fC v předchozí části kompilátor generuje oba fB a fC ukončovací bloky následujícím způsobem:

Ukončete thunk na int fB(int a, double b, int i1, int i2, int i3);

$iexit_thunk$cdecl$i8$i8di8i8i8:
    stp         fp,lr,[sp,#-0x10]!
    mov         fp,sp
    sub         sp,sp,#0x30
    adrp        x8,__os_arm64x_dispatch_call_no_redirect
    ldr         xip0,[x8]
    str         x3,[sp,#0x20]  ; Spill 5th param (i3) into the stack
    fmov        d1,d0          ; Move 2nd param (b) from d0 to XMM1 (x1)
    mov         x3,x2          ; Move 4th param (i2) from x2 to R9 (x3)
    mov         x2,x1          ; Move 3rd param (i1) from x1 to R8 (x2)
    blr         xip0           ; Call the emulator
    mov         x0,x8          ; Move return from RAX (x8) to x0
    add         sp,sp,#0x30
    ldp         fp,lr,[sp],#0x10
    ret

Ukončete thunk to int fC(int a, struct SC c, int i1, int i2, int i3);

$iexit_thunk$cdecl$i8$i8m3i8i8i8:
    stp         fp,lr,[sp,#-0x20]!
    mov         fp,sp
    sub         sp,sp,#0x30
    adrp        x8,__os_arm64x_dispatch_call_no_redirect
    ldr         xip0,[x8]
    str         w1,[sp,#0x40]       ; Spill 2nd param (c) onto the stack
    add         x1,sp,#0x40         ; Make RDX (x1) point to the spilled 2nd param
    str         x4,[sp,#0x20]       ; Spill 5th param (i3) into the stack
    blr         xip0                ; Call the emulator
    mov         x0,x8               ; Move return from RAX (x8) to x0
    add         sp,sp,#0x30
    ldp         fp,lr,[sp],#0x20
    ret

V případě fB přítomnost parametru double způsobí přeorganizování zbývajícího přiřazení registru GP, což je důsledek různých pravidel přiřazení pro Arm64 a x64. Vidíte také to, že x64 přiřazuje registrům pouze čtyři parametry, takže pátý parametr musí být uložen do zásobníku.

fC V případě je druhým parametrem struktura délky 3 bajtů. Arm64 umožňuje přiřazení jakékoli struktury velikosti přímo k registru. x64 umožňuje pouze velikosti 1, 2, 4 a 8. Tento Exit Thunk musí přenést struct z registru na zásobník a místo toho přiřadit ukazatel do registru. Tento přístup stále využívá jeden registr (k přenosu ukazatele), takže nezmění přiřazení zbývajících registrů: u třetího a čtvrtého parametru nedojde k přemíchání registru. Stejně jako u fB případu musí být pátý parametr vylit do zásobníku.

Další důležité informace pro Exit Thunks:

  • Kompilátor je pojmenuje ne podle názvu funkce, z níž jsou překládány, ale spíše podle signatury, kterou zpracovávají. Tato konvence vytváření názvů usnadňuje hledání redundancí.
  • Kontrola volání nastaví registr x9 tak, aby přenášel adresu cílové funkce (x64). Exit Thunk volá emulátor a předává x9 beze změn.

Po změně uspořádání parametrů Exit Thunk volá do emulátoru prostřednictvím __os_arm64x_dispatch_call_no_redirect.

Stojí za to v tuto chvíli přezkoumat funkci kontrolování volání a uživatelského ABI. Jak vypadá nepřímé volání fB:

mov     x11, <target>
adrp    x9, __os_arm64x_check_icall_cfg
ldr     x9, [x9, __os_arm64x_check_icall_cfg] 
adrp    x10, $iexit_thunk$cdecl$i8$i8di8i8i8    ; fB function's exit thunk
add     x10, x10, $iexit_thunk$cdecl$i8$i8di8i8i8
blr     x9                                      ; check target function
blr     x11                                     ; call function

Při volání kontroloru hovorů

  • x11 udává adresu cílové funkce pro volání (v tomto případěfB). V tomto okamžiku nemusí kontrola volání vědět, jestli je cílová funkce Arm64EC nebo x64.
  • x10 poskytuje výstupní thunk odpovídající signatuře volané funkce (fB v tomto případě).

Data, která vrátí kontrola volání, závisí na tom, jestli je cílová funkce Arm64EC nebo x64.

Pokud je cílem Arm64EC:

  • x11 vrátí adresu kódu Arm64EC, který se má volat. Tato hodnota může být stejná jako hodnota uvedená zde.

Pokud je cílem kód x64:

  • x11 vrátí adresu Exit Thunk. Tato adresa se zkopíruje ze vstupu zadaného v x10.
  • x10 vrátí adresu Exit Thunku, neporušenou vstupem.
  • x9 vrátí cílovou funkci x64. Tato hodnota může být stejná jako hodnota zadaná prostřednictvím x11.

Kontrolory volání vždy ponechávají registry parametrů volací konvence nerušené. Volající kód by měl ihned po volání kontroly hovoru pokračovat s blr x11 (nebo br x11 v případě koncového hovoru). Kontroly hovorů vždy uchovávají tyto registry nad rámec standardních nevolatilních registrů: x0-x8, x15(chkstk) a q0-q7.

Vstupní thunky

Entry Thunks se postará o transformace potřebné z x64 na konvence volání Arm64. Tato transformace je v podstatě obrácená z exit thunks, ale zahrnuje několik dalších aspektů, které je potřeba vzít v úvahu.

Představte si předchozí příklad kompilace fA. Vstupní Thunk je generován, aby mohl kód x64 volat fA.

Položka Thunk pro int fA(int a, double b, struct SC c, int i1, int i2, int i3)

$ientry_thunk$cdecl$i8$i8dm3i8i8i8:
    stp         q6,q7,[sp,#-0xA0]!  ; Spill full non-volatile XMM registers
    stp         q8,q9,[sp,#0x20]
    stp         q10,q11,[sp,#0x40]
    stp         q12,q13,[sp,#0x60]
    stp         q14,q15,[sp,#0x80]
    stp         fp,lr,[sp,#-0x10]!
    mov         fp,sp
    ldrh        w1,[x2]             ; Load 3rd param (c) bits [15..0] directly into x1
    ldrb        w8,[x2,#2]          ; Load 3rd param (c) bits [16..23] into temp w8
    bfi         w1,w8,#0x10,#8      ; Merge 3rd param (c) bits [16..23] into x1
    mov         x2,x3               ; Move the 4th param (i1) from R9 (x3) to x2
    fmov        d0,d1               ; Move the 2nd param (b) from XMM1 (d1) to d0
    ldp         x3,x4,[x4,#0x20]    ; Load the 5th (i2) and 6th (i3) params
                                    ; from the stack into x3 and x4 (using x4)
    blr         x9                  ; Call the function (fA)
    mov         x8,x0               ; Move the return from x0 to x8 (RAX)
    ldp         fp,lr,[sp],#0x10
    ldp         q14,q15,[sp,#0x80]  ; Restore full non-volatile XMM registers
    ldp         q12,q13,[sp,#0x60]
    ldp         q10,q11,[sp,#0x40]
    ldp         q8,q9,[sp,#0x20]
    ldp         q6,q7,[sp],#0xA0
    adrp        xip0,__os_arm64x_dispatch_ret
    ldr         xip0,[xip0,__os_arm64x_dispatch_ret]
    br          xip0

Emulátor poskytuje adresu cílové funkce v x9.

Před voláním entry Thunk emulátor x64 zobrazí zpáteční adresu ze zásobníku LR do registru. LR se poté očekává, že bude odkazovat na kód x64, když se kontrola přenese do Entry Thunk.

Emulátor může také provést další úpravu zásobníku v závislosti na následujících případech: Arm64 i x64 ABI definují požadavek na zarovnání zásobníku, ve kterém musí být zásobník zarovnán na 16 bajtů v okamžiku, kdy je volána funkce. Při spuštění kódu Arm64 hardware vynucuje toto pravidlo, ale pro x64 neexistuje žádné vynucení hardwaru. Při spouštění kódu x64 může dojít k chybnému volání funkcí s nevyrovnaným zásobníkem, aniž by byla chyba po určitou dobu zaznamenána, dokud není použita nějaká 16-bajtová zarovnávací instrukce (jakou jsou některé instrukce SSE) nebo dokud není zavolán kód Arm64EC.

Pokud chcete tento potenciální problém s kompatibilitou vyřešit, před voláním entry Thunk emulátor vždy zarovná ukazatel zásobníku na 16 bajtů a uloží jeho původní hodnotu v x4 registru. Entry Thunks tak vždy začínají s zarovnaným zásobníkem, ale stále mohou správně odkazovat na parametry předané v zásobníku prostřednictvím x4.

Pokud jde o nevolatelní registrace SIMD, existuje významný rozdíl mezi konvencemi volání Arm64 a x64. Na Arm64 se nízké 8 bajty (64 bitů) registru považují za nestálé. Jinými slovy, pouze Dn část Qn registrů je nestálá. Na platformě x64 se celých 16 bajtů XMMn registru považuje za nestálé. Kromě toho na platformě x64 jsou XMM6 a XMM7 neměnné registry, zatímco D6 a D7 (odpovídající registry Arm64) jsou nestálé.

Aby bylo možné tyto asymetrie manipulace s registrem SIMD vyřešit, musí vstupní thunky explicitně uložit všechny registry SIMD, které jsou v x64 považovány za nestálé. Toto ukládání je potřeba pouze u vstupních thunků (nikoli výstupních thunků), protože x64 je přísnější než Arm64. Jinými slovy, pravidla pro uložení a zachování registrů v x64 překračují požadavky Arm64 ve všech případech.

Chcete-li vyřešit správné obnovení těchto hodnot registru při odvíjení zásobníku (například setjmp + longjmp nebo throw + catch), byl zaveden nový unwind opcode: save_any_reg (0xE7). Tento nový 3-bajtový unwind opcode umožňuje uložit jakýkoli registr pro obecné účely nebo SIMD (včetně těch, které jsou považovány za nestálé) a včetně registrů Qn v plné velikosti. Tento nový opcode se používá pro Qn operace přetečení a naplnění registru. save_any_reg je kompatibilní s save_next_pair (0xE6).

Pro lepší orientaci následující informace o odvíjení patří k Entry Thunk uvedenému dříve:

   Prolog unwind:
      06: E76689.. +0004 stp   q6,q7,[sp,#-0xA0]! ; Actual=stp   q6,q7,[sp,#-0xA0]!
      05: E6...... +0008 stp   q8,q9,[sp,#0x20]   ; Actual=stp   q8,q9,[sp,#0x20]
      04: E6...... +000C stp   q10,q11,[sp,#0x40] ; Actual=stp   q10,q11,[sp,#0x40]
      03: E6...... +0010 stp   q12,q13,[sp,#0x60] ; Actual=stp   q12,q13,[sp,#0x60]
      02: E6...... +0014 stp   q14,q15,[sp,#0x80] ; Actual=stp   q14,q15,[sp,#0x80]
      01: 81...... +0018 stp   fp,lr,[sp,#-0x10]! ; Actual=stp   fp,lr,[sp,#-0x10]!
      00: E1...... +001C mov   fp,sp              ; Actual=mov   fp,sp
                   +0020 (end sequence)
   Epilog #1 unwind:
      0B: 81...... +0044 ldp   fp,lr,[sp],#0x10   ; Actual=ldp   fp,lr,[sp],#0x10
      0C: E74E88.. +0048 ldp   q14,q15,[sp,#0x80] ; Actual=ldp   q14,q15,[sp,#0x80]
      0F: E74C86.. +004C ldp   q12,q13,[sp,#0x60] ; Actual=ldp   q12,q13,[sp,#0x60]
      12: E74A84.. +0050 ldp   q10,q11,[sp,#0x40] ; Actual=ldp   q10,q11,[sp,#0x40]
      15: E74882.. +0054 ldp   q8,q9,[sp,#0x20]   ; Actual=ldp   q8,q9,[sp,#0x20]
      18: E76689.. +0058 ldp   q6,q7,[sp],#0xA0   ; Actual=ldp   q6,q7,[sp],#0xA0
      1C: E3...... +0060 nop                      ; Actual=90000030
      1D: E3...... +0064 nop                      ; Actual=ldr   xip0,[xip0,#8]
      1E: E4...... +0068 end                      ; Actual=br    xip0
                   +0070 (end sequence)

Jakmile se funkce Arm64EC vrátí, __os_arm64x_dispatch_ret rutina znovu přejde do emulátoru a vrátí se zpět na kód x64 (na který LR odkazuje).

Funkce Arm64EC si vyhrazují čtyři bajty před první instrukcí pro ukládání informací, které se používají za běhu. V těchto čtyřech bajtech lze najít relativní adresu Entry Thunk pro funkci. Při volání funkce x64 do funkce Arm64EC přečte emulátor čtyři bajty před začátkem funkce, zamaskuje nižší dvě bity a přidá toto množství k adrese funkce. Tento proces vytvoří adresu pro volání Entry Thunk.

Nastavovací thunky

Adjustor Thunks jsou funkce bez signatury, které přenášejí řízení (koncovým voláním) na jinou funkci. Před přenosem ovládacího prvku transformují jeden z parametrů. Typ transformovaných parametrů je známý, ale všechny zbývající parametry můžou být cokoli a můžou být v libovolném čísle. Adjustor Thunks se nedotýkají žádného registru, který může obsahovat parametr, a nedotýkají se zásobníku. Díky této vlastnosti jsou Adjustor Thunks funkce bez signatury.

Kompilátor může automaticky generovat Adjustor Thunks. Toto je běžné, například u vícenásobné dědičnosti v jazyce C++, kde jakákoli virtuální metoda může delegovat na nadřazenou třídu beze změny, s výjimkou úpravy this ukazatele.

Následující příklad ukazuje skutečný scénář:

[thunk]:CObjectContext::Release`adjustor{8}':
    sub         x0,x0,#8
    b           CObjectContext::Release

Thunk odečte 8 bajtů od ukazatele this a přesměruje volání na nadřazenou třídu.

Funkce Arm64EC, které lze volat z funkcí x64, musí mít odpovídající vstupní Thunk. Entry Thunk je specifický pro signaturu. Funkce bez podpisu Arm64, například Adjustor Thunks, potřebují jiný mechanismus, který dokáže zpracovat funkce bez podpisu.

Entry Thunk z Adjustor Thunk používá pomocníka __os_arm64x_x64_jump k odložení provedení skutečné práce Entry Thunk (úprava parametrů z jedné konvence na druhou) na další volání. V té chvíli se podpis projeví. To zahrnuje možnost vůbec neprovádět žádné úpravy volací konvence, pokud se ukáže, že cílem Adjustor Thunk je funkce x64. Nezapomeňte, že v okamžiku, kdy se začne spouštět vstupní thunk, jsou parametry ve formě x64.

V předchozím příkladu zvažte, jak kód vypadá v Arm64EC.

Adjustor Thunk v Arm64EC

[thunk]:CObjectContext::Release`adjustor{8}':
    sub         x0,x0,#8
    adrp        x9,CObjectContext::Release
    add         x11,x9,CObjectContext::Release
    stp         fp,lr,[sp,#-0x10]!
    mov         fp,sp
    adrp        xip0, __os_arm64x_check_icall
    ldr         xip0,[xip0, __os_arm64x_check_icall]
    blr         xip0
    ldp         fp,lr,[sp],#0x10
    br          x11

Vstupní kmen pro úpravce Thunk

[thunk]:CObjectContext::Release$entry_thunk`adjustor{8}':
    sub         x0,x0,#8
    adrp        x9,CObjectContext::Release
    add         x9,x9,CObjectContext::Release
    adrp        xip0,__os_arm64x_x64_jump
    ldr         xip0,[xip0,__os_arm64x_x64_jump]
    br          xip0

Sekvence s rychlým posunem vpřed

Některé aplikace provádí změny za běhu funkcí umístěných v binárních souborech, které nevlastní, ale závisejí na binárních souborech ( obvykle binárních souborech operačního systému) za účelem zrušení provádění při volání funkce. Tento proces se také označuje jako hookování.

Na vysoké úrovni je proces háčkování jednoduchý. Přesto je ale hookování specifické pro architekturu a poměrně složité kvůli potenciálním variantám, které musí logika řešit.

Obecně platí, že proces zahrnuje následující kroky:

  • Určete adresu funkce, která se má připojit.
  • První instrukci funkce nahraďte přesměrováním skokem do hookovací rutiny.
  • Po dokončení háku se vraťte do původní logiky, která zahrnuje spuštění nahrazené původní instrukce.

Varianty vznikají z následujících věcí:

  • Velikost první instrukce: Je vhodné ji nahradit JMP, která je stejná nebo menší, aby se zabránilo nahrazení horní části funkce, zatímco jiné vlákno může běžet ve letu.
  • Typ první instrukce: Pokud má první instrukce určitou relativní povahu počítače, může změna umístění vyžadovat změny, jako jsou pole posunu. Vzhledem k tomu, že může dojít k přetečení, pokud je instrukce přesunuta na vzdálené místo, může tato změna vyžadovat poskytnutí odpovídající logiky pomocí zcela odlišných instrukcí.

Vzhledem ke všemu tomuto složitosti je robustní a obecná logika připojení vzácně nalezena. Logika přítomná v aplikacích se často dokáže vypořádat pouze s omezenou sadou případů, se kterými aplikace očekává, že se setká v konkrétních rozhraních API, která zajímá. Není těžké si představit, jak velký problém s kompatibilitou aplikací to představuje. Dokonce i jednoduchá změna kódu nebo optimalizace kompilátoru může způsobit, že aplikace budou nepoužitelné, pokud kód již nebude vypadat přesně očekávaným způsobem.

Co by se stalo s těmito aplikacemi, pokud by při nastavování háku narazili na kód Arm64? Určitě by selžou.

Funkce FFS (Fast-Forward Sequence) řeší tento požadavek na kompatibilitu v Arm64EC.

FFS jsou velmi malé funkce x64, které neobsahují skutečnou logiku a koncové volání skutečné funkce Arm64EC. Jsou volitelné, ale ve výchozím nastavení jsou povolené pro všechny exporty dll a pro všechny funkce zdobené __declspec(hybrid_patchable).

V těchto případech, když kód získá ukazatel na danou funkci, buď GetProcAddress v případě exportu, nebo &function v __declspec(hybrid_patchable) případě, výsledná adresa obsahuje kód x64. Tento kód x64 předává legitimní funkci x64, která splňuje většinu aktuálně dostupné logiky připojení.

Podívejte se na následující příklad (zpracování chyb vynecháno kvůli stručnosti):

auto module_handle = 
    GetModuleHandleW(L"api-ms-win-core-processthreads-l1-1-7.dll");

auto pgma = 
    (decltype(&GetMachineTypeAttributes))
        GetProcAddress(module_handle, "GetMachineTypeAttributes");

hr = (*pgma)(IMAGE_FILE_MACHINE_Arm64, &MachineAttributes);

Hodnota ukazatele funkce v pgma proměnné obsahuje adresu GetMachineTypeAttributesFFS.

Tento příklad ukazuje posloupnost Fast-Forward:

kernelbase!EXP+#GetMachineTypeAttributes:
00000001`800034e0 488bc4          mov     rax,rsp
00000001`800034e3 48895820        mov     qword ptr [rax+20h],rbx
00000001`800034e7 55              push    rbp
00000001`800034e8 5d              pop     rbp
00000001`800034e9 e922032400      jmp     00000001`80243810

Funkce FFS x64 má kanonický prolog a epilog, který končí konečným voláním (skokem) na reálnou funkci GetMachineTypeAttributes v kódu Arm64EC.

kernelbase!GetMachineTypeAttributes:
00000001`80243810 d503237f pacibsp
00000001`80243814 a9bc7bfd stp         fp,lr,[sp,#-0x40]!
00000001`80243818 a90153f3 stp         x19,x20,[sp,#0x10]
00000001`8024381c a9025bf5 stp         x21,x22,[sp,#0x20]
00000001`80243820 f9001bf9 str         x25,[sp,#0x30]
00000001`80243824 910003fd mov         fp,sp
00000001`80243828 97fbe65e bl          kernelbase!#__security_push_cookie
00000001`8024382c d10083ff sub         sp,sp,#0x20
                           [...]

Bylo by poměrně neefektivní, kdyby bylo nutné spustit pět emulovaných instrukcí x64 mezi dvěma funkcemi Arm64EC. Funkce FFS jsou speciální. Funkce FFS se ve skutečnosti nespouštějí, pokud zůstanou nezměněné. Pomocná rutina pro kontrolu volání efektivně kontroluje, jestli se služba FFS nezměnila. V takovém případě se volání přenese přímo do skutečného cíle. Pokud se FFS změní jakýmkoli možným způsobem, pak už se nejedná o FFS. Provádění se přenese do upraveného FFS a spustí libovolný kód, který by tam mohl být, emuluje objížďku a jakoukoli logiku připojení.

Když háček přenese provádění zpět na konec FFS, nakonec dosáhne koncového volání kódu Arm64EC, který se pak spustí po háku, stejně jako aplikace očekává.

Autorská tvorba Arm64EC v assembleru

Hlavičky sady Windows SDK a kompilátor jazyka C zjednodušují úlohu vytváření sestavení Arm64EC. Pomocí kompilátoru jazyka C můžete například vygenerovat vstupní a výstupní bloky pro funkce, které nejsou zkompilovány z kódu jazyka C.

Představte si příklad ekvivalentu následující funkce fD , kterou musíte vytvořit v sestavení (ASM). Kód Arm64EC i x64 může tuto funkci volat a pfE ukazatel funkce může odkazovat na kód Arm64EC nebo x64.

typedef int (PF_E)(int, double);

extern PF_E * pfE;

int fD(int i, double d) {
    return (*pfE)(i, d);
}

Zápis fD v ASM může vypadat jako následující kód:

#include "ksarm64.h"

        IMPORT  __os_arm64x_check_icall_cfg
        IMPORT |$iexit_thunk$cdecl$i8$i8d|
        IMPORT pfE

        NESTED_ENTRY_COMDAT A64NAME(fD)
        PROLOG_SAVE_REG_PAIR fp, lr, #-16!

        adrp    x11, pfE                                  ; Get the global function
        ldr     x11, [x11, pfE]                           ; pointer pfE

        adrp    x9, __os_arm64x_check_icall_cfg           ; Get the EC call checker
        ldr     x9, [x9, __os_arm64x_check_icall_cfg]     ; with CFG
        adrp    x10, |$iexit_thunk$cdecl$i8$i8d|          ; Get the Exit Thunk for
        add     x10, x10, |$iexit_thunk$cdecl$i8$i8d|     ; int f(int, double);
        blr     x9                                        ; Invoke the call checker

        blr     x11                                       ; Invoke the function

        EPILOG_RESTORE_REG_PAIR fp, lr, #16!
        EPILOG_RETURN

        NESTED_END

        end

V předchozím příkladu:

  • Arm64EC používá stejnou deklaraci procedury a makra prologu/epilogu jako Arm64.
  • Zalamujte názvy funkcí pomocí A64NAME makra. Při kompilaci kódu C nebo C++ jako Arm64EC kompilátor označí OBJ jako ARM64EC, který obsahuje kód Arm64EC. Toto označení se nestane s ARMASM. Při kompilaci kódu ASM můžete informovat překladač, že vytvořený kód je Arm64EC, tím že před názvem funkce přidáte předponu #. Makro A64NAME provede tuto operaci, pokud _ARM64EC_ je definována a ponechá název beze změny, pokud _ARM64EC_ není definován. Tento přístup umožňuje sdílet zdrojový kód mezi Arm64 a Arm64EC.
  • Pokud je cílová funkce x64, musíte nejprve spustit pfE ukazatel funkce pomocí kontroly volání EC spolu s příslušným výstupním útržkem.

Generování vstupních a výstupních bloků

Dalším krokem je vygenerování vstupního thunku pro fD a výstupního thunku pro pfE. Kompilátor jazyka C může tuto úlohu provést s minimálním úsilím pomocí klíčového slova kompilátoru _Arm64XGenerateThunk .

void _Arm64XGenerateThunk(int);

int fD2(int i, double d) {
    UNREFERENCED_PARAMETER(i);
    UNREFERENCED_PARAMETER(d);
    _Arm64XGenerateThunk(2);
    return 0;
}

int fE(int i, double d) {
    UNREFERENCED_PARAMETER(i);
    UNREFERENCED_PARAMETER(d);
    _Arm64XGenerateThunk(1);
    return 0;
}

Klíčové slovo _Arm64XGenerateThunk instruuje kompilátor jazyka C, aby použil signaturu funkce, ignoroval tělo a vygeneroval buď výstupní thunk (pokud je parametr 1) nebo vstupní thunk (pokud je parametr 2).

Generování thunk umístěte do vlastního souboru C. Být v samostatných souborech usnadňuje potvrzení názvů symbolů vypsáním odpovídajících OBJ symbolů nebo dokonce rozložením.

Vlastní vstupní bloky

Sada SDK obsahuje makra, která vám pomůžou vytvářet vlastní ručně zakódované bloky položek. Tato makra můžete použít při vytváření vlastních bloků úprav.

Většina bloků pro úpravce je generována kompilátorem jazyka C++, ale můžete je také generovat ručně. Můžete ručně vygenerovat blok úpravce, když obecné zpětné volání přenese řízení do skutečného zpětného volání a jeden z parametrů identifikuje skutečné zpětné volání.

Následující příklad ukazuje adjustor thunk v rámci Arm64 Classic kódu:

    NESTED_ENTRY MyAdjustorThunk
    PROLOG_SAVE_REG_PAIR    fp, lr, #-16!
    ldr     x15, [x0, 0x18]
    adrp    x16, __guard_check_icall_fptr
    ldr     x16, [x16, __guard_check_icall_fptr]
    blr     xip0
    EPILOG_RESTORE_REG_PAIR fp, lr, #16
    EPILOG_END              br  x15
    NESTED_END

V tomto příkladu první parametr poskytuje odkaz na strukturu. Kód načte cílovou adresu funkce z prvku této struktury. Vzhledem k tomu, že je struktura zapisovatelná, musí ochrana toku řízení (CFG) ověřit cílovou adresu.

Následující příklad ukazuje, jak portovat ekvivalentní blok úpravce do Arm64EC:

    NESTED_ENTRY_COMDAT A64NAME(MyAdjustorThunk)
    PROLOG_SAVE_REG_PAIR    fp, lr, #-16!
    ldr     x11, [x0, 0x18]
    adrp    xip0, __os_arm64x_check_icall_cfg
    ldr     xip0, [xip0, __os_arm64x_check_icall_cfg]
    blr     xip0
    EPILOG_RESTORE_REG_PAIR fp, lr, #16
    EPILOG_END              br  x11
    NESTED_END

Předchozí kód nezahrnuje výstupní thunk (v registru x10). Tento přístup není možný, protože kód se může spustit pro mnoho různých signatur. Tento kód využívá nastavení volajícího x10 na výstupní thunk. Volající provede volání, které cílí na explicitní podpis.

Předchozí kód potřebuje vstupní proceduru pro vyřešení situace, kdy je volajícím kód x64. Následující příklad ukazuje, jak vytvořit odpovídající entry thunk pomocí makra pro vlastní entry thunk:

    ARM64EC_CUSTOM_ENTRY_THUNK A64NAME(MyAdjustorThunk)
    ldr     x9, [x0, 0x18]
    adrp    xip0, __os_arm64x_x64_jump
    ldr     xip0, [xip0, __os_arm64x_x64_jump]
    br      xip0
    LEAF_END

Na rozdíl od ostatních funkcí, tento vstupní thunk nepředá nakonec řízení do přidružené funkce (úpravný thunk). V tomto případě vstupní thunk vloží samotnou funkčnost (provádí úpravu parametru) a přes pomocnou rutinu __os_arm64x_x64_jump přenese řízení přímo na koncový cíl.

Dynamické generování kódu ARM64EC (kompilace JIT)

V procesech Arm64EC existují dva typy spustitelné paměti: kód Arm64EC a kód x64.

Operační systém extrahuje tyto informace z načtených binárních souborů. Binární soubory x64 jsou všechny x64 a binární soubory Arm64EC obsahují rozsahovou tabulku pro Arm64EC proti x64 kódovým stránkám.

A co dynamicky generovaný kód? Kompilátory JIT (Just-in-Time) generují kód za běhu, který není podporován žádným binárním souborem.

Tento proces obvykle zahrnuje následující kroky:

  • Přidělování zapisovatelné paměti (VirtualAlloc).
  • Vytvoření kódu v přidělené paměti.
  • Změna ochrany paměti z čtení a zápisu na čtení a vykonávání (VirtualProtect).
  • Přidání položek záznamů unwind funkcí pro všechny netriviální (nedědičné) generované funkce (RtlAddFunctionTable nebo RtlAddGrowableFunctionTable).

Z důvodů triviální kompatibility, pokud aplikace provádí tyto kroky v procesu Arm64EC, operační systém považuje kód za kód x64. K tomuto chování dochází u jakéhokoli procesu, který používá nezměněný modul runtime x64 Java, modul runtime .NET, modul JavaScript atd.

Pokud chcete vygenerovat dynamický kód Arm64EC, postupujte podle stejného procesu se dvěma rozdíly:

  • Při přidělování paměti použijte novější VirtualAlloc2 (místo VirtualAlloc nebo VirtualAllocEx) a zadejte MEM_EXTENDED_PARAMETER_EC_CODE atribut.
  • Při přidávání položek funkce:
    • Musí být ve formátu Arm64. Při kompilaci kódu Arm64EC se typ RUNTIME_FUNCTION shoduje s formátem x64. Pro formát Arm64 při kompilaci Arm64EC použijte místo toho typ ARM64_RUNTIME_FUNCTION.
    • Nepoužívejte starší RtlAddFunctionTable rozhraní API. Vždy používejte novější RtlAddGrowableFunctionTable rozhraní API.

Následující příklad ukazuje přidělení paměti:

    MEM_EXTENDED_PARAMETER Parameter = { 0 };
    Parameter.Type = MemExtendedParameterAttributeFlags;
    Parameter.ULong64 = MEM_EXTENDED_PARAMETER_EC_CODE;

    HANDLE process = GetCurrentProcess();
    ULONG allocationType = MEM_RESERVE;
    DWORD protection = PAGE_EXECUTE_READ | PAGE_TARGETS_INVALID;

    address = VirtualAlloc2 (
        process,
        NULL,
        numBytesToAllocate,
        allocationType,
        protection,
        &Parameter,
        1);

A následující příklad ukazuje, jak přidat jednu položku funkce unwind:

ARM64_RUNTIME_FUNCTION FunctionTable[1];

FunctionTable[0].BeginAddress = 0;
FunctionTable[0].Flags = PdataPackedUnwindFunction;
FunctionTable[0].FunctionLength = nSize / 4;
FunctionTable[0].RegF = 0;                   // no D regs saved
FunctionTable[0].RegI = 0;                   // no X regs saved beyond fp,lr
FunctionTable[0].H = 0;                      // no home for x0-x7
FunctionTable[0].CR = PdataCrChained;        // stp fp,lr,[sp,#-0x10]!
                                             // mov fp,sp
FunctionTable[0].FrameSize = 1;              // 16 / 16 = 1

this->DynamicTable = NULL;
Result == RtlAddGrowableFunctionTable(
    &this->DynamicTable,
    reinterpret_cast<PRUNTIME_FUNCTION>(FunctionTable),
    1,
    1,
    reinterpret_cast<ULONG_PTR>(pBegin),
    reinterpret_cast<ULONG_PTR>(reinterpret_cast<PBYTE>(pBegin) + nSize)
);