Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
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 données de déroulement d’exception et cette description présentent les objectifs suivants :
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 chargé en mémoire. Il empêche le déroulement dans certaines situations où cela est utile (traçage, échantillonnage, débogage).
L’analyse du code est complexe ; le compilateur doit être prudent pour ne générer que des instructions que le désencheveur peut décoder.
Si le déroulement ne peut pas être entièrement décrit à l’aide de codes de déroulement, il peut, dans certains cas, être nécessaire de recourir 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 au milieu du prologue et au milieu de l’épilogue.
- Le désempilement est utilisé dans Windows pour bien plus que la gestion des exceptions. Il est essentiel que le code puisse se dérouler avec précision même au milieu d’une séquence de code de prologue ou d’épilogue.
Occupez le moins d'espace possible.
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 prologs et les épilogues ont tendance à se mettre en miroir. En tirant parti de cette caractéristique commune, la taille des métadonnées nécessaires pour décrire le processus de déroulement peut être considérablement réduite. Dans le corps de la fonction, l’annulation des opérations du prologue ou l’exécution des opérations de l’épilogue de manière avancée ne posent pas de problème. 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
spest enregistré dans un autre registre (x29) dans le prolog, ce registre reste inchangé dans toute la fonction. Cela signifie que l’originalsppeut être récupéré à tout moment.Sauf si le
spest enregistré dans un autre registre, toute manipulation du pointeur de pile se produit strictement dans le prologue et l’épilogue.La disposition du frame de pile est organisée comme décrit dans la section suivante.
Disposition du frame de pile ARM64
Pour les fonctions à trames chaînées, la paire fp et lr peut être enregistrée à n’importe quelle position dans la zone des variables locales, 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 les fonctions alloca, il est essentiel qu'elles soient enchaînées, et x29 doit pointer vers le bas de la pile. Pour permettre une meilleure couverture du mode d'adressage des paires de registres, les zones de sauvegarde des registres non volatils sont positionnées en haut de la pile de la zone locale. Voici des exemples qui illustrent plusieurs séquences de prologue les plus efficaces. Par souci de clarté et de meilleure localité du cache, le stockage des registres enregistrés par les appelés dans tous les prologues canoniques se fait par ordre croissant.
#framesz représente la taille de la pile entière (à l'exception de la zone alloca).
#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.
Enchaî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)Enchaî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 areaFonctions terminales non enchaînées (
lrnon enregistré)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 areaTous les paramètres régionaux sont accessibles en fonction de
sp.<x29,lr>pointe vers le cadre précédent. Pour une taille de frame <= 512,sub sp, ...peut être optimisé si la zone des registres enregistrés 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. Les registres enregistrés font partie de la plage pour les paires de registres et pour les modes d’adressage à décalage pré-indexé et post-indexé.Fonctions non terminales non enchaînées (enregistre
lrdans 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 areaOu, avec un nombre pair de registres Int enregistrés,
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 areaUniquement
x19enregistré :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 la zone d’enregistrement des registres n’est pas intégrée dans
stp, car unstpreg-lr pré-indexé ne peut pas être représenté avec les codes de déroulement.Tous les paramètres régionaux sont accessibles en fonction de
sp.<x29>pointe vers le cadre précédent.Enchaî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 pairPar rapport au premier exemple de prologue ci-dessus, cet exemple présente un avantage : toutes les instructions de sauvegarde de registre sont prêtes à s'exécuter après une seule instruction d'allocation de pile. Cela signifie qu’il n’y a pas d’antidépendance à
spqui empêche le parallélisme au niveau de l’instruction.Enchaîné, taille de frame > 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,
x29peut être placé à n’importe quelle position dans la zone locale pour offrir une meilleure couverture pour "reg-pair" et le mode d’adressage de décalage pré- et post-indexé. Les paramètres régionaux sous les pointeurs de frame peuvent être accessibles en fonction desp.Enchaîné, taille de frame > 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
Enregistrements .pdata
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 fonctionnements terminales qui ne nécessitent aucun stockage local et n’ont pas besoin d’enregistrer ou de 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 à partir de l’une de ces fonctions peut récupérer l’adresse de retour directement depuis lr pour remonter vers 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 .xdata de longueur variable, ou un mot compressé décrivant une séquence canonique de déroulement de fonction.
Les champs sont les suivants :
Function Start RVA est l’adresse 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
.pdatamot. Si Flag indique 0, les bits restants forment une RVA d’informations d’exception (avec les deux bits les plus bas implicitement 0). Si Flag n’est pas égal à zéro, les bits restants forment une structure de données de déroulement compactées.Exception Information RVA est l’adresse de la structure d’informations d’exception de longueur variable, stockée dans la
.xdatasection. 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
.xdatan’est nécessaire.
Enregistrements .xdata
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 à 1 million, alors plusieurs enregistrements
.pdataet.xdatadoivent ê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 de l'élément restant
.xdata. Actuellement, seule la version 0 est définie, de sorte que les valeurs de 1 à 3 ne sont pas autorisées.v. 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 de demander plus de mots de portée plus tard (0).
é. 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 Code Words doit être défini sur 0 pour indiquer qu'un mot d’extension est nécessaire.
Si E est 1, ce champ spécifie l’index du premier code de déroulement qui décrit l’unique épilogue.
f. Code Words est un champ de 5 bits qui spécifie le nombre de mots de 32 bits nécessaires pour contenir tous les codes d'annulation 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. Compteur d'épilogues étendus et mots de code étendus sont des champs de 16 bits et 8 bits, respectivement. Ils offrent davantage d’espace pour l’encodage d’un nombre inhabituellement élevé d’épilogues ou 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ées sur un mot chacune, suit 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.
v. Epilog Start Index est un champ de 10 bits (2 bits de plus que les mots de code étendus). Il indique l’index d’octets du premier code de déroulement qui décrit cet épilogue.
Après la liste des étendues d’épilogues vient un tableau d’octets contenant des codes de déroulement, décrits 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, viennent les informations du gestionnaire d’exceptions. Il se compose d’une seule RVA de gestionnaire d’exceptions qui indique l’adresse du gestionnaire 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;
}
Même si le prologue et chaque épilogue ont leur propre index dans les codes de déroulement, la table est partagée entre eux. Il est tout à fait possible (et pas tout à fait rare) qu’ils peuvent tous partager les mêmes codes. (Pour un exemple, consultez l’exemple 2 dans la section Exemples.) Les auteurs 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 d'annulation 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 celui dans lequel 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.
S’il était garanti que les exceptions ne se produisaient que dans le corps d’une fonction et jamais dans un prologue ou un épilogue, une seule séquence de déroulement serait nécessaire. Cependant, le modèle de déroulement Windows impose de pouvoir effectuer un déroulement de code à partir d’un prologue 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 correspondent sans ambiguïté selon une correspondance 1:1 à chaque code d’opération 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 d’é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 finir 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ésenroulement. Nous pouvons exécuter le reste de la suite pour annuler uniquement les parties du prologue qui ont déjà été exécutées.
Les codes de déroulement sont encodés en fonction du tableau ci-dessous. Tous les codes de déroulement sont un simple/double octet, sauf celui qui alloue une grande pile (alloc_l). Il existe 22 codes de déroulement au total. Chaque code de déroulement mappe exactement une instruction dans le prologue ou l’épilogue, afin de permettre le déroulement des prologues et des épilogues partiellement exécutés.
| Code de déroulement | Bits et interprétation |
|---|---|
alloc_s |
000xxxxx : allouez une petite pile avec la taille < 512 (2^5 * 16). |
save_r19r20_x |
001zzzzz : enregistrer la paire <x19,x20> sous [sp-#Z*8]!, décalage préindexé >= -248 |
save_fplr |
01zzzzzz : enregistrer la paire <x29,lr> sous [sp+#Z*8], décalage <= 504. |
save_fplr_x |
10zzzzzz : enregistrer la paire <x29,lr> sous [sp-(#Z+1)*8]!, décalage préindexé >= -512 |
alloc_m |
11000xxx'xxxxxxxx : allouer une grande pile avec la taille < 32K (2^11 * 16). |
save_regp |
110010xx'xxzzzzz : enregistrer la paire x(19+#X) sous [sp+#Z*8], décalage <= 504 |
save_regp_x |
110011xx'xxzzzzzz : enregistrer la paire x(19+#X) sous [sp-(#Z+1)*8]!, décalage préindexé >= -512 |
save_reg |
110100xx'xxzzz : enregistrer le registre x(19+#X) sous [sp+#Z*8], décalage <= 504 |
save_reg_x |
1101010x'xxxzzzz : enregistrer le registre x(19+#X) sous [sp-(#Z+1)*8]!, décalage préindexé >= -256 |
save_lrpair |
1101011x'xxzzzzzz : enregistrer la paire <x(19+2*#X),lr> sous [sp+#Z*8], décalage <= 504 |
save_fregp |
1101100x'xxzzzzzz : enregistrer la paire d(8+#X) sous [sp+#Z*8], décalage <= 504 |
save_fregp_x |
1101101x'xxzzzzzz : enregistrer la paire d(8+#X) sous [sp-(#Z+1)*8]!, décalage préindexé >= -512 |
save_freg |
1101110x'xxzzzzzz : enregistrer le registre d(8+#X) sous [sp+#Z*8], décalage < = 504 |
save_freg_x |
11011110'xxxzzzzz : enregistrer le registre d(8+#X) sous [sp-(#Z+1)*8]!, décalage préindexé >= -256 |
alloc_z |
11011111'zzzzzzzz : allouer une pile de taille z * SVE-VL |
alloc_l |
11100000'xxxxxxxx'xxxxxxxx'xxxxxxxx : allouer une grande pile de taille < 256M (2^24 * 16) |
set_fp |
11100001 : configurer x29 avec mov x29,sp |
add_fp |
11100010'xxxxxxxx : configurer 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ésenroulement. Implique ret dans l’épilogue. |
end_c |
11100101 : fin du code de déroulement dans l’étendue enchaînée actuelle. |
save_next |
11100110 : sauvegarder la paire de registres suivante. |
save_any_xreg |
11100111'0pxrrrrr'00oooooo : enregistrer le ou les registres
|
save_any_dreg |
11100111'0pxrrrrr'01oooooo : enregistrer le ou les registres
|
save_any_qreg |
11100111'0pxrrrrr’10oooooo : enregistrer le ou les registres
|
save_zreg |
11100111'0oo0rrrr’11oooooo : enregistrer le registre Z(#r+8) sous [sp + #o * VL], (Z8 à Z23) |
save_preg |
11100111'0oo1rr’11oo : enregistrer le registre P(#r) sous [sp + #o * (VL / 8)], (P4 à P15 ; les valeurs r[0, 3] sont réservées) |
| 11100111’1yyyyyyy’: 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’yyyyyyyy : réservé | |
| 11111001’yyyyyyyy’yyyyyyyy : réservé | |
| 11111010’yyyyyyyy’yyyyyyyy’yyyyyyyy : réservé | |
| 11111011’yyyyyyyy’yyyyyyyy’yyyyyyyy’yyyyyyyy : réservé | |
pac_sign_lr |
11111100 : signer l’adresse de retour dans lr 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 l'adressage de stp/str, à l’exception de save_r19r20_x, pour lequel 248 est suffisant pour toutes les zones de sauvegarde (10 registres Int + 8 registres FP + 8 registres d’entrée).
save_next doit suivre une opération d’enregistrement pour une paire de registres : save_regp, save_regp_x, save_fregp, save_fregp_x, save_r19r20_x ou un autre save_next. Il peut également être utilisé conjointement avec save_any_xreg, save_any_dreg ou save_any_qreg, mais uniquement quand p = 1. Il enregistre la paire de registres suivante dans l’ordre numérique croissant vers l’espace de pile suivant.
save_next ne doit pas être utilisé au-delà du dernier registre du même type.
É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 end séparé dans les scénarios d’appel de fin.
end_c est conçu pour gérer les fragments de fonction noncontigues à des fins d’optimisation. Un end_c 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 véritable end. Les codes de déroulement entre end_c et end représentent les opérations de prologue dans la région parent (prologue « fantôme »). Des détails et des exemples supplémentaires sont décrits dans la section ci-dessous.
Données de déroulement compressées
Pour les fonctions dont les prologues et les épilogues suivent la forme canonique décrite ci-dessous, il est possible d’utiliser des données de déroulement compressées. Cela évite complètement d’avoir à effectuer un enregistrement .xdata et réduit considérablement le coût de livraion des données de déroulement. Les prologues canoniques et les épilogues sont conçus pour répondre aux exigences courantes d'une fonction simple : une fonction qui ne nécessite pas de gestionnaire d'exceptions et qui effectue ses opérations de configuration et de démontage dans un ordre standard.
Le format d’un enregistrement .pdata avec des données de déroulement compressées ressemble à ceci :
Les champs sont les suivants :
- Function Start RVA est l’adresse 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 ; les bits restants indiquent un enregistrement de type
.xdata - 01 = données de déroulement compressées utilisées avec un seul prologue et un seul épilogue au début et à la fin de l’étendue.
- 10 = données de déroulement compressées utilisées pour le code sans prologue ni é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 ; les bits restants indiquent un enregistrement de type
-
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
.xdatadoit ê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
.xdatacomplet. Cela inclut la zone de variable locale, la zone de paramètre sortante, la zone Int et FP enregistrée par l’appelé 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
pacibspadresse de retour signée - 11 = fonction enchaînée, une instruction de paire stockage/charge est utilisée dans le prologue/l’épilogue
<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 = n’héberge pas les registres, 1 = héberge les registres).
- 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 compressées ne peuvent pas être utilisées pour la fonction qui enregistre un seul registre FP.
Les prologues 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 compact. 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 par les appelés.
É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 FP enregistrés par l’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, la paire <x29,lr> et la zone des paramètres sortants. 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 | Code de déroulement |
|---|---|---|---|---|
| 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_xsave_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)] |
nopnopnopnop |
| 6a | (CR == 10 || CR == 11) &&#locsz
<= 512 |
2 | stp x29,lr,[sp,#-locsz]!mov x29,sp*** |
save_fplr_xset_fp |
| 6b | (CR == 10 || CR == 11) && 512 < #locsz<= 4080 |
3 | sub sp,sp,#locszstp x29,lr,[sp,0]add x29,sp,0 |
alloc_msave_fplrset_fp |
| 6c | (CR == 10 || CR == 11) &&#locsz
> 4080 |
4 | sub sp,sp,4080sub sp,sp,#(locsz-4080)stp x29,lr,[sp,0]add x29,sp,0 |
alloc_malloc_s/alloc_msave_fplrset_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,4080sub sp,sp,#(locsz-4080) |
alloc_malloc_s/alloc_m |
* Si CR == 01 et RegI est un nombre impair, l’étape 3 et la dernière save_reg de l’étape 2 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 compressées ne peuvent pas être utilisées si une fonction nécessite la restauration de sp à partir de x29.
Déroulement des prologues et des épilogues partiels
Dans les cas de déroulement les plus courants, l’exception ou l’appel se produit dans le corps de la fonction, en dehors du prologue et de tous les épilogues. Dans ces situations, le déroulement est simple : le dérouleur 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 frame 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
En regard de chaque code d’opération figure le code déroulement approprié qui décrit l’opération. Vous pouvez voir comment la série de codes de déroulement pour le prologue est une image miroir exacte 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, il nous reste un ensemble commun de codes de désenroulement :
set_fp, , save_regp 0,240save_fregp,0,224, , save_fplr_x_256end
Le cas de l’épilogue est simple, car il est dans l’ordre normal. En commençant au décalage 0 dans l’épilogue (qui commence au décalage 0x100 dans la fonction), la séquence de désenroulement complète devrait s’exécuter, car aucun nettoyage n’a encore été effectué. Après avoir passé une instruction (au décalage 2 dans l’épilogue), nous pouvons mener à bien le désenroulement en ignorant le premier code de déroulement. Nous pouvons généraliser cette situation et supposer une correspondance exacte entre les codes d’opération et les codes de déroulement. Ensuite, pour commencer le déroulement à partir de l’instruction n dans l’épilogue, nous devons ignorer les n premiers codes de déroulement et commencer l’exécution à partir de là.
Il s’avère qu’une logique similaire fonctionne pour le prologue, à l’exception de l’inverse. Si nous commençons à effectuer le déroulement à partir du décalage 0 dans le prologue, nous ne devons rien exécuter. Si nous effectons le déroulement à partir du décalage 2, à savoir après une instruction, nous devons commencer l’exécution de la séquence de déroulement à partir d’un code de déroulement avant 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 le déroulement à 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 prologue et épilogue ne correspondent pas toujours exactement, c’est pourquoi le tableau de désempilement peut avoir besoin de contenir plusieurs séquences de code. Pour déterminer le décalage d’où commencer le traitement des codes, utilisez la logique suivante :
Si le déroulement démarre dans le corps de la fonction, commencez à exécuter les codes de déroulement à l’indice 0 et continuez jusqu’à ce qu’un code d’opération
endsoit atteint.Si vous effectuez le désenroulement à partir d’un épilogue, utilisez l’indice 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 ces codes jusqu’à ce que toutes les instructions déjà exécutées soient prises en compte. Exécutez ensuite à partir de ce point.
Si vous effectuez le désenroulement à partir du prologue, utilisez l’indice 0 comme point de départ. Calculez la longueur du code du prologue à partir de la séquence, puis déterminez combien d'octets séparent le PC en question de la fin du prologue. Avancez ensuite dans les codes de déroulement, ignorez ces codes jusqu’à ce que toutes les instructions pas encore exécutées soient prises en compte. Exécutez ensuite à partir de ce point.
Ces règles impliquent que les codes de déroulement du prologue doivent toujours être placés en premier dans le tableau. Ce sont aussi les codes généralement utilisés dans les déroulements qui démarre à 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 parent (ou région hôte). Cette préallocation maintient 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_regp 0,240,save_fplr_x_256,end.Épilogues uniquement (région 2 : le prologue est dans la région hôte)
Il est supposé qu’au moment où le contrôle passe dans cette région, tous les codes de prologue 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.xdatacomplet, il peut être encodé avec un prologue « fantôme », encadré par une paire de codes de déroulementend_cetend. Le débutend_cindique que la taille du prologue est égale à zéro. L’indice de début de l’épilogue unique pointe 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
.pdatapeut être appliqué via le paramètre Indicateur = 10. Avec l’enregistrement.xdatacomplet, nombre d’épilogues = 1. Le code de déroulement est identique au code de la région 2 ci-dessus, mais l’indice de début de l’épilogue 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 le « shrink wrapping ». Le compilateur peut choisir de retarder l’enregistrement de certains registres enregistrés par l’appelé jusqu’à être en dehors du prologue d’entrée de la 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 la pile est pré-alloué. Vous pouvez voir que la région 2 a le même code de déroulement, même lorsque elle est déplacée hors de sa fonction d’origine.
Région 1 : set_fp, , save_regp 0,240save_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. L’indice de début de l’épilogue pointe vers le premier code de déroulement save_regp 2, 224.
Grandes fonctions
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 ne contient ni prologue ni épilogue, il a toujours besoin de son propre enregistrement .pdata (et éventuellement .xdata) pour décrire le mode de déroulement à partir du corps de la fonction.
Exemples
Exemple 1 : Enchaîné par frame, forme compacte
|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 : Enchaîné par frame, forme complète avec Prologue & Épilogue miroir
|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
L’indice de début de l’épilogue [0] pointe vers la même séquence du code de déroulement du prologue.
Exemple 3 : Fonction non chaînée variadique
|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 de début de l’épilogue [4] pointe vers le milieu du code de déroulement du prologue (réutilisez partiellement le tableau de déroulement).
Voir aussi
Vue d’ensemble des conventions ABI ARM64
Gestion des exceptions ARM