Delen via


Begrip van Arm64EC ABI en assemblycode

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.

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 x11 in plaats van x15.
  • 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, . x3 De resterende parameters lopen over op de stack. Deze regel volgt de x64 variadic calling convention exact en verschilt van Arm64 Classic, waarbij registers x0x7 worden 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 als f1(int, double), op x64, wordt de tweede parameter toegewezen aan beide RDX en XMM1. In Arm64EC wordt de tweede parameter toegewezen aan alleen x1.
  • 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 x5 register 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.
  • ull1 is een geheel getal van 8 bytes. Het wordt toegewezen aan x1.
  • ull2 is een geheel getal van 8 bytes. Wordt toegewezen aan x2.
  • ull3 is een geheel getal van 8 bytes. Het wordt toegewezen aan x3.

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.
  • ull1 is een geheel getal van 8 bytes. Het wordt toegewezen aan x2.
  • ull2 is een geheel getal van 8 bytes. Het wordt toegewezen aan x3.
  • ull3 is een geheel getal van 8 bytes. Deze wordt rechtstreeks aan de stack toegewezen.
  • x4 laadt de locatie van ull3 in de stack.
  • x5 laadt de omvang van ull3.

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 x9 in 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:

  • x11 levert het adres van de doelfunctie die moet worden aangeroepen (fB in dit geval). Op dit moment weet de aanroepcontrole mogelijk niet of de doelfunctie Arm64EC of x64 is.
  • x10 levert een Exit Thunk die overeenkomt met de handtekening van de functie die wordt aangeroepen (fB in dit geval).

De gegevens die de aanroepcontrole retourneert, zijn afhankelijk van of de doelfunctie Arm64EC of x64 is.

Als het doel Arm64EC is:

  • x11 retourneert 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:

  • x11 retourneert het adres van de Exit Thunk. Dit adres wordt gekopieerd uit de invoer in x10.
  • x10 retourneert het adres van de Exit Thunk, onveranderd vanaf de invoer.
  • x9 retourneert de x64-functie. Deze waarde kan hetzelfde zijn als de waarde die is opgegeven via x11.

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 A64NAME macro. Wanneer u C- of C++-code compileert als Arm64EC, markeert de compiler de OBJ code ARM64EC met Arm64EC. Deze markering gebeurt niet met ARMASM. 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.# De A64NAME macro 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 pfE functie-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 (RtlAddFunctionTable of RtlAddGrowableFunctionTable).

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 van VirtualAlloc of VirtualAllocEx) en geeft u het MEM_EXTENDED_PARAMETER_EC_CODE kenmerk op.
  • Bij het toevoegen van functievermeldingen:
    • Ze moeten de Arm64-indeling hebben. Bij het compileren van Arm64EC-code komt het RUNTIME_FUNCTION type overeen met de x64-indeling. Gebruik in een Arm64-indeling, bij het compileren van Arm64EC, in plaats daarvan het ARM64_RUNTIME_FUNCTION type.
    • Gebruik de oudere RtlAddFunctionTable API niet. Gebruik altijd de nieuwere RtlAddGrowableFunctionTable API.

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