Comprendere l'ABI di Arm64EC e il codice assembly
Arm64EC ("Compatibile con emulazione") è una nuova interfaccia ABI (Application Binary Interface) per la creazione di app per Windows 11 su Arm. Per una panoramica su Arm64EC e su come iniziare a costruire applicazioni Win32 come Arm64EC, consulta . Usare Arm64EC per costruire applicazioni per Windows 11 su dispositivi Arm.
Lo scopo di questo documento è quello di fornire una visione dettagliata dell'ABI di Arm64EC con informazioni sufficienti per consentire a uno sviluppatore di applicazioni di scrivere e debuggare il codice compilato per Arm64EC, compreso il debug a basso livello dell'assemblatore e la scrittura di codice assembly mirato all'ABI di Arm64EC.
Progettazione di Arm64EC
Arm64EC è stato progettato per offrire funzionalità e prestazioni di livello nativo, fornendo al contempo un'interoperabilità trasparente e diretta con il codice x64 in esecuzione sotto emulazione.
Arm64EC è principalmente additivo all'ABI Arm64 classico. L'ABI classico è stato modificato in minima parte, ma sono state aggiunte alcune parti per consentire l'interoperabilità x64.
In questo documento, l'ABI standard Arm64 standard deve essere definito "ABI classico". In questo modo si evita l'ambiguità intrinseca ai termini di overload, ad esempio "Nativo". Arm64EC, per essere chiari, è nativo come l'ABI originale.
Arm64EC vs Arm64 Classic ABI
Il seguente elenco indica dove Arm64EC si è discostato dall'ABI di Arm64 Classic.
- Mappatura dei registri e registri bloccati
- Controlli di chiamata
- Controlli di impilamento
- Convenzione di chiamata variabile
Si tratta di piccoli cambiamenti se visti in prospettiva rispetto a quanto definisce l'intero ABI.
Mappatura dei registri e registri bloccati
Affinché ci sia interoperabilità a livello di tipo con il codice x64, il codice Arm64EC è compilato con le stesse definizioni di architettura del preprocessore del codice x64.
In altre parole, _M_AMD64
e _AMD64_
sono definiti. Una delle tipologie interessate da questa regola è la struttura CONTEXT
. La struttura CONTEXT
definisce lo stato della CPU in un determinato momento. È utilizzato per cose come Exception Handling
e GetThreadContext
API. Il codice x64 esistente si aspetta che il contesto della CPU sia rappresentato come una struttura x64 CONTEXT
o, in altre parole, la struttura CONTEXT
come viene definita durante la compilazione x64.
Questa struttura deve essere utilizzata per rappresentare il contesto della CPU durante l'esecuzione di codice x64 e di codice Arm64EC. Il codice esistente non capirebbe un concetto nuovo, come il set di registri della CPU che cambia da una funzione all'altra. Se la struttura x64 CONTEXT
viene utilizzata per rappresentare gli stati di esecuzione di Arm64, questo implica che i registri Arm64 sono effettivamente mappati in registri x64.
Implica anche che i registri Arm64 che non possono essere inseriti in CONTEXT
x64 non devono essere utilizzati, poiché i loro valori possono essere persi ogni volta che si verifica un'operazione che utilizza CONTEXT
(e alcuni possono essere asincroni e inaspettati, come l'operazione di Garbage Collection di un Managed Language Runtime o un APC).
Le regole di mappatura tra i registri Arm64EC e x64 sono rappresentate dalla struttura ARM64EC_NT_CONTEXT
negli header di Windows, presenti nell'SDK. Questa struttura è essenzialmente un'unione della struttura CONTEXT
, esattamente come è definita per x64, ma con una sovrapposizione di registri Arm64.
Ad esempio, RCX
mappa a X0
, RDX
a X1
, RSP
a SP
, RIP
a PC
, ecc. Possiamo anche vedere come i registri x13
, x14
, x23
, x24
, x28
, v16
-v31
non hanno alcuna rappresentazione e, quindi, non possono essere utilizzati in Arm64EC.
Questa restrizione nell'uso dei registri è la prima differenza tra le ABI di Arm64 Classic ed EC.
Controlli di chiamata
I controllori di chiamate sono parte integrante di Windows da quando Control Flow Guard (CFG) è stato introdotto in Windows 8.1. I controllori di chiamata sono dei sanificatori di indirizzi per i puntatori di funzione (prima che questi oggetti venissero chiamati sanificatori di indirizzi). Ogni volta che il codice viene compilato con l'opzione /guard:cf
, il compilatore genererà una chiamata extra alla funzione di controllo prima di ogni chiamata indiretta o salto. La funzione di controllo è fornita da Windows e, per quanto riguarda il CFG, esegue un controllo di validità rispetto agli obiettivi di chiamata noti per essere buoni. Queste informazioni sono incluse anche nei binari compilati con /guard:cf
.
Questo è un esempio di utilizzo del call checker in Classic Arm64:
mov x15, <target>
adrp x16, __guard_check_icall_fptr
ldr x16, [x16, __guard_check_icall_fptr]
blr x16 ; check target function
blr x15 ; call function
Nel caso del CFG, il controllore di chiamate restituirà semplicemente se l'obiettivo è valido, o fallirà rapidamente il processo in caso contrario. I call checker hanno convenzioni di chiamata personalizzate. Prendono il puntatore della funzione in un registro non utilizzato dalla normale convenzione di chiamata e conservano tutti i registri della normale convenzione di chiamata. In questo modo, non introducono perdite di registrazione intorno a loro.
I controllori di chiamata sono opzionali su tutte le altre ABI di Windows, ma obbligatori su Arm64EC. Su Arm64EC, i call checker accumulano il compito di verificare l'architettura della funzione chiamata. Verificano se la chiamata è un'altra funzione EC ("Emulation Compatible") o una funzione x64 che deve essere eseguita in emulazione. In molti casi, questo può essere verificato solo in fase di esecuzione.
I controllori di chiamate Arm64EC si basano sui controllori Arm64 esistenti, ma hanno una convenzione di chiamata personalizzata leggermente diversa. Richiedono un parametro aggiuntivo e possono modificare il registro contenente l'indirizzo di destinazione. Ad esempio, se il target è un codice x64, il controllo deve essere trasferito prima alla logica di emulazione.
In Arm64EC, lo stesso uso del controllore di chiamata diventerebbe:
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
Le leggere differenze rispetto al Classic Arm64 includono:
- Il nome del simbolo per il controllore di chiamata è diverso.
- L'indirizzo di destinazione viene fornito in
x11
invece che inx15
. - L'indirizzo di destinazione (
x11
) è[in, out]
invece di[in]
. - Esiste un parametro aggiuntivo, fornito tramite
x10
, denominato "Exit Thunk".
An Exit Thunk è una funclet che trasforma i parametri delle funzioni dalla convenzione di chiamata Arm64EC alla convenzione di chiamata x64.
Il controllore delle chiamate Arm64EC si trova in un simbolo diverso da quello utilizzato per le altre ABI di Windows. Nell'ABI Classic Arm64, il simbolo del controllore di chiamata è __guard_check_icall_fptr
. Questo simbolo sarà presente in Arm64EC, ma è presente per il codice x64 collegato staticamente, non per il codice Arm64EC stesso. Il codice Arm64EC utilizzerà __os_arm64x_check_icall
o __os_arm64x_check_icall_cfg
.
Su Arm64EC, i controllori di chiamata non sono opzionali. Tuttavia, CFG è ancora opzionale, come nel caso di altri ABI. Il CFG potrebbe essere disabilitato al momento della compilazione, oppure potrebbe esserci un motivo legittimo per non eseguire un controllo CFG anche quando il CFG è abilitato (ad esempio, il puntatore di funzione non risiede mai nella memoria RW). Per una chiamata indiretta con controllo CFG, è necessario utilizzare il controllore __os_arm64x_check_icall_cfg
. Se il CFG è disabilitato o non è necessario, si deve utilizzare __os_arm64x_check_icall
.
Di seguito è riportata una tabella riassuntiva dell'utilizzo del call checker su Arm64 classico, x64 e Arm64EC, tenendo conto del fatto che un binario Arm64EC può avere due opzioni a seconda dell'architettura del codice.
Binario | Codice | Chiamata indiretta non protetta | Chiamata indiretta protetta CFG |
---|---|---|---|
x64 | x64 | controllo di assenza di chiamate | __guard_check_icall_fptr oppure __guard_dispatch_icall_fptr |
Arm64 Classic | Arm64 | controllo di assenza di chiamate | __guard_check_icall_fptr |
Arm64EC | x64 | controllo di assenza di chiamate | __guard_check_icall_fptr oppure __guard_dispatch_icall_fptr |
Arm64EC | __os_arm64x_check_icall |
__os_arm64x_check_icall_cfg |
Indipendentemente dall'ABI, avere codice abilitato al CFG (codice con riferimento ai call-checker CFG) non implica la protezione CFG a tempo di esecuzione. I binari protetti da CFG possono essere eseguiti a livello inferiore, su sistemi che non supportano CFG: il call-checker viene inizializzato con un helper no-op al momento della compilazione. Un processo può anche avere il CFG disabilitato dalla configurazione. Quando il CFG è disabilitato (o il supporto del sistema operativo non è presente) su ABI precedenti, il sistema operativo semplicemente non aggiorna il call-checker quando viene caricato il binario. Su Arm64EC, se la protezione CFG è disabilitata, il sistema operativo imposterà __os_arm64x_check_icall_cfg
come __os_arm64x_check_icall
, che fornirà comunque il controllo dell'architettura di destinazione in tutti i casi, ma non la protezione CFG.
Come per il CFG in Classic Arm64, la chiamata alla funzione di destinazione (x11
) deve seguire immediatamente la chiamata al Call Checker. L'indirizzo del Call Checker deve essere inserito in un registro volatile e né esso né l'indirizzo della funzione di destinazione devono mai essere copiati in un altro registro o riversati in memoria.
Dama a pila
__chkstk
viene utilizzato automaticamente dal compilatore ogni volta che una funzione alloca un'area dello stack più grande di una pagina. Per evitare di saltare la pagina di protezione dello stack che protegge la fine dello stack, viene chiamato __chkstk
per assicurarsi che tutte le pagine dell'area allocata vengano sondate.
__chkstk
viene solitamente richiamato dal prologo della funzione. Per questo motivo, e per una generazione ottimale del codice, utilizza una convenzione di chiamata personalizzata.
Questo implica che il codice x64 e il codice Arm64EC hanno bisogno di funzioni __chkstk
proprie e distinte, dato che i thunk Entry e Exit assumono convenzioni di chiamata standard.
X64 e Arm64EC condividono lo stesso spazio dei nomi dei simboli, quindi non possono essere presenti due funzioni denominate __chkstk
. Per favorire la compatibilità con il codice x64 preesistente, il nome __chkstk
sarà associato allo stack checker x64. Il codice Arm64EC utilizzerà invece __chkstk_arm64ec
.
La convenzione di chiamata personalizzata per __chkstk_arm64ec
è la stessa di quella per Classic Arm64 __chkstk
: x15
indica la dimensione dell'allocazione in byte, divisa per 16. Tutti i registri non volatili e tutti i registri volatili coinvolti nella convenzione di chiamata standard vengono conservati.
Tutto ciò che è stato detto in precedenza su __chkstk
si applica anche a __security_check_cookie
e alla sua controparte Arm64EC: __security_check_cookie_arm64ec
.
Convenzione di chiamata variabile
Arm64EC segue la convenzione di chiamata dell'ABI Arm64 classica, tranne che per le funzioni Variadic (alias varargs, alias funzioni con l'ellissi (. . .) parola chiave del parametro).
Per il caso specifico di variadic, Arm64EC segue una convenzione di chiamata molto simile a quella di x64 variadic, con solo alcune differenze. Di seguito sono riportate le regole principali per Arm64EC variadic:
- Solo i primi 4 registri vengono utilizzati per il passaggio dei parametri:
x0
,x1
,x2
,x3
. I parametri rimanenti vengono riversati nello stack. Questo segue esattamente la convenzione di chiamata variadica x64 e differisce da Arm64 Classic, dove vengono utilizzati i registrix0
->x7
. - I parametri in virgola mobile / SIMD passati per registro utilizzeranno un registro generico, non uno SIMD. Questo è simile ad Arm64 Classic e differisce da x64, dove i parametri FP/SIMD vengono passati sia in un registro General-Purpose che in uno SIMD. Ad esempio, per una funzione
f1(int, …)
che viene chiamata comef1(int, double)
, su x64, il secondo parametro sarà assegnato sia aRDX
che aXMM1
. Su Arm64EC, il secondo parametro sarà assegnato solo ax1
. - Quando si passano strutture per valore attraverso un registro, si applicano le regole di dimensione x64: Le strutture con dimensioni esattamente 1, 2, 4 e 8 saranno caricate direttamente nel registro generale. Le strutture con altre dimensioni vengono riversate nello stack e un puntatore alla posizione riversata viene assegnato al registro. In questo modo, il by-value si trasforma in by-reference, a basso livello. Nell'ABI Arm64 classico, le strutture di qualsiasi dimensione fino a 16 byte sono assegnate direttamente ai registri General-Purposed.
- Il registro X4 viene caricato con un puntatore al primo parametro passato via stack (il quinto parametro). Questo non include le strutture fuoriuscite a causa delle limitazioni di dimensioni sopra descritte.
- Il registro X5 viene caricato con la dimensione, in byte, di tutti i parametri passati dallo stack (dimensione di tutti i parametri, a partire dal quinto). Questo non include le strutture superate dal valore versato a causa delle limitazioni di dimensioni sopra descritte.
Nel seguente esempio: pt_nova_function
di seguito prende i parametri in forma non variabile, seguendo quindi la convenzione di chiamata classica di Arm64. Chiama quindi pt_va_function
con gli stessi parametri ma in una chiamata variadica.
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
richiede 5 parametri che saranno assegnati secondo le regole della convenzione di chiamata classica di Arm64:
- 'f ' è un doppio. Sarà assegnato a d0.
- 'tc' è uno struct, con una dimensione di 3 byte. Sarà assegnato a x0.
- ull1 è un numero intero di 8 byte. Sarà assegnato a x1.
- ull2 è un numero intero di 8 byte. Verrà assegnato a x2.
- ull3 è un numero intero di 8 byte. Sarà assegnato a x3.
pt_va_function
è una funzione variadica, quindi seguirà le regole variadiche di Arm64EC descritte sopra:
- 'f ' è un doppio. Sarà assegnato a x0.
- 'tc' è uno struct, con una dimensione di 3 byte. Verrà versato sulla pila e la sua posizione verrà caricata in x1.
- ull1 è un numero intero di 8 byte. Verrà assegnato a x2.
- ull2 è un numero intero di 8 byte. Sarà assegnato a x3.
- ull3 è un numero intero di 8 byte. Verrà assegnato direttamente allo stack.
- x4 viene caricato con la posizione di ull3 nello stack.
- x5 viene caricato con la dimensione di ull3.
Di seguito viene mostrato un possibile output di compilazione per pt_nova_function
, che illustra le differenze di assegnazione dei parametri descritte in precedenza.
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
Aggiunte di funzioni a gestione dinamica (ABI)
Per ottenere un'interoperabilità trasparente con il codice x64, sono state fatte molte aggiunte all'ABI di Arm64 classico. Gestiscono le differenze nelle convenzioni di chiamata tra Arm64EC e x64.
L'elenco seguente include queste aggiunte:
- Thunk di entrata e di uscita
- Esci da Thunks
- Ingresso Thunks
- Regolatore Thunks
- Sequenze di avanzamento rapido
Thunk di entrata e di uscita
I Thunk di entrata e di uscita si occupano di tradurre la convenzione di chiamata Arm64EC (per lo più la stessa di Arm64 classico) nella convenzione di chiamata x64 e viceversa.
Un'idea sbagliata comune è che le convenzioni di chiamata possano essere convertite seguendo un'unica regola applicata a tutte le firme delle funzioni. La realtà è che le convenzioni di chiamata hanno regole di assegnazione dei parametri. Queste regole dipendono dal tipo di parametro e sono diverse da ABI a ABI. Una conseguenza è che la traduzione tra le ABI sarà specifica per ogni firma di funzione, variando con il tipo di ogni parametro.
Si consideri la funzione seguente:
int fJ(int a, int b, int c, int d);
L'assegnazione dei parametri avverrà come segue:
- Arm64: a -> x0, b -> x1, c -> x2, d -> x3
- x64: a -> RCX, b -> RDX, c -> R8, d -> r9
- Arm64 -> x64 traduzione: x0 -> RCX, x1 -> RDX, x2 -> R8, x3 -> R9
Ora consideriamo una funzione diversa:
int fK(int a, double b, int c, double d);
L'assegnazione dei parametri avverrà come segue:
- Arm64: a -> x0, b -> d0, c -> x1, d -> d1
- x64: a -> RCX, b -> XMM1, c -> R8, d -> XMM3
- Arm64 -> traduzione x64: x0 -> RCX, d0 -> XMM1, x1 -> R8, d1 -> XMM3
Questi esempi dimostrano che l'assegnazione e la traduzione dei parametri variano in base al tipo, ma anche che i tipi dei parametri precedenti nell'elenco dipendono da essi. Questo dettaglio è illustrato dal terzo parametro. In entrambe le funzioni il tipo del parametro è "int", ma la traduzione risultante è diversa.
I Thunk di entrata e di uscita esistono per questo motivo e sono specificamente adattati per ogni firma di funzione individuale.
Entrambi i tipi di thunks sono, a loro volta, funzioni. Gli Entry Thunks sono invocati automaticamente dall'emulatore quando le funzioni x64 chiamano le funzioni Arm64EC (esecuzione Enters Arm64EC). I Thunk di uscita sono invocati automaticamente dai controllori di chiamata quando le funzioni Arm64EC chiamano funzioni x64 (esecuzione Exits Arm64EC).
Durante la compilazione del codice Arm64EC, il compilatore genera un Entry Thunk per ogni funzione Arm64EC che corrisponde alla sua firma. Il compilatore genererà anche un Exit Thunk per ogni funzione chiamata da una funzione Arm64EC.
Si consideri l'esempio seguente:
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);
}
Quando si compila il codice di cui sopra per Arm64EC, il compilatore genera:
- Codice per 'fA'.
- Entry Thunk per 'fA'
- Esci da Thunk per 'fB'
- Esci da Thunk per 'fC'
Il thunk fA
Entry è generato nel caso fA
e richiamato dal codice x64. I Thunk di uscita per fB
e fC
vengono generati nel caso in cui fB
e/o fC
si rivelino essere codice x64.
Lo stesso Exit Thunk può essere generato più volte, dato che il compilatore lo genera nel sito di chiamata piuttosto che nella funzione stessa. Questo può portare a una quantità considerevole di thunks ridondanti quindi, in realtà, il compilatore applicherà delle banali regole di ottimizzazione per assicurarsi che solo i thunks necessari vengano inseriti nel binario finale.
Ad esempio, in un binario in cui la funzione Arm64EC A
chiama la funzione Arm64EC B
, B
non viene esportata e il suo indirizzo non è mai conosciuto al di fuori di A
. È sicuro eliminare l'Exit Thunk da A
a B
, insieme all'Entry Thunk per B
. È inoltre sicuro che tutti i thunk di Uscita e di Entrata che contengono lo stesso codice, anche se sono stati generati per funzioni distinte, vengano aliasati insieme.
Esci da Thunks
Utilizzando le funzioni di esempio fA
, fB
e fC
di cui sopra, ecco come il compilatore genererebbe gli Exit Thunk fB
e fC
:
Esci da Thunk a 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
Esci da Thunk a 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
fB
Nel caso, è possibile vedere come la presenza di un parametro "double" causerà il rimshuffle dell'assegnazione del registro Criteri di gruppo rimanente, un risultato delle regole di assegnazione diverse di Arm64 e x64. Possiamo anche notare che x64 assegna solo 4 parametri ai registri, quindi il quinto parametro deve essere versato sullo stack.
Nel caso di fC
, il secondo parametro è una struttura di 3 byte. Arm64 consentirà di assegnare direttamente a un registro una struttura di qualsiasi dimensione. x64 consente solo le dimensioni 1, 2, 4 e 8. Questo Exit Thunk deve quindi trasferire questo struct
dal registro allo stack e assegnare un puntatore al registro. Questo consuma ancora un registro (per trasportare il puntatore), quindi non cambia l'assegnazione dei registri rimanenti: non avviene alcun rimescolamento dei registri per il terzo e quarto parametro. Come nel caso di fB
, il quinto parametro deve essere versato in pila.
Considerazioni aggiuntive per Exit Thunks:
- Il compilatore non li chiamerà con il nome della funzione che traducono da>a, ma piuttosto con la firma a cui si rivolgono. In questo modo è più facile trovare degli esuberi.
- L'Exit Thunk viene chiamato con il registro
x9
che contiene l'indirizzo della funzione di destinazione (x64). Questo viene impostato dal controllore delle chiamate e passa attraverso l'Exit Thunk, indisturbato, nell'emulatore.
Dopo aver riorganizzato i parametri, l'Exit Thunk chiama l'emulatore tramite __os_arm64x_dispatch_call_no_redirect
.
A questo punto vale la pena di rivedere la funzione del call checker e i dettagli sulla sua ABI personalizzata. Ecco come si presenta una chiamata indiretta a fB
:
mov x11, <target>
adrp x9, __os_arm64x_check_icall_cfg
ldr x9, [x9, __os_arm64x_check_icall_cfg]
adrp x10, $iexit_thunk$cdecl$i8$i8di8i8i8 ; fB function's exit thunk
add x10, x10, $iexit_thunk$cdecl$i8$i8di8i8i8
blr x9 ; check target function
blr x11 ; call function
Quando si chiama il controllore di chiamate:
x11
fornisce l'indirizzo della funzione di destinazione da chiamare (in questo casofB
). A questo punto potrebbe non essere noto se la funzione di destinazione è Arm64EC o x64.x10
fornisce un Thunk di uscita che corrisponde alla firma della funzione chiamata (in questo casofB
).
I dati restituiti dal verificatore di chiamate dipendono dal fatto che la funzione di destinazione sia Arm64EC o x64.
Se il target è Arm64EC:
x11
restituirà l'indirizzo del codice Arm64EC da chiamare. Questo valore può essere o non essere lo stesso che è stato fornito in.
Se l'obiettivo è un codice x64:
x11
restituirà l'indirizzo dell'Exit Thunk. Questo è copiato dall'input fornito inx10
.x10
restituirà l'indirizzo del Thunk di uscita, indisturbato dall'input.x9
restituirà la funzione x64 di destinazione. Questo valore può essere o meno lo stesso fornito dax11
.
I controllori di chiamata lasceranno sempre indisturbati i registri dei parametri della convenzione di chiamata, quindi il codice chiamante deve seguire immediatamente la chiamata al controllore di chiamata con blr x11
(o br x11
nel caso di una chiamata in coda). Questi sono i registri che chiamano checker. Conserveranno sempre un livello di conservazione superiore a quello dei registri non volatili standard: x0
-x8
, x15
(chkstk
) e q0
-q7
.
Ingresso Thunks
Gli Entry Thunk si occupano delle trasformazioni necessarie per passare dalle convenzioni di chiamata x64 a quelle Arm64. Si tratta, in sostanza, dell'inverso di Exit Thunks, ma ci sono alcuni aspetti in più da considerare.
Considerando l'esempio precedente di compilazione di fA
, viene generato un Entry Thunk in modo che fA
possa essere chiamato dal codice x64.
Ingresso Thunk per 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
L'indirizzo della funzione di destinazione è fornito dall'emulatore in x9
.
Prima di chiamare l'Entry Thunk, l'emulatore x64 inserisce l'indirizzo di ritorno dallo stack nel registro LR
. Si prevede quindi che LR
punti al codice x64 quando il controllo viene trasferito all'Entry Thunk.
L'emulatore può anche eseguire un'altra regolazione dello stack, a seconda di quanto segue: Entrambe le ABI Arm64 e x64 definiscono un requisito di allineamento dello stack che deve essere allineato a 16 byte nel momento in cui viene chiamata una funzione. Quando si esegue codice Arm64, l'hardware applica questa regola, ma non c'è alcuna applicazione hardware per x64. Durante l'esecuzione di codice x64, la chiamata erronea di funzioni con uno stack non allineato può passare inosservata all'infinito, fino a quando non viene utilizzata un'istruzione di allineamento a 16 byte (alcune istruzioni SSE lo fanno) o non viene richiamato codice Arm64EC.
Per risolvere questo potenziale problema di compatibilità, prima di chiamare l'Entry Thunk, l'emulatore allinea sempre lo Stack Pointer a 16 byte e memorizza il suo valore originale nel registro x4
. In questo modo gli Entry Thunk iniziano sempre l'esecuzione con uno stack allineato ma possono comunque fare riferimento correttamente ai parametri passati sullo stack, tramite x4
.
Per quanto riguarda i registri SIMD non volatili, c'è una differenza significativa tra le convenzioni di chiamata di Arm64 e x64. Su Arm64, gli 8 byte inferiori (64 bit) del registro sono considerati non volatili. In altre parole, solo la parte Dn
dei registri Qn
è non volatile. Su x64, gli interi 16 byte del registro XMMn
sono considerati non volatili. Inoltre, su x64, XMM6
e XMM7
sono registri non volatili mentre D6 e D7 (i corrispondenti registri di Arm64) sono volatili.
Per risolvere queste asimmetrie nella manipolazione dei registri SIMD, gli Entry Thunk devono salvare esplicitamente tutti i registri SIMD che sono considerati non volatili in x64. Questo è necessario solo su Entry Thunks (non Exit Thunks) perché x64 è più severo di Arm64. In altre parole, le regole di salvataggio/conservazione dei registri in x64 superano i requisiti di Arm64 in tutti i casi.
Per risolvere il problema del recupero corretto di questi valori di registro quando si srotola lo stack (ad esempio setjmp + longjmp, o throw + catch), è stato introdotto un nuovo opcode di srotolamento: save_any_reg (0xE7)
. Questo nuovo opcode di sbroglio a 3 byte permette di salvare qualsiasi registro General Purpose o SIMD (compresi quelli considerati volatili) e anche i registri Qn
a grandezza naturale. Questo nuovo opcode viene utilizzato per le operazioni di versamento/riempimento del registro Qn
di cui sopra. save_any_reg
è compatibile con save_next_pair (0xE6)
.
Come riferimento, di seguito sono riportate le informazioni di svolgimento corrispondenti all'Entry Thunk presentato in precedenza:
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)
Dopo il ritorno della funzione Arm64EC, la routine __os_arm64x_dispatch_ret
viene utilizzata per rientrare nell'emulatore, tornando al codice x64 (indicato da LR
).
Le funzioni Arm64EC hanno i 4 byte prima della prima istruzione della funzione riservati alla memorizzazione di informazioni da utilizzare in fase di esecuzione. È in questi 4 byte che si trova l'indirizzo relativo dell'Entry Thunk della funzione. Quando si esegue una chiamata da una funzione x64 a una funzione Arm64EC, l'emulatore leggerà i 4 byte prima dell'inizio della funzione, maschererà i due bit inferiori e li aggiungerà all'indirizzo della funzione. Questo produrrà l'indirizzo dell'Entry Thunk da chiamare.
Regolatore Thunks
Gli Adjustor Thunk sono funzioni senza firma che trasferiscono semplicemente il controllo a (tail-call) un'altra funzione, dopo aver eseguito una qualche trasformazione su uno dei parametri. Il tipo del parametro o dei parametri che vengono trasformati è noto, ma tutti i parametri rimanenti possono essere qualsiasi cosa e, in qualsiasi numero - Adjustor Thunks non toccherà nessun registro potenzialmente contenente un parametro e non toccherà lo stack. Questo è ciò che rende le funzioni di Adjustor Thunks prive di firma.
I Thunk di regolazione possono essere generati automaticamente dal compilatore. Questo è comune, ad esempio, con l'ereditarietà multipla del C++, dove qualsiasi metodo virtuale può essere delegato alla classe genitore, senza subire alcuna modifica, a parte una modifica al puntatore this
.
Di seguito è riportato un esempio reale:
[thunk]:CObjectContext::Release`adjustor{8}':
sub x0,x0,#8
b CObjectContext::Release
Il thunk sottrae 8 byte al puntatore this
e inoltra la chiamata alla classe madre.
In sintesi, le funzioni Arm64EC richiamabili dalle funzioni x64 devono avere un Entry Thunk associato. L'Entry Thunk è specifico per la firma. Le funzioni Arm64 senza firma, come gli Adjustor Thunks, necessitano di un meccanismo diverso che possa gestire le funzioni senza firma.
L'Entry Thunk di un Adjustor Thunk utilizza l'helper __os_arm64x_x64_jump
per rimandare l'esecuzione del vero lavoro dell'Entry Thunk (regolare i parametri da una convenzione all'altra) alla chiamata successiva. È in questo momento che la firma diventa evidente. Questo include l'opzione di non effettuare alcun aggiustamento della convenzione di chiamata, se l'obiettivo dell'Adjustor Thunk risulta essere una funzione x64. Ricorda che quando un Entry Thunk inizia l'esecuzione, i parametri sono nella loro forma x64.
Nell'esempio precedente, considera come appare il codice in Arm64EC.
Il regolatore si è incastrato nel braccio64EC
[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
Tronchi di ingresso di Adjustor Thunk
[thunk]:CObjectContext::Release$entry_thunk`adjustor{8}':
sub x0,x0,#8
adrp x9,CObjectContext::Release
add x9,x9,CObjectContext::Release
adrp xip0,__os_arm64x_x64_jump
ldr xip0,[xip0,__os_arm64x_x64_jump]
br xip0
Sequenze di avanzamento rapido
Alcune applicazioni apportano modifiche in tempo di esecuzione a funzioni che risiedono in binari che non possiedono ma da cui dipendono, in genere binari del sistema operativo, allo scopo di deviare l'esecuzione quando la funzione viene chiamata. Questo è noto anche come aggancio.
Ad alto livello, il processo di aggancio è semplice. Nel dettaglio, tuttavia, l'aggancio è specifico per l'architettura e piuttosto complesso viste le potenziali variazioni che la logica di aggancio deve affrontare.
In termini generali, il processo prevede quanto segue:
- Determina l'indirizzo della funzione da agganciare.
- Sostituisci la prima istruzione della funzione con un salto alla routine di aggancio.
- Una volta terminato l'aggancio, torna alla logica originale, che comprende l'esecuzione dell'istruzione originale spostata.
Le variazioni derivano da fattori quali:
- La dimensione della prima istruzione: È una buona idea sostituirlo con un JMP della stessa dimensione o più piccolo, per evitare di sostituire la parte superiore della funzione mentre un altro thread potrebbe eseguirla in volo.
- Il tipo della prima istruzione: Se la prima istruzione ha una natura relativa al PC, la sua ricollocazione potrebbe richiedere la modifica di elementi come i campi di spostamento. Poiché è probabile che vadano in overflow quando un'istruzione viene spostata in un punto lontano, potrebbe essere necessario fornire una logica equivalente con istruzioni diverse.
A causa di tutta questa complessità, è raro trovare una logica di aggancio robusta e generica. Spesso la logica presente nelle applicazioni può gestire solo un insieme limitato di casi che l'applicazione si aspetta di incontrare nelle API specifiche a cui è interessata. Non è difficile immaginare quanto questo sia un problema di compatibilità delle applicazioni. Anche una semplice modifica del codice o le ottimizzazioni del compilatore possono rendere inutilizzabili le applicazioni se il codice non è più esattamente come previsto.
Cosa succederebbe a queste applicazioni se dovessero incontrare il codice Arm64 durante l'impostazione di un hook? Fallirebbero di sicuro.
Le funzioni Fast-Forward Sequence (FFS) soddisfano questo requisito di compatibilità in Arm64EC.
Le FFS sono funzioni x64 molto piccole che non contengono alcuna logica reale e fanno da coda alla funzione Arm64EC reale. Sono opzionali ma abilitati di default per tutte le esportazioni di DLL e per qualsiasi funzione decorata con __declspec(hybrid_patchable)
.
In questi casi, quando il codice ottiene un puntatore a una determinata funzione, sia tramite GetProcAddress
nel caso di esportazione, sia tramite &function
nel caso di __declspec(hybrid_patchable)
, l'indirizzo risultante conterrà codice x64. Il codice x64 passerà per una funzione x64 legittima, soddisfacendo la maggior parte della logica di aggancio attualmente disponibile.
Considera il seguente esempio (la gestione degli errori è stata omessa per brevità):
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);
Il valore del puntatore della funzione nella pgma
variabile conterrà l'indirizzo GetMachineTypeAttributes
FFS di .
Questo è un esempio di sequenza veloce:
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
La funzione FFS x64 ha un prologo e un epilogo canonici, che terminano con un tail-call (salto) alla vera funzione GetMachineTypeAttributes
nel codice Arm64EC:
kernelbase!GetMachineTypeAttributes:
00000001`80243810 d503237f pacibsp
00000001`80243814 a9bc7bfd stp fp,lr,[sp,#-0x40]!
00000001`80243818 a90153f3 stp x19,x20,[sp,#0x10]
00000001`8024381c a9025bf5 stp x21,x22,[sp,#0x20]
00000001`80243820 f9001bf9 str x25,[sp,#0x30]
00000001`80243824 910003fd mov fp,sp
00000001`80243828 97fbe65e bl kernelbase!#__security_push_cookie
00000001`8024382c d10083ff sub sp,sp,#0x20
[...]
Sarebbe piuttosto inefficiente se fosse necessario eseguire 5 istruzioni emulate x64 tra due funzioni Arm64EC. Le funzioni FFS sono speciali. Le funzioni FFS non vengono effettivamente eseguite se rimangono invariate. L'helper di controllo delle chiamate verificherà in modo efficiente se FFS non è stato modificato. In questo caso, la chiamata verrà trasferita direttamente alla destinazione reale. Se l'FFS è stato modificato in qualsiasi modo, non sarà più un FFS. L'esecuzione verrà trasferita all'FFS alterato ed eseguirà il codice eventualmente presente, emulando la deviazione e qualsiasi logica di aggancio.
Quando il gancio trasferisce l'esecuzione alla fine dell'FFS, alla fine raggiungerà la chiamata di coda al codice Arm64EC, che verrà eseguito dopo il gancio, proprio come l'applicazione si aspetta.
Creare Arm64EC in Assembly
Gli header dell'SDK di Windows e il compilatore C possono semplificare il lavoro di creazione dell'assembly Arm64EC. Ad esempio, il compilatore C può essere utilizzato per generare i Thunk di entrata e di uscita per le funzioni non compilate da codice C.
Consideriamo l'esempio di una funzione equivalente alla seguente fD
che deve essere realizzata in Assembly (ASM). Questa funzione può essere chiamata sia dal codice Arm64EC che da quello x64 e il puntatore della funzione pfE
può puntare sia al codice Arm64EC che a quello x64.
typedef int (PF_E)(int, double);
extern PF_E * pfE;
int fD(int i, double d) {
return (*pfE)(i, d);
}
La scrittura di fD
in ASM sarebbe qualcosa di simile:
#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
Nell'esempio precedente:
- Arm64EC utilizza la stessa dichiarazione di procedura e le stesse macro prolog/epilog di Arm64.
- I nomi delle funzioni devono essere avvolti dalla macro
A64NAME
. Quando si compila codice C/C++ come Arm64EC, il compilatore contrassegnaOBJ
comeARM64EC
contenente codice Arm64EC. Questo non accade conARMASM
. Quando si compila il codice ASM esiste un modo alternativo per informare il linker che il codice prodotto è Arm64EC. Questo avviene anteponendo al nome della funzione il prefisso#
. La macroA64NAME
esegue questa operazione quando_ARM64EC_
è definito e lascia il nome invariato quando_ARM64EC_
non è definito. Questo rende possibile la condivisione del codice sorgente tra Arm64 e Arm64EC. - Il puntatore della funzione
pfE
deve essere prima sottoposto al controllo delle chiamate EC, insieme al Thunk di uscita appropriato, nel caso in cui la funzione di destinazione sia x64.
Generare i Thunk di entrata e di uscita
Il passo successivo consiste nel generare l'Entry Thunk per fD
e l'Exit Thunk per pfE
. Il compilatore C può eseguire questo compito con il minimo sforzo, utilizzando la parola chiave del compilatore _Arm64XGenerateThunk
.
void _Arm64XGenerateThunk(int);
int fD2(int i, double d) {
UNREFERENCED_PARAMETER(i);
UNREFERENCED_PARAMETER(d);
_Arm64XGenerateThunk(2);
return 0;
}
int fE(int i, double d) {
UNREFERENCED_PARAMETER(i);
UNREFERENCED_PARAMETER(d);
_Arm64XGenerateThunk(1);
return 0;
}
La parola chiave _Arm64XGenerateThunk
indica al compilatore C di utilizzare la firma della funzione, ignorare il corpo e generare un Exit Thunk (quando il parametro è 1) o un Entry Thunk (quando il parametro è 2).
Si consiglia di inserire la generazione dei thunk nel proprio file C. Essendo in file isolati, è più semplice confermare i nomi dei simboli scaricando i simboli corrispondenti di OBJ
o addirittura disassemblando.
Entrate personalizzate
Sono state aggiunte delle macro all'SDK per facilitare la creazione di Entry Thunk personalizzati e codificati a mano. Un caso in cui questo può essere utilizzato è la creazione di Thunk di regolazione personalizzati.
La maggior parte degli Adjustor Thunk sono generati dal compilatore C++, ma possono anche essere generati manualmente. Questo può verificarsi nei casi in cui una callback generica trasferisce il controllo alla callback reale, identificata da uno dei parametri.
Di seguito è riportato un esempio in codice Arm64 Classic:
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 questo esempio, l'indirizzo della funzione di destinazione viene recuperato dall'elemento di una struttura, fornito come riferimento, attraverso il primo parametro. Poiché la struttura è scrivibile, l'indirizzo di destinazione deve essere convalidato tramite Control Flow Guard (CFG).
L'esempio che segue mostra come l'equivalente Thunk dell'Aggiustatore verrebbe portato su 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
Il codice precedente non fornisce un Thunk di uscita (nel registro x10). Questo non è possibile perché il codice può essere eseguito per molte firme diverse. Questo codice sfrutta il fatto che il chiamante ha impostato x10 sul Thunk di uscita. Il chiamante avrebbe effettuato la chiamata puntando a una firma esplicita.
Il codice di cui sopra ha bisogno di un Entry Thunk per risolvere il caso in cui il chiamante sia un codice x64. Ecco come creare l'Entry Thunk corrispondente, utilizzando la macro per gli Entry Thunk personalizzati:
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
A differenza di altre funzioni, questo Entry Thunk non trasferisce il controllo alla funzione associata (il Adjustor Thunk). In questo caso, la funzionalità stessa (l'esecuzione della regolazione dei parametri) è incorporata nell'Entry Thunk e il controllo viene trasferito direttamente al target finale, tramite l'helper __os_arm64x_x64_jump
.
Generazione dinamica (compilazione JIT) del codice Arm64EC
Nei processi Arm64EC ci sono due tipi di memoria eseguibile: Codice Arm64EC e codice x64.
Il sistema operativo estrae queste informazioni dai file binari caricati. i file binari x64 sono tutti x64 e Arm64EC contiene una tabella di variazione per le pagine di codice Arm64EC vs x64.
E il codice generato dinamicamente? I compilatori just-in-time (JIT) generano codice, in fase di esecuzione, che non è supportato da alcun file binario.
Di solito questo implica:
- Allocazione della memoria scrivibile (
VirtualAlloc
). - Produrre il codice nella memoria allocata.
- Riprotezione della memoria da Read-Write a Read-Execute (
VirtualProtect
). - Aggiungi le voci delle funzioni di sbroglio per tutte le funzioni generate non banali (non a foglia) (
RtlAddFunctionTable
oRtlAddGrowableFunctionTable
).
Per banali ragioni di compatibilità, qualsiasi applicazione che esegua questi passaggi in un processo Arm64EC farà sì che il codice venga considerato codice x64. Questo accadrà per qualsiasi processo che utilizza il runtime Java x64 non modificato, il runtime .NET, il motore JavaScript, ecc.
Per generare codice dinamico Arm64EC, il processo è per lo più lo stesso con due sole differenze:
- Quando si alloca la memoria, utilizza il nuovo
VirtualAlloc2
(invece diVirtualAlloc
oVirtualAllocEx
) e fornisci l'attributoMEM_EXTENDED_PARAMETER_EC_CODE
. - Quando si aggiungono voci di funzione:
- Devono essere in formato Arm64. Quando si compila il codice Arm64EC, il tipo
RUNTIME_FUNCTION
corrisponderà al formato x64. Per il formato Arm64 durante la compilazione di Arm64EC, usa invece il tipoARM64_RUNTIME_FUNCTION
. - Non utilizzare la vecchia API
RtlAddFunctionTable
. Utilizza invece sempre la nuova APIRtlAddGrowableFunctionTable
.
- Devono essere in formato Arm64. Quando si compila il codice Arm64EC, il tipo
Di seguito è riportato un esempio di allocazione della memoria:
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);
E un esempio di aggiunta di una voce della funzione Unwind:
ARM64_RUNTIME_FUNCTION FunctionTable[1];
FunctionTable[0].BeginAddress = 0;
FunctionTable[0].Flags = PdataPackedUnwindFunction;
FunctionTable[0].FunctionLength = nSize / 4;
FunctionTable[0].RegF = 0; // no D regs saved
FunctionTable[0].RegI = 0; // no X regs saved beyond fp,lr
FunctionTable[0].H = 0; // no home for x0-x7
FunctionTable[0].CR = PdataCrChained; // stp fp,lr,[sp,#-0x10]!
// mov fp,sp
FunctionTable[0].FrameSize = 1; // 16 / 16 = 1
this->DynamicTable = NULL;
Result == RtlAddGrowableFunctionTable(
&this->DynamicTable,
reinterpret_cast<PRUNTIME_FUNCTION>(FunctionTable),
1,
1,
reinterpret_cast<ULONG_PTR>(pBegin),
reinterpret_cast<ULONG_PTR>(reinterpret_cast<PBYTE>(pBegin) + nSize)
);