Gestione delle eccezioni ARM64
Windows in ARM64 usa lo stesso meccanismo di gestione delle eccezioni strutturate per le eccezioni generate dall'hardware asincrone e le eccezioni generate dal software sincrone. I gestori di eccezioni specifici del linguaggio sono costruiti sulla base della gestione strutturata delle eccezioni di Windows mediante le funzioni helper del linguaggio. Questo documento descrive la gestione delle eccezioni in Windows in ARM64. Illustra gli helper del linguaggio usati dal codice generato dall'assembler Microsoft ARM e dal compilatore MSVC.
Obiettivi e motivazione
Le convenzioni dei dati di rimozione delle eccezioni e questa descrizione sono destinate a:
Fornire una descrizione sufficiente per consentire la rimozione senza eseguire il probe del codice in tutti i casi.
L'analisi del codice richiede che il codice venga inserito nel paging. Impedisce la rimozione in alcune circostanze in cui è utile (traccia, campionamento, debug).
L'analisi del codice è complessa; il compilatore deve prestare attenzione a generare solo istruzioni che il selvitore può decodificare.
Se la rimozione non può essere descritta completamente usando i codici di rimozione, in alcuni casi deve eseguire il fallback alla decodifica delle istruzioni. La decodifica delle istruzioni aumenta la complessità complessiva e idealmente dovrebbe essere evitata.
Supporto della rimozione nel prologo intermedio e nell'epilogo intermedio.
- La rimozione viene usata in Windows per più di una gestione delle eccezioni. È fondamentale che il codice possa rimuovere accuratamente anche quando si trova al centro di una sequenza di codice prologo o epilogo.
Occupare una quantità minima di spazio.
I codici di rimozione non devono essere aggregati per aumentare significativamente le dimensioni binarie.
Poiché è probabile che i codici di rimozione siano bloccati in memoria, un footprint ridotto garantisce un sovraccarico minimo per ogni file binario caricato.
Presupposti
Questi presupposti vengono effettuati nella descrizione della gestione delle eccezioni:
I prologi e gli epilogi tendono a rispecchiarsi tra loro. Sfruttando questo tratto comune, le dimensioni dei metadati necessari per descrivere la rimozione possono essere notevolmente ridotte. All'interno del corpo della funzione, non importa se le operazioni del prologo vengono annullate o le operazioni dell'epilogo vengono eseguite in modo forward. Entrambi devono produrre risultati identici.
Le funzioni tendono a essere relativamente piccole. Diverse ottimizzazioni per lo spazio si basano su questo fatto per ottenere la compressione più efficiente dei dati.
Non esiste codice condizionale negli epilogi.
Registro puntatore frame dedicato: se l'oggetto
sp
viene salvato in un altro registro (x29
) nel prologo, tale registro rimane invariato in tutta la funzione. Significa che l'originalesp
può essere recuperato in qualsiasi momento.A meno che non
sp
venga salvato in un altro registro, tutte le modifiche del puntatore dello stack si verificano rigorosamente all'interno del prologo e dell'epilogo.Il layout dello stack frame è organizzato come descritto nella sezione successiva.
Layout dello stack frame ARM64
Per le funzioni concatenati di frame, la fp
coppia e lr
può essere salvata in qualsiasi posizione nell'area variabile locale, a seconda delle considerazioni sull'ottimizzazione. L'obiettivo è ottimizzare il numero di variabili locali che possono essere raggiunte da una singola istruzione in base al puntatore frame () o al puntatore dello stack (x29
sp
). Tuttavia, per alloca
le funzioni, deve essere concatenato e x29
deve puntare alla parte inferiore dello stack. Per consentire una migliore copertura register-pair-addressing-mode, le aree di salvataggio del registro non volatile vengono posizionate nella parte superiore dello stack di aree locali. Ecco alcuni esempi che illustrano diverse delle sequenze di prologo più efficienti. Per motivi di chiarezza e migliore localizzazione della cache, l'ordine di archiviazione dei registri salvati dal chiamato in tutti i prolog canonici è in ordine "crescente". #framesz
sotto rappresenta le dimensioni dell'intero stack (escluso l'area alloca
). #localsz
e #outsz
indicano le dimensioni dell'area locale (inclusa l'area di salvataggio per la coppia) e le dimensioni dei <x29, lr>
parametri in uscita, rispettivamente.
Concatenato, #localsz <= 512
stp x19,x20,[sp,#-96]! // pre-indexed, save in 1st FP/INT pair stp d8,d9,[sp,#16] // save in FP regs (optional) stp x0,x1,[sp,#32] // home params (optional) stp x2,x3,[sp,#48] stp x4,x5,[sp,#64] stp x6,x7,[sp,#82] stp x29,lr,[sp,#-localsz]! // save <x29,lr> at bottom of local area mov x29,sp // x29 points to bottom of local sub sp,sp,#outsz // (optional for #outsz != 0)
Concatenato, #localsz > 512
stp x19,x20,[sp,#-96]! // pre-indexed, save in 1st FP/INT pair stp d8,d9,[sp,#16] // save in FP regs (optional) stp x0,x1,[sp,#32] // home params (optional) stp x2,x3,[sp,#48] stp x4,x5,[sp,#64] stp x6,x7,[sp,#82] sub sp,sp,#(localsz+outsz) // allocate remaining frame stp x29,lr,[sp,#outsz] // save <x29,lr> at bottom of local area add x29,sp,#outsz // setup x29 points to bottom of local area
Funzioni foglia senza portamento (
lr
non salvate)stp x19,x20,[sp,#-80]! // pre-indexed, save in 1st FP/INT reg-pair stp x21,x22,[sp,#16] str x23,[sp,#32] stp d8,d9,[sp,#40] // save FP regs (optional) stp d10,d11,[sp,#56] sub sp,sp,#(framesz-80) // allocate the remaining local area
Tutte le variabili locali sono accessibili in base a
sp
.<x29,lr>
punta al frame precedente. Per le dimensioni <del frame = 512, l'oggettosub sp, ...
può essere ottimizzato se l'area salvata regs viene spostata nella parte inferiore dello stack. Lo svantaggio è che non è coerente con altri layout precedenti. Inoltre, i reg salvati fanno parte dell'intervallo per la modalità di indirizzamento pair-regs e pre-indicizzata e post-indicizzata.Funzioni non foglia non portabili (salva
lr
nell'area salvata int)stp x19,x20,[sp,#-80]! // pre-indexed, save in 1st FP/INT reg-pair stp x21,x22,[sp,#16] // ... stp x23,lr,[sp,#32] // save last Int reg and lr stp d8,d9,[sp,#48] // save FP reg-pair (optional) stp d10,d11,[sp,#64] // ... sub sp,sp,#(framesz-80) // allocate the remaining local area
Oppure, con un numero pari di registri Int salvati,
stp x19,x20,[sp,#-80]! // pre-indexed, save in 1st FP/INT reg-pair stp x21,x22,[sp,#16] // ... str lr,[sp,#32] // save lr stp d8,d9,[sp,#40] // save FP reg-pair (optional) stp d10,d11,[sp,#56] // ... sub sp,sp,#(framesz-80) // allocate the remaining local area
Salvato solo
x19
:sub sp,sp,#16 // reg save area allocation* stp x19,lr,[sp] // save x19, lr sub sp,sp,#(framesz-16) // allocate the remaining local area
* L'allocazione dell'area di salvataggio reg non viene piegata in
stp
perché non è possibile rappresentare un reg-lrstp
pre-indicizzato con i codici di rimozione.Tutte le variabili locali sono accessibili in base a
sp
.<x29>
punta al frame precedente.Concatenato, #framesz <= 512, #outsz = 0
stp x29,lr,[sp,#-framesz]! // pre-indexed, save <x29,lr> mov x29,sp // x29 points to bottom of stack stp x19,x20,[sp,#(framesz-32)] // save INT pair stp d8,d9,[sp,#(framesz-16)] // save FP pair
Rispetto al primo esempio di prologo precedente, questo esempio presenta un vantaggio: tutte le istruzioni di salvataggio del registro sono pronte per l'esecuzione dopo una sola istruzione di allocazione dello stack. Ciò significa che non c'è alcuna dipendenza da
sp
che impedisce il parallelismo a livello di istruzione.Concatenato, dimensioni > frame 512 (facoltativo per le funzioni senza
alloca
)stp x29,lr,[sp,#-80]! // pre-indexed, save <x29,lr> stp x19,x20,[sp,#16] // save in INT regs stp x21,x22,[sp,#32] // ... stp d8,d9,[sp,#48] // save in FP regs stp d10,d11,[sp,#64] mov x29,sp // x29 points to top of local area sub sp,sp,#(framesz-80) // allocate the remaining local area
A scopo di ottimizzazione,
x29
può essere inserito in qualsiasi posizione nell'area locale per offrire una copertura migliore per la modalità di indirizzamento di offset pre-/post-indicizzato. È possibile accedere alle variabili locali al di sotto dei puntatori ai fotogrammi in base asp
.Concatenato, dimensioni > frame 4K, con o senza alloca(),
stp x29,lr,[sp,#-80]! // pre-indexed, save <x29,lr> stp x19,x20,[sp,#16] // save in INT regs stp x21,x22,[sp,#32] // ... stp d8,d9,[sp,#48] // save in FP regs stp d10,d11,[sp,#64] mov x29,sp // x29 points to top of local area mov x15,#(framesz/16) bl __chkstk sub sp,sp,x15,lsl#4 // allocate remaining frame // end of prolog ... sub sp,sp,#alloca // more alloca() in body ... // beginning of epilog mov sp,x29 // sp points to top of local area ldp d10,d11,[sp,#64] ... ldp x29,lr,[sp],#80 // post-indexed, reload <x29,lr>
Informazioni sulla gestione delle eccezioni ARM64
.pdata
archivio
I .pdata
record sono una matrice ordinata di elementi a lunghezza fissa che descrivono ogni funzione di modifica dello stack in un file binario PE. La frase "modifica dello stack" è significativa: le funzioni foglia che non richiedono alcuna risorsa di archiviazione locale e non devono salvare/ripristinare registri non volatili, non richiedono un .pdata
record. Questi record devono essere omessi in modo esplicito per risparmiare spazio. Una rimozione da una di queste funzioni può ottenere l'indirizzo restituito direttamente da lr
per passare al chiamante.
Ogni .pdata
record per ARM64 ha una lunghezza di 8 byte. Il formato generale di ogni record inserisce l'RVA a 32 bit della funzione inizia nella prima parola, seguita da una seconda parola contenente un puntatore a un blocco a lunghezza .xdata
variabile o una parola compressa che descrive una sequenza di rimozione di una funzione canonica.
I campi sono i seguenti:
RVA start della funzione è l'RVA a 32 bit dell'inizio della funzione.
Flag è un campo a 2 bit che indica come interpretare i 30 bit rimanenti della seconda
.pdata
parola. Se Flag è 0, i bit rimanenti formano un RVA delle informazioni sulle eccezioni (con i due bit più bassi in modo implicito 0). Se Flag è diverso da zero, i bit rimanenti formano una struttura di dati di rimozione compressi.L'RVA delle informazioni sulle eccezioni è l'indirizzo della struttura delle informazioni sulle eccezioni a lunghezza variabile archiviata nella
.xdata
sezione . Questi dati devono essere allineati a 4 byte.I dati di rimozione compressi sono una descrizione compressa delle operazioni necessarie per la rimozione da una funzione, presupponendo una forma canonica. In questo caso, non è necessario alcun
.xdata
record.
.xdata
archivio
Quando il formato di rimozione compresso non è sufficiente per descrivere la rimozione di una funzione, è necessario creare un record a lunghezza .xdata
variabile. L'indirizzo di questo record viene archiviato nella seconda parola del .pdata
record. Il formato di .xdata
è un set di parole a lunghezza variabile compresso:
Questi dati sono suddivisi in quattro sezioni:
Un'intestazione di 1 parola o 2 parole che descrive le dimensioni complessive della struttura e fornisce i dati della funzione chiave. La seconda parola è presente solo se i campi Epilog Count e Code Words sono impostati su 0. L'intestazione include questi campi di bit:
a. La lunghezza della funzione è un campo a 18 bit. Indica la lunghezza totale della funzione in byte, divisa per 4. Se una funzione è maggiore di 1M, è necessario usare più
.pdata
record e.xdata
per descrivere la funzione. Per altre informazioni, vedere la sezione Funzioni Large.b. Vers è un campo a 2 bit. Descrive la versione dell'oggetto rimanente
.xdata
. Attualmente è definita solo la versione 0, quindi i valori di 1-3 non sono consentiti.c. X è un campo a 1 bit. Indica la presenza (1) o l'assenza (0) dei dati delle eccezioni.
d. E è un campo a 1 bit. Indica che le informazioni che descrivono un singolo epilogo vengono compresse nell'intestazione (1) anziché richiedere più parole di ambito più avanti (0).
e. Epilog Count è un campo a 5 bit con due significati, a seconda dello stato di E bit:
Se E è 0, specifica il conteggio del numero totale di ambiti epilogi descritti nella sezione 2. Se nella funzione esistono più di 31 ambiti, il campo Parole codice deve essere impostato su 0 per indicare che è necessaria una parola di estensione.
Se E è 1, questo campo specifica l'indice del primo codice di rimozione che descrive quello e solo l'epilogo.
f. Parole di codice è un campo a 5 bit che specifica il numero di parole a 32 bit necessarie per contenere tutti i codici di rimozione nella sezione 3. Se sono necessarie più di 31 parole (ovvero 124 codici di rimozione), questo campo deve essere 0 per indicare che è necessaria una parola di estensione.
g. Il conteggio degli epilogi estesi e le parole di codice esteso sono rispettivamente campi a 16 bit e a 8 bit. Forniscono più spazio per la codifica di un numero insolitamente elevato di epilogi o un numero insolitamente elevato di parole di codice di rimozione. La parola di estensione che contiene questi campi è presente solo se i campi Epilog Count e Code Words nella prima parola di intestazione sono 0.
Se il numero di epilogi non è zero, un elenco di informazioni sugli ambiti dell'epilogo, compresso uno a una parola, viene dopo l'intestazione e l'intestazione estesa facoltativa. Vengono archiviati in ordine di offset iniziale crescente. Ogni ambito contiene i bit seguenti:
a. Epilog Start Offset è un campo a 18 bit con offset in byte, diviso per 4, dell'epilogo rispetto all'inizio della funzione.
b. Res è un campo a 4 bit riservato per l'espansione futura. Il suo valore deve essere 0.
c. Epilog Start Index è un campo a 10 bit (2 bit in più rispetto alle parole di codice estese). Indica l'indice byte del primo codice di rimozione che descrive questo epilogo.
Dopo che l'elenco degli ambiti dell'epilogo viene fornita una matrice di byte che contengono codici di rimozione, descritti in dettaglio in una sezione successiva. Questa matrice viene riempita alla fine fino al più vicino confine di parola completa. I codici di rimozione vengono scritti in questa matrice. Iniziano con quello più vicino al corpo della funzione e si spostano verso i bordi della funzione. I byte per ogni codice di rimozione vengono archiviati in ordine big-endian in modo che il byte più significativo venga recuperato per primo, che identifica l'operazione e la lunghezza del resto del codice.
Infine, dopo i byte del codice di rimozione, se il bit X nell'intestazione è stato impostato su 1, vengono fornite le informazioni sul gestore eccezioni. È costituito da un'unica RVA del gestore eccezioni che fornisce l'indirizzo del gestore eccezioni stesso. Viene seguito immediatamente da una quantità di dati a lunghezza variabile richiesta dal gestore eccezioni.
Il .xdata
record è progettato in modo da poter recuperare i primi 8 byte e usarli per calcolare le dimensioni complete del record, meno la lunghezza dei dati delle eccezioni di dimensioni variabili che seguono. Il frammento di codice seguente calcola le dimensioni del record:
ULONG ComputeXdataSize(PULONG Xdata)
{
ULONG Size;
ULONG EpilogScopes;
ULONG UnwindWords;
if ((Xdata[0] >> 22) != 0) {
Size = 4;
EpilogScopes = (Xdata[0] >> 22) & 0x1f;
UnwindWords = (Xdata[0] >> 27) & 0x1f;
} else {
Size = 8;
EpilogScopes = Xdata[1] & 0xffff;
UnwindWords = (Xdata[1] >> 16) & 0xff;
}
if (!(Xdata[0] & (1 << 21))) {
Size += 4 * EpilogScopes;
}
Size += 4 * UnwindWords;
if (Xdata[0] & (1 << 20)) {
Size += 4; // Exception handler RVA
}
return Size;
}
Anche se il prologo e ogni epilogo ha un proprio indice nei codici di rimozione, la tabella viene condivisa tra di esse. È del tutto possibile (e non del tutto insolito) che possano condividere tutti gli stessi codici. Per un esempio, vedere l'esempio 2 nelSezione Esempi . I writer del compilatore devono essere ottimizzati per questo caso in particolare. Il motivo è che l'indice più grande che può essere specificato è 255, che limita il numero totale di codici di rimozione per una determinata funzione.
Codici di rimozione
La matrice di codici di rimozione è un pool di sequenze che descrivono esattamente come annullare gli effetti del prologo. Vengono archiviati nello stesso ordine in cui le operazioni devono essere annullate. I codici di rimozione possono essere considerati come un piccolo set di istruzioni, codificato come stringa di byte. Al termine dell'esecuzione, l'indirizzo restituito alla funzione chiamante si trova nel lr
registro. Inoltre, tutti i registri non volatili vengono ripristinati ai relativi valori al momento della chiamata della funzione.
Se le eccezioni venivano garantite solo all'interno di un corpo della funzione e non all'interno di un prologo o di un epilogo, sarebbe necessaria solo una singola sequenza. Tuttavia, il modello di rimozione di Windows richiede che il codice possa rimuoversi da un prologo o un epilogo parzialmente eseguito. Per soddisfare questo requisito, i codici di rimozione sono stati progettati attentamente in modo da mappare in modo univoco 1:1 a ogni codice operativo pertinente nel prologo ed epilogo. Questa progettazione ha diverse implicazioni:
Conteggiando il numero di codici di rimozione, è possibile calcolare la lunghezza del prologo ed epilogo.
Conteggiando il numero di istruzioni oltre l'inizio di un ambito epilogo, è possibile ignorare il numero equivalente di codici di rimozione. È possibile eseguire il resto di una sequenza per completare la rimozione parzialmente eseguita dall'epilogo.
Conteggiando il numero di istruzioni prima della fine del prologo, è possibile ignorare il numero equivalente di codici di rimozione. È possibile eseguire il resto della sequenza per annullare solo le parti del prologo che hanno completato l'esecuzione.
I codici di rimozione vengono codificati in base alla tabella seguente. Tutti i codici di rimozione sono un byte singolo/doppio, ad eccezione di quello che alloca uno stack enorme (alloc_l
). Ci sono 22 codici di rimozione in totale. Ogni codice di rimozione esegue il mapping esattamente di un'istruzione nel prologo/epilogo, per consentire la rimozione di prologi e epilogi parzialmente eseguiti.
Codice di rimozione | Bit e interpretazione |
---|---|
alloc_s |
000xxxxx: allocare uno stack di piccole dimensioni con dimensioni < 512 (2^5 * 16). |
save_r19r20_x |
001zzzzz: save <x19,x20> pair at [sp-#Z*8]! , pre-indexed offset >= -248 |
save_fplr |
01zzzzzzzz: coppia di salvataggio <x29,lr> in corrispondenza di [sp+#Z*8] , offset <= 504. |
save_fplr_x |
10zzzzzz: save <x29,lr> pair at [sp-(#Z+1)*8]! , pre-indexed offset >= -512 |
alloc_m |
11000xxx'xxxxxxxx: allocare stack di grandi dimensioni con dimensioni < 32K (2^11 * 16). |
save_regp |
110010xx'xxzzzzzz: coppia di salvataggio x(19+#X) in [sp+#Z*8] , offset <= 504 |
save_regp_x |
110011xx'xxzzzzzz: save pair x(19+#X) at [sp-(#Z+1)*8]! , pre-indexed offset >= -512 |
save_reg |
110100xx'xxzzzzzz: save reg x(19+#X) at [sp+#Z*8] , offset <= 504 |
save_reg_x |
1101010x'xxxzzzzz: save reg x(19+#X) at [sp-(#Z+1)*8]! , pre-indexed offset >= -256 |
save_lrpair |
1101011x'xxzzzzzz: coppia <x(19+2*#X),lr> di salvataggio in [sp+#Z*8] , offset <= 504 |
save_fregp |
1101100x'xxzzzzzz: coppia d(8+#X) di salvataggio in [sp+#Z*8] , offset <= 504 |
save_fregp_x |
1101101x'xxzzzzzz: save pair d(8+#X) at [sp-(#Z+1)*8]! , pre-indexed offset >= -512 |
save_freg |
1101110x'xxzzzzzz: save reg d(8+#X) at [sp+#Z*8] , offset <= 504 |
save_freg_x |
11011110'xxxzzzzz: save reg d(8+#X) at [sp-(#Z+1)*8]! , pre-indexed offset >= -256 |
alloc_l |
11100000'xxxxxxxx'xxxxxxxx'xxxxxxxx: allocare stack di grandi dimensioni con dimensioni < 256M (2^24 * 16) |
set_fp |
11100001: configurare x29 con mov x29,sp |
add_fp |
11100010'xxxxxxxx: configurato x29 con add x29,sp,#x*8 |
nop |
11100011: non è necessaria alcuna operazione di rimozione. |
end |
11100100: fine del codice di rimozione. Implica ret nell'epilogo. |
end_c |
11100101: fine del codice di rimozione nell'ambito concatenato corrente. |
save_next |
11100110: salvare la coppia di registrazione int o FP successiva non volatile. |
11100111: riservato | |
11101xxx: riservato per i casi di stack personalizzati sotto generati solo per le routine asm | |
11101000: stack personalizzato per MSFT_OP_TRAP_FRAME |
|
11101001: stack personalizzato per MSFT_OP_MACHINE_FRAME |
|
11101010: stack personalizzato per MSFT_OP_CONTEXT |
|
11101011: stack personalizzato per MSFT_OP_EC_CONTEXT |
|
11101100: stack personalizzato per MSFT_OP_CLEAR_UNWOUND_TO_CALL |
|
11101101: riservato | |
11101110: riservato | |
11101111: riservato | |
11110xxx: riservato | |
11111000'yyy : riservato | |
11111001'yy'yyy : riservato | |
11111010'yy'y'yyyy: riservato | |
11111011'yy'yy'yy'y: riservato | |
pac_sign_lr |
11111100: firmare l'indirizzo restituito con lr pacibsp |
11111101: riservato | |
11111110: riservato | |
11111111: riservato |
Nelle istruzioni con valori di grandi dimensioni che coprono più byte, i bit più significativi vengono archiviati per primi. Questa progettazione consente di trovare le dimensioni totali in byte del codice di rimozione cercando solo il primo byte del codice. Poiché ogni codice di rimozione viene mappato esattamente a un'istruzione in un prologo o un epilogo, è possibile calcolare le dimensioni del prologo o dell'epilogo. Passare dall'inizio della sequenza alla fine e usare una tabella di ricerca o un dispositivo simile per determinare la lunghezza del codice operativo corrispondente.
L'indirizzamento dell'offset post-indicizzato non è consentito in un prologo. Tutti gli intervalli di offset (#Z) corrispondono alla codifica dell'indirizzamento ad eccezione save_r19r20_x
di stp
/str
, in cui 248 è sufficiente per tutte le aree di salvataggio (10 registri Int + 8 registri FP + 8 registri di input).
save_next
deve seguire un salvataggio per la coppia di registri int o FP volatile: save_regp
, save_regp_x
, save_fregp_x
save_fregp
, save_r19r20_x
, o un altro save_next
. Salva la coppia di registri successiva nello slot di 16 byte successivo in ordine di crescita. Un save_next
fa riferimento alla prima coppia di registri FP quando segue che save-next
indica l'ultima coppia di registri Int.
Poiché le dimensioni delle istruzioni di ritorno e salto regolari sono le stesse, non è necessario un codice di rimozione separato end
in scenari di chiamata finale.
end_c
è progettato per gestire frammenti di funzione non contigui a scopo di ottimizzazione. Oggetto end_c
che indica la fine dei codici di rimozione nell'ambito corrente deve essere seguito da un'altra serie di codici di rimozione che terminano con un reale end
. I codici di rimozione tra end_c
e end
rappresentano le operazioni di prologo nell'area padre (un prologo "fantasma"). Altri dettagli ed esempi sono descritti nella sezione seguente.
Dati di rimozione compressi
Per le funzioni i cui prologi ed epilogi seguono la forma canonica descritta di seguito, è possibile usare i dati di rimozione compressi. Elimina completamente la necessità di un .xdata
record e riduce significativamente il costo di fornire dati di rimozione. I prologi e gli epilogi canonici sono progettati per soddisfare i requisiti comuni di una funzione semplice: uno che non richiede un gestore eccezioni e che esegue le operazioni di configurazione e di disinstallazione in un ordine standard.
Il formato di un .pdata
record con dati di rimozione compressi è simile al seguente:
I campi sono i seguenti:
- RVA start della funzione è l'RVA a 32 bit dell'inizio della funzione.
- Flag è un campo a 2 bit come descritto in precedenza, con i significati seguenti:
- 00 = dati di rimozione compressi non utilizzati; i bit rimanenti puntano a un
.xdata
record - 01 = dati di rimozione compressi usati con un singolo prologo ed epilogo all'inizio e alla fine dell'ambito
- 10 = dati di rimozione compressi usati per il codice senza prologo ed epilogo. Utile per descrivere segmenti di funzione separati
- 11 = riservato.
- 00 = dati di rimozione compressi non utilizzati; i bit rimanenti puntano a un
- La lunghezza della funzione è un campo a 11 bit che fornisce la lunghezza dell'intera funzione in byte, divisa per 4. Se la funzione è maggiore di 8.000, è necessario usare invece un record completo
.xdata
. - Dimensioni frame è un campo a 9 bit che indica il numero di byte dello stack allocato per questa funzione, diviso per 16. Le funzioni che allocano più di (8k-16) byte dello stack devono usare un record completo
.xdata
. Include l'area della variabile locale, l'area dei parametri in uscita, l'area int salvata dal chiamato Int e FP e l'area dei parametri home. Esclude l'area di allocazione dinamica. - CR è un flag a 2 bit che indica se la funzione include istruzioni aggiuntive per configurare una catena di frame e restituire un collegamento:
- 00 = funzione non associata,
<x29,lr>
la coppia non viene salvata nello stack - 01 = funzione non portabile,
<lr>
viene salvata nello stack - 10 = funzione concatenato con un
pacibsp
indirizzo restituito firmato - 11 = funzione concatenato, viene usata un'istruzione di coppia di archiviazione/caricamento nel prologo/epilogo
<x29,lr>
- 00 = funzione non associata,
- H è un flag a 1 bit che indica se la funzione ospita i registri dei parametri integer (x0-x7) archiviandoli all'inizio della funzione. (0 = non si registra a casa, 1 = registri case).
- RegI è un campo a 4 bit che indica il numero di registri INT non volatili (x19-x28) salvati nella posizione dello stack canonico.
- RegF è un campo a 3 bit che indica il numero di registri FP non volatili (d8-d15) salvati nella posizione dello stack canonico. (RegF=0: non viene salvato alcun registro FP; RegF>0: i registri FP RegF+1 vengono salvati). I dati di rimozione compressi non possono essere usati per la funzione che salvano un solo registro FP.
I prolog canonici che rientrano nelle categorie 1, 2 (senza area dei parametri in uscita), 3 e 4 nella sezione precedente possono essere rappresentati dal formato di rimozione compresso. Gli epilogi per le funzioni canoniche seguono una forma simile, ad eccezione di H non ha alcun effetto, l'istruzione set_fp
viene omessa e l'ordine dei passaggi e le istruzioni in ogni passaggio vengono invertiti nell'epilogo. L'algoritmo per il pacchetto .xdata
segue questi passaggi, descritti in dettaglio nella tabella seguente:
Passaggio 0: Pre-calcolo delle dimensioni di ogni area.
Passaggio 1: Firmare l'indirizzo restituito.
Passaggio 2: Salvare i registri salvati dal chiamato Int.
Passaggio 3: Questo passaggio è specifico per il tipo 4 nelle sezioni iniziali. lr
viene salvato alla fine dell'area Int.
Passaggio 4: Salvare i registri salvati dal chiamato FP.
Passaggio 5: Salvare gli argomenti di input nell'area dei parametri home.
Passaggio 6: Allocare lo stack rimanente, inclusa l'area locale, <x29,lr>
la coppia e l'area dei parametri in uscita. 6a corrisponde al tipo canonico 1. 6b e 6c sono per il tipo canonico 2. 6d e 6e sono sia per il tipo 3 che per il tipo 4.
N.ro passaggio | Valori dei flag | # di istruzioni | Opcode | Codice di rimozione |
---|---|---|---|---|
0 | #intsz = RegI * 8; if (CR==01) #intsz += 8; // lr #fpsz = RegF * 8; if(RegF) #fpsz += 8; #savsz=((#intsz+#fpsz+8*8*H)+0xf)&~0xf) #locsz = #famsz - #savsz |
|||
1 | CR == 10 | 1 | pacibsp |
pac_sign_lr |
2 | 0 <RegI<= 10 | RegI / 2 + RegI % 2 |
stp x19,x20,[sp,#savsz]! stp x21,x22,[sp,#16] ... |
save_regp_x save_regp ... |
3 | CR == 01* | 1 | str lr,[sp,#(intsz-8)] * |
save_reg |
4 | 0 <RegF<= 7 | (RegF + 1) / 2 + (RegF + 1) % 2) |
stp d8,d9,[sp,#intsz] **stp d10,d11,[sp,#(intsz+16)] ... str d(8+RegF),[sp,#(intsz+fpsz-8)] |
save_fregp ... save_freg |
5 | H == 1 | 4 | stp x0,x1,[sp,#(intsz+fpsz)] stp x2,x3,[sp,#(intsz+fpsz+16)] stp x4,x5,[sp,#(intsz+fpsz+32)] stp x6,x7,[sp,#(intsz+fpsz+48)] |
nop nop nop nop |
6a | (CR == 10 || CR == 11) &&#locsz <= 512 |
2 | stp x29,lr,[sp,#-locsz]! mov x29,sp *** |
save_fplr_x set_fp |
6b | (CR == 10 || CR == 11) && 512 < #locsz <= 4080 |
3 | sub sp,sp,#locsz stp x29,lr,[sp,0] add x29,sp,0 |
alloc_m save_fplr set_fp |
6c | (CR == 10 || CR == 11) &&#locsz > 4080 |
4 | sub sp,sp,4080 sub sp,sp,#(locsz-4080) stp x29,lr,[sp,0] add x29,sp,0 |
alloc_m alloc_s /alloc_m save_fplr set_fp |
6d | (CR == 00 || CR == 01) &&#locsz <= 4080 |
1 | sub sp,sp,#locsz |
alloc_s /alloc_m |
6e | (CR == 00 || CR == 01) &&#locsz > 4080 |
2 | sub sp,sp,4080 sub sp,sp,#(locsz-4080) |
alloc_m alloc_s /alloc_m |
* Se CR == 01 e RegI è un numero dispari , il passaggio 3 e l'ultimo save_reg
nel passaggio 2 vengono uniti in un unico save_regp
.
** Se RegI == CR == 0 e RegF != 0, il primo stp
per il virgola mobile esegue la dichiarazione preliminare.
Nessuna istruzione corrispondente a mov x29,sp
è presente nell'epilogo. I dati di rimozione compressi non possono essere usati se una funzione richiede il ripristino da sp
x29
.
Rimozione di prologi parziali ed epilogi
Nelle situazioni di rimozione più comuni, l'eccezione o la chiamata si verifica nel corpo della funzione, lontano dal prologo e da tutti gli epilogi. In queste situazioni, la rimozione è semplice: lo rimozione esegue semplicemente i codici nella matrice di rimozione. Inizia con l'indice 0 e continua fino a quando non viene rilevato un end
codice operativo.
È più difficile rimuovere correttamente nel caso in cui si verifichi un'eccezione o un interrupt durante l'esecuzione di un prologo o un epilogo. In queste situazioni, lo stack frame è costruito solo parzialmente. Il problema consiste nel determinare esattamente cosa è stato fatto, per annullarlo correttamente.
Ad esempio, prendere questa sequenza di prologo ed epilogo:
0000: stp x29,lr,[sp,#-256]! // save_fplr_x 256 (pre-indexed store)
0004: stp d8,d9,[sp,#224] // save_fregp 0, 224
0008: stp x19,x20,[sp,#240] // save_regp 0, 240
000c: mov x29,sp // set_fp
...
0100: mov sp,x29 // set_fp
0104: ldp x19,x20,[sp,#240] // save_regp 0, 240
0108: ldp d8,d9,[sp,224] // save_fregp 0, 224
010c: ldp x29,lr,[sp],#256 // save_fplr_x 256 (post-indexed load)
0110: ret lr // end
Accanto a ogni codice operativo è il codice di rimozione appropriato che descrive questa operazione. Si può vedere come la serie di codici di rimozione per il prologo è un'immagine speculare esatta dei codici di rimozione per l'epilogo (senza contare l'istruzione finale dell'epilogo). Si tratta di una situazione comune: è per questo che si presuppongono sempre che i codici di rimozione per il prologo vengano archiviati in ordine inverso rispetto all'ordine di esecuzione del prologo.
Quindi, sia per il prologo che per l'epilogo, siamo rimasti con un set comune di codici di rimozione:
set_fp
, save_regp 0,240
, save_fregp,0,224
, save_fplr_x_256
end
Il caso dell'epilogo è semplice, perché è in ordine normale. A partire dall'offset 0 all'interno dell'epilogo (che inizia all'offset 0x100 nella funzione), si prevede che la sequenza di rimozione completa venga eseguita, perché non è stata ancora eseguita alcuna pulizia. Se si trova un'istruzione in (in corrispondenza dell'offset 2 nell'epilogo), è possibile rimuovere correttamente ignorando il primo codice di rimozione. Possiamo generalizzare questa situazione e presupporre un mapping 1:1 tra opcodes e codici di rimozione. Quindi, per iniziare la rimozione dall'istruzione n nell'epilogo, è consigliabile ignorare i primi n codici di rimozione e iniziare l'esecuzione da lì.
Si scopre che una logica simile funziona per il prologo, tranne inverso. Se si inizia la rimozione dall'offset 0 nel prologo, non si vuole eseguire alcuna operazione. Se si rimuove l'offset 2, ovvero un'istruzione in , si vuole iniziare a eseguire la sequenza di rimozione un codice di rimozione dalla fine. Tenere presente che i codici vengono archiviati in ordine inverso. E anche qui, possiamo generalizzare: se iniziamo a rimuovere dall'istruzione n nel prologo, dovremmo iniziare a eseguire n codici di rimozione dalla fine dell'elenco di codici.
I codici prologo ed epilogo non corrispondono sempre esattamente, motivo per cui la matrice di rimozione potrebbe dover contenere diverse sequenze di codici. Per determinare l'offset di dove iniziare l'elaborazione dei codici, usare la logica seguente:
Se si esegue la rimozione dall'interno del corpo della funzione, iniziare a eseguire codici di rimozione in corrispondenza dell'indice 0 e continuare fino a raggiungere un
end
codice operativo.Se si esegue la rimozione da un epilogo, usare l'indice iniziale specifico dell'epilogo fornito con l'ambito dell'epilogo come punto iniziale. Calcolare il numero di byte che il PC in questione proviene dall'inizio dell'epilogo. Avanti quindi attraverso i codici di rimozione, ignorando i codici di rimozione fino a quando non vengono riportate tutte le istruzioni già eseguite. Eseguire quindi a partire da quel punto.
Se si esegue la rimozione dal prologo, usare l'indice 0 come punto iniziale. Calcolare la lunghezza del codice di prologo dalla sequenza e quindi calcolare il numero di byte in questione dal termine del prologo. Avanti quindi attraverso i codici di rimozione, ignorando i codici di rimozione fino a quando non vengono riportate tutte le istruzioni non ancora eseguite. Eseguire quindi a partire da quel punto.
Queste regole indicano che i codici di rimozione per il prologo devono essere sempre il primo nella matrice. E sono anche i codici usati per rilassarsi nel caso generale di rimozione dall'interno del corpo. Tutte le sequenze di codice specifiche dell'epilogo devono essere seguite immediatamente dopo.
Frammenti di funzione
Ai fini dell'ottimizzazione del codice e altri motivi, può essere preferibile suddividere una funzione in frammenti separati (dette anche aree). In caso di divisione, ogni frammento di funzione risultante richiede un record separato .pdata
(ed eventualmente .xdata
).
Per ogni frammento secondario separato con un proprio prologo, è previsto che non venga eseguita alcuna regolazione dello stack nel prologo. Tutto lo spazio dello stack richiesto da un'area secondaria deve essere preallocato dall'area padre o dall'area host. Questa preallocazione mantiene la manipolazione del puntatore dello stack rigorosamente nel prologo originale della funzione.
Un caso tipico di frammenti di funzione è "separazione del codice", in cui il compilatore può spostare un'area di codice fuori dalla funzione host. Esistono tre casi insoliti che potrebbero derivare dalla separazione del codice.
Esempio
(area 1: inizio)
stp x29,lr,[sp,#-256]! // save_fplr_x 256 (pre-indexed store) stp x19,x20,[sp,#240] // save_regp 0, 240 mov x29,sp // set_fp ...
(area 1: fine)
(area 3: inizio)
...
(area 3: fine)
(area 2: inizio)
... mov sp,x29 // set_fp ldp x19,x20,[sp,#240] // save_regp 0, 240 ldp x29,lr,[sp],#256 // save_fplr_x 256 (post-indexed load) ret lr // end
(area 2: fine)
Solo prologo (area 1: tutti gli epilogi si trovano in aree separate):
È necessario descrivere solo il prologo. Questo prologo non può essere rappresentato nel formato compatto
.pdata
. Nel caso completo.xdata
, può essere rappresentato impostando Epilog Count = 0. Vedere area 1 nell'esempio precedente.Codici di rimozione:
set_fp
,save_regp 0,240
,save_fplr_x_256
,end
.Solo epilogi (area 2: prologo si trova nell'area host)
Si presuppone che dal controllo dell'ora si inserisca in questa area, sono stati eseguiti tutti i codici di prologo. La rimozione parziale può verificarsi negli epilogi allo stesso modo di una funzione normale. Questo tipo di area non può essere rappresentato da compatta
.pdata
. In un record completo.xdata
, può essere codificato con un prologo "fantasma", racchiuso tra parentesi da unaend_c
coppia di codice di rimozione eend
rimozione. Il carattere inizialeend_c
indica che la dimensione del prologo è zero. L'indice iniziale dell'epilogo del singolo epilogo punta aset_fp
.Codice di rimozione per l'area 2:
end_c
,set_fp
,save_regp 0,240
,save_fplr_x_256
,end
.Nessun prologo o epilogo (area 3: prologi e tutti gli epilogi si trovano in altri frammenti):
Il formato compatto
.pdata
può essere applicato tramite l'impostazione Flag = 10. Con record completo.xdata
, Epilog Count = 1. Il codice di rimozione è uguale al codice per l'area 2 precedente, ma Epilog Start Index punta anche aend_c
. La rimozione parziale non verrà mai eseguita in questa area di codice.
Un altro caso più complesso di frammenti di funzione è "compattare il wrapping". Il compilatore può scegliere di ritardare il salvataggio di alcuni registri salvati dal chiamato fino all'esterno del prologo della voce della funzione.
(area 1: inizio)
stp x29,lr,[sp,#-256]! // save_fplr_x 256 (pre-indexed store) stp x19,x20,[sp,#240] // save_regp 0, 240 mov x29,sp // set_fp ...
(area 2: inizio)
stp x21,x22,[sp,#224] // save_regp 2, 224 ... ldp x21,x22,[sp,#224] // save_regp 2, 224
(area 2: fine)
... mov sp,x29 // set_fp ldp x19,x20,[sp,#240] // save_regp 0, 240 ldp x29,lr,[sp],#256 // save_fplr_x 256 (post-indexed load) ret lr // end
(area 1: fine)
Nel prologo dell'area 1, lo spazio dello stack viene preallocato. È possibile notare che l'area 2 avrà lo stesso codice di rimozione anche se viene spostato dalla funzione host.
Area 1: set_fp
, save_regp 0,240
, save_fplr_x_256
, end
. L'indice iniziale dell'epilogo punta come set_fp
di consueto.
Area 2: save_regp 2, 224
, end_c
, set_fp
, save_regp 0,240
, save_fplr_x_256
, . end
Epilog Start Index punta al primo codice save_regp 2, 224
di rimozione.
Funzioni di grandi dimensioni
I frammenti possono essere usati per descrivere le funzioni superiori al limite di 1M imposto dai campi di bit nell'intestazione .xdata
. Per descrivere una funzione insolitamente grande come questa, deve essere suddivisa in frammenti più piccoli di 1M. Ogni frammento deve essere regolato in modo da non suddividere un epilogo in più parti.
Solo il primo frammento della funzione conterrà un prologo; tutti gli altri frammenti sono contrassegnati come senza prologo. A seconda del numero di epilogi presenti, ogni frammento può contenere zero o più epilogi. Tenere presente che ogni ambito dell'epilogo in un frammento specifica l'offset iniziale rispetto all'inizio del frammento, non all'inizio della funzione.
Se un frammento non ha prologo e nessun epilogo, richiede comunque il proprio .pdata
record (ed eventualmente .xdata
) per descrivere come eseguire la rimozione dall'interno del corpo della funzione.
Esempi
Esempio 1: Concatenamento con frame, formato compatto
|Foo| PROC
|$LN19|
str x19,[sp,#-0x10]! // save_reg_x
sub sp,sp,#0x810 // alloc_m
stp fp,lr,[sp] // save_fplr
mov fp,sp // set_fp
// end of prolog
...
|$pdata$Foo|
DCD imagerel |$LN19|
DCD 0x416101ed
;Flags[SingleProEpi] functionLength[492] RegF[0] RegI[1] H[0] frameChainReturn[Chained] frameSize[2080]
Esempio 2: Concatenamento con frame, formato completo con prologo e epilogo mirror
|Bar| PROC
|$LN19|
stp x19,x20,[sp,#-0x10]! // save_regp_x
stp fp,lr,[sp,#-0x90]! // save_fplr_x
mov fp,sp // set_fp
// end of prolog
...
// begin of epilog, a mirror sequence of Prolog
mov sp,fp
ldp fp,lr,[sp],#0x90
ldp x19,x20,[sp],#0x10
ret lr
|$pdata$Bar|
DCD imagerel |$LN19|
DCD imagerel |$unwind$cse2|
|$unwind$Bar|
DCD 0x1040003d
DCD 0x1000038
DCD 0xe42291e1
DCD 0xe42291e1
;Code Words[2], Epilog Count[1], E[0], X[0], Function Length[6660]
;Epilog Start Index[0], Epilog Start Offset[56]
;set_fp
;save_fplr_x
;save_r19r20_x
;end
Epilog Start Index [0] punta alla stessa sequenza di codice di rimozione prologo.
Esempio 3: Funzione variadic unchained
|Delegate| PROC
|$LN4|
sub sp,sp,#0x50
stp x19,lr,[sp]
stp x0,x1,[sp,#0x10] // save incoming register to home area
stp x2,x3,[sp,#0x20] // ...
stp x4,x5,[sp,#0x30]
stp x6,x7,[sp,#0x40] // end of prolog
...
ldp x19,lr,[sp] // beginning of epilog
add sp,sp,#0x50
ret lr
AREA |.pdata|, PDATA
|$pdata$Delegate|
DCD imagerel |$LN4|
DCD imagerel |$unwind$Delegate|
AREA |.xdata|, DATA
|$unwind$Delegate|
DCD 0x18400012
DCD 0x200000f
DCD 0xe3e3e3e3
DCD 0xe40500d6
DCD 0xe40500d6
;Code Words[3], Epilog Count[1], E[0], X[0], Function Length[18]
;Epilog Start Index[4], Epilog Start Offset[15]
;nop // nop for saving in home area
;nop // ditto
;nop // ditto
;nop // ditto
;save_lrpair
;alloc_s
;end
L'indice iniziale dell'epilogo [4] punta al centro del codice di rimozione prologo (matrice di rimozione parzialmente riutilizzata).
Vedi anche
Panoramica delle convenzioni ABI arm64
Gestione delle eccezioni arm