ClickCease Déroulement de la pile dans les processeurs AArch64 : qu'est-ce que c'est et comment ça marche ?

Table des matières

Rejoignez notre populaire bulletin d'information

Rejoignez 4 500+ professionnels de Linux et de l'Open Source !

2 fois par mois. Pas de spam.

Déroulement de la pile dans les processeurs AArch64 : qu'est-ce que c'est et comment ça marche ?

Le 1er mars 2023 - L'équipe de relations publiques de TuxCare

Le logiciel de correction en direct du noyau Linux de KernelCare Enterprise prend en charge ARMv8 (AArch64) en plus de x86_64 (Intel IA32/AMD AMD64) depuis quelque temps déjà. Cependant, pour que KernelCare sur ARMvous aurez besoin d'un outil appelé "stack frame unwinder".

Cet article explique ce qu'ils sont, à quoi ils servent, et pourquoi nous avons dû écrire notre propre dérouleur de trames de pile.

  1. Dérouleurs de piles : Que sont-ils, à quoi servent-ils et quelle est leur histoire ?
  2. Comment fonctionne le déroulement d'une pile : L'abc de la pile et son fonctionnement dans les processeurs AArch64
  3. Instructions pour les dérouleurs de piles : Instructions pour sauter, adresse à laquelle sauter et dépannage

Partie A - Déroulements de piles

Qu'est-ce qu'un dérouleur de pile ?

A dérouleur de pile est un logiciel qui répertorie les adresses de chaque fonction actuellement dans la pile d'appel. Il vous montre où vous vous trouvez dans l'exécution d'un programme, mais surtout, il vous montre aussi comment vous y êtes arrivé. La pile d'appel est la liste de toutes les fonctions en cours d'exécution. On l'appelle pile parce que, lorsqu'une fonction en appelle une autre, la nouvelle fonction est ajoutée au sommet de la pile (alors qu'elle est la fonction en cours d'exécution). Lorsqu'une fonction renvoie une valeur ou atteint une instruction de sortie, elle est retirée de ladite pile.

Un dérouleur efficace doit présenter toutes ces caractéristiques :

  • Rapidepour que le traitement puisse reprendre rapidement (si possible).
  • Bon marché: pour qu'il ne draine pas les ressources du système.
  • Précision: pour que les adresses de mémoire et les espaces de noms soient rapportés avec précision.

À quoi sert un dérouleur de pile ?

  1. Pour fournir des traces de pile lorsqu'un programme se plante. (Dans le monde du noyau Linux, ces plantages sont appelés "Oops").
  2. Pour aider à l'analyse des performances, montrer le parcours (quelles fonctions appelées par qui) d'un programme à l'intérieur d'un programme.
  3. Permettre la correction en direct du noyau Linux, c'est-à-dire la correction des bogues du noyau sans arrêter (redémarrer) le système.

Histoire du déroulement de la pile

Historiquement, le déroulement de la pile a aidé les développeurs à déboguer les logiciels. Après tout, quiconque a écrit au moins quelques programmes sait que les programmes contiennent très probablement des erreurs. 

Certaines erreurs sont faciles à repérer, tandis que d'autres passent presque inaperçues. Plus le programme est gros, plus il est difficile de le déboguer : les programmes énormes sont presque impossibles à déboguer en utilisant uniquement l'analyse du code source comme seule technique de débogage. 

