• 30 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_video

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 29/01/2024

Explorez la mémoire dans les architectures ARM

Vous venez de voir quelques éléments de langage d’assemblage. Dans ce nouveau chapitre, vous allez continuer à explorer cela en s’intéressant de plus prêt à ce que sont les opérandes, en particulier ceux donnant accès à la mémoire, puis en explorant les structures classiques de type if-then-else ou for.

Opérandes et accès mémoire

Comme nous venons de le voir dans le chapitre précédent, une instruction classique s’opère avec des opérandes, généralement au nombre de 2 ou de 3. On peut donc extraire les formes génériques :

INSTR Op1, Op2 qui réalise Op1 = Instr(Op1,Op2) 

INSTR Op1,Op2,Op3 qui réalise Op1 = Instr(Op2,Op3)

  Les opérandes sont de 3 types distincts :

  • Des registres (la plupart du temps les généraux R0 à R12). Exemple : SUBS R4,R2,R5 où R4 va recevoir le résultat de la soustraction entre R2 et R5 

  • Des valeurs immédiates qui se repèrent par un # précédent la valeur spécifiée. Exemple : ORR R11,#0x3CR11 va recevoir le résultat d’un OU logique entre le contenu de R11 et la valeur immédiate codée en hexadécimal 0x3C.

  • Des adresses mémoires qui ne se rencontrent que pour les instructions LDR et STR, qui se font par indirection (l’adresse est dans un registre). Syntaxiquement le registre est encapsulé dans des crochets pour indiquer l’indirection. Exemple LDR R7,[SP] où R7 sera chargé avec la valeur pointée par le pointeur de pile SP.

Le type registre n’offre pas de difficulté particulière. Regardons de plus près les deux autres types d'opérandes.

L’opérande immédiat

Dans le cas de l’opérande immédiat, la valeur est stockée directement dans l’instruction elle-même. Prenons l’exemple du code 1C40 que nous pouvons voir dans l’exemple à la fin du chapitre précédent et qui correspond à l’instruction ADDS R0, R0, #1. Dans le jeu d’instruction Thumb (en 16 bits) on découvre que l’instruction ADDS Rn, Rd, #imm3 peut uniquement prendre une valeur immédiate codée sur 3 bits et dont la structure du code est :

Légende
Légende

 ce qui produit bien un code de valeur 1C40.

 A partir de cet exemple, nous pouvons deviner que pour additionner des valeurs codées sur 32 bits il n’est plus possible d’utiliser des opérandes immédiates. Même en utilisant un codage d’instruction sur 32 bits, si tous les bits sont occupés pour contenir la donnée immédiate, il ne restera plus de place pour mettre le code propre de l’instruction ni le numéro du registre du second opérande. L’assembleur va donc décider de stocker la valeur à récupérer en fin du code dans une zone appelée literal pool. Le processeur va accéder à la valeur  non plus en décodant l’instruction, mais en réalisant une lecture mémoire dans l’espace réservé au code.

Pour illustrer cela, vous trouverez dans le code du chapitre précédent l’instruction LDR R0,[PC,#12] qui permet de charger dans R0 la valeur stockée 12 octets après cette instruction (le registre PC contient l’adresse de l’instruction en cours). Cela signifie que dans l’espace du code, il y a un espace réservé pour manipuler certaines  valeurs (ce que l’on appelle le literal pool).   L’information est par conséquent toujours stockée dans le code mais elle ne se trouve plus directement imbriquée dans le code de l’instruction. Par contre, un accès mémoire supplémentaire est nécessaire pour la récupérer.

Opérande mémoire

Nous l’avons déjà évoqué, l’accès à la mémoire ne se fait que par indirection. Cela suppose qu’au préalable une instruction récupère l’adresse de la variable dans un registre. Cette récupération se fait avec de l’adressage de type immédiat (l’assembleur connaît exactement les adresses où sont stockées les variables) et dans la quasi-totalité des cas, cela se traduira par une lecture en literal pool comme expliqué dans le paragraphe précédent.

Maintenant que l’adresse est dans un registre les accès mémoire se font sans souci (via LDR et STR) en mettant des crochets autour du registre du second opérande contenant l’adresse. Par exemple : LDR R6,[R3] ou STR R7,[R9].

Compliquons encore un petit peu notre exemple pour découvrir d’autres éléments. Ajoutons la déclaration d’un tableau de trois valeurs de type char et l’utilisation du second élément de ce tableau dans l’incrémentation de la variable Locale.

Exemple
Exemple

A partir de l’initialisation (❶) on peut voir que les variables ont été créées en tout début de la mémoire RAM du STM32, c’est finalement logique. L’entier Locale occupe 4 octets et le tableau Carac 3 octets. Une chose est peut-être plus surprenante : les différents octets composant la valeur initiale sont rangés « à l’envers ». Cela s’explique par la convention little endian qu’utilise ARM pour ranger les valeurs en mémoire. Une valeur codée sur 32 bits occupe 4 adresses consécutives : la première est utilisée pour stocker le poids faible, la seconde est utilisée pour l’octet de poids suivant, et ainsi de suite...  Par contre, dans le cas du tableau Carac rien de tel (0x0A correspond à 10 en hexa et le code ASCII de la chiffre ‘3’ est la valeur 0X33, celui de la lettre ‘A ‘ est 0x41) : chaque case du tableau correspond à un seul octet, l’inversion de poids faible et de poids fort n’est pas nécessaire.

À savoir également : la mémoire est alignée. Cela signifie que pour accéder à une valeur codée sur 16 bits (short int) l’adresse la plus basse (celle qui contient les 8 bits de poids faible) doit être paire (donc doit se terminer par un bit à 0). Pour accéder à une valeur codée sur 32 bits (comme ici la variable Locale) l’adresse la plus basse doit être doublement paire (deux derniers bits de l’adresse sont à 0) C’est bien le cas pour Locale qui occupe l’adresse 0x20000000. Le problème ne se propage pas pour les 64 bits (double par exemple) car le processeur ne peut lire que 32 bits à la fois. Lire un 64 bits se fait donc par 2 lectures successives de 32 bits. Il suffit alors que les 64 bits soient rangés à une adresse doublement paire pour que cela fonctionne.

Le point ❷ illustre la récupération de données immédiates en littéral pool. Les données ont été stockées après le code (aux adresses 0x080003A0 et 0x080003A4). Lors du LDR, ces adresses se situent à 16 ou 8 octets plus loin que l’instruction courante (pointée par PC). Le premier [PC,#16] va donc lire à l’adresse  0x080003A0 et récupérer dans R0 l’adresse du tableau Carac, le second [PC,#16] va lire à l’adresse 0x080003A4 et récupérer dans R1 l’adresse de l’entier Locale et enfin [PC,#8] va derechef lire à l’adresse 0x080003A4 (ce qui est dommage car le compilateur aurait pu choisir de conserver cette adresse dans un registre commun aux deux appels et optimiser le code).

Le point ❸ montre comment on peut accéder à un octet (char) d’un tableau. Déjà en utilisant l’instruction LDRB. En effet LRD se décline en :

  • LDRB pour lire un octet non signé (les bits 8 à 31 du registre seront mis à 0),

  • LDRSB pour lire un octet signé (les bits 8 à 31 recopieront le bit de poids fort de l’octet lu —c’est-à-dire le 8ème bit de l’octet ce qui correspond à son signe—, pour que la valeur signée sur 32 bits soit la même que celle sur 8 bits),

  • LDRH pour lire un 16 bits non signés (les bits 16 à 31 du registre seront mis à 0),

  • LDRSH pour lire un 16 bits signé (les bits 16 à 31 recopieront le bit de poids fort — qui est le signe des 16 bits lus— pour que la valeur signée sur 32 bits soit la même que celle sur 16 bits),

  • LDR pour lire un 32 bits (signé ou pas car dans ce cas les 32 bits sont tous directement affectés par le transfert).

Pour l’instruction STR on trouve les mêmes déclinaisons sauf qu’il n’y a pas l’aspect signé ou pas. L’écriture de 8 (resp. 16) bits en mémoire prend les 8 (resp. 16) bits de poids faible pour les écrire dans 1 (resp 2) adresse(s) mémoire. Il n’y a donc pas d’interprétation de signe à faire.

Ensuite, l’accès à la mémoire peut se faire avec les variations qui suivent. On parle de mode d’adressage (l'architecture ARM n'offre pas de mode d'adressage dit direct pour les accès à la mémoire, il faut donc toujours passer par un registre). Ouvrez bien les yeux car d’un point de vue lisibilité de code, on s’approche de l’hérésie ! La liste suivante est donnée avec LDR mais cela s’applique à toutes les formes de LDR et de STR de la même façon.

  • LDR Rt,[Rn] : Transfert dans Rt du contenu de l’adresse pointée par Rn. C’est la notion de pointeur classique.

  • LDR Rt,[Rn,#±imm8] : Transfert dans Rt du contenu de l’adresse contenue dans Rn décalée de  ±imm8. Rn n’est pas modifié par ce transfert. Cela permet d’adresser des structures de données où l’on connaît l’adresse de base de la structure et où les différents éléments qui la composent sont rangés en position relative par rapport à cette adresse de base.

  • LDR Rt,[Rn,Rm] : Transfert dans Rt du contenu de l’adresse pointée par Rn décalée du contenu de Rm. Ni Rn ni Rm ne sont modifiés par ce transfert. Ce mode d’adresse est idéal pour la gestion de tableaux où l’indice du tableau est géré par le registre Rm.

  • LDR Rt,[Rn],#±imm8 : Transfert dans Rt du contenu de l’adresse pointée par Rn puis ajout de  ±imm8 à Rn. Le registre pointeur Rn est post-déplacé, c’est-à-dire déplacé après avoir transféré le contenu de l’adresse pointée par Rn Cela correspond aux pointeurs d’incrémentation *(Ptr++) du langage C. Ce mode d’adresse est parfait pour parcourir une zone mémoire avec un pointeur.

  • LDR Rt,[Rn,#±imm8] ! : Ajout de ±imm8 à Rn puis transfert dans Rt du contenu de l’adresse nouvellement pointée par Rn. Le registre pointeur Rn est pré-déplacé, c’est-à-dire déplacé avant de transférer le contenu de la nouvelle adresse adresse pointée par Rn. Grande similitude avec le cas précédent. Ce mode d’adresse est notamment utile pour la gestion de piles LIFO à pré-décrémentation.

Éléments principaux du jeu d’instructions et des structures algorithmiques 

Le jeu d’instructions même s’il est réduit (RISC) est tout de même conséquent. Il serait vain et fastidieux d’en établir ici la liste exhaustive. Elle se retrouve d’ailleurs facilement soit dans l’aide en ligne de Keil, soit sur le site d’ARM, soit en utilisant ce petit récapitulatif.

Nous allons cependant continuer à découvrir certaines instructions essentielles en désassemblant l’exemple précédent que nous allons agrémenter de quelques ajouts.

Dans la version qui suit, nous nous intéressons au codage en « L.A. » d’une structure alternative if then … else.

Exemple
Exemple

La boucle while (❶) est toujours en place comme précédemment. Il est à noter que la condition de la boucle étant toujours vraie, cette boucle se code intégralement avec des sauts à des adresses absolues (instruction B 0x0800038E), c’est-à-dire dont l’adresse de saut ne change pas.

Le traitement de variables avec des opérateurs logiques est souvent nécessaire pour travailler sur des champs de bits d’un registre. Le jeu d’instruction est donc très riche en terme d’opérateurs de ce type. Ici (❷) l’exemple s’appuie sur le & (ET logique bit-à-bit du langage C) qui correspond au mnémonique AND. Nous vous invitons à parcourir le jeu d’instructions pour découvrir tous les opérateurs logiques classiques, les décalages et les rotations de toutes sortes, etc.

Passons à la structure alternative if then…(❸) else (❹). Sans surprise (quoique…) la mise en place en « L.A. » se fait par le biais de sauts. Ceux-ci sont maintenant des sauts conditionnés. La condition est liée à l’état des fanions (Z,C,N,V). Quand la condition est vraie le saut est réalisé et le processeur se branche à l’adresse indiquée. Si la condition est fausse le saut ne se fait pas et le processeur réalise l’instruction qui suit, sans rupture de séquence. Cela permet donc d’éviter une partie de code. Les conditions correspondent à un suffixe qui s’ajoute à B. Là aussi il faut consulter la documentation pour avoir le champ des possibles.  Dans notre cas nous avons un BLE pour Branch if Less or Equal qui correspond à un certain état des fanions. Ces fanions ont été mis à jour par l’instruction précédente, soit CMP R0, #0xC80xC8 est l'écriture hexadécimale de la valeur décimale 200 et R0 est le registre qui contient une copie de la valeur de Locale. CMP effectue la comparaison entre les deux, nous retrouvons bien notre test if(Locale > 200). Un CMP est une instruction de soustraction qui ne retourne pas le résultat mais qui affecte les fanions. L’ALU réalise Locale-200. Donc notre saut conditionné sera réalisé si Locale ≤ 200 ; c’est-à-dire que l’on va éviter de faire les instructions ❸ si l’on ne vérifie pas que Locale >200. Le « L.A. » oblige la plupart du temps à raisonner en logique inversée.

La partie else est un peu plus simple. Il n’y a plus de comparaison à faire puisque c’est le complément du test précédent. C’est donc tout naturellement le point de rendez-vous du saut conditionné lié au if.  Ceci n’est cependant pas suffisant : il est nécessaire également d’insérer un branchement absolu à la fin du if pour éviter d'exécuter les instructions liées au else si celles liées au if ont été exécutées.

Si vous avez compris le mécanisme de mise en place d’un if then…else, les boucles ne vous poseront pas vraiment de souci. C’est le même principe sauf qu’au lieu de sauter vers l’avant, l’assembleur demande au processeur de sauter vers l’arrière pour retourner au début du traitement de la boucle. Nous l’avons expliqué avec une boucle while(1). Voyons le cas d’une boucle for qui n’est finalement qu’une boucle while particulière :

for (Indice = 1 ; Indice <1O0 ; Indice++)
{
	…
}

est équivalent (surtout en « L.A. ») à

Indice = 1
while(Indice <10O)
{
	…
	Indice++ ;
}

Voyons donc un exemple de boucle for :

Exemple
Exemple

L’initialisation du compteur de boucle (❶) se fait juste avant le corps de la boucle. Notons au passage qu’Indice est une variable locale, le compilateur ne réserve aucun emplacement pour cette variable mais dédie le registre R1 pour le stockage de ce compteur.

L’incrémentation du compteur (❸) se fait en fin de structure juste avant le test de la borne de fin et du saut conditionnée (❷) qui effectue le rebouclage. La condition indice <100 est ici traitée en logique directe. Le processeur compare Indice (R1) et 100. Si R1 est strictement plus petit que 100 (BLT = Branch if Less Than), le retour au début de la boucle se fait.

Le corps de la structure for ne présente pas de difficultés particulières. Les décalages à droite (ASRS) et à gauche (LSLS) affectent les fanions et la multiplication (MULS) est faite sans problème. Attention toutefois car cette multiplication donne un résultat codé sur 32 bits. Il pourrait y avoir un dépassement de capacité et le résultat deviendrait faux.

Il y aurait encore beaucoup d’exemples à traiter pour couvrir l’ensemble des possibilités de ce jeu d’instructions.  Le plus simple est d’essayer quelques lignes de code (en restant simple au départ) pour avancer pas à pas dans cette découverte.

 

À la fin de ce chapitre, vous êtes capable :

  • de distinguer et expliquer les différences entre une opérande immédiate d'une opérande par indirection,

  • de décrire différents mode d'accès à la mémoire,

  • de lire et suivre le déroulement d'une structure algorithmique écrite en assembleur. 

Exemple de certificat de réussite
Exemple de certificat de réussite