Poznámka:
Přístup k této stránce vyžaduje autorizaci. Můžete se zkusit přihlásit nebo změnit adresáře.
Přístup k této stránce vyžaduje autorizaci. Můžete zkusit změnit adresáře.
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.
- mapování registrů a blokovaných registrů
- nástroje pro kontrolu hovorů
- kontrolní mechanismy zásobníku
- Variadická konvence volání
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á
x11místox15. - 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í registryx0ažx7. - 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 jakof1(int, double), na x64, druhý parametr je přiřazen k oběmaRDXaXMM1. U Arm64EC je druhý parametr přiřazen pouzex1. - 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
x4nač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
x5nač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. -
ull1je celé číslo 8 bajtů. Přiřazuje se kx1. -
ull2je celé číslo 8 bajtů. Přiřazuje se kx2. -
ull3je celé číslo 8 bajtů. Přiřazuje se kx3.
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. -
ull1je celé číslo 8 bajtů. Přiřazuje se kx2. -
ull2je celé číslo 8 bajtů. Přiřazuje se kx3. -
ull3je celé číslo 8 bajtů. Přiřadí se přímo ke zásobníku. -
x4načte umístěníull3v zásobníku. -
x5načte velikostull3.
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
- Ukončení thunks
- Vstupní thunks
- Přizpůsobovací thunky
- Fast-Forward sekvence
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
x9tak, aby přenášel adresu cílové funkce (x64). Exit Thunk volá emulátor a předáváx9beze 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ů
-
x11udá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. -
x10poskytuje výstupní thunk odpovídající signatuře volané funkce (fBv 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:
-
x11vrá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:
-
x11vrátí adresu Exit Thunk. Tato adresa se zkopíruje ze vstupu zadaného vx10. -
x10vrátí adresu Exit Thunku, neporušenou vstupem. -
x9vrátí cílovou funkci x64. Tato hodnota může být stejná jako hodnota zadaná prostřednictvímx11.
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í
A64NAMEmakra. Při kompilaci kódu C nebo C++ jako Arm64EC kompilátor označíOBJjakoARM64EC, který obsahuje kód Arm64EC. Toto označení se nestane sARMASM. 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#. MakroA64NAMEprovede 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
pfEukazatel 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 (
RtlAddFunctionTableneboRtlAddGrowableFunctionTable).
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ístoVirtualAllocneboVirtualAllocEx) a zadejteMEM_EXTENDED_PARAMETER_EC_CODEatribut. - 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_FUNCTIONshoduje s formátem x64. Pro formát Arm64 při kompilaci Arm64EC použijte místo toho typARM64_RUNTIME_FUNCTION. - Nepoužívejte starší
RtlAddFunctionTablerozhraní API. Vždy používejte novějšíRtlAddGrowableFunctionTablerozhraní API.
- Musí být ve formátu Arm64. Při kompilaci kódu Arm64EC se typ
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)
);
Windows on Arm