C'est pourquoi il existe plusieurs techniques secondaires qui visent à faciliter le processus de débogage :

  1. La journalisation (c'est-à-dire la sortie de débogage). Ici, le programmeur utilise les opérateurs d'impression de son langage de prédilection pour afficher le contenu de variables spécifiques à des moments précis du déroulement du programme. Cela lui permet de repérer les endroits où le déroulement du programme s'est écarté du scénario prévu. Les logiciels modernes s'appuient largement sur la journalisation mais utilisent différents niveaux de journalisation - débogage, avis, avertissement, erreur, etc. - afin de pouvoir désactiver certains messages de journalisation moins importants en production. En général, les messages de journalisation sont envoyés dans un fichier de journalisation dédié ou, à défaut, dans un fichier ou un dépôt de journalisation à l'échelle du système.
  2. Débogage par points d'arrêt. Afin de faciliter le processus de débogage, presque toutes les architectures de processeurs modernes comportent une instruction spéciale appelée "point d'arrêt" (par exemple, bkpt pour ARM et int3 pour x86). Le but de cette instruction est de provoquer une interruption spéciale du processeur. Le matériel sauvegarde alors le compteur de programme actuel et peut sauvegarder quelques registres à usage général afin d'aider la routine de service d'interruption à démarrer avec succès. Le contrôle est ensuite transféré à la routine de service d'interruption.

    Cette routine extrait le contenu sauvegardé des registres à usage général afin d'aider le programmeur à examiner certaines variables du programme à l'endroit où celui-ci a été arrêté. Juste avant d'exécuter l'instruction spéciale de retour de la routine de service d'interruption, ce gestionnaire d'interruption rétablit généralement l'instruction d'origine et, après le retour au fil interrompu, le programme s'exécute comme s'il n'avait jamais été touché.

    L'utilisation moderne de cette technique repose sur le soutien du noyau du système d'exploitation, car toutes les routines de service d'interruption en font partie. Habituellement, le noyau du système d'exploitation fournit des appels système spéciaux (par exemple, ptrace sous Linux) que les processus de l'espace utilisateur peuvent utiliser pour exécuter des fonctions de débogage. Le célèbre débogueur open-source de Linux, appelé GDB, utilise ptrace pour effectuer un débogage étape par étape (via des points d'arrêt).

    De plus, certaines architectures de CPU (x86-64 par exemple) définissent des capacités supplémentaires, qui sont basées sur l'idée originale. Par exemple, il peut y avoir des registres système spéciaux contenant l'adresse d'un point d'arrêt (virtuel) : lorsque le compteur du programme atteint cette adresse, la routine de service d'interruption est appelée immédiatement sans qu'il soit nécessaire de corriger (et donc de restaurer) le code du programme.
  3. Débogage par assertions. L'assertion est une fonction spéciale qui vérifie la condition donnée et, si cette condition est fausse, provoque la fin du programme. Les assertions peuvent être utilisées par les programmeurs pour s'assurer que les variables internes sont saines. Les assertions sont généralement désactivées dans les programmes de production. Ici, le cas le plus intéressant est celui où l'assertion est fausse. Cet état signale clairement qu'une erreur de programme a eu lieu. Pour faciliter l'investigation d'un problème, le déroulement de la pile est effectué. En déroulant la pile du programme, nous pouvons obtenir un "chemin d'exécution" qui mène notre programme au point où il s'est planté.

Les dérouleurs avancés permettent de voir les paramètres pour chaque fonction dans la chaîne d'appel.

Ainsi, le déroulement de la pile provient à l'origine du débogage de logiciels. Aujourd'hui, cependant, il a d'autres applications, dont l'une se trouve dans Kernelcare Enterprise.

Pourquoi l'équipe de KernelCare Enterprise avait besoin d'un dérouleur pour ARM

Pour installer un correctif en direct du noyau Linux, le logiciel de correction doit savoir quelles fonctions se trouvent dans la pile d'appel actuelle. Si une fonction actuellement dans la pile d'appel est corrigée, le système peut se planter au retour. Il existe déjà une certaine fonctionnalité de déroulement de la pile dans le noyau Linux. Voici une brève revue de cette fonctionnalité et les raisons pour lesquelles elle ne peut pas être utilisée pour le live patching :

  • Le dérouleur 'Guess' : Il devine le contenu de la pile. Il n'est pas précis et donc pas utile pour les correctifs en direct. Il n'est disponible que pour les architectures x86_64.
  • Le site dérouleur "pointeur de cadre".: Disponible uniquement pour x86_64
  • Le site dérouleur 'ORC: Introduit dans Linux Kernel v4.14ORC " est un acronyme pour " Oops Rewind Capability ". Développé à l'origine pour x86_64 uniquement, il a continué à être amélioré et des correctifs sont envisagés pour être inclus dans le noyau (à partir de Linux Kernel 6.3) afin d'étendre son support à ARM. son support à ARMainsi que l'ajout de contrôles de fiabilité pour garantir le retour d'informations exactes.

Partie B - Comment fonctionne le déroulage des piles ?

