Aller au contenu

Jeux d'instructions

Les processeurs ARM implémentent une architecture “Load & Store”. Cette architecture implique qu’une donnée soit préalablement chargée dans un registre interne du CPU avant de pouvoir la traiter. Le design du CPU ne permet la manipulation de données que sur des mots de 32 bits. C’est donc lors du chargement de la donnée de la mémoire centrale ou d’un contrôleur de périphériques d’entrées/sorties que le CPU devra adapter la taille de cette donnée aux 32 bits de ses registres.

Load & Store

Ce chapitre ne donne pas une description exhaustive des trois jeux d’instructions ARM, Thumb et Thumb-2. Car, hormis des différences sur l’exécution conditionnelle d’instructions et le nombre de registres accessible, les trois jeux d’instructions des processeurs ARM sont similaires. Dès lors, ce chapitre ne présente que les instructions principales, compatibles ARM et Thumb-2, permettant la réalisation des fonctions et routines nécessaires à la mise en œuvre du processeur et aux traitements des interruptions et exceptions.

Exécution conditionnelle

Deux différences majeures existent entre le jeu d’instructions ARM et le jeu d’instructions Thumb-2, l’exécution conditionnelle des instructions et la mise à jour optionnelle des fanions après l’exécution d’une instruction.

Conditions

Les fanions de conditions contenus dans le registre APSR, registre d’état du programme, permettent de dériver les conditions de branchement. Pour leurs utilisations, deux variantes sont possibles, soit un test direct du fanion, soit un test similaire aux conditions utilisées en C/C++. Les jeux d’instructions proposent des mnémoniques pour chacune de ces variantes (figure ci-dessous).

Le test des fanions consiste à regarder si le fanion est à 1 (set) ou s’il est à 0 (clear). L’alternative consistant à utiliser les conditions C/C++ est souvent plus évidente. Elle permet également d’effectuer des branchements conditionnels dépendant du résultat d’une opération sur des nombres entiers. Pour évaluer ces conditions, il est nécessaire de tester plusieurs fanions simultanément.

Fanions vs conditions

Il est dès lors important de distinguer un nombre entier signé d’un nombre entier non signé afin de choisir le mnémonique correspondant. Pour rappel, ces deux types d’entiers n’utilisent pas les mêmes fanions pour évaluer une même condition; les nombres non signés utilisent les fanions Z et C, tandis que les signés emploient les fanions Z, V et N.

Jeu d’instructions ARM

Avec le jeu d’instructions ARM, pratiquement toutes les instructions peuvent s’exécuter de façon conditionnelle. Suite à un test ou une mise à jour des fanions de condition, l’instruction est capable de tester si son exécution peut se terminer ou doit s’interrompre.

La forme générale des instructions ARM est

opcode{cond}{S} Rd, <Sop>
ou
opcode{cond}{S} Rd, Rm, <Sop>

Le choix entre une exécution conditionnelle ou pas s’indique en plaçant la condition d’exécution {cond} après le mnémonique de l’instruction. Sans indication, l’instruction s’exécute toujours complètement, ce qui correspond à utiliser la condition AL (always). Voici une équivalence en C/C++:

if (cond) Rd = opcode (Sop);
ou
if (cond) Rd = opcode (Rm, Sop);

Un S placé après le mnémonique de l’instruction indique que le changement de valeur des fanions, dû à l’exécution de l’instruction, doit être reporté dans le registre APSR pour un usage ultérieur.

Jeu d’instructions Thumb-2

Le jeu d’instructions Thumb-2 ne supporte qu’indirectement l’exécution conditionnelle. L’instruction IT, (If-Then) permet une exécution conditionnelle pour les une à quatre instructions suivantes, évitant ainsi l’utilisation de branchements conditionnels. La syntaxe de l’instruction est

IT{pattern}{q} <cond>

La première instruction s’exécute si la condition <cond> est vraie. Pour les trois autres instructions, le choix est libre et défini par l’option {pattern}, laquelle permet de spécifier jusqu’à trois conditions. Un T indique que l’instruction doit être exécutée si la condition est vraie, tandis qu’un E indique l’inverse. L’option {q} spécifie le codage des instructions ; .n (narrow) pour choisir un codage sur 16 bits de préférence et .w (wide) pour un codage sur 32 bits.

Le jeu d’instructions Thumb-2 supporte également la mise à jour optionnelle des fanions (N, Z, C, et V). Avec le suffixe S, les fanions sont mis à jour et l’instruction est codée de manière compacte sur 16 bits. Sans le suffixe S, les fanions ne sont pas mis à jour et l’instruction est codée sur 32 bits.

Exemple

La fonction C max ci-dessous, calculant le plus grand nombre entre deux entiers, démontre l’utilisation d’instructions conditionnelles.

int max(int a, int b) { return a > b ? a : b; } 
et en assembleur
// Instructions ARM             // Instructions Thumb-2
max:                            max:
   cmp     r0, r1                  cmp     r0, r1
   movlt   r0, r1                  it.n    lt
   bx      lr                      movlt   r0, r1
                                   bx      lr

Avec le jeu d’instructions ARM, l’instruction movlt charge la valeur de r1 dans le registre r0 si a est plus petit que b. Avec le jeu d’instructions Thumb-2, if est nécessaire d’utiliser la combinaison des instructions it lt et movlt r0, r1 pour réaliser cette condition.

Il peut sembler redondant de répéter la condition pour chaque instruction qui suit it, mais ça permet de bien montrer dans le code que l’exécution est conditionnelle et ca permet à l’assembleur d’effectuer une vérification supplémentaire.

Opérande de décalage

