Vue d’ensemble des conventions ABI ARM64
L’interface binaire d’application de base (ABI) pour Windows lors de la compilation et de l’exécution sur des processeurs ARM en mode 64 bits (architectures ARMv8 ou ultérieures), pour la plupart, suit la norme AArch64 EABI d’ARM. Cet article met en évidence certaines des hypothèses clés et des modifications de ce qui est documenté dans l’EABI. Pour plus d’informations sur l’ABI 32 bits, consultez Vue d’ensemble des conventions ABI ARM. Pour plus d’informations sur l’EABI ARM standard, consultez L’interface binaire d’application (ABI) pour l’architecture ARM (lien externe).
Définitions
Avec l’introduction de la prise en charge 64 bits, ARM a défini plusieurs termes :
- AArch32 : architecture de jeu d’instructions 32 bits héritée définie par ARM, y compris l’exécution en mode Pouce.
- AArch64 : nouvelle architecture de jeu d’instructions 64 bits (ISA) définie par ARM.
- ARMv7 : spécification du matériel ARM de « 7e génération », qui inclut uniquement la prise en charge d’AArch32. Cette version du matériel ARM est la première version prise en charge par Windows pour ARM.
- ARMv8 : spécification du matériel ARM de « 8e génération », qui inclut la prise en charge d’AArch32 et DArch64.
Windows utilise également ces termes :
- ARM : fait référence à l’architecture ARM 32 bits (AArch32), parfois appelée WoA (Windows sur ARM).
- ARM32 : identique à ARM, ci-dessus ; utilisé dans ce document pour plus de clarté.
- ARM64 : fait référence à l’architecture ARM 64 bits (AArch64). Il n’y a pas de telle chose que WoA64.
Enfin, lorsque vous faites référence à des types de données, les définitions suivantes d’ARM sont référencées :
- Short-Vector : type de données directement représenté dans SIMD, vecteur de 8 octets ou 16 octets d’éléments. Elle est alignée sur sa taille, soit 8 octets ou 16 octets, où chaque élément peut être de 1, 2, 4 ou 8 octets.
- HFA (agrégat à virgule flottante homogène) : type de données avec 2 à 4 membres à virgule flottante identiques, flottants ou doubles.
- HVA (agrégat à vecteur court homogène) : type de données avec 2 à 4 membres de vecteur court identiques.
Configuration de base requise
La version ARM64 de Windows présuppose qu’elle s’exécute sur une architecture ARMv8 ou ultérieure à tout moment. La prise en charge à virgule flottante et à NEON est supposée être présente dans le matériel.
La spécification ARMv8 décrit les nouveaux opcodes de chiffrement facultatif et d’assistance CRC pour AArch32 et AArch64. La prise en charge de ces derniers est actuellement facultative, mais recommandée. Pour tirer parti de ces opcodes, les applications doivent d’abord effectuer des vérifications d’exécution pour leur existence.
Endianness
Comme avec la version ARM32 de Windows, sur ARM64 Windows s’exécute en mode little-endian. Le changement d’endianness est difficile à réaliser sans prise en charge du mode noyau dans AArch64. Il est donc plus facile de s’appliquer.
Alignement
Windows s’exécutant sur ARM64 permet au matériel processeur de gérer les accès mal alignés de manière transparente. Dans une amélioration de AArch32, cette prise en charge fonctionne désormais également pour tous les accès entiers (y compris les accès à plusieurs mots) et pour les accès à virgule flottante.
Toutefois, les accès à la mémoire non mise en cache (appareil) doivent toujours être alignés. Si le code peut éventuellement lire ou écrire des données mal alignées à partir de la mémoire non mise en cache, il doit s’assurer d’aligner tous les accès.
Alignement de disposition par défaut pour les locaux :
Taille en octets | Alignement en octets |
---|---|
1 | 1 |
2 | 2 |
3, 4 | 4 |
> 4 | 8 |
Alignement de disposition par défaut pour les globals et les statiques :
Taille en octets | Alignement en octets |
---|---|
1 | 1 |
2 - 7 | 4 |
8 - 63 | 8 |
>= 64 | 16 |
Registres entiers
L’architecture AArch64 prend en charge 32 registres entiers :
Inscrire | Volatilité | Rôle |
---|---|---|
x0-x8 | Volatil | Registres de zéro paramètre/résultat |
x9-x15 | Volatil | Registres à zéro |
x16-x17 | Volatil | Registres de zéro appel intra-procédure |
x18 | N/A | Inscription de la plateforme réservée : en mode noyau, pointe vers KPCR pour le processeur actuel ; En mode utilisateur, pointe vers TEB |
x19-x28 | Non volatil | Registres à zéro |
x29/fp | Non volatil | Pointeur de frame |
x30/lr | Les deux | Registre de liens : la fonction Appelé doit la conserver pour son propre retour, mais la valeur de l’appelant est perdue. |
Chaque registre est accessible en tant que valeur 64 bits complète (via x0-x30) ou en tant que valeur 32 bits (via w0-w30). Les opérations 32 bits n’étendent pas leurs résultats jusqu’à 64 bits.
Pour plus d’informations sur l’utilisation des registres de paramètres, consultez la section Passage de paramètre.
Contrairement à AArch32, le compteur de programme (PC) et le pointeur de pile (SP) ne sont pas indexés. Ils sont limités dans la façon dont ils sont accessibles. Notez également qu’il n’existe aucun registre x31. Cet encodage est utilisé à des fins spéciales.
Le pointeur d’image (x29) est requis pour la compatibilité avec la marche rapide de pile utilisée par ETW et d’autres services. Il doit pointer vers la paire {x29, x30} précédente sur la pile.
Registres à virgule flottante/SIMD
L’architecture AArch64 prend également en charge 32 registres à virgule flottante/SIMD, résumés ci-dessous :
Inscrire | Volatilité | Rôle |
---|---|---|
v0-v7 | Volatil | Registres de zéro paramètre/résultat |
v8-v15 | Les deux | Les 64 bits bas sont non volatiles. Les 64 bits élevés sont volatiles. |
v16-v31 | Volatil | Registres à zéro |
Chaque registre est accessible en tant que valeur 128 bits complète (via v0-v31 ou q0-q31). Elle est accessible en tant que valeur 64 bits (via d0-d31), en tant que valeur 32 bits (via s0-s31), en tant que valeur 16 bits (via h0-h31) ou en tant que valeur 8 bits (via b0-b31). Les accès inférieurs à 128 bits accèdent uniquement aux bits inférieurs du registre complet 128 bits. Ils conservent les bits restants intouchés, sauf indication contraire. (AArch64 est différent d’AArch32, où les registres plus petits ont été emballés au-dessus des registres plus grands.)
Le registre de contrôle à virgule flottante (FPCR) a certaines exigences sur les différents champs de bits qu’il contient :
Bits | Signification | Volatilité | Rôle |
---|---|---|---|
26 | AHP | Non volatile | Contrôle demi-précision alternatif. |
25 | DN | Non volatile | Contrôle de mode NaN par défaut. |
24 | FZ | Non volatil | Contrôle de mode vide à zéro. |
23-22 | RMode | Non volatil | Contrôle du mode arrondi. |
15,12-8 | IDE/IXE/etc. | Non volatile | L’interruption d’exception active les bits, doit toujours être 0. |
Registres système
Comme AArch32, la spécification AArch64 fournit trois registres « ID de thread » contrôlés par le système :
Inscrire | Rôle |
---|---|
TPIDR_EL0 | Réservé. |
TPIDRRO_EL0 | Contient le numéro d’UC pour le processeur actuel. |
TPIDR_EL1 | Pointe vers la structure KPCR pour le processeur actuel. |
Exceptions à virgule flottante
La prise en charge des exceptions à virgule flottante IEEE est facultative sur les systèmes AArch64. Pour les variantes de processeur qui ont des exceptions à virgule flottante matérielle, le noyau Windows intercepte silencieusement les exceptions et les désactive implicitement dans le registre FPCR. Ce piège garantit un comportement normalisé entre les variantes du processeur. Sinon, le code développé sur une plateforme sans prise en charge d’exception peut se retrouver à prendre des exceptions inattendues lors de l’exécution sur une plateforme avec prise en charge.
Passage de paramètres
Pour les fonctions non variodiques, l’ABI Windows suit les règles spécifiées par ARM pour le passage de paramètre. Ces règles sont extraites directement de la norme d’appel de procédure pour l’architecture AArch64 :
Étape A – Initialisation
Cette étape s’effectue exactement une fois, avant le début du traitement des arguments.
Le numéro de registre à usage général suivant (NGRN) est défini sur zéro.
Le numéro de registre à virgule flottante et SIMD suivant est défini sur zéro.
L’adresse d’argument empilée suivante (NSAA) est définie sur la valeur actuelle du pointeur de pile (SP).
Étape B : pré-remplissage et extension des arguments
Pour chaque argument de la liste, la première règle de correspondance de la liste suivante est appliquée. Si aucune règle ne correspond, l’argument est utilisé non modifié.
Si le type d’argument est un type composite dont la taille ne peut pas être déterminée statiquement par l’appelant et l’appelé, l’argument est copié en mémoire et l’argument est remplacé par un pointeur vers la copie. (Il n’existe aucun type de ce type en C/C++, mais il existe dans d’autres langages ou dans des extensions de langage).
Si le type d’argument est une HFA ou une HVA, l’argument est utilisé non modifié.
Si le type d’argument est un type composite supérieur à 16 octets, l’argument est copié en mémoire allouée par l’appelant et l’argument est remplacé par un pointeur vers la copie.
Si le type d’argument est un type composite, la taille de l’argument est arrondie au multiple le plus proche de 8 octets.
Étape C : affectation d’arguments aux registres et à la pile
Pour chaque argument de la liste, les règles suivantes sont appliquées à leur tour jusqu’à ce que l’argument ait été alloué. Lorsqu’un argument est affecté à un registre, les bits inutilisés du registre ont une valeur non spécifiée. Si un argument est affecté à un emplacement de pile, les octets de remplissage inutilisés ont une valeur non spécifiée.
Si l’argument est un type à virgule flottante à virgule flottante simple, double ou quad-précision, et que le NSRN est inférieur à 8, l’argument est alloué aux bits les moins significatifs du registre v[NSRN]. Le NSRN est incrémenté d’un. L’argument a maintenant été alloué.
Si l’argument est une HFA ou une HVA et qu’il existe suffisamment de registres SIMD et virgule flottante non alloués (NSRN + nombre de membres ≤ 8), l’argument est alloué aux registres SIMD et à virgule flottante, un registre par membre du HFA ou HVA. Le NSRN est incrémenté par le nombre de registres utilisés. L’argument a maintenant été alloué.
Si l’argument est une HFA ou une HVA, le NSRN est défini sur 8 et la taille de l’argument est arrondie au multiple le plus proche de 8 octets.
Si l’argument est une HFA, une HVA, un type à virgule flottante à quatre précisions ou à vecteur court, la NSAA est arrondie à la plus grande de 8 ou à l’alignement naturel du type de l’argument.
Si l’argument est un type à virgule flottante demi-précision ou simple précision, la taille de l’argument est définie sur 8 octets. L’effet est comme si l’argument avait été copié dans les bits les moins significatifs d’un registre 64 bits, et les bits restants remplis de valeurs non spécifiées.
Si l’argument est un HFA, une HVA, un demi-demi-, un double ou un type à virgule flottante à quad-précision ou un type de vecteur court, l’argument est copié en mémoire au niveau de la NSAA ajustée. L’adresse NSAA est incrémentée de la taille de l’argument. L’argument a maintenant été alloué.
Si l’argument est un type intégral ou pointeur, la taille de l’argument est inférieure ou égale à 8 octets et la valeur NGRN est inférieure à 8, l’argument est copié dans les bits les moins significatifs dans x[NGRN]. Le NGRN est incrémenté d’un. L’argument a maintenant été alloué.
Si l’argument a un alignement de 16, le NGRN est arrondi au nombre pair suivant.
Si l’argument est un type intégral, la taille de l’argument est égale à 16 et la valeur NGRN est inférieure à 7, l’argument est copié dans x[NGRN] et x[NGRN+1]. x[NGRN] doit contenir le double mot adressé inférieur de la représentation mémoire de l’argument. Le NGRN est incrémenté par deux. L’argument a maintenant été alloué.
Si l’argument est un type composite et que la taille en deux mots de l’argument n’est pas supérieure à 8 moins NGRN, l’argument est copié dans des registres à usage général consécutifs, en commençant à x[NGRN]. L’argument est passé comme s’il avait été chargé dans les registres à partir d’une adresse alignée sur deux mots, avec une séquence appropriée d’instructions LDR qui chargent des registres consécutifs à partir de la mémoire. Le contenu des parties inutilisées des registres n’est pas spécifié par cette norme. Le NGRN est incrémenté par le nombre de registres utilisés. L’argument a maintenant été alloué.
Le NGRN est défini sur 8.
La NSAA est arrondie à la plus grande de 8 ou à l’alignement naturel du type de l’argument.
Si l’argument est un type composite, l’argument est copié en mémoire au niveau de la NSAA ajustée. L’adresse NSAA est incrémentée de la taille de l’argument. L’argument a maintenant été alloué.
Si la taille de l’argument est inférieure à 8 octets, la taille de l’argument est définie sur 8 octets. L’effet est comme si l’argument a été copié dans les bits les moins significatifs d’un registre 64 bits, et les bits restants ont été remplis avec des valeurs non spécifiées.
L’argument est copié en mémoire au niveau de la NSAA ajustée. L’adresse NSAA est incrémentée de la taille de l’argument. L’argument a maintenant été alloué.
Addenda : Fonctions variadiques
Les fonctions qui acceptent un nombre variable d’arguments sont gérées différemment de celles ci-dessus, comme suit :
Tous les composites sont traités de la même façon ; aucun traitement spécial des AFC ou des HVA.
Les registres SIMD et à virgule flottante ne sont pas utilisés.
En fait, il est identique aux règles C.12-C.15 suivantes pour allouer des arguments à une pile imaginaire, où les 64 premiers octets de la pile sont chargés en x0-x7 et tous les arguments de pile restants sont placés normalement.
Valeurs de retour
Les valeurs intégrales sont retournées en x0.
Les valeurs à virgule flottante sont retournées dans s0, d0 ou v0, selon les besoins.
Un type est considéré comme une HFA ou une HVA si toutes les opérations suivantes sont conservées :
- Ce n’est pas vide,
- Il n’a pas de constructeurs par défaut ou de copie non trivials, destructeurs ou opérateurs d’affectation,
- Tous ses membres ont le même type HFA ou HVA, ou sont des types float, double ou neon qui correspondent aux types HFA ou HVA des autres membres.
Les valeurs HVA avec quatre éléments ou moins sont retournées dans s0-s3, d0-d3 ou v0-v3, selon les besoins.
Les types retournés par valeur sont gérés différemment selon qu’ils ont certaines propriétés et si la fonction est une fonction membre non statique. Types qui ont toutes ces propriétés,
- ils sont agrégés par la définition standard C++14, c’est-à-dire qu’ils n’ont aucun constructeur fourni par l’utilisateur, aucun membre de données privé ou protégé non statique, aucune classe de base et aucune fonction virtuelle, et
- ils ont un opérateur d’assignation de copie triviale et
- ils ont un destructeur trivial,
et sont retournés par des fonctions non membres ou des fonctions membres statiques, utilisez le style de retour suivant :
- Les types qui ont quatre éléments ou moins sont retournés dans s0-s3, d0-d3 ou v0-v3, selon les besoins.
- Les types inférieurs ou égaux à 8 octets sont retournés en x0.
- Les types inférieurs ou égaux à 16 octets sont retournés en x0 et x1, avec x0 contenant l’ordre inférieur de 8 octets.
- Pour les autres types d’agrégation, l’appelant réserve un bloc de mémoire de taille suffisante et d’alignement pour contenir le résultat. L’adresse du bloc de mémoire doit être passée en tant qu’argument supplémentaire à la fonction dans x8. L’appelé peut modifier le bloc de mémoire de résultat à tout moment pendant l’exécution de la sous-routine. L’appelé n’est pas nécessaire pour conserver la valeur stockée dans x8.
Tous les autres types utilisent cette convention :
- L’appelant réserve un bloc de mémoire de taille suffisante et d’alignement pour contenir le résultat. L’adresse du bloc de mémoire doit être passée en tant qu’argument supplémentaire à la fonction en x0, ou x1 si $this est passée en x0. L’appelé peut modifier le bloc de mémoire de résultat à tout moment pendant l’exécution de la sous-routine. L’appelé retourne l’adresse du bloc de mémoire en x0.
Pile
Après l’ABI mis en place par ARM, la pile doit rester alignée sur 16 octets à tout moment. AArch64 contient une fonctionnalité matérielle qui génère des erreurs d’alignement de pile chaque fois que le fournisseur de services n’est pas aligné sur 16 octets et qu’une charge ou un magasin relatif au fournisseur de services est effectuée. Windows s’exécute avec cette fonctionnalité activée à tout moment.
Les fonctions qui allouent 4 ko ou plus de la pile doivent s’assurer que chaque page avant la page finale est touchée dans l’ordre. Cette action garantit qu’aucun code ne peut « sauter sur » les pages de garde que Windows utilise pour développer la pile. En règle générale, le toucher est effectué par l’assistance __chkstk
, qui a une convention d’appel personnalisée qui passe l’allocation totale de pile divisée par 16 en x15.
Zone rouge
La zone de 16 octets située juste en dessous du pointeur de pile actuel est réservée à une utilisation par les scénarios d’analyse et de mise à jour corrective dynamique. Cette zone permet d’insérer du code généré soigneusement qui stocke deux registres à [sp, #-16] et les utilise temporairement à des fins arbitraires. Le noyau Windows garantit que ces 16 octets ne sont pas remplacés si une exception ou une interruption est effectuée, en mode utilisateur et noyau.
Pile du noyau
La pile du mode noyau par défaut dans Windows est de six pages (24 000). Faites attention aux fonctions avec des mémoires tampons de pile volumineuses en mode noyau. Une interruption mal chronométrée pourrait entrer avec peu de marge de tête et créer un contrôle de bogue de panique de pile.
Marche sur la pile
Le code dans Windows est compilé avec des pointeurs d’images activés (/Oy-) pour activer la marche rapide de la pile. En règle générale, x29 (fp) pointe vers le lien suivant dans la chaîne, qui est une paire {fp, lr}, indiquant le pointeur vers l’image précédente sur la pile et l’adresse de retour. Le code tiers est également encouragé à activer les pointeurs d’images pour permettre un profilage et un suivi améliorés.
Déroulement des exceptions
Le déroulement pendant la gestion des exceptions est assisté par l’utilisation de codes de déroulement. Les codes de déroulement sont une séquence d’octets stockés dans la section .xdata de l’exécutable. Ils décrivent l’opération du prologue et de l’épilogue de manière abstraite, de sorte que les effets du prologue d’une fonction peuvent être annulés en vue de la sauvegarde du cadre de pile de l’appelant. Pour plus d’informations sur les codes de déroulement, consultez gestion des exceptions ARM64.
L’EABI ARM spécifie également un modèle de déroulement d’exception qui utilise des codes de déroulement. Toutefois, la spécification telle qu’elle est présentée est insuffisante pour le déroulement dans Windows, qui doit gérer les cas où le PC se trouve au milieu d’un prologue de fonction ou d’un épilogue.
Le code généré dynamiquement doit être décrit avec des tables de fonctions dynamiques via RtlAddFunctionTable
et des fonctions associées, afin que le code généré puisse participer à la gestion des exceptions.
Compteur de cycles
Toutes les UC ARMv8 sont requises pour prendre en charge un registre de compteurs de cycles, un registre 64 bits que Windows configure pour être lisible à n’importe quel niveau d’exception, y compris le mode utilisateur. Il est accessible via le registre de PMCCNTR_EL0 spécial, à l’aide du code opcode MSR dans le code d’assembly ou de l’intrinsèque dans le _ReadStatusReg
code C/C++.
Le compteur de cycle ici est un vrai compteur de cycle, pas une horloge murale. La fréquence de comptage varie selon la fréquence du processeur. Si vous pensez que vous devez connaître la fréquence du compteur de cycle, vous ne devez pas utiliser le compteur de cycle. Au lieu de cela, vous souhaitez mesurer l’heure de l’horloge murale, pour laquelle vous devez utiliser QueryPerformanceCounter
.
Voir aussi
Problèmes courants de migration ARM Visual C++
Gestion des exceptions ARM64