Gestion des exceptions ARM64
Windows sur ARM64 utilise le même mécanisme de gestion des exceptions structurées pour les exceptions générées par le matériel asynchrone et les exceptions générées par le logiciel synchrone. Les gestionnaires d'exceptions propres aux langages s'appuient sur la gestion des exceptions structurées Windows en utilisant des fonctions d'assistance de langage. Ce document décrit la gestion des exceptions dans Windows sur ARM64. Il illustre les helpers de langage utilisés par le code généré par l’assembleur Microsoft ARM et le compilateur MSVC.
Objectifs et motivation
Les conventions de déroulement des données d’exception et cette description sont destinées à :
Fournissez une description suffisante pour permettre le déroulement sans détection de code dans tous les cas.
L’analyse du code nécessite que le code soit paginé. Il empêche le déroulement dans certaines circonstances où il est utile (suivi, échantillonnage, débogage).
L’analyse du code est complexe ; le compilateur doit être prudent pour générer uniquement des instructions que le déroulement peut décoder.
Si le déroulement ne peut pas être entièrement décrit à l’aide de codes de déroulement, il doit, dans certains cas, revenir au décodage des instructions. Le décodage des instructions augmente la complexité globale et, idéalement, doit être évité.
Prise en charge du déroulement dans le prolog milieu et mi-épilogue.
- Le déroulement est utilisé dans Windows pour plus que la gestion des exceptions. Il est essentiel que le code puisse se dérouler avec précision même au milieu d’un prolog ou d’une séquence de code épilogue.
Prenez une quantité minimale d’espace.
Les codes de déroulement ne doivent pas être agrégés pour augmenter considérablement la taille binaire.
Étant donné que les codes de déroulement sont susceptibles d’être verrouillés en mémoire, une petite empreinte garantit une surcharge minimale pour chaque binaire chargé.
Hypothèses
Ces hypothèses sont faites dans la description de gestion des exceptions :
Les prologues et les épilogues ont tendance à miroir les uns les autres. En tirant parti de cette caractéristique commune, la taille des métadonnées nécessaires pour décrire le déroulement peut être considérablement réduite. Dans le corps de la fonction, il n’importe pas si les opérations du prologue sont annulées ou si les opérations de l’épilogue sont effectuées de manière avancée. Les deux doivent produire des résultats identiques.
Les fonctions ont tendance à être relativement petites. Plusieurs optimisations de l’espace s’appuient sur ce fait pour obtenir l’emballage le plus efficace des données.
Il n’existe aucun code conditionnel dans les épilogues.
Registre de pointeur d’image dédié : si l’enregistrement
sp
est enregistré dans un autre registre (x29
) dans le prolog, ce registre reste inchangé dans toute la fonction. Cela signifie que l’originalsp
peut être récupéré à tout moment.Sauf si le
sp
fichier est enregistré dans un autre registre, toute manipulation du pointeur de pile se produit strictement dans le prolog et l’épilogue.La disposition du cadre de pile est organisée comme décrit dans la section suivante.
Disposition du cadre de pile ARM64
Pour les fonctions chaînées d’images, la paire et lr
la fp
paire peuvent être enregistrées à n’importe quelle position dans la zone de variable locale, en fonction des considérations d’optimisation. L’objectif est d’optimiser le nombre de locaux qui peuvent être atteints par une seule instruction basée sur le pointeur d’image (x29
) ou le pointeur de pile (sp
). Toutefois, pour alloca
les fonctions, il doit être chaîné et x29
doit pointer vers le bas de la pile. Pour permettre une meilleure couverture en mode d’adressage des paires d’inscriptions, les zones d’enregistrement des registres nonvolatiles sont positionnées en haut de la pile des zones locales. Voici des exemples qui illustrent plusieurs séquences de prologue les plus efficaces. Par souci de clarté et de meilleure localité de cache, l’ordre de stockage des registres appelés enregistrés dans tous les prologs canoniques est dans l’ordre « croissant ». #framesz
ci-dessous représente la taille de la pile entière (à l’exception alloca
de la zone). #localsz
et #outsz
indiquent respectivement la taille de la zone locale (y compris la zone d’enregistrement de la <x29, lr>
paire) et la taille des paramètres sortants.
Chaîné, #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)
Chaîné, #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
Fonctions feuille non chaînées (
lr
non enregistrées)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
Tous les locaux sont accessibles en fonction de
sp
.<x29,lr>
pointe vers le cadre précédent. Pour la taille <d’image = 512, voussub sp, ...
pouvez l’optimiser si la zone enregistrée regs est déplacée vers le bas de la pile. L’inconvénient est qu’il n’est pas cohérent avec d’autres dispositions ci-dessus. Et, les regs enregistrés font partie de la plage pour le mode d’adressage de paire-regs et le mode d’adressage de décalage post-indexé.Fonctions non liées à la feuille (enregistre
lr
dans la zone enregistrée 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
Ou, avec un nombre pair enregistré des registres Int,
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
Uniquement
x19
enregistré :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’allocation de zone d’enregistrement reg n’est pas pliée dans la
stp
mesure où un reg-lrstp
préindexé ne peut pas être représenté avec les codes de déroulement.Tous les locaux sont accessibles en fonction de
sp
.<x29>
pointe vers le cadre précédent.Chaîné, #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
Par rapport au premier exemple de prolog ci-dessus, cet exemple présente un avantage : toutes les instructions d’enregistrement d’enregistrement sont prêtes à s’exécuter après une seule instruction d’allocation de pile. Cela signifie qu’il n’y a pas d’anti-dépendance à
sp
ce qui empêche le parallélisme au niveau de l’instruction.Chaîné, taille > d’image 512 (facultatif pour les fonctions sans
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
À des fins d’optimisation,
x29
vous pouvez placer à n’importe quelle position dans la zone locale pour fournir une meilleure couverture pour le mode d’adressage de décalage pré-indexé et pré-indexé. Les points de terminaison d’images ci-dessous sont accessibles en fonctionsp
de .Chaîné, taille > de cadre 4K, avec ou sans 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>
Informations de gestion des exceptions ARM64
.pdata
Dossiers
Les .pdata
enregistrements sont un tableau ordonné d’éléments de longueur fixe qui décrivent chaque fonction de manipulation de pile dans un fichier binaire PE. L’expression « manipulation de pile » est importante : les fonctions feuilles qui ne nécessitent aucun stockage local et n’ont pas besoin d’enregistrer/restaurer des registres non volatiles, ne nécessitent pas d’enregistrement .pdata
. Ces enregistrements doivent être explicitement omis pour économiser de l’espace. Un déroulement de l’une de ces fonctions peut obtenir l’adresse de retour directement depuis lr
pour passer à l’appelant.
Chaque .pdata
enregistrement pour ARM64 est de 8 octets de longueur. Le format général de chaque enregistrement place la RVA 32 bits du début de la fonction dans le premier mot, suivie d’un deuxième mot qui contient un pointeur vers un bloc de longueur .xdata
variable, ou un mot packé décrivant une séquence de déroulement de fonction canonique.
Les champs sont les suivants :
Function Start RVA est l’appliance virtuelle RVA 32 bits du début de la fonction.
L’indicateur est un champ 2 bits qui indique comment interpréter les 30 bits restants du deuxième
.pdata
mot. Si l’indicateur est 0, les bits restants forment une RVA d’informations d’exception (avec les deux bits les plus bas implicitement 0). Si l’indicateur n’est pas égal à zéro, les bits restants forment une structure Dewind Data Packed.Exception Information RVA est l’adresse de la structure d’informations d’exception de longueur variable, stockée dans la
.xdata
section. Ces données doivent être alignées sur 4 octets.Packed Unwind Data est une description compressée des opérations nécessaires pour décompresser à partir d’une fonction, en supposant une forme canonique. Dans ce cas, aucun enregistrement n’est
.xdata
requis.
.xdata
Dossiers
Lorsque le format de déroulement compressé est insuffisant pour décrire le déroulement d’une fonction, un enregistrement de longueur .xdata
variable doit être créé. L’adresse de cet enregistrement est stockée dans le deuxième mot de l’enregistrement .pdata
. Le format du fichier .xdata
est un ensemble de mots de longueur variable empaqueté :
Ces données sont divisées en quatre sections :
En-tête de 1 mot ou 2 mots décrivant la taille globale de la structure et fournissant des données de fonction clés. Le deuxième mot est présent uniquement si les champs Nombre d’Épilog et Mots de code sont définis sur 0. L’en-tête comporte les champs de bits suivants :
a. La longueur de la fonction est un champ 18 bits. Elle indique la longueur totale de la fonction en octets, divisée par 4. Si une fonction est supérieure à 1M, plusieurs
.pdata
enregistrements doivent.xdata
être utilisés pour décrire la fonction. Pour plus d’informations, consultez la section Fonctions volumineuses .b. Vers est un champ 2 bits. Il décrit la version du restant
.xdata
. Actuellement, seule la version 0 est définie, de sorte que les valeurs de 1 à 3 ne sont pas autorisées.c. X est un champ 1 bits. Il indique la présence (1) ou l’absence (0) des données d’exception.
d. E est un champ 1 bits. Il indique que les informations décrivant un épilogue unique sont empaquetées dans l’en-tête (1) plutôt que d’exiger plus de mots d’étendue plus tard (0).
e. Epilog Count est un champ 5 bits qui a deux significations, en fonction de l’état du bit E :
Si E est égal à 0, il spécifie le nombre total d’étendues d’épilogue décrites dans la section 2. Si plus de 31 étendues existent dans la fonction, le champ Mots de code doit être défini sur 0 pour indiquer qu’un mot d’extension est requis.
Si E est 1, ce champ spécifie l’index du premier code de déroulement qui décrit l’épilogue unique et unique.
f. Les mots de code sont un champ 5 bits qui spécifie le nombre de mots 32 bits nécessaires pour contenir tous les codes de déroulement de la section 3. Si plus de 31 mots (autrement dit, 124 codes de déroulement) sont requis, ce champ doit être 0 pour indiquer qu’un mot d’extension est requis.
g. Le nombre d’épilogs étendus et les mots de code étendus sont respectivement des champs 16 bits et 8 bits. Ils offrent davantage d’espace pour l’encodage d’un nombre inhabituel d’épilogues ou d’un nombre inhabituellement élevé de mots de code de déroulement. Le mot d’extension qui contient ces champs n’est présent que si les champs Nombre d’Épilog et Mots de code dans le premier mot d’en-tête sont 0.
Si le nombre d’épilogues n’est pas égal à zéro, une liste d’informations sur les étendues d’épilogue, empaquetée d’un mot, vient après l’en-tête et l’en-tête étendu facultatif. Ils sont stockés dans l’ordre d’augmentation du décalage de départ. Chaque étendue contient les bits suivants :
a. Epilog Start Offset est un champ 18 bits qui a le décalage en octets, divisé par 4, de l’épilogue par rapport au début de la fonction.
b. Res est un champ 4 bits réservé à l’expansion future. Il doit avoir la valeur 0.
c. Epilog Start Index est un champ 10 bits (2 bits plus que les mots de code étendus). Il indique l’index d’octets du premier code de déroulement qui décrit cet épilogue.
Une fois que la liste des étendues d’épilogues est un tableau d’octets qui contiennent des codes de déroulement, décrit en détail dans une section ultérieure. Ce tableau est rempli à la fin jusqu'à la limite du mot complet le plus proche. Les codes de déroulement sont écrits dans ce tableau. Ils commencent par le plus proche du corps de la fonction, et se déplacent vers les bords de la fonction. Les octets de chaque code de déroulement sont stockés dans l’ordre big-endian afin que l’octet le plus significatif soit récupéré en premier, ce qui identifie l’opération et la longueur du reste du code.
Enfin, après les octets de code de déroulement, si le bit X dans l’en-tête a été défini sur 1, vient les informations du gestionnaire d’exceptions. Il se compose d’une seule RVA de gestionnaire d’exceptions qui fournit l’adresse du gestionnaire d’exceptions lui-même. Elle est suivie immédiatement d’une quantité variable de données requise par le gestionnaire d’exceptions.
L’enregistrement .xdata
est conçu de sorte qu’il est possible d’extraire les 8 premiers octets et de les utiliser pour calculer la taille complète de l’enregistrement, moins la longueur des données d’exception de taille variable qui suivent. L’extrait de code suivant calcule la taille d’enregistrement :
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;
}
Bien que le prolog et chaque épilogue possède son propre index dans les codes de déroulement, la table est partagée entre elles. Il est tout à fait possible (et pas tout à fait rare) qu’ils peuvent tous partager les mêmes codes. (Pour obtenir un exemple, consultez l’exemple 2 dans le Section Exemples .) Les enregistreurs de compilateur doivent optimiser pour ce cas en particulier. C’est parce que le plus grand index qui peut être spécifié est 255, ce qui limite le nombre total de codes de déroulement pour une fonction particulière.
Codes de déroulement
Le tableau de codes de déroulement est un pool de séquences qui décrivent exactement comment annuler les effets du prologue. Ils sont stockés dans le même ordre que les opérations doivent être annulées. Les codes de déroulement peuvent être considérés comme un petit jeu d’instructions, encodé en tant que chaîne d’octets. Une fois l’exécution terminée, l’adresse de retour à la fonction appelante se trouve dans le lr
registre. Et tous les registres non volatiles sont restaurés sur leurs valeurs au moment où la fonction a été appelée.
Si des exceptions étaient garanties pour ne se produire qu’à l’intérieur d’un corps de fonction, et jamais au sein d’un prologue ou d’un épilogue, une seule séquence serait nécessaire. Toutefois, le modèle de déroulement Windows nécessite que le code puisse se dérouler à partir d’un prolog ou d’un épilogue partiellement exécuté. Pour répondre à cette exigence, les codes de déroulement ont été soigneusement conçus afin qu’ils mappent sans ambiguïté 1 :1 à chaque opcode pertinent dans le prologue et l’épilogue. Cette conception a plusieurs implications :
En comptant le nombre de codes de déroulement, il est possible de calculer la longueur du prologue et de l’épilogue.
En comptant le nombre d’instructions au-delà du début d’une étendue épilogue, il est possible d’ignorer le nombre équivalent de codes de déroulement. Nous pouvons exécuter le reste d’une séquence pour terminer le déroulement partiellement exécuté par l’épilogue.
En comptant le nombre d’instructions avant la fin du prologue, il est possible d’ignorer le nombre équivalent de codes de déroulement. Nous pouvons exécuter le reste de la séquence pour annuler uniquement les parties du prologue qui ont terminé l’exécution.
Les codes de déroulement sont encodés en fonction du tableau ci-dessous. Tous les codes de déroulement sont un octet simple/double, sauf celui qui alloue une pile énorme (alloc_l
). Il existe 22 codes de déroulement au total. Chaque code de déroulement mappe exactement une instruction dans le prolog/epilog, afin de permettre le déroulement des prologs partiellement exécutés et des épilogues.
Déroulage du code | Bits et interprétation |
---|---|
alloc_s |
000xxxxx : allouez une petite pile avec la taille < 512 (2^5 * 16). |
save_r19r20_x |
001zzzz : paire d’enregistrement <x19,x20> à [sp-#Z*8]! , décalage >préindexé = -248 |
save_fplr |
01zzz : paire d’enregistrement <x29,lr> à [sp+#Z*8] , offset <= 504. |
save_fplr_x |
10zzzzz : paire d’enregistrement <x29,lr> à [sp-(#Z+1)*8]! , décalage >préindexé = -512 |
alloc_m |
11000xxx’xxxxxxxx : allouez une grande pile de taille < 32K (2^11 * 16). |
save_regp |
110010xx’xxzzzzz : save x(19+#X) pair at [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’xxzzz : save reg x(19+#X) at [sp+#Z*8] , offset <= 504 |
save_reg_x |
1101010x’xxxzzzz : save reg x(19+#X) at [sp-(#Z+1)*8]! , pre-indexed offset >= -256 |
save_lrpair |
1101011x’xxzzzz : save pair <x(19+2*#X),lr> at [sp+#Z*8] , offset <= 504 |
save_fregp |
1101100x’xxzzzz : save pair d(8+#X) at [sp+#Z*8] , offset <= 504 |
save_fregp_x |
1101101x’xxzzzz : save pair d(8+#X) at [sp-(#Z+1)*8]! , pre-indexed offset >= -512 |
save_freg |
1101110x’xxzzz : 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 : allouez une grande pile avec la taille < 256M (2^24 * 16) |
set_fp |
11100001 : configurer x29 avec mov x29,sp |
add_fp |
11100010'xxxxxxxx : configuré x29 avec add x29,sp,#x*8 |
nop |
11100011 : aucune opération de déroulement n’est requise. |
end |
11100100 : fin du code de déroulement. Implique ret dans l’épilogue. |
end_c |
11100101 : fin du code de déroulement dans l’étendue chaînée actuelle. |
save_next |
11100110 : enregistrez la paire d’inscriptions Int ou FP non volatile suivante. |
11100111 : réservé | |
11101xxx : réservé aux cas de pile personnalisés ci-dessous uniquement générés pour les routines asm | |
11101000 : pile personnalisée pour MSFT_OP_TRAP_FRAME |
|
11101001 : pile personnalisée pour MSFT_OP_MACHINE_FRAME |
|
11101010 : pile personnalisée pour MSFT_OP_CONTEXT |
|
11101011 : pile personnalisée pour MSFT_OP_EC_CONTEXT |
|
11101100 : pile personnalisée pour MSFT_OP_CLEAR_UNWOUND_TO_CALL |
|
11101101 : réservé | |
11101110 : réservé | |
11101111 : réservé | |
11110xxx : réservé | |
11111000'aaaay : réservé | |
11111001'yyy : réservé | |
11111010'aaaay’y’aaaay : réservé | |
11111011'aaaay’y’aaaa’y : réservé | |
pac_sign_lr |
11111100 : signer l’adresse de lr retour avec pacibsp |
11111101 : réservé | |
11111110 : réservé | |
11111111 : réservé |
Dans les instructions avec des valeurs volumineuses couvrant plusieurs octets, les bits les plus significatifs sont stockés en premier. Cette conception permet de trouver la taille totale en octets du code de déroulement en recherchant uniquement le premier octet du code. Étant donné que chaque code de déroulement est mappé exactement à une instruction dans un prologue ou un épilogue, vous pouvez calculer la taille du prologue ou de l’épilogue. Passez du début de la séquence à la fin et utilisez une table de recherche ou un appareil similaire pour déterminer la longueur du opcode correspondant.
L’adressage de décalage post-indexé n’est pas autorisé dans un prologue. Toutes les plages de décalage (#Z) correspondent à l’encodage de stp
/str
l’adressage, à l’exception save_r19r20_x
de laquelle 248 est suffisant pour toutes les zones d’enregistrement (10 registres Int + 8 registres FP + 8 registres d’entrée).
save_next
doit suivre une sauvegarde pour la paire d’inscriptions volatiles Int ou FP : save_regp
, , save_regp_x
save_fregp
, save_fregp_x
, , save_r19r20_x
ou une autre save_next
. Il enregistre la paire d’inscriptions suivante à l’emplacement de 16 octets suivant dans l’ordre « croissant ». Un save_next
fait référence à la première paire de registres FP lorsqu’elle suit la save-next
dernière paire de registres Int.
Étant donné que les tailles des instructions régulières de retour et de saut sont identiques, il n’est pas nécessaire d’utiliser un code de déroulement séparé end
dans les scénarios de fin d’appel.
end_c
est conçu pour gérer les fragments de fonction noncontigues à des fins d’optimisation. Un end_c
élément qui indique la fin des codes de déroulement dans l’étendue actuelle doit être suivi d’une autre série de codes de déroulement se terminant par un réel end
. Les codes de déroulement entre end_c
et end
représentent les opérations de prolog dans la région parente (un prologue « fantôme »). Des détails et des exemples supplémentaires sont décrits dans la section ci-dessous.
Données de déroulement empaquetées
Pour les fonctions dont les prologs et les épilogues suivent le formulaire canonique décrit ci-dessous, les données de déroulement empaquetées peuvent être utilisées. Il élimine entièrement la nécessité d’un .xdata
enregistrement et réduit considérablement le coût de la fourniture de données de déroulement. Les prologs canoniques et les épilogues sont conçus pour répondre aux exigences courantes d’une fonction simple : un gestionnaire d’exceptions qui ne nécessite pas de gestionnaire d’exceptions et qui effectue ses opérations d’installation et de déchirure dans un ordre standard.
Le format d’un .pdata
enregistrement avec des données de déroulement empaquetées ressemble à ceci :
Les champs sont les suivants :
- Function Start RVA est l’appliance virtuelle RVA 32 bits du début de la fonction.
- L’indicateur est un champ 2 bits, comme décrit ci-dessus, avec les significations suivantes :
- 00 = données de déroulement empaquetées non utilisées ; bits restants pointent vers un
.xdata
enregistrement - 01 = empaquetées données de déroulement utilisées avec un seul prologue et épilogue au début et à la fin de l’étendue
- 10 = données de déroulement empaquetées utilisées pour le code sans prologue et épilogue. Utile pour décrire des segments de fonction séparés
- 11 = réservé.
- 00 = données de déroulement empaquetées non utilisées ; bits restants pointent vers un
- La longueur de la fonction est un champ 11 bits qui fournit la longueur de la fonction entière en octets, divisé par 4. Si la fonction est supérieure à 8 ko, un enregistrement complet
.xdata
doit être utilisé à la place. - La taille du frame est un champ 9 bits indiquant le nombre d’octets de pile alloués pour cette fonction, divisé par 16. Les fonctions qui allouent plus de (8k-16) octets de pile doivent utiliser un enregistrement complet
.xdata
. Il inclut la zone de variable locale, la zone de paramètre sortante, la zone Int et FP enregistrées et la zone de paramètres d’accueil. Elle exclut la zone d’allocation dynamique. - CR est un indicateur 2 bits indiquant si la fonction inclut des instructions supplémentaires pour configurer une chaîne d’images et un lien de retour :
- 00 = fonction non chaîne,
<x29,lr>
la paire n’est pas enregistrée dans la pile - 01 = fonction non chaîne,
<lr>
est enregistrée dans la pile - 10 = fonction chaînée avec une
pacibsp
adresse de retour signée - 11 = fonction chaînée, une instruction de paire de magasin/chargement est utilisée dans prolog/epilog
<x29,lr>
- 00 = fonction non chaîne,
- H est un indicateur 1 bits indiquant si la fonction possède les registres de paramètres entiers (x0-x7) en les stockant au début de la fonction. (0 = ne s’inscrit pas à domicile, 1 = registres d’habitation).
- RegI est un champ 4 bits indiquant le nombre de registres INT non volatiles (x19-x28) enregistrés à l’emplacement de la pile canonique.
- RegF est un champ 3 bits indiquant le nombre de registres FP non volatiles (d8-d15) enregistrés à l’emplacement de la pile canonique. (RegF=0 : aucun registre FP n’est enregistré ; RegF>0 : Les registres RegF+1 FP sont enregistrés). Les données de déroulement empaquetées ne peuvent pas être utilisées pour la fonction qui enregistrent un seul registre FP.
Les prologs canoniques qui appartiennent aux catégories 1, 2 (sans zone de paramètre sortante), 3 et 4 dans la section ci-dessus peuvent être représentés par un format de déroulement packed. Les épilogues pour les fonctions canoniques suivent une forme similaire, à l’exception de H n’a aucun effet, l’instruction set_fp
est omise et l’ordre des étapes et les instructions de chaque étape sont inversées dans l’épilogue. L’algorithme pour packed .xdata
suit ces étapes, détaillées dans le tableau suivant :
Étape 0 : Pré-calcul de la taille de chaque zone.
Étape 1 : signer l’adresse de retour.
Étape 2 : Enregistrer les registres enregistrés dans int callee.
Étape 3 : cette étape est spécifique au type 4 dans les premières sections. lr
est enregistré à la fin de la zone Int.
Étape 4 : Enregistrer les registres enregistrés par FP appelé.
Étape 5 : Enregistrer les arguments d’entrée dans la zone de paramètres d’accueil.
Étape 6 : Allouer la pile restante, y compris la zone locale, <x29,lr>
la paire et la zone de paramètres sortante. 6a correspond au type canonique 1. 6b et 6c sont pour le type canonique 2. 6d et 6e sont pour le type 3 et le type 4.
N° de l’étape | Valeurs d’indicateur | Nombre d’instructions | Opcode | Déroulage du code |
---|---|---|---|---|
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 |
* Si CR == 01 et RegI est un nombre impair, l’étape 2 et la dernière save_rep
de l’étape 1 sont fusionnées en un save_regp
.
** Si RegI == CR == 0 et RegF != 0, le premier stp
pour le point flottant fait le prédécrément.
Aucune instruction correspondant à mov x29,sp
n’est présente dans l’épilogue. Les données de déroulement empaquetées ne peuvent pas être utilisées si une fonction nécessite la restauration à partir de sp
x29
.
Déroulement des prologues partiels et des épilogues
Dans les situations de déroulement les plus courantes, l’exception ou l’appel se produit dans le corps de la fonction, loin du prologue et de tous les épilogues. Dans ces situations, le déroulement est simple : le déroulement exécute simplement les codes dans le tableau de déroulement. Elle commence à l’index 0 et continue jusqu’à ce qu’un end
opcode soit détecté.
Il est plus difficile de décompresser correctement dans le cas où une exception ou une interruption se produit lors de l’exécution d’un prolog ou d’un épilogue. Dans ces situations, le cadre de pile n’est construit que partiellement. Le problème est de déterminer exactement ce qui a été fait, pour l’annuler correctement.
Par exemple, prenez cette séquence prologue et épilogue :
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
À côté de chaque opcode est le code de déroulement approprié décrivant cette opération. Vous pouvez voir comment la série de codes de déroulement pour le prologue est une image exacte miroir des codes de déroulement pour l’épilogue (sans compter l’instruction finale de l’épilogue). C’est une situation courante : c’est pourquoi nous partons toujours du principe que les codes de déroulement du prologue sont stockés dans l’ordre inverse de l’ordre d’exécution du prologue.
Ainsi, pour le prologue et l’épilogue, nous sommes laissés avec un ensemble commun de codes de déroulement :
set_fp
, , save_regp 0,240
save_fregp,0,224
, , save_fplr_x_256
end
Le cas de l’épilogue est simple, car il est dans l’ordre normal. À partir du décalage 0 dans l’épilogue (qui commence au décalage 0x100 dans la fonction), nous nous attendons à ce que la séquence de déroulement complète s’exécute, car aucune propre up n’a encore été effectuée. Si nous nous trouvons une instruction dans (à offset 2 dans l’épilogue), nous pouvons correctement décompresser en ignorant le premier code de déroulement. Nous pouvons généraliser cette situation et supposer un mappage 1 :1 entre les codes opcodes et les codes de déroulement. Ensuite, pour commencer le déroulement de l’instruction n dans l’épilogue, nous devons ignorer les premiers codes de déroulement et commencer à s’exécuter à partir de là.
Il s’avère qu’une logique similaire fonctionne pour le prologue, à l’exception de l’inverse. Si nous commençons à dérouler du décalage 0 dans le prologue, nous ne voulons rien exécuter. Si nous déroulons du décalage 2, qui est une instruction, nous voulons commencer à exécuter la séquence de déroulement d’un code de déroulement à partir de la fin. (N’oubliez pas que les codes sont stockés dans l’ordre inverse.) Et ici aussi, nous pouvons généraliser : si nous commençons à déroulage à partir de l’instruction n dans le prologue, nous devons commencer à exécuter n codes de déroulement à partir de la fin de la liste des codes.
Les codes prolog et épilogue ne correspondent pas toujours exactement, c’est pourquoi le tableau de déroulement peut avoir besoin de contenir plusieurs séquences de codes. Pour déterminer le décalage d’où commencer le traitement des codes, utilisez la logique suivante :
Si vous décompressez à partir du corps de la fonction, commencez à exécuter des codes de déroulement à l’index 0 et continuez jusqu’à atteindre un
end
opcode.Si vous décompressez à partir d’un épilogue, utilisez l’index de départ spécifique à l’épilogue fourni avec l’étendue de l’épilogue comme point de départ. Calculez le nombre d’octets du PC en question à partir du début de l’épilogue. Avancez ensuite dans les codes de déroulement, ignorez les codes de déroulement jusqu’à ce que toutes les instructions déjà exécutées soient prises en compte. Exécutez ensuite à partir de ce point.
Si vous décompressez à partir du prolog, utilisez l’index 0 comme point de départ. Calculez la longueur du code de prolog à partir de la séquence, puis calculez le nombre d’octets que le PC en question est à partir de la fin du prologue. Avancez ensuite dans les codes de déroulement, ignorez les codes de déroulement jusqu’à ce que toutes les instructions non encore exécutées soient prises en compte. Exécutez ensuite à partir de ce point.
Ces règles signifient que les codes de déroulement du prologue doivent toujours être les premiers dans le tableau. Et ils sont également les codes utilisés pour déroulage dans le cas général du déroulement à partir du corps. Toutes les séquences de code spécifiques à l’épilogue doivent suivre immédiatement après.
Fragments de fonction
Pour des raisons d’optimisation du code et d’autres raisons, il peut être préférable de fractionner une fonction en fragments séparés (également appelés régions). En cas de fractionnement, chaque fragment de fonction résultant nécessite son propre enregistrement distinct .pdata
(et éventuellement .xdata
).
Pour chaque fragment secondaire séparé qui a son propre prologue, il est attendu qu’aucun ajustement de pile ne soit effectué dans son prologue. Tous les espaces de pile requis par une région secondaire doivent être pré-alloués par sa région parente (ou appelée région hôte). Cette préallocation conserve strictement la manipulation du pointeur de pile dans le prolog d’origine de la fonction.
Un cas classique de fragments de fonction est la « séparation du code », où le compilateur peut déplacer une région de code hors de sa fonction hôte. Il existe trois cas inhabituels qui peuvent résulter de la séparation du code.
Exemple
(région 1 : début)
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 ...
(région 1 : fin)
(région 3 : début)
...
(région 3 : fin)
(région 2 : début)
... 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
(région 2 : fin)
Prolog uniquement (région 1 : tous les épilogues se trouvent dans des régions séparées) :
Seul le prologue doit être décrit. Ce prologue ne peut pas être représenté au format compact
.pdata
. Dans le cas complet.xdata
, il peut être représenté en définissant Epilog Count = 0. Consultez la région 1 dans l’exemple ci-dessus.Codes de déroulement :
set_fp
, ,save_fplr_x_256
save_regp 0,240
,end
.Epilogs uniquement (région 2 : prolog est dans la région hôte)
Il est supposé que, par le temps, le contrôle passe dans cette région, tous les codes prolog ont été exécutés. Le déroulement partiel peut se produire dans les épilogues de la même façon que dans une fonction normale. Ce type de région ne peut pas être représenté par compact
.pdata
. Dans un enregistrement complet.xdata
, il peut être encodé avec un prolog « fantôme », entre crochets par uneend_c
paire deend
code et déroulage. Le débutend_c
indique que la taille du prologue est égale à zéro. Index de début d’epilog des points d’épilogue uniques versset_fp
.Code de déroulement de la région 2 :
end_c
,set_fp
,save_regp 0,240
,save_fplr_x_256
,end
.Aucun prologue ou épilogue (région 3 : prologues et tous les épilogues se trouvent dans d’autres fragments) :
Le format compact
.pdata
peut être appliqué via le paramètre Indicateur = 10. Avec l’enregistrement complet.xdata
, Epilog Count = 1. Le code de déroulement est identique au code de la région 2 ci-dessus, mais l’index de démarrage Epilog pointe également versend_c
. Le déroulement partiel ne se produit jamais dans cette région de code.
Un autre cas plus compliqué de fragments de fonction est « réduire l’habillage ». Le compilateur peut choisir de retarder l’enregistrement de certains registres appelés enregistrés jusqu’à l’extérieur du prolog d’entrée de fonction.
(région 1 : début)
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 ...
(région 2 : début)
stp x21,x22,[sp,#224] // save_regp 2, 224 ... ldp x21,x22,[sp,#224] // save_regp 2, 224
(région 2 : fin)
... 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
(région 1 : fin)
Dans le prologue de la région 1, l’espace de pile est pré-alloué. Vous pouvez voir que la région 2 aura le même code de déroulement, même s’il est déplacé hors de sa fonction hôte.
Région 1 : set_fp
, , save_regp 0,240
save_fplr_x_256
, end
. Epilog Start Index pointe vers set_fp
comme d’habitude.
Région 2 : save_regp 2, 224
, , end_c
, set_fp
save_regp 0,240
, save_fplr_x_256
. end
Epilog Start Index pointe vers le premier code save_regp 2, 224
de déroulement .
Fonctions volumineuses
Les fragments peuvent être utilisés pour décrire les fonctions supérieures à la limite 1M imposée par les champs de bits dans l’en-tête .xdata
. Pour décrire une fonction inhabituellement grande comme celle-ci, elle doit être divisée en fragments inférieurs à 1M. Chaque fragment doit être ajusté afin qu’il ne fractionne pas d’épilogue en plusieurs parties.
Seul le premier fragment de la fonction contiendra un prologue ; tous les autres fragments sont marqués comme n’ayant pas de prologue. Selon le nombre d’épilogues présents, chaque fragment peut contenir zéro ou plusieurs épilogues. N’oubliez pas que chaque étendue d’épilogue dans un fragment spécifie son décalage de départ par rapport au début du fragment, et non au début de la fonction.
Si un fragment n’a pas de prologue et pas d’épilogue, il nécessite toujours son propre .pdata
enregistrement (et éventuellement .xdata
) pour décrire comment se dérouler à partir du corps de la fonction.
Exemples
Exemple 1 : Chaînée en trame, compact-form
|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]
Exemple 2 : Chaînée en trame complète avec miroir Prolog &Epilog
|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] pointe vers la même séquence de code de déroulement Prolog.
Exemple 3 : Fonction nonchainée variadicée
|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
Epilog Start Index [4] pointe vers le milieu du code de déroulement Prolog (réutilisez partiellement le tableau de déroulement).
Voir aussi
Vue d’ensemble des conventions ABI ARM64
Gestion des exceptions ARM