Les jeux d’instructions ARM et Thumb-2 nomme le troisième opérande d’une instruction l’opérande de décalage (Sop - Shifter Operand). Cet opérande peut prendre trois formes différentes:

  • Valeur immédiate
  • Registre
  • Décalage et rotation

Avec la forme valeur immédiate, l’opérande est généralement une valeur numérique entière comprise entre 0 et 255. Cependant, cette valeur peut subir une rotation par multiple de deux avant d’être utilisée. Quelques valeurs possibles:

   movs  r0, #15        // r0 = 0x0000'000f
   movs  r0, #983040    // r0 = 0x000f'0000
   movs  r0, #245760    // r0 = 0x0003'c000

Avec la forme registre, l’opérande est simple un des seize registres du processeur. Par exemple:

   mov   r0,  r2        // r0 = r2
   mov   r15, r14       // pc = lr (return to the caller)

Avec la forme décalage et rotation, l’opérande est le résultat d’un décalage vers la droite, vers la gauche ou d’une rotation d’un des registres du processeur (Rm). Il est possible de spécifier la valeur de ce décalage ou de cette rotation, le nombre de bits, avec une valeur immédiate (#<imm>)ou avec le contenu d’un registre (Rs). Les formes possibles:

  • Valeur immédiate: Sop <= Rm, LSL|LSR|ASR|ROR #<imm>
  • Registre: Sop <= Rm, LSL|LSR|ASR|ROR Rs
  • Rotation étendue: Sop <= Rm, RRX

LSL - Décalage logique (non signé) vers la gauche

Avecl’opérateur LSL, l’opérande de décalage est formé en décalant la valeur du registre Rm vers la gauche. Un 0 est inséré à la place du bit vacant. Le dernier bit de poids fort décalé (Rm[31], si le décalage est de 1) est, quant à lui, placé dans le carry (APSR[C]) si une mise à jour des fanions est souhaitée (S==1).

LSL - Décalage logique (non signé) vers la gauche

Exemple:

   movs  r0, r1, lsl #2    // r0 = r1 << 2

ou

   lsls  r0, r1, #2

Si R1 vaut 0xb000'8080, alors l’exécution de l’instruction R0 vaudra 0xc002'0200. La valeur du carry est 0, car la valeur du 30e bit vaut 1 (R1[30] == 0).

En C/C++, cette opération correspond à un décalage d’un nombre entier non signé ou non signé vers la gauche.

   unsigned r1 = 0xb0008080;
   unsigned r0 = r1 << 2;

LSR - Décalage logique (non signé) vers la droite

Avec l’opérateur LSR, l’opérande de décalage est formé en décalant la valeur du registre Rm par la droite. Un 0 est inséré à la place du ou des bits vacants. Le dernier bit de poids faible décalé (Rm[3], si le décalage est de 4) est, quant à lui, placé dans le carry (APSR[C]) si une mise à jour des fanions est souhaitée (S==1).

LSR - Décalage logique (non signé) vers la droite

Exemple:

   movs  r0, r1, lsr #8    // r0 = (unsigned)r1 >> 8

ou

   lsrs  r0, r1, #8        

Si R1 vaut 0x8000'8080, alors l’exécution de l’instruction R0 vaudra 0x0080'0080. La valeur du carry est 1, car la valeur du 8e bit vaut 1 (R1[7] == 1).

En C/C++, cette opération correspond à un décalage d’un nombre entier non signé vers la droite.

   unsigned r1 = 0x80008080; 
   unsigned r0 = r1 >> 8;

ASR - Décalage arithmétique (signé) vers la droite

Avec l’opérateur ASR, l’opérande de décalage est formé en décalant la valeur du registre Rm par la droite. Le bit du poids fort (Rm[31]) est inséré à la place du bit vacant. Le dernier bit de poids faible décalé (Rm[0]) est, quant à lui, placé dans le carry (APSR[C]) si une mise à jour des fanions est souhaitée (S==1).

ASR - Décalage arithmétique (signé) vers la droite

Exemple:

   mov   r0, r1, asr #8    // r0 = (signed)r1 >> 8

ou

   asr   r0, r1, #8        

Si R1 vaut 0x8000'8080, alors l’exécution de l’instruction R0 vaudra 0xFF80'0080. Par contre, si R1 vaut 0x7000'8080, alors l’exécution de l’instruction R0 vaudra 0x0070'0080. La valeur du carry reste inchangée.

En C/C++, cette opération correspond à un décalage d’un nombre entier signé vers la droite.

   int r1 = 0x80008080; 
   int r0 = r1 >> 8;

ROR - Rotation vers la droite

Avec l’opérateur ROR, l’opérande de décalage est formé en effectuant une rotation du contenu du registre Rm par la droite (figure ci-dessous). Le bit du poids faible (Rm[0]) est inséré à la place du bit de poids fort (Rm[31]). Cette opération se répète pour le nombre de bits correspondant à la rotation souhaitée. Le dernier bit de poids faible décalé (Rm[4], si la rotation est de 5) est, quant à lui, placé dans le carry (APSR[C]) si une mise à jour des fanions est souhaitée (S==1).

ROR - Rotation vers la droite

Exemple:

   movs   r0, r1, ror #4

ou

   rors   r0, r1, #4        

Si R1 vaut 0x0700'8083, alors l’exécution de l’instruction R0 vaudra 0x3070'0808. La valeur du carry est 0, car la valeur du 4e bit vaut 0 (R1[3] == 0).

Cette opération n’a pas d’équivalent en C/C++.

RRX - Rotation étendue vers la droite

Avec l’opérateur RRX, l’opérande de décalage est formé en effectuant une rotation de 1 bit du contenu du registre Rm par la droite. Le carry (APSR[C]) est inséré à la place du bit de poids fort Rm[31]. Le bit de poids faible (Rm[0]) est quant à lui placé dans le carry (APSR[C]) si une mise à jour des fanions est souhaitée (S==1).

RRX - Rotation étendue vers la droite

Exemple:

   movs   r0, r1, rrx

ou

   rrxs   r0, r1

Si R1 vaut 0x0700'8080 et que le carry vaut 1, alors après l’exécution de l’instruction, R0 vaudra 0x8380'4040. La valeur du carry est 0, car la valeur du 1er bit vaut 1 (R1[0] == 0).

Cette opération n’a pas d’équivalent en C/C++.

Echange de données avec les registres

Les opérations move permettent d’échanger de données avec les registres. Elles supportent le chargement d’une valeur numérique immédiate dans un registre et la copie du contenu d’un registre dans un autre.

  • MOV{S} Rd, <Sop> : l’instruction charge dans le registre Rd le résultat de l’opérande de décalage <Sop> (shifter operand).

  • MOVW Rd, #<imm16> : l’instruction charge une valeur de 16 bits (0 à 65535) dans les deux octets inférieurs du registre Rd (Rd[31:16] = 0; Rd[15:0] = #<imm16>).

  • MOVT Rd, #<imm16> : l’instruction charge une valeur de 16 bits (0 à 65535) dans les deux octets supérieurs du registre Rd (Rd[31:16] = #<imm16>, Rd[15:0] = unchanged).

  • MVN{S} Rd, <Sop> : l’instruction charge dans le registre Rd le complément à 1 (valeur binaire inverse) de l’opérande de décalage <Sop>.

Quelques exemples:

   movs  r0, r1               // r0 = r1
   movs  r0, r1, lsl #5       // r0 = r1 << 5
   movs  r0, #0               // r0 = 0
   movw  r0, #0x034a          // r0 = 0x0000'034a
   movt  r0, #0x1f00          // r0 = 0x1f00'034a
   mvns  r1, #0               // r0 = 0xffff'ffff

Echange de données avec la mémoire

Par son architecture et pour traiter une donnée, le CPU des processeurs ARM doit préalablement copier la donnée (load) de la mémoire centrale ou d’un contrôleur de périphériques dans un de ses registres. Une fois disponible, le CPU peut la manipuler selon les instructions du programme. Le traitement terminé, le CPU peut stocker le résultat (store) dans la mémoire ou le contrôleur.

Le CPU ne pouvant opérer que sur des données de 32 bits, c’est lors de leur chargement (figure suivante) que l’instruction doit connaître leur taille (8, 16, 32 ou 64 bits) et leur type (signée ou non signée). Lors du stockage du résultat, seule la taille de la donnée est nécessaire.

Echange de données avec la mémoire \label{as05.07}

Echange de données avec la mémoire

L’accès à une donnée requiert obligatoirement l’utilisation d’un registre (Rn) contenant l’adresse de cette donnée en mémoire centrale ou l’adresse du registre du contrôleur de périphériques. Pour charger cette adresse, il existe deux variantes selon qu’il s’agit d’une variable ou d’un contrôleur.

Prenons un pseudo-code C pour illustrer ces accès

   struct ctrl {
      uint32_t stat;
      uint32_t ctrl;
      uint32_t pad[2];
      uint32_t rx;
      uint32_t tx;
   };
   static volatile struct ctrl* io = (struct ctrl*)0x40001000;
   static uint16_t array[] = { 1, 2, 3, 4 };
   static int8_t s_octet = -1;

   array[1] = array[2];
   io->tx = io->rx;
   int8_t tmp = s_octet;

Représentation mémoire de ces données

Si la donnée est une variable (array ou s_octet), l’assembleur permet d’utiliser son étiquette pour obtenir son adresse. Par contre, s’il s’agit d’un contrôleur (io), c’est plutôt que la valeur numérique de l’adresse de base du contrôleur qui est employée, adresse généralement disponible dans le manuel d’utilisateur du contrôleur.

   ldr   r0, =array       // r0 = &array[0]
   ldr   r1, #0x40001000  // r1 = 0x0x4000'1000
   ldr   r2, =s_octet     // r2 = &s_octet

Pour l’instant, le code assembleur ci-dessus ne met à disposition que l’adresse de base de la variable ou du contrôleur. Alors en regardant le code C, il est légitime de se poser la question :

Comment procéder pour accéder un registre particulier du contrôleur ou un élément du tableau ?

Une solution est d’utiliser directement l’adresse de la variable, de l’élément d’un tableau ou du registre du contrôleur. Cette technique est simple, mais peu efficace, car pour chaque donnée à traiter une adresse doit être stockée avec le code et chargée dans un registre du CPU durant l’exécution du programme. Une autre approche nettement plus performante consiste à ne charger que l’adresse de base de la variable ou du contrôleur et d’y ajouter un offset (<ofs>) afin d’obtenir l’adresse de la donnée à traiter. Cette offset est le déplacement ou l’écart en octets entre l’adresse de base de la variable ou du contrôleur et l’adresse de l’élément/du registre. Cette offset peut être donnée soit par une valeur immédiate, soit par le contenu d’un registre du CPU (Rm).

Pour charger une donnée dans un registre, les jeux d’instructions ARM proposent les instructions :

  • LDR Rd, [Rn, <ofs>] : pour charger un mot de 32 bits stocké à l’adresse (Rn + <ofs>) dans le registre Rd.

  • LDRH Rd, [Rn, <ofs>] : pour charger un mot de 16 bits non signé stocké à l’adresse (Rn + <ofs>) dans les 16 premiers bits du registre Rd (Rd[15:0]), les 16 bits de poids fort seront simplement mis à 0 (Rd[31:16] = 0).

  • LDRB Rd, [Rn, <ofs>] : pour charger un mot de 8 bits non signé stocké à l’adresse (Rn + <ofs>) dans les 8 premiers du registre Rd (Rd[7:0]), les 24 bits de poids fort seront simplement mis à 0 (Rd[31:8] = 0).

  • LDRD Rd, [Rn, <ofs>] : pour charger un mot de 64 bits stocké à l’adresse (Rn + <ofs>) dans la paire de registres Rd et Rd+1. Le registre Rd doit être un registre pair (R0, R2, R4, …).

  • LDRSH Rd, [Rn, <ofs>] : pour charger un mot de 16 bits signé stocké à l’adresse (Rn + <ofs>) dans les 16 premiers bits du registre Rd (Rd[15:0]), la valeur des 16 bits de poids fort sera identique au seizième bit de la donnée à charger (Rd[31:16] = data[15]). On parle d’extension du bit de signe.

  • LDRSB Rd, [Rn, <ofs>] : pour charger un mot de 8 bits signé stocké à l’adresse (Rn + <ofs>) dans les 8 premiers du registre Rd (Rd[7:0]), la valeur des 24 bits de poids fort sera identique au huitième bit de la donnée à charger (Rd[31:8] = data[7]).

Pour stocker le contenu d’un registre en mémoire, les jeux d’instructions ARM proposent les instructions :

  • STR Rd, [Rn, <ofs>] : pour stocker les 32 bits du registre Rd.
  • STRH Rd, [Rn, <ofs>] : pour stocker les 16 bits premiers du registre Rd (Rd[15:0]).
  • STRB Rd, [Rn, <ofs>] : pour stocker les 8 bits premiers du registre Rd (Rd[7:0]).
  • STRD Rd, [Rn, <ofs>] : pour stocker les 64 bits dans la paire de registres Rd et Rd+1.

Maintenant il est possible d’implémenter le pseudo-code C en code assembleur

   ldrh  r3, [r0, #(2*2)]     // r3 = array[2]
   strh  r3, [r0, #(1*2)]     // array[1] = r3 --> array[1] = array[2]

   ldr   r3, [r1, #0x10]      // r3 = io->rx
   str   r3, [r1, #0x14]      // io->tx = r3   --> io->tx = io->rx

   ldrsb r3, [r2]             // r3 = s_octet

Le mode d’adressage présenté précédemment est le mode d’adressage élémentaire des processeurs ARM. Il devrait cependant suffire à réaliser une grande majorité des routines nécessitant une implémentation en code assembleur. Mais si toutefois des algorithmes plus complexes exigent des fonctionnalités plus avancées, le jeu d’instructions ARM offre d’autres modes d’adressage, tels que pré-indexé, post-indexé ou par décalage. Alors avant de s’aventurer à écrire beaucoup de lignes de code, il est recommandé et judicieux de jeter un petit coup d’œil sur la documentation du processeur.

Echange multiple de données avec la mémoire

Les échanges multiples de données avec la mémoire servent à sauver, respectivement à restaurer, le contenu de plusieurs registres du CPU en une seule instruction. Il existe principalement deux paires d’instructions push/pop et stmia/ldmia.

La première paire push/pop sert à la sauvegarde des registres du CPU sur la pile à l’entrée d’une fonction et à leur restauration à sa sortie.

  • PUSH {<reglist>} : pousse sur la pile le contenu des registres indiqués avec l’instruction.
  • POP {<reglist>} : place le contenu de la pile dans les registres indiqués avec l’instruction.

<reglist> spécifie la liste des registres à pousser sur la pile ou à retirer de la pile. Il est possible d’énumérer chaque registre en les séparant les uns des autres par une virgule (,) ou de donner une série en séparant le premier registre du dernier avec un trait d’union (-).

Sauvegarde des registres sur la pile

Pour rappel, les processeurs ARM implémentent une pile pleine descendante (full descending); en poussant le contenu de registres sur la pile, le registre SP se décrémente, respectivement en retirant le contenu d’un registre, le registre SP s’incrémente (figure précédente}). Le registre SP pointe toujours sur le sommet de pile. L’instruction push place le contenu des registres sur pile dans l’ordre inverse (le plus grand numéro en premier et le plus petit en dernier). L’instruction pop travaille en sens inverse.

La deuxième paire stmia/ldmia sert à la sauvegarde des registres du CPU en mémoire à une adresse spécifiée par le registre Rn. Cette paire est généralement utilisée par les OS pour sauver le contexte d’une tâche dans une structure de données servant à sa gestion (TCB - Task Control Block).

  • STMIA Rn, {<reglist>} : stocke le contenu des registres indiqués avec l’instruction à l’emplacement mémoire spécifié par le registre Rn.
  • LDMIA Rn, {<reglist>} : restaure le contenu des registres indiqués avec l’instruction avec les données stockées à l’emplacement mémoire spécifié par le registre Rn.

Sauvegarde des registres en mémoire

Ce chapitre présente seulement les paires d’instructions les plus importantes. Les jeux d’instructions ARM et Thumb-2 offrent encore d’autres paires d’instructions. Il peut être judicieux de jeter un petit coup d’œil sur la documentation, si nécessaire.

Echange de données avec les registres spéciaux et des co-processeurs

L’accès aux registres spéciaux du CPU ainsi qu’aux registres des co-processeurs nécessite l’emploi d’instructions spécifiques.

  • MRS/MSR : cette paire d’instructions donne accès aux registres APSR, CPSR et SPSR pour les µP Cortex-A et aux registres APSR, IPSR, EPSR, PSR, PRIMASK, FAULTMASK, BASEPRI et CONTROL pour les µC Cortex-M.
  • CPS : cette instruction permet de gérer l’état du processeur et plus particulièrement l’activation des interruptions.
  • MRC/MCR : cette paire d’instructions donne accès aux registres des co-processeurs.

L’utilisation de ces instructions étant très spécifique, il est impératif de jeter un coup d’œil sur la documentation avant de s’en servir.

Opérations arithmétiques

Les deux jeux d’instructions ARM et Thumb-2 proposent des instructions pour effectuer des opérations d’addition, de soustraction et de multiplication sur des nombres entiers signés et non signés. Par contre pour l’opération de division, seul le jeu d’instructions Thumb-2 la propose.

Ce chapitre présente seulement les instructions les plus importantes. Les jeux d’instructions ARM et Thumb-2 en offrent encore bien d’autres. Il peut être judicieux de jeter un petit coup d’œil sur la documentation, si nécessaire.

Additions

  • ADD{S} Rd, Rn, <Sop> : additionne le contenu du registre Rn avec l’opérande de décalage <Sop> et stocke le résultat dans le registre Rd (Rd = Rn + Sop).
int32_t Rn  = 10;
int32_t Sop = 15;
int32_t Rd  = Rn + Sop;
ldr   r0, =Rn        // r0 = &Rn
ldr   r0, [r0]       // r0 = Rn
ldr   r1, =Sop       // r1 = &Sop
ldr   r1, [r1]       // r1 = Sop
adds  r0, r0, r1     // r0 = r0 + r1 
ldr   r1, =Rd        // r1 = &Rd
str   r0, [r1]       // Rd = r0
  • ADC{S} Rd, Rn, <Sop> : additionne le contenu du registre Rn avec l’opérande de décalage <Sop> et la valeur du fanion C (report
  • carry) et stocke le résultat dans le registre Rd (`Rd = Rn + Sop
  • C`). Cette instruction permet l’addition de nombre dont la taille est supérieure à 32 bits.
uint64_t Rn  = 0xffffffff;
uint64_t Sop = 1;
uint64_t Rd  = Rn + Sop;
ldr   r0, =Rn        // r0    = &Rn
ldrd  r0, [r0]       // r0,r1 = Rn
ldr   r2, =Sop       // r2    = &Sop
ldrd  r2, [r2]       // r2,r3 = Sop
adds  r0, r0, r2     // r0    = r0 + r2         
adcs  r1, r1, r3     // r1    = r1 + r3 + carry 
ldr   r2, =Rd        // r2    = &Rd
strd  r0, [r2]       // Rd    = r0,r1

Soustractions

  • SUB{S} Rd, Rn, <Sop> : soustrait le contenu l’opérande de décalage <Sop> au contenu du registre Rn et stocke le résultat dans le registre Rd (Rd = Rn - Sop).
uint32_t Rn  = 10;
uint32_t Sop = 15;
uint32_t Rd  = Rn - Sop;
ldr   r0, =Rn        // r0 = &Rn
ldr   r0, [r0]       // r0 = Rn
ldr   r1, =Sop       // r1 = &Sop
ldr   r1, [r1]       // r1 = Sop
subs  r0, r0, r1     // r0 = r0 - r1 
ldr   r1, =Rd        // r1 = &Rd
str   r0, [r1]       // Rd = r0
  • SBC{S} Rd, Rn, <Sop> : soustrait le contenu l’opérande de décalage <Sop> et de l’inverse du fanion C (retenue - inverse carry) du contenu du registre Rn et stocke le résultat dans le registre Rd (Rd = Rn - (Sop + ~C)). Cette instruction permet la soustraction de nombre dont la taille est supérieure à 32 bits.
int64_t Rn  = 0x100000000;
int64_t Sop = 1;
int64_t Rd  = Rn - Sop;
ldr   r0, =Rn     // r0    = &rn
ldrd  r0, [r0]    // r0,r1 = Rn
ldr   r2, =Sop    // r2    = &Sop
ldrd  r2, [r2]    // r2,r3 = Sop
subs  r0, r0, r2  // r0    = r0 - r1
sbcs  r1, r1, r3  // r1    = r1 - (r3 + ~carry)
ldr   r2, =Rd     // r2    = &Rd
strd  r0, [r2]    // Rd    = r0,r1
  • RSB{S} Rd, Rn, <Sop> : effectue la soustraction inverse entre le contenu de l’opérande de décalage <Sop> et contenu du registre Rn et stocke le résultat dans le registre Rd (Rd = Sop - Rn). Si <Sop> vaut 0, alors cette instruction calcule la valeur négative (Rd = -Rn).
int32_t Rn  = 10;
int32_t Rd  = -Rn;
ldr   r0, =Rn        // r0 = &Rn
ldr   r0, [r0]       // r0 = Rn
rsbs  r0, r0, #0     // r0 = 0 - r0
ldr   r1, =Rd        // r1 = &Rd
str   r0, [r1]       // Rd = r0

Multiplications

  • MUL Rd, Rn, Rm : effectue la multiplication entre le contenu du registre Rn et le contenu du registre Rm et stocke le résultat dans le registre Rd (Rd = Rn * Rm).
int32_t Rn  = 10;
int32_t Rm  = -25;
int32_t Rd  = Rn * Rm;
ldr   r0, =Rn        // r0 = &Rn
ldr   r0, [r0]       // r0 = Rn
ldr   r1, =Rm        // r1 = &Rm
ldr   r1, [r1]       // r1 = Rm
mul   r2, r0, r1     // r2 = r0 * r1
ldr   r1, =Rd        // r1 = &Rd
str   r2, [r1]       // Rd = r2

Divisions

  • SDIV Rd, Rn, Rm : effectue la division entre deux nombres signés contenus dans le registre Rn et le contenu du registre Rm et stocke le résultat dans le registre Rd (Rd = Rn / Rm).
int32_t Rn  = 250;
int32_t Rm  = -25;
int32_t Rd  = Rn / Rm;
ldr   r0, =Rn        // r0 = &Rn
ldr   r0, [r0]       // r0 = Rn
ldr   r1, =Rm        // r1 = &Rm
ldr   r1, [r1]       // r1 = Rm
sdiv  r2, r0, r1     // r2 = r0 / r1
ldr   r1, =Rd        // r1 = &Rd
str   r2, [r1]       // Rd = r2
  • UDIV Rd, Rn, Rm : effectue la division entre deux nombres non signés contenus dans le registre Rn et le contenu du registre Rm et stocke le résultat dans le registre Rd (Rd = Rn / Rm).
uint32_t Rn  = 0xf0000000;
uint32_t Rm  = 0x00000010;
uint32_t Rd  = Rn / Rm;
ldr   r0, =Rn        // r1 = &Rn
ldr   r0, [r0]       // r1 = Rn
ldr   r1, =Rm        // r0 = &Rm
ldr   r1, [r1]       // r0 = Rm
udiv  r2, r0, r1     // r2 = r0 / r1
ldr   r1, =Rd        // r1 = &Rd
str   r2, [r1]       // Rd = r2

Opérations logiques

Les deux jeux d’instructions ARM et Thumb-2 proposent des instructions pour effectuer des opérations logiques (et, ou, ou-exclusif) sur des nombres entiers.

  • AND{S} Rd, Rn, <Sop> : effectue un et logique entre deux nombres contenus dans le registre Rn et le contenu de l’opérande de décalage <Sop> et stocke le résultat dans le registre Rd (Rd = Rn & Sop).
uint32_t Rn  = 0xf800;
uint32_t Sop = 0x8800;
uint32_t Rd  = Rn & Sop;
ldr   r0, =Rn        // r0 = &Rn
ldr   r0, [r0]       // r0 = Rn
ldr   r1, =Sop       // r1 = &Sop
ldr   r1, [r1]       // r1 = Sop
ands  r0, r0, r1     // r0 = r0 & r1 
ldr   r1, =Rd        // r1 = &Rd
str   r0, [r1]       // Rd = r0
  • ORR{S} Rd, Rn, <Sop> : effectue un ou logique entre deux nombres contenus dans le registre Rn et le contenu de l’opérande de décalage <Sop> et stocke le résultat dans le registre Rd Rd (Rd = Rn | Sop).
uint32_t Rn  = 0xf800;
uint32_t Sop = 0x0700;
uint32_t Rd  = Rn & Sop;
ldr   r0, =Rn        // r0 = &Rn
ldr   r0, [r0]       // r0 = Rn
ldr   r1, =Sop       // r1 = &Sop
ldr   r1, [r1]       // r1 = Sop
orrs  r0, r0, r1     // r0 = r0 | r1 
ldr   r1, =Rd        // r1 = &Rd
str   r0, [r1]       // Rd = r0
  • EOR{S} Rd, Rn, <Sop> : effectue un ou-exclusif logique entre deux nombres contenus dans le registre Rn et le contenu de l’opérande de décalage <Sop> et stocke le résultat dans le registre Rd (Rd = Rn ^ Sop).
uint32_t Rn  = 0xf800;
uint32_t Sop = 0x8888;
uint32_t Rd  = Rn & Sop;
ldr   r0, =Rn        // r0 = &Rn
ldr   r0, [r0]       // r0 = Rn
ldr   r1, =Sop       // r1 = &Sop
ldr   r1, [r1]       // r1 = Sop
eors  r0, r0, r1     // r0 = r0 ^ r1 
ldr   r1, =Rd        // r1 = &Rd
str   r0, [r1]       // Rd = r0
  • BIC{S} Rd, Rn, <Sop> : efface les bits à 1 contenus dans l’opérande de décalage <Sop> du contenu du registre Rn. Cette instruction effectue un et logique entre le nombres contenu dans le registre Rn et l’inverse du contenu de l’opérande de décalage <Sop> et stocke le résultat dans le registre Rd (Rd = Rn & ~Sop).
uint32_t Rn  = 0xf800;
uint32_t Sop = 0xf000;
uint32_t Rd  = Rn & ~Sop;
ldr   r0, =Rn        // r0 = &Rn
ldr   r0, [r0]       // r0 = Rn
ldr   r1, =Sop       // r1 = &Sop
ldr   r1, [r1]       // r1 = Sop
bics  r0, r0, r1     // r0 = r0 & ~r1 
ldr   r1, =Rd        // r1 = &Rd
str   r0, [r1]       // Rd = r0

Ce chapitre ne présente que les instructions principales. Les jeux d’instructions ARM et Thumb-2 en offrent encore d’autres. Il peut être judicieux de jeter un petit coup d’œil sur la documentation, si nécessaire.

Opérations de décalage et de rotation

Ces opérations mettent simplement en œuvre l’opérande de décalage pour effectuer les décalages et rotations sur les bits contenus dans les registres du CPU. Si on veut juste faire un décalage ou une rotation, un utilise l’instruction MOV. Par exemple MOV R0, R0, LSL #2 effectue un décalage de deux bits vers la gauche du registre R0.

Opérations de contrôle de flux

L’exécution des instructions d’un programme se déroule principalement séquentiellement. Cependant, ce flux séquentiel peut être interrompu pour effectuer des appels de fonctions ou des branchements.

Contrôle de flux

Appel de fonctions

En C/C++, deux techniques cohabitent pour l’appel de fonction, l’appel direct et l’appel indirect. L’appel direct utilise simplement le nom de la fonction, son adresse, lors de son appel. L’appel indirect utilise, quant à lui, une variable contenant l’adresse de la fonction à appeler. Cette variable est connue sous le nom de pointeur de fonction en C et de méthode virtuelle en C++.

Les deux jeux d’instructions ARM et Thumb-2 proposent une instruction pour chacune de ces techniques.

  • BL <label> : appelle la fonction identifiée par l’étiquette <label>. L’adresse de retour est stockée dans le registre LR. Avec cette instruction, le processeur effectue un saut relatif par rapport à la valeur actuelle de son compteur ordinaire (registre PC) pour appeler la fonction spécifiée avec l’étiquette <label>. Le déplacement, offset entre l’adresse de l’instruction BL et celle du début de la fonction, est calculée lors de la génération de l’application. La plage de cette offset dépend du jeu d’instructions utilisé, ARM ±32MiB, Thumb-2 ±16Mib et Thumb ±4MiB.
extern void function(void);

function();
bl    function
  • BLX Rm : appelle la fonction identifiée par le contenu du registre Rm. L’adresse de retour est stockée dans le registre LR. Avec cette instruction, le processeur effectue un saut absolu. Le contenu du registre Rm est simplement chargé dans le compteur ordinal (registre PC).
extern void function(void);

void (*fnct)(void) = function;
fnct();
ldr   r0, =fnct+1
ldr   r0, [r0]
blx   r0

!!! note Pour continuer l’exécution du code avec le jeu d’instructions Thumb-2, l’adresse utilisée pour un appel indirect d’une fonction doit impérativement être impaire. Le code étant toujours aligné sur des adresses paires, il suffit d’incrémenter l’adresse de la fonction par 1 pour conserver l’exécution du code en Thumb-2.

Branchements

En C/C++, les branchements sont en général contrôlés par les instructions if/else et switch. Le choix de la branche à exécuter dépend de l’évaluation d’une condition ou d’une valeur.

Branchements

En langage assembleur, les branchements conditionnels s’effectuent en deux étapes, premièrement l’évaluation de la condition puis le branchement. L’évaluation de la condition est reflétée dans les fanions du registre APSR, ceci suite à l’exécution d’une opération arithmétique ou logique, d’une comparaison ou d’un test. Lors l’exécution de l’opération de branchement, l’instruction évalue les fanions, la condition <cond> (figure précédente), et effectue le branchement si la condition est remplie. Si la condition n’est pas satisfaite, le CPU poursuit l’exécution des instructions placées à la suite de l’instruction de branchement (voir les exemples ci-dessous).

Comparaisons et tests

En ajoutant le suffixe S aux instructions, le CPU reflète, en plus de stocker le résultat de l’opération dans le registre Rd, la mise à jour des fanions dans le registre APSR. Pour éviter de modifier le contenu des registres du CPU, les deux jeux d’instructions ARM et Thumb-2 proposent des instructions pour comparer des valeurs et tester l’état de bits contenus dans les registres sans les modifier.

  • CMP Rn, <Sop> : compare le contenu du registre Rn avec le contenu de l’opérande de décalage <Sop>. Les fanions reflètent le résultat de la soustraction de l’opérande de décalage <Sop> au registre Rn (Rn - Sop). Hormis les fanions, aucun registre n’est affecté par cette instruction.

  • TST Rn, <Sop> : effectue un et logique entre le contenu du registre Rn et le contenu de l’opérande de décalage <Sop> (Rn & Sop) et reflètent le résultat dans les fanions. Hormis les fanions, aucun registre n’est affecté par cette instruction.

  • TEQ Rn, <Sop> : effectue un ou-exclusif logique entre le contenu du registre Rn et le contenu de l’opérande de décalage <Sop> (Rn ^ Sop) et reflètent le résultat dans les fanions. Hormis les fanions, aucun registre n’est affecté par cette instruction.

Branchements conditionnels

Les deux jeux d’instructions ARM et Thumb-2 proposent une instruction pour effectuer des branchements conditionnels.

  • B<cond> <label> : réalise un branchement vers l’étiquette <label> si la condition <cond> est satisfaite. L’instruction évalue le contenu des fanions du registre APSR selon la table présentée au chapitre “Exécution conditionnelle” (figure Branchements). Si la condition est vraie, le processeur effectue un saut relatif par rapport à la valeur actuelle de son compteur ordinaire (registre PC) pour exécuter les instructions placées à la suite de l’étiquette <label>. Le déplacement, offset entre l’adresse de l’instruction B<cond> et celle correspondante à l’étiquette, est calculé lors de la génération de l’application. La plage de cette offset dépend du jeu d’instructions utilisé, ARM ±32MiB, Thumb-2 ±16Mib et Thumb -252B à +256B.

Note

L’assembleur permet l’utilisation de label xf et xb, où x est un nombre entier. Ces labels permettent d’effectuer des branchements vers le label x le plus proche en cherchant en avant (forward) pour f ou en arrière (backward) pour b.

Cela signifie que dans un code assembleur, l’instruction b 1f recherchera le premier 1 placé après l’instruction b 1f et que l’instruction b 1b recherchera le premier “1” placé avant l’instruction b 1b.

Branchements inconditionnels

Les deux jeux d’instructions ARM et Thumb-2 proposent deux instructions pour effectuer des branchements inconditionnels.

  • B <label> : réalise un branchement vers l’étiquette <label>. Le processeur effectue un saut relatif par rapport à la valeur actuelle de son compteur ordinaire (registre PC) pour exécuter les instructions placées à la suite de l’étiquette <label>. Le déplacement, offset entre l’adresse de l’instruction B et celle correspondante à l’étiquette, est calculé lors de la génération de l’application. La plage de cette offset dépend du jeu d’instructions utilisé, ARM ±32MiB, Thumb-2 ±16Mib et Thumb ±2MiB.

  • BX Rm : réalise un branchement à l’adresse contenue dans le registre Rm. Avec cette instruction, le processeur effectue un saut absolu. Le contenu du registre Rm est simplement chargé dans le compteur ordinal (registre PC).

!!! note Pour rappel, l’adresse contenue dans le registre Rm doit être impair pour continuer l’exécution du code avec le jeu d’instructions Thumb-2.

Exemples

Le premier exemple ci-dessous présente la réalisation de l’instruction if/else en assembleur.

unsigned exemple_1 (int i) {
   unsigned j = 0;
   if (i > 0) {
      j = i % 16;
   } else if (i == 0) {
      j = 0xaa;
   } else {
      j = -i / 16;
   }
   return j;
}
   .global exemple_1
   .type exemple_1, %function
exemple_1:
    cmp     r0, #0         // comparaison de l'argument i avec 0
    bgt     3f             // if (i >  0) alors va vers 3:
    beq     2f             // if (i == 0) alors va vers 2:
                           // sinon va vers 1:
    // else
1:  rsbs    r1, r0, #0     // i = -i
    lsrs    r1, #4         // j = i / 16
    b       4f

    // if (i == 0)
2:  ldr     r1, =0xaa      // j = 0xaa;
    b       4f

    // if (i > 0)
3:  ands    r1, r0, #0xf   // j = i % 16;
    b       4f

4:  movs    r0, r1
    bx      lr
   .size exemple_1, .-exemple_1

Le deuxième exemple présente la réalisation en assembleur de l’instruction switch évaluant un nombre entier borné (entre 0 à 3). L’implémentation ci-dessous est très intéressante, car l’évaluation du cas à exécuter est totalement indépendante de la valeur de l’expression switch.

int exemple_2 (int i)
{
    int j = 0;
    switch(i) {
    case 0:  j=11; break;
    case 1:  j=8;  break;
    case 2:  j=25; break;
    case 3:  j=99; break;
    default: j=-1; break;
    }
    return j;
}
   .global exemple_2
   .type exemple_2, %function
exemple_2:
    cmp     r0, #3         // teste si i entre 0 et 3
    it      hi             // si vrai, prend cette valeur comme index
    movshi  r0, #4         // si faux, utilise la valeur 4 comme défaut
    ldr     r2, =lut       // prend l'adresse de base de la table "lut"
    lsls    r1, r0, #2     // calcule offset dans la table "lut"
    ldr     r2, [r2, r1]   // charge l'adresse case correspondant
    bx      r2

// table avec les "switch-case"
// Note: les adresses des différentes "case" sont corrigées de 1 byte 
// afin de garder le processeur en mode Thumb et éviter ainsi qu'il 
// ne change pour le jeu d'instructions ARM, lequel n'est pas 
// supporté un µP Cortex-M4
lut: .word case_0+1, case_1+1, case_2+1, case_3+1, case_d+1

case_0: 
    ldr   r1, =11
    b     2f
case_1: 
    ldr   r1, =8
    b     2f
case_2: 
    ldr   r1, =25 
    b     2f
case_3: 
    ldr   r1, =99
    b     2f
case_d: 
    ldr   r1, =-1
    b     2f

2:  movs  r0, r1
    bx    lr
   .size exemple_2, .-exemple_2

Ce troisième exemple présente une autre réalisation en assembleur de l’instruction switch évaluant un nombre entier, mais non borné.

int exemple_3 (int i)
{
   j=0;
   switch (i) {
   case -10 : j=-20; break;
   case   2 : j=-2;  break;
   case 100 : j=0;   break;
   case 801 : j=24;  break;
   default:   j=-1;  break;
   }
   return j;
}
   .global exemple_3
   .type exemple_3, %function
exemple_3:
    cmp     r0,#-10     // case i == -10
    beq     case_m10
    cmp     r0,#2       // case i == 2
    beq     case_2
    cmp     r0,#100     // case i == 100
    beq     case_100
    ldr     r1,=801
    cmp     r0,#801     // case i == 801
    beq     case_801
    b       case_d      // default

case_m10:  
    ldr   r1, =-20
    b     2f  
case_2:  
    ldr   r1, =-2
    b     2f  
case_100:  
    ldr   r1, =0
    b     2f  
case_801:  
    ldr   r1, =24
    b     2f  
case_d:  
    ldr   r1, =-1
    b     2f  

2:  movs  r0, r1
    bx    lr
   .size exemple_3, .-exemple_3

Là également, il est possible de construire un code générique et indépendant de la valeur de l’expression switch. Pour cela, il faut à nouveau utiliser une table de recherche (LUT - Lookup Table). La LUT doit contenir, pour chaque cas de l’instruction switch, la valeur à tester ainsi que l’adresse des instructions correspondantes au cas. Le code ci-dessous ne présente que la recherche du cas.

    ldr     r1,  =lut      // adresse de base la table de recherche
    ldr     r2,  =lut_end  // adresse de fin
    ldr     r12, =case_d+1 // charge l'adresse du traitement par défaut
1:  ldr     r3, [r1]       // charge la valeur à test
    cmp     r0, r3         // teste si le valeur correspond au cas 
    it      eq             // si vrai, ...
    ldreq   r12, [r1, #4]  // ... charge l'adresse de traitement du cas
    adds    r1, #8         // charge l'adresse de la prochaine entrée
    cmp     r1, r2         // teste si toute la table a été examinée
    bne     1b             // si faux, passe à la prochaine entée
    bx      r12            // si vrai, traite le cas

lut:   
    .word -10, case_m10+1
    .word   2, case_2+1
    .word 100, case_100+1
    .word 801, case_801+1
lut_end: