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.
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.
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; }
// 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
).
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
).
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
).
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
).
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
).
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 registreRd
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 registreRd
(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 registreRd
(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.
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;
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 registreRd
. -
LDRH Rd, [Rn, <ofs>]
: pour charger un mot de 16 bits non signé stocké à l’adresse(Rn + <ofs>)
dans les 16 premiers bits du registreRd
(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 registreRd
(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 registresRd
etRd+1
. Le registreRd
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 registreRd
(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 registreRd
(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 registreRd
.STRH Rd, [Rn, <ofs>]
: pour stocker les 16 bits premiers du registreRd
(Rd[15:0]
).STRB Rd, [Rn, <ofs>]
: pour stocker les 8 bits premiers du registreRd
(Rd[7:0]
).STRD Rd, [Rn, <ofs>]
: pour stocker les 64 bits dans la paire de registresRd
etRd+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 (-
).
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 registreRn
.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 registreRn
.
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 registresAPSR
,CPSR
etSPSR
pour les µP Cortex-A et aux registresAPSR
,IPSR
,EPSR
,PSR
,PRIMASK
,FAULTMASK
,BASEPRI
etCONTROL
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 registreRn
avec l’opérande de décalage<Sop>
et stocke le résultat dans le registreRd
(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 registreRn
avec l’opérande de décalage<Sop>
et la valeur du fanionC
(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 registreRn
et stocke le résultat dans le registreRd
(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 fanionC
(retenue - inverse carry) du contenu du registreRn
et stocke le résultat dans le registreRd
(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 registreRn
et stocke le résultat dans le registreRd
(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 registreRn
et le contenu du registreRm
et stocke le résultat dans le registreRd
(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 registreRn
et le contenu du registreRm
et stocke le résultat dans le registreRd
(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 registreRn
et le contenu du registreRm
et stocke le résultat dans le registreRd
(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 unet
logique entre deux nombres contenus dans le registreRn
et le contenu de l’opérande de décalage<Sop>
et stocke le résultat dans le registreRd
(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 unou
logique entre deux nombres contenus dans le registreRn
et le contenu de l’opérande de décalage<Sop>
et stocke le résultat dans le registreRd
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 unou-exclusif
logique entre deux nombres contenus dans le registreRn
et le contenu de l’opérande de décalage<Sop>
et stocke le résultat dans le registreRd
(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 registreRn
. Cette instruction effectue unet
logique entre le nombres contenu dans le registreRn
et l’inverse du contenu de l’opérande de décalage<Sop>
et stocke le résultat dans le registreRd
(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.
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 registreLR
. Avec cette instruction, le processeur effectue un saut relatif par rapport à la valeur actuelle de son compteur ordinaire (registrePC
) pour appeler la fonction spécifiée avec l’étiquette<label>
. Le déplacement, offset entre l’adresse de l’instructionBL
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 registreRm
. L’adresse de retour est stockée dans le registreLR
. Avec cette instruction, le processeur effectue un saut absolu. Le contenu du registreRm
est simplement chargé dans le compteur ordinal (registrePC
).
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.
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 registreRn
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 registreRn
(Rn - Sop
). Hormis les fanions, aucun registre n’est affecté par cette instruction. -
TST Rn, <Sop>
: effectue unet
logique entre le contenu du registreRn
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 unou-exclusif
logique entre le contenu du registreRn
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 registreAPSR
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 (registrePC
) pour exécuter les instructions placées à la suite de l’étiquette<label>
. Le déplacement, offset entre l’adresse de l’instructionB<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 (registrePC
) pour exécuter les instructions placées à la suite de l’étiquette<label>
. Le déplacement, offset entre l’adresse de l’instructionB
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 registreRm
. Avec cette instruction, le processeur effectue un saut absolu. Le contenu du registreRm
est simplement chargé dans le compteur ordinal (registrePC
).
!!! 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: