Partager via


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 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 sp est enregistré dans un autre registre (x29) dans le prolog, ce registre reste inchangé dans toute la fonction. Cela signifie que l’original sp peut être récupéré à tout moment.

  • Sauf si le sp est 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

Diagramme montrant le dispositif du cadre de pile pour les fonctions.

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.

  1. 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)
    
  2. 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 area
    
  3. Fonctions terminales non enchaînées (lr non 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 area
    

    Tous 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é.

  4. Fonctions non terminales non enchaînées (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 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 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 la zone d’enregistrement des registres n’est pas intégrée dans stp, car un stp reg-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.

  5. 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 pair
    

    Par 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 à sp qui empêche le parallélisme au niveau de l’instruction.

  6. 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, x29 peut ê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 de sp.

  7. 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.

Disposition des enregistrements .pdata.

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 .pdata mot. 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 .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 .xdata n’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é :

Disposition des enregistrements .xdata.

Ces données sont divisées en quatre sections :

  1. 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 .pdata et .xdata doivent ê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 :

    1. 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.

    2. 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.

  2. 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.

  3. 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.

  4. 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
  • p : 0/1 => unique X(#r) ou paire X(#r) + X(#r+1)
  • x : 0/1 => décalage de pile préindexé positif ou négatif
  • o: offset = o * 16, si x=1 ou p=1, sinon o * 8
(Windows >= 11 requis)
save_any_dreg 11100111'0pxrrrrr'01oooooo : enregistrer le ou les registres
  • p : 0/1 => unique D(#r) ou paire D(#r) + D(#r+1)
  • x : 0/1 => décalage de pile préindexé positif ou négatif
  • o: offset = o * 16, si x=1 ou p=1, sinon o * 8
(Windows >= 11 requis)
save_any_qreg 11100111'0pxrrrrr’10oooooo : enregistrer le ou les registres
  • p : 0/1 => unique Q(#r) ou paire Q(#r) + Q(#r+1)
  • x : 0/1 => décalage de pile préindexé positif ou négatif
  • o : décalage = o * 16
(Windows >= 11 requis)
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 :

Enregistrement .pdata avec des données de déroulement empaquetées.

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é.
  • 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 .xdata complet. 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 pacibsp adresse 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>
  • 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_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 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 :

  1. 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 end soit atteint.

  2. 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.

  3. 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)

  1. 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.

  2. É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 .xdata complet, il peut être encodé avec un prologue « fantôme », encadré par une paire de codes de déroulement end_c et end. Le début end_c indique que la taille du prologue est égale à zéro. L’indice de début de l’épilogue unique pointe vers set_fp.

    Code de déroulement de la région 2 : end_c, set_fp, save_regp 0,240, save_fplr_x_256, end.

  3. 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 .xdata complet, 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 vers end_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