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’original sp 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

Diagram that shows the stack frame layout for functions.

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.

  1. 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)
    
  2. 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
    
  3. 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, vous sub 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é.

  4. 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-lr stp 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.

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

  6. 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 fonction spde .

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

.pdata record layout.

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é :

.xdata record layout.

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 à 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 :

    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 Mots de code doit être défini sur 0 pour indiquer qu’un mot d’extension est requis.

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

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

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

  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, 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_xde laquelle 248 est suffisant pour toutes les zones d’enregistrement (10 registres Int + 8 registres FP + 8 registres d’entrée).

save_nextdoit suivre une sauvegarde pour la paire d’inscriptions volatiles Int ou FP : save_regp, , save_regp_xsave_fregp, save_fregp_x, , save_r19r20_xou 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 :

.pdata record with packed unwind data.

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é.
  • 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>
  • 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,240save_fregp,0,224, , save_fplr_x_256end

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 :

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

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

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

  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_fplr_x_256save_regp 0,240, end.

  2. 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 une end_c paire de end code et déroulage. Le début end_c indique que la taille du prologue est égale à zéro. Index de début d’epilog des points d’épilogue uniques 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 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 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 « 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,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_fpsave_regp 0,240, save_fplr_x_256. end Epilog Start Index pointe vers le premier code save_regp 2, 224de 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