Notitie
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen u aan te melden of de directory te wijzigen.
Voor toegang tot deze pagina is autorisatie vereist. U kunt proberen de mappen te wijzigen.
Arm64EC ("Emulation Compatible") is een nieuwe binaire interface (ABI) voor het bouwen van apps voor Windows 11 op Arm. Zie Arm64EC gebruiken om apps te bouwen voor Windows 11 op Arm 11-apparatenvoor een overzicht van Arm64EC en hoe u Win32-apps kunt bouwen als Arm64EC.
Dit artikel biedt een gedetailleerd overzicht van de Arm64EC ABI met genoeg informatie voor een applicatie-ontwikkelaar om code te schrijven en te debuggen die voor Arm64EC is gecompileerd, inclusief foutopsporing op laag niveau en het schrijven van assemblagecode gericht op de Arm64EC ABI.
Ontwerp van Arm64EC
Arm64EC biedt functionaliteit en prestaties op systeemeigen niveau, terwijl transparante en directe interoperabiliteit wordt geboden met x64-code die wordt uitgevoerd onder emulatie.
Arm64EC is voornamelijk additief aan de klassieke Arm64 ABI. De klassieke ABI is zeer weinig veranderd, maar de Arm64EC ABI heeft gedeelten toegevoegd om x64-interoperabiliteit mogelijk te maken.
In dit document wordt de oorspronkelijke, standaard Arm64 ABI aangeduid als 'Klassieke ABI'. Deze term voorkomt de dubbelzinnigheid die inherent is aan overladen termen, zoals 'Native'. Arm64EC is elke bit zo systeemeigen als de oorspronkelijke ABI.
Arm64EC versus Arm64 Classic ABI
In de volgende lijst wordt aangegeven waar Arm64EC afwijkt van Arm64 Classic ABI.
- Registertoewijzing en geblokkeerde registers
- Oproepcontrollers
- Stack-controleprogramma's
- Variadische aanroepconventie
Deze verschillen zijn kleine wijzigingen wanneer ze worden gezien in het perspectief van hoeveel de hele ABI definieert.
Registoewijzing en geblokkeerde registers
Als u interoperabiliteit op typeniveau met x64-code wilt inschakelen, compileert Arm64EC-code met dezelfde preprocessorarchitectuurdefinities als x64-code.
Met andere woorden, _M_AMD64 en _AMD64_ worden gedefinieerd. Een van de typen die door deze regel worden beïnvloed, is de CONTEXT structuur. De CONTEXT structuur definieert de status van de CPU op een bepaald moment. Het wordt gebruikt voor zaken zoals Exception Handling en GetThreadContext API's. Bestaande x64-code verwacht dat de CPU-context wordt weergegeven als een x64-CONTEXT structuur of, met andere woorden, de CONTEXT structuur zoals deze wordt gedefinieerd tijdens x64-compilatie.
U moet deze structuur gebruiken om de CPU-context weer te geven tijdens het uitvoeren van x64-code en Arm64EC-code. Bestaande code begrijpt geen nieuw concept, zoals de CPU-registerset die verandert van functie in functie. Als u de x64-structuur CONTEXT gebruikt om arm64-uitvoeringsstatussen weer te geven, wijst u Arm64-registers effectief toe aan x64-registers.
Deze toewijzing betekent ook dat u geen Arm64-registers kunt gebruiken die niet in de x64 CONTEXTpassen. De waarden kunnen verloren gaan telkens wanneer een bewerking CONTEXT gebruikt (en sommige bewerkingen kunnen asynchroon en onverwacht zijn, zoals de Garbage Collection-bewerking van een Managed Language Runtime of een APC).
De Windows-koppen in de SDK vertegenwoordigen de toewijzingsregels tussen Arm64EC- en x64-registers met de ARM64EC_NT_CONTEXT-structuur. Deze structuur is in wezen een samenvoeging van de CONTEXT structuur, precies zoals gedefinieerd voor x64, maar met een extra Arm64-registeroverlay.
U kunt bijvoorbeeld RCX toewijzen aan X0, RDX aan X1, RSP aan SP, RIP aan PC, enzovoort. De registersx13, x14, x23, , x24en x28v16 via v31 hebben geen vertegenwoordiging en kunnen dus niet worden gebruikt in Arm64EC.
Deze gebruiksbeperking voor registratie is het eerste verschil tussen de ARM64 Classic en EC-API's.
Oproepcontrole
Oproepcontrolemodules zijn in Windows aanwezig sinds de introductie van Control Flow Guard (CFG) in Windows 8.1. Aanroepcontroleprogramma's zijn adresopschoningsprogramma's voor functieaanwijzers (voordat deze dingen adresopschoningsprogramma's werden genoemd). Telkens wanneer u code compileert met de optie /guard:cf, genereert de compiler een extra aanroep naar de controlefunctie vlak voor elke indirecte aanroep of jump. Windows biedt de controlefunctie zelf. Voor CFG voert het een geldigheidscontrole uit op basis van de bekende, betrouwbare oproepdoelen. Binaire bestanden die met /guard:cf deze gegevens zijn gecompileerd, bevatten ook deze informatie.
In dit voorbeeld ziet u het gebruik van een gesprekscontrole in de klassieke Versie van 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
In het geval van CFG controleert de aanroep alleen nog of het doel geldig is, of faalt het proces onmiddellijk indien dit niet zo is. Oproepcontroleurs hebben aangepaste oproepconventies. Ze nemen de functiepointer in een register dat niet door de normale oproepconventie wordt gebruikt en behouden alle registers die normaal door de oproepconventie worden gebruikt. Op deze manier introduceren ze geen overloop van registraties.
Oproepcontrole is optioneel voor alle andere Windows-ABI's, maar verplicht op Arm64EC. In Arm64EC verzamelen aanroepcontroleers de taak om de architectuur van de aangeroepen functie te controleren. Ze controleren of de aanroep een andere EC-functie ('Emulatie compatibel') of een x64-functie is die moet worden uitgevoerd onder emulatie. In veel gevallen kan dit alleen tijdens runtime worden geverifieerd.
Arm64EC-aanroepcontrole is gebaseerd op de bestaande Arm64-controles, maar ze hebben een iets andere aangepaste oproepconventie. Ze nemen een extra parameter en ze kunnen het register met het doeladres wijzigen. Als het doel bijvoorbeeld x64-code is, moet de besturing eerst worden overgebracht naar de emulatie-steigerlogica.
In Arm64EC zou hetzelfde gebruik van call checker zijn:
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
Kleine verschillen van klassiek Arm64 zijn onder andere:
- De symboolnaam voor de call checker is verschillend.
- Het doeladres wordt opgegeven in
x11in plaats vanx15. - Het doeladres (
x11) is[in, out]in plaats van[in]. - Er is een extra parameter, opgegeven via
x10, genaamd een "Exit Thunk".
Een Exit Thunk is een funclet die functieparameters transformeert van de Arm64EC-aanroepconventie naar x64-aanroepconventie.
De Arm64EC-aanroepcontrole bevindt zich via een ander symbool dan wordt gebruikt voor de andere ABI's in Windows. Op de klassieke Arm64 ABI is het symbool voor de aanroepcontrole __guard_check_icall_fptr. Dit symbool is aanwezig in Arm64EC, maar het is er voor x64 statisch gekoppelde code die moet worden gebruikt, niet arm64EC-code zelf. Arm64EC-code gebruikt __os_arm64x_check_icall of __os_arm64x_check_icall_cfg.
In Arm64EC zijn gesprekscontroles niet optioneel. CFG is echter nog steeds optioneel, net als bij andere ABI's. CFG kan tijdens het compileren worden uitgeschakeld of er is mogelijk een legitieme reden om geen CFG-controle uit te voeren, zelfs niet wanneer CFG is ingeschakeld (bijvoorbeeld de functiepointer bevindt zich nooit in RW-geheugen). Voor een indirecte aanroep met CFG-controle moet de __os_arm64x_check_icall_cfg checker worden gebruikt. Als CFG is uitgeschakeld of niet nodig is, kan je __os_arm64x_check_icall gebruiken.
Hieronder ziet u een overzichtstabel van het gebruik van aanroepcontrole voor klassieke Arm64, x64 en Arm64EC, waarbij wordt aangegeven dat een binaire arm64EC twee opties kan hebben, afhankelijk van de architectuur van de code.
| Binaire | Code | Niet-beveiligde indirecte aanroep | Indirecte aanroep beveiligd met CFG |
|---|---|---|---|
| x64 | x64 | geen gesprekscontrole |
__guard_check_icall_fptr of __guard_dispatch_icall_fptr |
| Arm64 Classic | Arm64 | geen gesprekscontrole | __guard_check_icall_fptr |
| Arm64EC | x64 | geen gesprekscontrole |
__guard_check_icall_fptr of __guard_dispatch_icall_fptr |
| Arm64EC | __os_arm64x_check_icall |
__os_arm64x_check_icall_cfg |
Onafhankelijk van de ABI, waarbij CFG-code is ingeschakeld (code met verwijzing naar de CFG-aanroepcontroleers), impliceert geen CFG-beveiliging tijdens runtime. Binaire bestanden die beveiligd zijn met CFG kunnen op oudere systemen worden uitgevoerd, zelfs als ze CFG niet ondersteunen: het controlemechanisme voor aanroepen wordt tijdens het compileren geïnitialiseerd met een no-op-helper. Een proces kan CFG ook door de configuratie hebben uitgeschakeld. Wanneer CFG is uitgeschakeld (of ondersteuning voor het besturingssysteem niet aanwezig is) op eerdere ABIs, zal het besturingssysteem simpelweg de aanroepcontrole niet bijwerken op het moment dat het binaire bestand wordt geladen. Als CFG-beveiliging is uitgeschakeld in Arm64EC, zal het besturingssysteem __os_arm64x_check_icall_cfg instellen op hetzelfde als __os_arm64x_check_icall, wat nog steeds de benodigde doelarchitectuurcontrole in alle gevallen biedt, maar geen CFG-beveiliging.
Net als bij CFG in de klassieke Arm64 moet de aanroep naar de doelfunctie (x11) onmiddellijk de aanroep naar de aanroepcontrole volgen. Het adres van de aanroepcontrole moet in een vluchtig register worden geplaatst en noch dat adres, noch het adres van de doelfunctie, mag ooit naar een ander register worden gekopieerd of naar het geheugen worden verplaatst.
Stack Checkers
__chkstk wordt automatisch door de compiler gebruikt telkens wanneer een functie een gebied toewijst aan de stack die groter is dan een pagina. Om te voorkomen dat de stack guard-pagina die het einde van de stack beschermt, wordt overgeslagen, wordt __chkstk aangeroepen om ervoor te zorgen dat alle pagina's in het toegewezen gebied worden onderzocht.
__chkstk wordt meestal aangeroepen vanuit het prolog van de functie. Daarom maakt het voor een optimale codegeneratie gebruik van een aangepaste aanroepconventie.
Dit impliceert dat x64-code en Arm64EC-code hun eigen, afzonderlijke, __chkstk functies nodig hebben, omdat entry- en exit-thunks standaardconventies voor aanroepen aannemen.
x64 en Arm64EC delen dezelfde symboolnaamruimte, zodat er geen twee functies met de naam __chkstkkunnen zijn. Voor compatibiliteit met bestaande x64-code wordt __chkstk naam gekoppeld aan de x64-stackcontrole. Arm64EC-code gebruikt in plaats daarvan __chkstk_arm64ec.
De aangepaste aanroepconventie voor __chkstk_arm64ec is hetzelfde als voor Classic Arm64 __chkstk: x15 geeft de grootte van de toewijzing in bytes, gedeeld door 16. Alle niet-vluchtige registers, evenals alle vluchtige registers die betrokken zijn bij de standaard oproepconventie, blijven behouden.
Alles wat hierboven is gezegd over __chkstk geldt evenzeer voor __security_check_cookie en zijn Arm64EC-tegenhanger: __security_check_cookie_arm64ec.
Variadic aanroepconventie
Arm64EC volgt de klassieke Arm64 ABI-aanroepconventie, met uitzondering van variabele functies (ook wel varargs of functies genoemd met het beletselteken-trefwoord (...)).
Voor het specifieke variadic-geval volgt Arm64EC een aanroepconventie die erg lijkt op die van x64 variadic, met slechts enkele verschillen. De volgende lijst bevat de belangrijkste regels voor Arm64EC variadic:
- Alleen de eerste vier registers worden gebruikt voor het doorgeven van parameters:
x0,x1,x2, .x3De resterende parameters lopen over op de stack. Deze regel volgt de x64 variadic calling convention exact en verschilt van Arm64 Classic, waarbij registersx0x7worden gebruikt. - Drijvendekomma- en SIMD-parameters die via registers worden doorgegeven, maken gebruik van een algemeen-doeleinderegister, niet van een SIMD-register. Deze regel is vergelijkbaar met Arm64 Classic en verschilt van x64, waarbij FP/SIMD-parameters worden doorgegeven in zowel een algemeen als SIMD-register. Voor een functie
f1(int, …)die bijvoorbeeld wordt aangeroepen alsf1(int, double), op x64, wordt de tweede parameter toegewezen aan beideRDXenXMM1. In Arm64EC wordt de tweede parameter toegewezen aan alleenx1. - Bij het doorgeven van structuren via een register zijn x64-grootteregels van toepassing: Structuren met grootten exact 1, 2, 4 en 8 bytes worden rechtstreeks in het register voor algemeen gebruik geladen. Structuren van verschillende grootten lopen naar de stapel over, en een pointer naar de gevulde locatie wordt aan het register toegewezen. Deze regel verlaagt feitelijk bijwaarde tot bijreferentie op een laag niveau. Op de klassieke Arm64 ABI worden structuren van elke grootte van maximaal 16 bytes rechtstreeks toegewezen aan algemene registers.
- Het
x4-register laadt een pointer naar de eerste parameter die via de stack wordt doorgegeven (de vijfde parameter). Deze regel bevat geen structuren die overlopen vanwege de eerder beschreven groottebeperkingen. - Het
x5register laadt de grootte, in bytes, van alle parameters die door de stack worden doorgegeven (grootte van alle parameters, beginnend met de vijfde). Deze regel bevat geen structuren die worden doorgegeven door de waarde die is overgeslagen vanwege de eerder beschreven groottebeperkingen.
In het volgende voorbeeld pt_nova_function worden parameters in een niet-variadische vorm gebruikt, dus volgt deze de klassieke Arm64-oproepconventie. Vervolgens wordt pt_va_function aangeroepen met exact dezelfde parameters, maar dit keer in een aanroep die een variabel aantal argumenten accepteert.
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 gebruikt vijf parameters, die worden toegewezen door de regels van de klassieke Arm64-aanroepconventie te volgen.
- 'f' is een dubbel. Het wordt toegewezen aan
d0. - Tc is een struct met een grootte van 3 bytes. Het wordt toegewezen aan
x0. -
ull1is een geheel getal van 8 bytes. Het wordt toegewezen aanx1. -
ull2is een geheel getal van 8 bytes. Wordt toegewezen aanx2. -
ull3is een geheel getal van 8 bytes. Het wordt toegewezen aanx3.
pt_va_function is een variadic functie en volgt de Arm64EC-variadische regels zoals eerder besproken.
- 'f' is een dubbel. Het wordt toegewezen aan
x0. - Tc is een struct met een grootte van 3 bytes. Het overspoelt de stack en de locatie ervan wordt geladen in
x1. -
ull1is een geheel getal van 8 bytes. Het wordt toegewezen aanx2. -
ull2is een geheel getal van 8 bytes. Het wordt toegewezen aanx3. -
ull3is een geheel getal van 8 bytes. Deze wordt rechtstreeks aan de stack toegewezen. -
x4laadt de locatie vanull3in de stack. -
x5laadt de omvang vanull3.
In het volgende voorbeeld ziet u mogelijke compilatie-uitvoer voor pt_nova_function, waarin de verschillen in parametertoewijzing worden geïllustreerd die eerder zijn beschreven.
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
ABI-toevoegingen
Als u transparante interoperabiliteit met x64-code wilt bereiken, moet u veel toevoegingen aan de klassieke Arm64 ABI toevoegen. Deze toevoegingen verwerken de verschillen tussen belconventies tussen Arm64EC en x64.
De volgende lijst bevat de volgende toevoegingen:
Ingang- en uitgangsthunks
In- en uitgangsthunks vertalen de Arm64EC-belconventie (meestal hetzelfde als klassieke Arm64) naar de x64-belconventie en vice versa.
Een veelvoorkomend misvatting is dat u aanroepende conventies kunt converteren door één regel te volgen die is toegepast op alle functiehandtekeningen. De realiteit is dat aanroepende conventies regels voor parametertoewijzing hebben. Deze regels zijn afhankelijk van het parametertype en verschillen van ABI tot ABI. Een gevolg hiervan is dat de vertaling tussen ABI's specifiek is voor elke functiehandtekening, variërend van het type van elke parameter.
Houd rekening met de volgende functie:
int fJ(int a, int b, int c, int d);
Parametertoewijzing vindt als volgt plaats:
- Arm64: a -> x0, b -> x1, c -> x2, d -> x3
- x64: a -> RCX, b -> RDX, c -> R8, d -> r9
- Arm64 -> x64-vertaling: x0 -> RCX, x1 -> RDX, x2 -> R8, x3 -> R9
Overweeg nu een andere functie:
int fK(int a, double b, int c, double d);
Parametertoewijzing vindt als volgt plaats:
- Arm64: a -> x0, b -> d0, c -> x1, d -> d1
- x64: a -> RCX, b -> XMM1, c -> R8, d -> XMM3
- Arm64 -> x64-vertaling: x0 -> RCX, d0 -> XMM1, x1 -> R8, d1 -> XMM3
Deze voorbeelden laten zien dat parametertoewijzing en vertaling per type verschillen, maar ook afhankelijk zijn van de typen van de voorgaande parameters in de lijst. Deze details worden geïllustreerd door de derde parameter. In beide functies is inthet type parameter, maar de resulterende vertaling is anders.
Om deze reden bestaan invoer- en uitgangsstubs en zijn ze speciaal afgestemd op elke individuele functiesignatuur.
Beide typen thunks zijn functies. De emulator roept automatisch invoerthunks aan wanneer x64-functies Arm64EC-functies aanspreken (uitvoering gaat in Arm64EC). Aanroepcontrole roept automatisch exit thunks aan wanneer Arm64EC-functies x64-functies aanroepen (uitvoering Exits Arm64EC).
Bij het compileren van Arm64EC-code genereert de compiler een entry thunk voor elke Arm64EC-functie, die overeenkomt met de signatuur. De compiler genereert ook een exit thunk voor elke functie die een Arm64EC-functie aanroept.
Bekijk het volgende voorbeeld:
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);
}
Bij het compileren van de voorgaande code die gericht is op Arm64EC, genereert de compiler:
- Code voor
fA. - Toegang thunk voor
fA - Afsluitprocedure voor
fB - Afsluit-thunk voor
fC
De compiler genereert de fA entry thunk wanneer fA vanuit x64-code aangeroepen wordt. De compiler genereert exit thunks voor fB en fC in het geval fB en fC zijn x64-code.
De compiler kan dezelfde exit thunk meerdere keren genereren omdat deze worden gegenereerd op de aanroepsite in plaats van de functie zelf. Deze duplicatie kan leiden tot een aanzienlijke hoeveelheid redundante thunks. Om deze duplicatie te voorkomen, past de compiler triviale optimalisatieregels toe om ervoor te zorgen dat alleen de vereiste thunks in de definitieve binaire te maken terechtkomen.
In een binair bestand waarin de functie Arm64EC A de functie Arm64EC B aanroept, wordt B niet geëxporteerd en is het adres nooit bekend buiten A. Het is veilig om de exit thunk van A naar B te elimineren, samen met de entry thunk voor B. Het is ook veilig om alle afsluit- en invoer-thunks samen te aliasen die dezelfde code bevatten, zelfs als ze zijn gegenereerd voor afzonderlijke functies.
Exit-thunks beëindigen
Met behulp van de voorbeeldfuncties fA, fB, en fC in de voorgaande sectie, genereert de compiler zowel fB als fC exit thunks als volgt:
Exit thunk naar 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
Exit thunk naar 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
In het fB geval zorgt de aanwezigheid van een double parameter ervoor dat de resterende GP-registertoewijzing opnieuw wordt herschikt, als gevolg van de verschillende toewijzingsregels van Arm64 en x64. U kunt ook zien dat x64 slechts vier parameters toewijst aan registers, dus de vijfde parameter moet worden overgeslagen op de stack.
In het fC geval is de tweede parameter een structuur van lengte van 3 bytes. Met Arm64 kan elke groottestructuur rechtstreeks aan een register worden toegewezen. x64 staat alleen grootten 1, 2, 4 en 8 toe. Deze exit Thunk moet dit struct van het register naar de stapel overbrengen en in plaats daarvan een aanwijzer aan het register toewijzen. Deze methode verbruikt nog steeds één register (om de aanwijzer mee te nemen), zodat de toewijzingen voor de resterende registers niet worden gewijzigd: er vindt geen hertoewijzing van registers plaats voor de derde en vierde parameters. Net als voor het fB geval moet de vijfde parameter op de stack worden overgeslagen.
Aanvullende overwegingen voor Exit Thunks:
- De compiler noemt ze niet op basis van de functie waaruit en waarnaar ze vertalen, maar eerder door de handtekening die ze adresseren. Deze naamconventie maakt het gemakkelijker om redundantie te vinden.
- De aanroepcontrole stelt het register
x9in om het adres van de doelfunctie (x64) te dragen. De Exit Thunk roept de emulator aan, zonder wijzigingen door te geven.x9
Na het opnieuw rangschikken van de parameters roept de Exit Thunk in de emulator via __os_arm64x_dispatch_call_no_redirect aan.
Op dit moment is het de moeite waard om de functie van de oproepcontroller en de aangepaste ABI ervan te bekijken. Hier ziet u hoe een indirecte aanroep fB eruitziet:
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
Wanneer u de belchecker gebruikt:
-
x11levert het adres van de doelfunctie die moet worden aangeroepen (fBin dit geval). Op dit moment weet de aanroepcontrole mogelijk niet of de doelfunctie Arm64EC of x64 is. -
x10levert een Exit Thunk die overeenkomt met de handtekening van de functie die wordt aangeroepen (fBin dit geval).
De gegevens die de aanroepcontrole retourneert, zijn afhankelijk van of de doelfunctie Arm64EC of x64 is.
Als het doel Arm64EC is:
-
x11retourneert het adres van de Arm64EC-code die moet worden aangeroepen. Deze waarde kan hetzelfde zijn als de waarde die is opgegeven in.
Als het doel x64-code is:
-
x11retourneert het adres van de Exit Thunk. Dit adres wordt gekopieerd uit de invoer inx10. -
x10retourneert het adres van de Exit Thunk, onveranderd vanaf de invoer. -
x9retourneert de x64-functie. Deze waarde kan hetzelfde zijn als de waarde die is opgegeven viax11.
Controlemechanismes voor aanroepconventies laten de parameterregisters altijd ongemoeid. De aanroepende code moet de aanroep naar de aanroepcontroleur onmiddellijk volgen met blr x11 (of br x11 in het geval van een tail-call). Gesprekscontroles bewaren deze registers altijd naast standaard niet-vluchtige registers: x0-x8, x15(chkstk) en q0-q7.
Vermelding Thunks
Entry Thunks zorgt voor de transformaties die nodig zijn van de x64 tot de Arm64-belconventies. Deze transformatie is in wezen het omgekeerde van Exit Thunks, maar omvat nog enkele aspecten die u moet overwegen.
Bekijk het vorige voorbeeld van compileren fA. Er wordt een Entry Thunk gegenereerd zodat x64-code fA kan aanroepen.
Invoer Thunk voor 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
De emulator biedt het adres van de doelfunctie in x9.
Voordat de x64-emulator de Entry Thunk aanroept, haalt deze het retouradres van de stack en plaatst het in het LR-register.
LR wordt naar verwachting naar x64-code verwezen wanneer de besturing naar de Entry Thunk wordt overgedragen.
De emulator kan ook een andere aanpassing van de stack uitvoeren, afhankelijk van het volgende: Zowel Arm64 als x64 ABIs definiëren een stack-uitlijningsvereiste waarbij de stack moet worden uitgelijnd op 16 bytes op het punt dat een functie wordt aangeroepen. Bij het uitvoeren van Arm64-code dwingt hardware deze regel af, maar er is geen hardwareafdwinging voor x64. Tijdens het uitvoeren van x64-code kan het aanroepen van functies met een niet-uitgelijnde stack voor onbepaalde tijd onopgemerkt blijven, totdat er een 16-byte-uitlijningsinstructie wordt gebruikt (sommige SSE-instructies wel) of Arm64EC-code wordt aangeroepen.
Om dit mogelijke compatibiliteitsprobleem op te lossen, lijnt de emulator de Stack Pointer altijd uit op 16 bytes voordat de Entry Thunk wordt aangeroepen en slaat de oorspronkelijke waarde ervan op in het x4 register. Op deze manier begint Entry Thunks altijd met het uitvoeren van een uitgelijnde stack, maar kan nog steeds correct verwijzen naar de parameters die op de stack zijn doorgegeven, via x4.
Als het gaat om niet-vluchtige SIMD-registers, is er een belangrijk verschil tussen de Arm64- en x64-belconventies. In Arm64 worden de lage 8 bytes (64 bits) van het register beschouwd als niet-vluchtig. Met andere woorden, alleen het Dn deel van de Qn registers is niet vluchtig. Op x64 wordt het hele 16 bytes van het XMMn-register beschouwd als niet-vluchtig. Bovendien zijn XMM6 en XMM7 op x64 niet-vluchtige registers, terwijl D6 en D7 (de bijbehorende Arm64-registers) vluchtig zijn.
Om deze SIMD-registermanipulatie asymmetrieën aan te pakken, moet Entry Thunks expliciet alle SIMD-registers opslaan die als niet-vluchtig worden beschouwd in x64. Deze besparing is alleen nodig op Entry Thunks (niet Exit Thunks), omdat x64 strenger is dan Arm64. Met andere woorden, register opslaan en behoud regels in x64 overschrijden de Arm64-vereisten in alle gevallen.
Om het juiste herstel van deze registerwaarden te garanderen bij het afwikkelen van de stack (bijvoorbeeld setjmp + longjmp of throw + catch), is er een nieuwe opcode geïntroduceerd: save_any_reg (0xE7). Deze nieuwe 3-byte unwind-opcode maakt het mogelijk om alle algemene of SIMD-registers (inclusief degenen die als vluchtig worden beschouwd) op te slaan, inclusief volledige Qn-registers. Deze nieuwe opcode wordt gebruikt voor het Qn register uitstort- en vuloperaties.
save_any_reg is compatibel met save_next_pair (0xE6).
Ter referentie behoort de volgende afwikkelinformatie tot de eerder gepresenteerde Entry Thunk:
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)
Nadat de Functie Arm64EC is geretourneerd, wordt de __os_arm64x_dispatch_ret routine opnieuw ingevoerd in de emulator, terug naar x64-code (waarnaar wordt verwezen door LR).
Arm64EC-functies reserveren de vier bytes vóór de eerste instructie in de functie voor het opslaan van gegevens die tijdens runtime moeten worden gebruikt. In deze vier bytes vindt u het relatieve adres van Entry Thunk voor de functie. Bij het uitvoeren van een aanroep van een x64-functie naar een Arm64EC-functie, leest de emulator de vier bytes voor het begin van de functie, maskert de onderste twee bits en voegt deze hoeveelheid toe aan het adres van de functie. Dit proces produceert het adres van de Entry Thunk die moet worden aangeroepen.
Adjustor Thunks
Adjustor Thunks zijn functies zonder handtekening die de controle overdragen naar (tail-call) een andere functie. Voordat u het besturingselement overdraagt, transformeren ze een van de parameters. Het type van de parameters die worden getransformeerd, is bekend, maar alle resterende parameters kunnen alles zijn en kunnen zich in elk getal bevinden. Adjustor Thunks raken geen register aan dat mogelijk een parameter bevat en ze raken de stapel niet aan. Dit kenmerk maakt Adjustor Thunks handtekeningloze functies.
De compiler kan automatisch Adjustor Thunks genereren. Deze generatie is bijvoorbeeld gebruikelijk met C++ meervoudige overerving, waarbij elke virtuele methode zonder wijziging kan delegeren aan de ouderklasse, met uitzondering van een aanpassing aan de this pointer.
In het volgende voorbeeld ziet u een praktijkscenario:
[thunk]:CObjectContext::Release`adjustor{8}':
sub x0,x0,#8
b CObjectContext::Release
De thunk trekt 8 bytes af van de this aanwijzer en stuurt de aanroep door naar de superklasse.
Kortom, Arm64EC-functies die kunnen worden aangeroepen vanuit x64-functies moeten een bijbehorende Entry Thunk hebben. De Entry Thunk is specifiek voor de signatuur. Arm64 handtekeningloze functies, zoals Adjustor Thunks, hebben een ander mechanisme nodig waarmee handtekeningloze functies kunnen worden verwerkt.
De Entry Thunk van een Adjustor Thunk gebruikt de __os_arm64x_x64_jump helper om de uitvoering van het echte werk van de Entry Thunk uit te stellen, namelijk het aanpassen van de parameters van de ene conventie naar de andere, tot de volgende aanroep. Het is op dit moment dat de handtekening duidelijk wordt. Dit omvat de optie om helemaal geen aanroepende conventieaanpassingen uit te voeren, als het doel van de Adjustor Thunk een x64-functie blijkt te zijn. Houd er rekening mee dat de parameters zich al in hun x64-vorm bevinden op het moment dat een Entry Thunk wordt uitgevoerd.
Bekijk in het bovenstaande voorbeeld hoe de code eruitziet in Arm64EC.
Adjustor Thunk in 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
Adjustor Thunks Ingangstrunk
[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
Fast-forward-reeksen
Sommige toepassingen maken runtime-wijzigingen aan functies die zich bevinden in binaire bestanden waarvan ze niet eigenaar zijn, maar die afhankelijk zijn van , meestal binaire bestanden van het besturingssysteem, om de uitvoering om te leiden wanneer de functie wordt aangeroepen. Dit proces wordt ook wel 'hooking' genoemd.
Op hoog niveau is het haakproces eenvoudig. In detail, echter, is hooking architectuurspecifiek en vrij complex gezien de mogelijke variaties die de hookinglogica moet aanpakken.
In het algemeen omvat het proces de volgende stappen:
- Bepaal het adres van de functie om te koppelen.
- Vervang de eerste instructie van de functie door een sprong naar de hookroutine.
- Wanneer het haakje klaar is, keer terug naar de oorspronkelijke logica, inclusief het uitvoeren van de verplaatste oorspronkelijke instructie.
De variaties ontstaan als volgt:
- De grootte van de eerste instructie: het is een goed idee om deze te vervangen door een JMP die dezelfde grootte of kleiner is, om te voorkomen dat de bovenkant van de functie wordt vervangen terwijl andere thread deze tijdens de vlucht kan uitvoeren.
- Het type van de eerste instructie: Als de eerste instructie enige PC-relativiteit heeft, kan het zijn dat u velden zoals de verplaatsingsvelden moet wijzigen. Omdat ze mogelijk overlopen wanneer een instructie naar een verre locatie wordt verplaatst, kan deze wijziging vereisen dat equivalente logica met geheel verschillende instructies wordt geboden.
Vanwege al deze complexiteit is robuuste en algemene hookinglogica zelden te vinden. Vaak kan de logica in toepassingen alleen omgaan met een beperkt aantal gevallen waarin de toepassing verwacht te ondervinden in de specifieke API's waarin de toepassing geïnteresseerd is. Het is niet moeilijk om u voor te stellen hoeveel van een compatibiliteitsprobleem met toepassingen dit is. Zelfs een eenvoudige wijziging in de code- of compileroptimalisaties kan toepassingen onbruikbaar maken als de code er niet meer precies uitziet zoals verwacht.
Wat gebeurt er met deze toepassingen als ze Arm64-code tegenkwamen bij het instellen van een hook? Ze zouden zeker falen.
FFS-functies (Fast-Forward Sequence) voldoen aan deze compatibiliteitsvereiste in Arm64EC.
FFS zijn zeer kleine x64-functies die geen echte logica en tail-call voor de echte Arm64EC-functie bevatten. Ze zijn optioneel, maar standaard ingeschakeld voor alle DLL-exports en voor elke functie ingericht met __declspec(hybrid_patchable).
In dergelijke gevallen, wanneer code een aanwijzer naar een bepaalde functie verkrijgt, hetzij via GetProcAddress in de exportcase, of via &function in het __declspec(hybrid_patchable) geval, bevat het resulterende adres x64-code. Die x64-code wordt beschouwd als een legitieme x64-functie, die voldoet aan de meeste beschikbare hookinglogica.
Bekijk het volgende voorbeeld (foutafhandeling weggelaten voor beknoptheid):
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);
De waarde van de functiepointer in de pgma variabele bevat het adres van GetMachineTypeAttributes's FFS.
In dit voorbeeld ziet u een Fast-Forward reeks:
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
De FFS x64-functie heeft een canonieke proloog en epiloog, eindigend met een tail-call (sprong) naar de echte GetMachineTypeAttributes-functie in Arm64EC-code:
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
[...]
Het zou behoorlijk inefficiënt zijn als het nodig was vijf geëmuleerde x64-instructies tussen twee Arm64EC-functies uit te voeren. FFS-functies zijn speciaal. FFS-functies worden niet echt uitgevoerd als ze ongewijzigd blijven. De call-checker helper controleert efficiënt of de FFS niet is gewijzigd. Als dat het geval is, wordt de oproep rechtstreeks naar de echte bestemming overgedragen. Als de FFS op een mogelijke manier wordt gewijzigd, is het geen FFS meer. Uitvoering wordt overgedragen naar de gewijzigde FFS en voert uit welke code er mogelijk is, waarbij de omleiding en eventuele hookinglogica worden geëmuleren.
Wanneer de hook de uitvoering weer overdraagt naar het einde van de FFS, wordt uiteindelijk de tail-call naar de Arm64EC-code bereikt, die vervolgens wordt uitgevoerd na de hook, net zoals de toepassing verwacht.
Arm64EC ontwerpen in assembly
Windows SDK-headers en de C-compiler vereenvoudigen de taak van het ontwerpen van Arm64EC-assembly. U kunt bijvoorbeeld de C-compiler gebruiken om invoer- en afsluit-thunks te genereren voor functies die niet zijn gecompileerd vanuit C-code.
Bekijk het voorbeeld van een equivalent van de volgende functie fD die u moet ontwerpen in assembly (ASM). Zowel Arm64EC- als x64-code kan deze functie aanroepen en de pfE functieaanwijzer kan verwijzen naar Arm64EC- of x64-code.
typedef int (PF_E)(int, double);
extern PF_E * pfE;
int fD(int i, double d) {
return (*pfE)(i, d);
}
Schrijven fD in ASM kan eruitzien als de volgende code:
#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
In het voorgaande voorbeeld:
- Arm64EC gebruikt dezelfde proceduredeclaratie en prolog/epilog macro's als Arm64.
- Functienamen verpakken met de
A64NAMEmacro. Wanneer u C- of C++-code compileert als Arm64EC, markeert de compiler deOBJcodeARM64ECmet Arm64EC. Deze markering gebeurt niet metARMASM. Wanneer u ASM-code compileert, kunt u de linker informeren dat de geproduceerde code Arm64EC is door de naam van de functie vooraf te laten gaan.#DeA64NAMEmacro voert deze bewerking uit wanneer_ARM64EC_deze is gedefinieerd en laat de naam ongewijzigd wanneer_ARM64EC_deze niet is gedefinieerd. Deze benadering maakt het mogelijk om broncode te delen tussen Arm64 en Arm64EC. - U moet eerst de
pfEfunctie-aanwijzer uitvoeren via de EC-call checker, samen met de juiste "exit thunk", voor het geval dat de doelfunctie x64 is.
Invoer- en uitvoer-thunks genereren
De volgende stap is het genereren van de entry thunk voor fD en de exit thunk voor pfE. De C-compiler kan deze taak met minimale inspanning uitvoeren met behulp van het _Arm64XGenerateThunk trefwoord compiler.
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;
}
Het _Arm64XGenerateThunk trefwoord vertelt de C-compiler om de functie-ondertekening te gebruiken, de hoofdtekst te negeren en ofwel een exit-thunk te genereren (wanneer de parameter 1 is) of een invoer-thunk (wanneer de parameter 2 is).
Plaats de thunk-generatie in een apart C-bestand. Als u zich in geïsoleerde bestanden bevindt, is het eenvoudiger om de namen van symbolen te bevestigen door de bijbehorende OBJ symbolen te dumpen of zelfs te demonteren.
Aangepaste invoer-thunks
De SDK bevat macro's waarmee u aangepaste, handmatig gecodeerde entry thunks kunt maken. U kunt deze macro's gebruiken wanneer u aangepaste aanpasser thunks maakt.
De meeste adjustor thunks worden gegenereerd door de C++-compiler, maar u kunt ze ook handmatig genereren. Mogelijk genereert u handmatig een adjustor thunk wanneer een generieke callback de controle overdraagt aan de echte callback, en een van de parameters de echte callback identificeert.
In het volgende voorbeeld ziet u een adjustor thunk in de klassieke Arm64-code:
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
In dit voorbeeld geeft de eerste parameter een verwijzing naar een structuur. De code haalt het adres van de doelfunctie op uit een element van deze structuur. Omdat de structuur beschrijfbaar is, moet Control Flow Guard (CFG) het doeladres valideren.
In het volgende voorbeeld ziet u hoe u de equivalente adjustor thunk kunt overzetten naar 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
De voorgaande code levert geen exit thunk (in register x10). Deze benadering is niet mogelijk omdat de code kan worden uitgevoerd voor veel verschillende handtekeningen. Deze code maakt gebruik van de instelling x10 van de aanroeper naar de uitgang thunk. De beller voert de oproep uit die gericht is op een expliciete handtekening.
De voorgaande code heeft een entry thunk nodig om het geval aan te pakken wanneer de aanroeper x64-code is. In het volgende voorbeeld ziet u hoe u het corresponderende entry thunk opstelt met gebruikmaking van de macro voor aangepaste entry thunks.
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
In tegenstelling tot andere functies draagt deze entry thunk uiteindelijk geen controle over naar de bijbehorende functie (de adjustor thunk). In dit geval omvat de entry thunk de functionaliteit zelf (het uitvoeren van de parameteraanpassing) en draagt het direct de controle over naar het einddoel via de __os_arm64x_x64_jump helper.
Arm64EC-code dynamisch genereren (JIT-compiling)
In Arm64EC-processen bestaan twee typen uitvoerbaar geheugen: Arm64EC-code en x64-code.
Het besturingssysteem extraheert deze informatie uit de geladen binaire bestanden. binaire x64-bestanden zijn allemaal x64 en Binaire Arm64EC-bestanden bevatten een bereiktabel voor Arm64EC versus x64-codepagina's.
Hoe zit het met dynamisch gegenereerde code? JIT-compilers (Just-In-Time) genereren code tijdens runtime die niet wordt ondersteund door een binair bestand.
Normaal gesproken omvat dit proces de volgende stappen:
- Schrijfbaar geheugen toewijzen (
VirtualAlloc). - De code in het toegewezen geheugen produceren.
- Opnieuw beschermen van het geheugen van lezen/schrijven naar uitvoeren (
VirtualProtect). - Het toevoegen van afwikkelfunctievermeldingen voor alle niet-triviale (niet-leaf) gegenereerde functies (
RtlAddFunctionTableofRtlAddGrowableFunctionTable).
Als een toepassing deze stappen uitvoert in een Arm64EC-proces, beschouwt het besturingssysteem de code als x64-code. Dit gedrag gebeurt voor elk proces dat gebruikmaakt van de ongewijzigde x64 Java Runtime, .NET-runtime, JavaScript-engine, enzovoort.
Als u dynamische Arm64EC-code wilt genereren, volgt u hetzelfde proces met twee verschillen:
- Wanneer u het geheugen toedeelt, gebruikt u de nieuwer
VirtualAlloc2(in plaats vanVirtualAllocofVirtualAllocEx) en geeft u hetMEM_EXTENDED_PARAMETER_EC_CODEkenmerk op. - Bij het toevoegen van functievermeldingen:
- Ze moeten de Arm64-indeling hebben. Bij het compileren van Arm64EC-code komt het
RUNTIME_FUNCTIONtype overeen met de x64-indeling. Gebruik in een Arm64-indeling, bij het compileren van Arm64EC, in plaats daarvan hetARM64_RUNTIME_FUNCTIONtype. - Gebruik de oudere
RtlAddFunctionTableAPI niet. Gebruik altijd de nieuwereRtlAddGrowableFunctionTableAPI.
- Ze moeten de Arm64-indeling hebben. Bij het compileren van Arm64EC-code komt het
In het volgende voorbeeld ziet u geheugentoewijzing:
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);
In het volgende voorbeeld ziet u hoe u een vermelding voor een afwikkelfunctie toevoegt:
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