Introduction à la pile

  • Lorsqu'une fonction est appelée, une cadre de pile garde la trace des arguments de la fonction ainsi que de ses points d'entrée et de sortie.
  • Un registre du processeur se voit attribuer un point de pile ('SP') qui fait référence à l'objet le plus récemment placé sur la pile. La mémoire qui met en œuvre cette pile s'étend vers le bas, vers des adresses mémoire inférieures (ce que l'on appelle "full-descending").
  • La mémoire de la pile doit être alignée sur les limites des octets (16 octets pour AArch64). Ceci est imposé par le matériel (mais peut être désactivé sur certains modèles ARM).

Détails

Comment fonctionne le déroulement de la pile dans les processeurs AArch64 ?

Une fonction spéciale du noyau effectue le déroulement de la pile. Lorsqu'elle est appelée, la fonction obtient le pointeur de cadre (FP) de la fonction appelante. Le FP fait référence au cadre de la pile qui est représenté par la structure struct stack_frame. Il contient un pointeur vers le cadre de pile de la fonction qui a appelé la fonction appelante.

Cela signifie que nous avons une liste liée de trames de pile qui se termine lorsque le prochain FP obtenu est égal à 0, conformément à AAPCS64 (la norme d'appel de procédure pour AArch64). Dans chaque cadre de pile, nous pouvons récupérer une adresse de retour à laquelle la fonction appelante doit déléguer le contrôle après avoir terminé son travail. En utilisant le fait qu'une adresse de retour doit pointer vers l'intérieur de la fonction appelante, nous pouvons obtenir les noms symboliques de toutes les fonctions, jusqu'à et y compris le point où FP=0.

Pour ce faire, nous conservons les noms des fonctions et leurs adresses de début et de fin. Ceci peut être implémenté en utilisant le sous-système du noyau Linux appelé kallsyms.

Le problème central se résume à ceci : comment déplacer le compteur du programme d'un endroit à un autre (un saut) et reprendre le traitement sans problème ?

Cette action peut être exprimée en langage assembleur comme suit :

Examinons-les plus en détail.

1. Instruction de sauter

Les procédures sont invoquées avec BL. L'instruction 32 bits peut être visualisée comme suit :


31 30 29 28 27 26 25 (...) 2 1 0
1  0  0  1  0  1  imm26
op

Ici, imm26 est un offset PC de 26 bits. Pour un examen approfondi de cet exemple, vous pouvez consulter la documentation ARM ici.

2. Adresse à laquelle sauter

Nous calculons où sauter pour utiliser :


bits(64) offset = SignExtend(imm26:'00', 64)

The offset shifts by two bits to the left and converts to 64 bit (i.e. the high bits fill with 1 if imm26 < 0, and with 0, otherwise).

L'adresse à laquelle il faut sauter est alors :


Address = PC + offset

Le décalage est étiqueté et utilisé dans l'instruction BL. (Les instructions en AArch64 prennent toujours 4 octets, ce qui est un avantage par rapport à x86). Le registre X30 (également connu sous le nom de Link Register) est réglé sur PC+4. C'est l'adresse de retour pour RET (par défaut X30 si non spécifié).

Ainsi, pour l'instruction complémentaire RET, il suffit de récupérer la valeur LR sauvegardée, d'y transférer le contrôle et de retourner dans la fonction appelante.

Le problème : que se passe-t-il si la fonction appelée appelle elle-même une autre fonction ?

Et là, nous avons un problème : que se passe-t-il si la fonction appelée appelle elle-même une autre fonction ? Si nous ne faisons rien, alors la valeur enregistrée dans LR sera remplacée par une nouvelle adresse de retour - il ne sera pas possible de revenir à la fonction initiale et le programme s'arrêtera très probablement.

Résoudre le problème

Il existe quelques moyens de résoudre ce problème :

  1. Sauvegarder la valeur LR dans un autre registre
  2. Sauvegarder la valeur LR dans la RAM

Le premier cas est très restrictif, car le nombre de registres disponibles est limité (à 31 registres). ARM utilise la philosophie de l'architecture RISC load/store qui veut que les appels à la mémoire se fassent via les instructions LD (LOAD) et ST (STORE), alors que les opérations arithmétiques et logiques sont effectuées sur les registres. Par conséquent, des registres vides sont nécessaires pour l'exécution du programme, et il nous reste l'option 2, sauvegarder la valeur LR dans la RAM.

Sauvegarde des fonctions appelées pour retourner des adresses dans la pile

Une structure de trame implémentée en C ressemble à ceci :


struct stack_frame {
    unsigned long fp;
    unsigned long lr;
    char data[0];
};

En d'autres termes, chaque fonction alloue n octets dans la pile, en réduisant de n le pointeur de pile au moment où elle prend le contrôle avec l'instruction BL. Les contenus des registres x29 (FP) et x30 (LR) sont sauvegardés en fonction de la valeur du pointeur de pile obtenue - la fonction appelante utilisée par ces valeurs. Ensuite, la nouvelle valeur SP est assignée au registre x29(appelé le frame pointer (FP)). L'espace restant dans le cadre de la pile est utilisé par les variables locales de la fonction. Et la condition selon laquelle le pointeur de cadre (FP) et le registre de liaison (LR) de la fonction appelante sont toujours situés au début de n'importe quel cadre de pile, est toujours respectée. Après avoir terminé son travail, la fonction appelée prend les valeurs sauvegardées de FP et LR dans le cadre de la pile et augmente le pointeur de pile (SP) de n.

Comment le compilateur croisé gcc traite AArch64

L'utilisation du pointeur de trame nécessite que le noyau Linux soit compilé avec l'option -fno-omit-frame-pointer de gcc. Cette option indique à gcc de stocker le pointeur de cadre de la pile dans un registre. (NOTE : La valeur par défaut de gcc est -fomit-frame-pointer, cette option doit donc être explicitement définie).

Pour AArch64, le registre est X29. Il est réservé pour le pointeur de cadre de pile lorsque l'option est activée. (Sinon, il peut être utilisé à d'autres fins.) Le compilateur croisé GCC utilisé pour compiler Linux sous AArch64 place les instructions suivantes avant le corps de la fonction :


ffffff80080851b8 :
ffffff80080851b8: a9be7bfd stp x29, x30, [sp, #-32]!
ffffff80080851bc: 910003fd mov x29, sp

Ici, on parle d'adressage indirect avec préincrément où le pointeur de pile (SP) est diminué de 32 au début et ensuite x29, x30 sont séquentiellement sauvegardés dans la mémoire par la valeur obtenue dans la première instruction.

En général, la fonction se termine comme suit :


ffffff80080851fc: a8c27bfd ldp x29, x30, [sp], #32
ffffff8008085200: d65f03c0 ret

L'adressage indirect avec le post-incrément où les valeurs sauvegardées x29, x30, sont prises de la mémoire sur le pointeur de pile (SP) et ensuite SP augmente de 32. Les exemples de code ci-dessus sont appelés respectivement le prologue et l'épilogue de la fonction. GCC génère toujours de tels prologues et épilogues si le flag -fno-omit-frame-pointer est activé. Linux sur AArch64 est compilé avec ce drapeau de sorte que les cadres de pile ressemblent à du code normal (sauf le code d'assemblage). Ce fait nous permet de dérouler facilement la pile, c'est-à-dire de suivre la chaîne d'appels dans le programme.

Référence de l'assemblage :

Conclusion

Malgré son utilité, il n'existe pas d'approche commune du déroulement de la pile qui couvre toutes les architectures et tous les systèmes. Pour le live patching du noyau Linux, un dérouleur de pile fiable et rapide est essentiel. Pour Linux fonctionnant sur ARM, le besoin d'une solution robuste de déroulement de la pile est encore plus pressant, car ARM gagne du terrain sur les marchés des appareils IoT et de l'informatique en bordure de cloud. KernelCare étant présent sur ces deux marchés, nous avons dû nous pencher sur nos propres solutions de décompression de la pile du noyau.

Lectures complémentaires

À propos de KernelCare Enterprise

KernelCare Enterprise simplifie l'application des correctifs à vos noyaux Linux pour les serveurs sous CentOS, Amazon Linux, RHEL, Ubuntu, Debian et autres distributions Linux. autres distributions Linuxy compris Poky (la distribution du projet Yokto) et Raspbian.

KernelCare maintient la sécurité du noyau grâce à des mises à jour automatisées, sans redémarrage, sans interruption ni dégradation du service. Le service fournit rapidement les derniers correctifs de sécurité pour les différentes distributions Linux, appliqués automatiquement au noyau en cours d'exécution en quelques nanosecondes. KernelCare Enterprise fonctionne aussi bien dans des environnements réels que dans des systèmes locaux ou dans le nuage. Pour les serveurs situés derrière un pare-feu, il existe un outil ePortal sur site pour vous aider à le gérer.

KernelCare Enterprise améliore la conformité de centaines de milliers de serveurs de diverses entreprises où la disponibilité des services et la protection des données sont les éléments les plus cruciaux de l'activité : services financiers et d'assurance, fournisseurs de solutions de vidéoconférence, entreprises protégeant les victimes de violences domestiques, sociétés d'hébergement et fournisseurs de services publics.

Pour en savoir plus sur KernelCare Enterprise et les avantages qu'il offre ici.

Résumé
Nom de l'article
Déroulement de la pile dans les processeurs AArch64 : qu'est-ce que c'est et comment ça marche ?
Description
Le logiciel de correctifs en direct du noyau Linux de KernelCare Enterprise a pris en charge (AArch64) mais ce qu'ils sont, à quoi ils servent...En savoir plus
Auteur
Nom de l'éditeur
TuxCare
Logo de l'éditeur

Vous cherchez à automatiser la correction des vulnérabilités sans redémarrage du noyau, temps d'arrêt du système ou fenêtres de maintenance programmées ?

Découvrez le Live Patching avec TuxCare

Devenez rédacteur invité de TuxCare

Commencer

Courrier

Rejoindre

4,500

Professionnels de Linux et de l'Open Source
!

S'abonner à
notre lettre d'information