0% ont trouvé ce document utile (0 vote)
9 vues210 pages

Notes

Transféré par

nmaismael2001
Copyright
© © All Rights Reserved
Formats disponibles
Téléchargez comme PDF, TXT ou lisez en ligne sur Scribd
Télécharger au format pdf ou txt
0% ont trouvé ce document utile (0 vote)
9 vues210 pages

Notes

Transféré par

nmaismael2001
Copyright
© © All Rights Reserved
Formats disponibles
Téléchargez comme PDF, TXT ou lisez en ligne sur Scribd
Télécharger au format pdf ou txt
Télécharger au format pdf ou txt
Vous êtes sur la page 1/ 210

Programmation système

Notes de cours (IFT209)

 01001001
01000110
01010100
00110010
00110000
00111001
00000000

Michael Blondin
22 février 2024
Avant-propos

La rédaction de ce document a été entamée à la session d’hiver 2019 comme


notes complémentaires du cours IFT209 – Programmation système de l’Univer-
sité de Sherbrooke, dans le but d’offrir des notes de cours gratuites, libres et
francophones pouvant évoluer au gré des sessions et des refontes du cours.
Ces notes ne sont pas un livre: bien que je fasse un effort pour que ces notes
soient lisibles sans assister au cours, et bien que l’entièreté du contenu du cours
se trouve dans ce document, certains passages peuvent parfois être « expéditifs »
par rapport aux explications, exemples et discussions qui surgissent en classe.
Ainsi, ces notes devraient d’abord être considérées comme un complément aux
séances de cours, par ex. pour réduire, voire éliminer, la prise de notes; comme
références pour réaliser les devoirs; comme matériel de révision, etc.
La structure et le contenu sont basés sur le plan cadre du cours. Par consé-
quent, elles suivent une structure souvent similaire aux diaporamas utilisés
avant 2019 par les chargés de cours Vincent Ducharme et Mikaël Fortin, eux-
mêmes basés en bonne partie sur l’ancien manuel de référence du cours rédigé
par le professeur retraité Richard St-Denis: L’architecture du processeur SPARC et
sa programmation en langage d’assemblage [SD11]. De plus, l’idée d’utiliser les
architectures ARMv8 et du NES provient, à ma connaissance, de Mikaël Fortin.
Si vous trouvez des coquilles, ou si vous avez des suggestions, n’hésitez
pas à me les indiquer sur GitHub  (en ajoutant un « issue ») ou par courriel à
[email protected]. Je remercie notamment Félix Dion (H22).

cbn
Cette œuvre est mise à disposition selon les termes de la licence
Creative Commons Attribution-NonCommercial 4.0 International.

ii
Légende

Observation.

Les passages compris dans une région comme celle-ci correspondent à


des observations jugées intéressantes mais qui dérogent légèrement du
contenu principal.

Remarque.

Les passages compris dans une région comme celle-ci correspondent à


des remarques jugées intéressantes mais qui dérogent légèrement du
contenu principal.

Les exercices marqués par « ⋆ » sont considérés plus avancés que les autres.
Les exercices marqués par « ⋆⋆ » sont difficiles ou dépassent le cadre du cours.

Les icônes «  », «  » et «  » dans la marge fournissent respectivement un


lien vers le code source, le circuit et une capsule vidéo associé au passage.

Les icônes «   » dans la marge permettent de naviguer vers le matériel ad-


ditionnel et les solutions des exercices qui se trouvent en annexe.

iii
Table des matières

1 Systèmes de numération 1
1.1 Représentation des nombres . . . . . . . . . . . . . . . . . . . . 1
1.1.1 Système unaire . . . . . . . . . . . . . . . . . . . . . . . 1
1.1.2 Système de numération positionnelle . . . . . . . . . . . 2
1.2 Changement de base . . . . . . . . . . . . . . . . . . . . . . . . 4
1.2.1 Base b vers base 10 . . . . . . . . . . . . . . . . . . . . . 4
1.2.2 Base 10 vers base b . . . . . . . . . . . . . . . . . . . . . 5
1.2.3 Base b vers base b′ . . . . . . . . . . . . . . . . . . . . . 6
1.3 Addition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.4 Nombres fractionnaires . . . . . . . . . . . . . . . . . . . . . . . 7
1.5 Quiz récapitulatif . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.6 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9

2 Programmation en langage d’assemblage 11


2.1 Registres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.2 Un premier programme . . . . . . . . . . . . . . . . . . . . . . 12
2.2.1 Calcul de f (n) . . . . . . . . . . . . . . . . . . . . . . . 13
2.2.2 Affichage du contenu d’un registre . . . . . . . . . . . . 14
2.2.3 Lecture d’une valeur dans un registre . . . . . . . . . . . 15
2.2.4 Vers un premier programme . . . . . . . . . . . . . . . . 15
2.2.5 Calcul du temps de vol . . . . . . . . . . . . . . . . . . . 16
2.2.6 Programme complet . . . . . . . . . . . . . . . . . . . . 17
2.3 Détails pratiques . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.3.1 Normes de programmation . . . . . . . . . . . . . . . . 18
2.3.2 Segments de données . . . . . . . . . . . . . . . . . . . 20
2.3.3 Spécificateurs de format . . . . . . . . . . . . . . . . . . 21
2.4 Quiz récapitulatif . . . . . . . . . . . . . . . . . . . . . . . . . . 23
2.5 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23

3 Architecture des ordinateurs 24

iv
TABLE DES MATIÈRES v

3.1 Architecture de von Neumann . . . . . . . . . . . . . . . . . . . 24


3.1.1 Mémoire principale . . . . . . . . . . . . . . . . . . . . . 25
3.1.2 Processeur . . . . . . . . . . . . . . . . . . . . . . . . . . 30
3.1.3 Unités d’entrée/sortie . . . . . . . . . . . . . . . . . . . 33
3.1.4 Types d’architectures . . . . . . . . . . . . . . . . . . . . 33
3.2 Organisation . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
3.2.1 Pipeline . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
3.3 Quiz récapitulatif . . . . . . . . . . . . . . . . . . . . . . . . . . 35
3.4 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35

4 Nombres entiers 37
4.1 Représentation des entiers signés . . . . . . . . . . . . . . . . . 37
4.2 Addition . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
4.2.1 Report . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
4.2.2 Débordement . . . . . . . . . . . . . . . . . . . . . . . . 39
4.3 Soustraction . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
4.4 Multiplication . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
4.4.1 Multiplication non signée . . . . . . . . . . . . . . . . . 40
4.4.2 Multiplication signée . . . . . . . . . . . . . . . . . . . . 42
4.5 Division . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
4.5.1 Division non signée . . . . . . . . . . . . . . . . . . . . . 43
4.5.2 Division signée . . . . . . . . . . . . . . . . . . . . . . . 44
4.6 Particularités de l’architecture ARMv8 . . . . . . . . . . . . . . 44
4.6.1 Codes de condition . . . . . . . . . . . . . . . . . . . . . 44
4.6.2 Accès mémoire . . . . . . . . . . . . . . . . . . . . . . . 45
4.7 Quiz récapitulatif . . . . . . . . . . . . . . . . . . . . . . . . . . 47
4.8 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47

5 Accès aux données 48


5.1 Adresses . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
5.2 Adressage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
5.2.1 Immédiat . . . . . . . . . . . . . . . . . . . . . . . . . . 49
5.2.2 Direct . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
5.2.3 Par registre . . . . . . . . . . . . . . . . . . . . . . . . . 50
5.2.4 Indirect . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
5.2.5 Indirect par registre . . . . . . . . . . . . . . . . . . . . 51
5.2.6 Indirect par registre indexé . . . . . . . . . . . . . . . . 51
5.2.7 Indirect par registre pré/post-incrémenté . . . . . . . . . 52
5.2.8 Relatif . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
5.2.9 Sommaire des modes . . . . . . . . . . . . . . . . . . . . 52
5.3 Particularités de l’architecture ARMv8 . . . . . . . . . . . . . . 53
5.4 Assemblage d’un programme . . . . . . . . . . . . . . . . . . . 54
5.5 Quiz récapitulatif . . . . . . . . . . . . . . . . . . . . . . . . . . 56
5.6 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56

6 Tableaux 57
TABLE DES MATIÈRES vi

6.1 Accès aux éléments . . . . . . . . . . . . . . . . . . . . . . . . . 58


6.1.1 Cas unidimensionnel . . . . . . . . . . . . . . . . . . . . 59
6.1.2 Cas bidimensionnel . . . . . . . . . . . . . . . . . . . . . 60
6.2 Particularités de l’architecture ARMv8 . . . . . . . . . . . . . . 60
6.2.1 Allocation et initialisation . . . . . . . . . . . . . . . . . 60
6.2.2 Parcours d’un tableau . . . . . . . . . . . . . . . . . . . 61
6.3 Autre exemple: tableaux de pointeurs . . . . . . . . . . . . . . . 64
6.4 Quiz récapitulatif . . . . . . . . . . . . . . . . . . . . . . . . . . 67
6.5 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67

7 Programmation structurée 68
7.1 Structures de contrôle . . . . . . . . . . . . . . . . . . . . . . . 68
7.1.1 Séquence . . . . . . . . . . . . . . . . . . . . . . . . . . 68
7.1.2 Sélection . . . . . . . . . . . . . . . . . . . . . . . . . . 69
7.1.3 Itération . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
7.2 Sous-programmes . . . . . . . . . . . . . . . . . . . . . . . . . . 73
7.2.1 Paramètres et appel . . . . . . . . . . . . . . . . . . . . 73
7.2.2 Retour . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
7.2.3 Sauvegarde des registres . . . . . . . . . . . . . . . . . . 75
7.3 Autres particularités de l’architecture ARMv8 . . . . . . . . . . 76
7.3.1 Distance des adresses . . . . . . . . . . . . . . . . . . . 76
7.3.2 Assignation par sélection . . . . . . . . . . . . . . . . . . 76
7.4 Quiz récapitulatif . . . . . . . . . . . . . . . . . . . . . . . . . . 78
7.5 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78

8 Circuits logiques 79
8.1 Arithmétique . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
8.1.1 Addition de deux bits . . . . . . . . . . . . . . . . . . . . 79
8.1.2 Addition de deux nombres . . . . . . . . . . . . . . . . . 81
8.2 Décodage du jeu d’instructions . . . . . . . . . . . . . . . . . . 82
8.3 Mémoire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
8.4 Quiz récapitulatif . . . . . . . . . . . . . . . . . . . . . . . . . . 85
8.5 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85

9 Valeurs booléennes et chaînes de bits 86


9.1 Algèbre de Boole . . . . . . . . . . . . . . . . . . . . . . . . . . 86
9.2 Représentation des valeurs booléennes . . . . . . . . . . . . . . 87
9.3 Manipulation de bits . . . . . . . . . . . . . . . . . . . . . . . . 87
9.3.1 Opérateurs logiques . . . . . . . . . . . . . . . . . . . . 88
9.3.2 Décalages logiques . . . . . . . . . . . . . . . . . . . . . 89
9.3.3 Décalages circulaires . . . . . . . . . . . . . . . . . . . . 90
9.3.4 Décalages arithmétiques . . . . . . . . . . . . . . . . . . 91
9.4 Masquage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
9.5 Exemple: cryptographie visuelle . . . . . . . . . . . . . . . . . . 94
9.5.1 Format PBM . . . . . . . . . . . . . . . . . . . . . . . . . 95
9.5.2 Implémentation . . . . . . . . . . . . . . . . . . . . . . . 95
TABLE DES MATIÈRES vii

9.6 Quiz récapitulatif . . . . . . . . . . . . . . . . . . . . . . . . . . 97


9.7 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97

10 Chaînes de caractères 99
10.1 ASCII . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
10.2 ISO 8859-1 (Latin-1) . . . . . . . . . . . . . . . . . . . . . . . . 100
10.3 UTF-8 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
10.4 Chaînes de caractères . . . . . . . . . . . . . . . . . . . . . . . 103
10.5 Quiz récapitulatif . . . . . . . . . . . . . . . . . . . . . . . . . . 104
10.6 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104

11 Sous-programmes et mémoire 106


11.1 Pile d’exécution . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
11.1.1 Appels de sous-programmes . . . . . . . . . . . . . . . . 106
11.1.2 Disposition de la mémoire . . . . . . . . . . . . . . . . . 106
11.1.3 Fonctionnement de la pile . . . . . . . . . . . . . . . . . 108
11.1.4 Sauvegarde et restauration . . . . . . . . . . . . . . . . 108
11.2 Récursion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109
11.3 Limitations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
11.4 Quiz récapitulatif . . . . . . . . . . . . . . . . . . . . . . . . . . 112
11.5 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112

12 Nombres en virgule flottante 115


12.1 Représentation . . . . . . . . . . . . . . . . . . . . . . . . . . . 115
12.2 Précision . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 116
12.2.1 Erreur d’approximation . . . . . . . . . . . . . . . . . . 117
12.3 Arithmétique . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
12.3.1 Addition . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
12.3.2 Multiplication . . . . . . . . . . . . . . . . . . . . . . . . 119
12.4 Norme IEEE 754 . . . . . . . . . . . . . . . . . . . . . . . . . . 120
12.4.1 Codage des formats . . . . . . . . . . . . . . . . . . . . 121
12.5 Particularités de l’architecture ARMv8 . . . . . . . . . . . . . . 123
12.5.1 Registres . . . . . . . . . . . . . . . . . . . . . . . . . . 123
12.5.2 Instructions . . . . . . . . . . . . . . . . . . . . . . . . . 123
12.5.3 Exemple de programme . . . . . . . . . . . . . . . . . . 125
12.6 Quiz récapitulatif . . . . . . . . . . . . . . . . . . . . . . . . . . 127
12.7 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127

13 Introduction aux entrées/sorties: NES 129


13.1 Architecture du NES . . . . . . . . . . . . . . . . . . . . . . . . 129
13.1.1 Organisation de la mémoire . . . . . . . . . . . . . . . . 130
13.2 Registres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 132
13.3 Jeu d’instructions . . . . . . . . . . . . . . . . . . . . . . . . . . 132
13.3.1 Valeurs immédiates . . . . . . . . . . . . . . . . . . . . . 132
13.3.2 Modes d’adressage . . . . . . . . . . . . . . . . . . . . . 133
13.3.3 Accès mémoire . . . . . . . . . . . . . . . . . . . . . . . 133
TABLE DES MATIÈRES viii

13.3.4 Arithmétique . . . . . . . . . . . . . . . . . . . . . . . . 134


13.3.5 Logique . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
13.3.6 Comparaisons et branchements . . . . . . . . . . . . . . 134
13.4 Sorties graphiques . . . . . . . . . . . . . . . . . . . . . . . . . 135
13.4.1 Tuiles . . . . . . . . . . . . . . . . . . . . . . . . . . . . 135
13.4.2 Affichage de tuiles . . . . . . . . . . . . . . . . . . . . . 136
13.5 Entrées à partir des manettes . . . . . . . . . . . . . . . . . . . 136
13.6 Exemple de programme simple . . . . . . . . . . . . . . . . . . 137
13.7 Quiz récapitulatif . . . . . . . . . . . . . . . . . . . . . . . . . . 141
13.8 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 141

14 Entrées/sorties 142
14.1 Attente active . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
14.2 Interruptions . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143
14.2.1 Gestionnaires d’interruption . . . . . . . . . . . . . . . . 143
14.2.2 Traitement des interruptions . . . . . . . . . . . . . . . . 144
14.2.3 Niveaux de priorité . . . . . . . . . . . . . . . . . . . . . 146
14.2.4 Interruptions logicielles . . . . . . . . . . . . . . . . . . 147
14.3 Accès direct à la mémoire . . . . . . . . . . . . . . . . . . . . . 147
14.4 Appels système . . . . . . . . . . . . . . . . . . . . . . . . . . . 148
14.5 Quiz récapitulatif . . . . . . . . . . . . . . . . . . . . . . . . . . 151
14.6 Exercices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151

A Fiches récapitulatives 152

B Solutions des exercices 158

C Matériel additionnel 186

D Architecture ARMv8: sommaire 189

E Architecture du NES: sommaire 196

Bibliographie 199

Index 200
1
Systèmes de numération

Les nombres forment l’un des types élémentaires de données de l’ordinateur et


ils surgissent dans le développement de la quasi totalité des programmes. Afin
de mieux comprendre leur implémentation et leur utilisation, nous explorons
différentes façons élémentaires de les représenter, manipuler et convertir. Nous
nous limitons pour l’instant aux nombres entiers et fractionnaires non négatifs.
Nous couvrirons les entiers négatifs et les nombres réels aux chapitres 4 et 12
respectivement.

1.1 Représentation des nombres

1.1.1 Système unaire


L’un des plus anciens systèmes de numération, et probablement le plus simple,
est le système unaire, où chaque nombre entier n ∈ N est représenté en répétant
n fois un même symbole arbitraire σ. Nous utilisons parfois le système unaire
lorsque nous comptons, par exemple, avec les doigts, des traits, des entailles ou
bien des bâtonnets. Par exemple, si σ = |, alors:
1 s’écrit: |
5 s’écrit: |||||
n s’écrit: | | · · · |.
| {z }
n fois

Le symbole σ = 1 est aussi parfois utilisé puisqu’il préserve certaines propriétés


de la notation positionnelle que nous verrons sous peu. Ainsi, avec ce choix de
symbole:
1 s’écrit: 1
5 s’écrit: 11111
n s’écrit: · · · 1}.
|11 {z
n fois

1
CHAPITRE 1. SYSTÈMES DE NUMÉRATION 2

Dans ce système, le nombre 0 n’est représentable que par l’absence de symbole.

Remarque.

Le système unaire est en quelque sorte utilisé dans l’arithmétique de


Peano, où chaque nombre naturel est ou bien le symbole spécial 0, ou
bien le successeur d’un autre nombre:

1 := succ(0)
2 := succ(succ(0))
n := succ(succ(· · · succ(0))) .
| {z }
n fois

La plupart des opérations sont particulièrement simples à implémenter dans


le système unaire. Par exemple, l’addition correspond à la concaténation:

3 + 5 = 111 · 11111 = 11111111.

L’une des lacunes considérable du système unaire est son manque de conci-
sion: la représentation d’un nombre n ∈ N requiert n symboles. D’autres sys-
tèmes de numération plus concis ont été inventés au fil du temps. Par exemple,
la numération romaine peut être vue comme une extension du système unaire
où l’on contracte certaines répétitions de symboles par d’autres symboles. Ce
système abrège notamment IIIII par V, et VV par X. Ainsi, le nombre 16 s’écrit
avec seulement trois symboles (XVI) plutôt que seize symboles (IIIIIIIIIIIIIIII).
Cette concision a un coût; les opérations arithmétiques sont plus complexes que
dans le système unaire; par ex. pensez à un algorithme pour l’addition.

1.1.2 Système de numération positionnelle


En comparaison aux système unaire et romain, le système de numération que
vous utilisez probablement chaque jour, le système décimal, permet à la fois
d’être concis et d’implémenter efficacement les opérations arithmétiques.
Ce système fait partie de la famille plus générale des systèmes de numération
positionnelle. Dans celle-ci, une base b ∈ N≥2 est fixée et chaque nombre est
constitué de symboles, appelés chiffres, parmi {0, 1, . . . , b − 1}. La position de
chaque chiffre correspond à une puissance de b. Un nombre est une séquence
non vide de chiffres de la forme xn−1 · · · x1 x0 ∈ {0, 1, . . . , b − 1}n . La valeur
d’une telle séquence x est dénotée xb et définie par:


n−1
(xn−1 · · · x1 x0 )b := x i · bi .
i=0
CHAPITRE 1. SYSTÈMES DE NUMÉRATION 3

Exemple.

Dans le système décimal, c.-à-d. où b = 10, nous avons:

10310 = 1 · 102 + 0 · 101 + 3 · 100 = 103,


56410 = 5 · 102 + 6 · 101 + 4 · 100 = 564.

Si l’on utilise plutôt la base b = 7, nous obtenons:

1037 = 1 · 72 + 0 · 71 + 3 · 70 = 52,
5647 = 5 · 72 + 6 · 71 + 4 · 70 = 291.

Notons que l’ajout du chiffre 0 à gauche d’un nombre ne change pas sa valeur
puisque cela ajoute zéro fois une puissance de b, donc zéro. Autrement dit, nous
avons xb = (0x)b = (00x)b = (00 · · · 0x)b , peu importe les valeurs de x et b. Nous
disons que de tels zéros sont non significatifs.
Remarquons que le plus grand nombre formé de n chiffres dans le système
décimal est 10n − 1. Par exemple, pour n = 3, le plus grand nombre est 999 =
103 − 1. Cette observation se généralise à une base arbitraire:

Proposition 1. Soit b une base et soit n ∈ N≥1 . Le plus grand nombre pouvant 
être représenté en base b avec n chiffres est bn − 1.

Le système de numération positionnelle est exponentiellement plus concis que


le système unaire: tout nombre se représente avec une quantité logarithmique
de chiffres. Plus précisément:

Proposition 2. La plus courte représentation de x ∈ N≥1 en base b contient exac- 


tement ⌊logb x⌋ + 1 chiffres.

Systèmes binaire, hexadécimal et octal. Le système binaire est celui utilisé


dans essentiellement tous les ordinateurs en raison de sa simplicité; il ne pos-
sède que deux chiffres: 0 et 1. Il s’agit de l’instance du système de numération
positionnelle où b = 2. Dans ce système, les chiffres sont appelés bits. Ainsi,
une séquence de n bits peut représenter un nombre compris entre 0 et 2n − 1
inclusivement.
Le système hexadécimal est également répandu en informatique, notamment
pour représenter des séquences de bits de façon plus succincte, par ex. les
adresses en mémoire ou encore les codes de couleur. Il s’agit de l’instance du
système de numération positionnelle où b = 16. Afin d’éviter toute ambiguïté,
les chiffres 10, 11, . . . , 15 sont remplacés respectivement par A, B, . . . , F. Bien
qu’il s’agisse de lettres dans l’alphabet latin, nous les considérons comme des
chiffres dans ce contexte.
CHAPITRE 1. SYSTÈMES DE NUMÉRATION 4

Exemple.

Nous avons 8B516 = 8 · 162 + 11 · 161 + 5 · 160 = 2229.

Le système octal est aussi occasionnellement utilisé en informatique et est


supporté par certains langages de programmation. Il s’agit de l’instance du sys-
tème de numération positionnelle où b = 8.

Décimal Binaire Hexadécimal Octal Décimal Binaire Hexadécimal Octal


0 0 0 0 16 10000 10 20
1 1 1 1 17 10001 11 21
2 10 2 2 18 10010 12 22
3 11 3 3 19 10011 13 23
4 100 4 4 20 10100 14 24
5 101 5 5 21 10101 15 25
6 110 6 6 22 10110 16 26
7 111 7 7 23 10111 17 27
8 1000 8 10 24 11000 18 30
9 1001 9 11 25 11001 19 31
10 1010 A 12 26 11010 1A 32
11 1011 B 13 27 11011 1B 33
12 1100 C 14 28 11100 1C 34
13 1101 D 15 29 11101 1D 35
14 1110 E 16 30 11110 1E 36
15 1111 F 17 31 11111 1F 37

Figure 1.1 – Nombres de 0 à 31 écrits en bases 10, 2, 16 et 8.

Les 32 premiers nombres naturels décrits dans les bases 10, 2, 16 et 8 ap-
paraissent à la figure 1.1.

1.2 Changement de base


Il s’avère parfois pratique de convertir un nombre d’un système de numération
positionnelle d’une certaine base vers une autre base. Nous expliquons comment
effectuer un tel changement de façon algorithmique.

1.2.1 Base b vers base 10


Soit x = xn−1 · · · x1 x0 un nombre décrit en base b avec n chiffres. Il est possible
de convertir x en base 10 en évaluant simplement la somme:


n−1
xi · bi .
i=0

Notons que si ce calcul est implémenté de façon naïve, il requiert 1+2+. . .+n =
n(n + 1)/2 multiplications et n − 1 additions. Il est possible d’obtenir la même
CHAPITRE 1. SYSTÈMES DE NUMÉRATION 5

valeur avec n − 1 multiplications et n − 1 additions en évaluant la somme:

x0 + b · (x1 + b · (x2 + b · (. . . + b · xn−1 )).

Exemple.

Le nombre binaire 10110 donne la valeur suivante en décimal:

0 + 2 · (1 + 2 · (1 + 2 · (0 + 2 · 1))) = 0 + 2 · (1 + 2 · (1 + 2 · 2))
= 0 + 2 · (1 + 2 · 5)
= 0 + 2 · 11
= 22.

1.2.2 Base 10 vers base b


Soit x un nombre décrit en base 10. On peut convertir x vers la base b en divisant
itérativement x par b jusqu’à l’obtention de 0. Le reste de chaque division en-
tière donne un chiffre du nombre résultant (de droite à gauche). La procédure
générale de conversion est décrite à l’algorithme 1. 
Exemple.

Le nombre 22 vaut 10110 en binaire, puisque:

22 ÷ 2 = 11 reste 0,
11 ÷ 2 = 5 reste 1,
5 ÷ 2 = 2 reste 1,
2 ÷ 2 = 1 reste 0,
1 ÷ 2 = 0 reste 1.

Algorithme 1: Conversion d’un nombre décimal vers une base b.


Entrées: base b ∈ N≥2 et un nombre x décrit en base 10
Sorties: x décrit en base b
y ← [] // [ ] dénote la séquence vide
faire
y ← (x mod b) · y // · dénote la concaténation
x←x÷b
tant que x ̸= 0
retourner y
CHAPITRE 1. SYSTÈMES DE NUMÉRATION 6

1.2.3 Base b vers base b′


Une approche simple afin de convertir un nombre d’une base b vers une autre
base b′ consiste à passer de la base b vers la base 10, puis de la base 10 vers la
base b′ , en suivant les deux algorithmes décrits plus tôt. Lorsque l’une des bases
est une puissance de l’autre, il existe toutefois des approches plus simples. Cela
s’avère pratique notamment avec les systèmes binaire, hexadécimal et octal.

Base b vers base bm . Soit x un nombre décrit en base b et soit m ∈ N>1 . Afin
de convertir x en base bm , nous procédons en deux étapes:
— les chiffres de x sont regroupés en blocs de taille m, de la droite vers la
gauche, en ajoutant des 0 non significatifs tout à gauche au besoin;
— chaque bloc est remplacé par le chiffre correspondant en base bm .

Exemple.

Cherchons à convertir le nombre binaire 1010110001 en hexadécimal.


Remarquons que le système hexadécimal possède la base 16 = 24 . Ainsi,
nous avons b = 2 et m = 4, ce qui mène à:

1010110001 → 0010 1011 0001 → 2B1.

Base bm vers base b. Soit m ∈ N>1 et soit x un nombre décrit en base bm . Afin
de convertir x en base b, nous éclatons chaque chiffre de x vers sa représentation
de taille m en base b, puis nous effaçons les 0 non significatifs à gauche.

Exemple.

Le nombre hexadécimal 2B1 est converti en nombre binaire ainsi:

2B1 → 0010 1011 0001 → 1010110001.

Base bm vers base bk . Afin de convertir un nombre de la base bm vers la base


bk , où m ̸= k, on peut passer par la base intermédiaire b, plutôt que 10. En effet,
il suffit de convertir de la base bm vers la base b, puis de la base b vers la base
bk à l’aide des deux procédures décrites plus tôt.

1.3 Addition
Voyons comment additionner deux nombres décrits dans une base commune b.
Nous couvrirons la soustraction, la multiplication et la division plus tard au cha-
pitre 4. Soient x et y deux nombres de n chiffres décrits en base b. La somme de
CHAPITRE 1. SYSTÈMES DE NUMÉRATION 7

x et y s’effectue de façon analogue à l’addition en base 10; c.-à-d. itérativement,


de droite à gauche, en faisant la somme des chiffres en base b et en propageant
une retenue.

Exemple.

L’addition D40C16 + 6FA516 = 143B116 se calcule selon ces cinq étapes:


1 1 1 1 11 1 1 1 1
D4 0 C D4 0 C D4 0 C D4 0 C D4 0 C
+ + + + +
6 FA5 6 FA5 6 FA5 6 FA5 6 FA5
1 B1 3B 1 4 3B 1 1 4 3B 1

Il est possible d’additionner deux nombres dont le nombre de chiffres diffère.


En effet, il suffit simplement d’ajouter des zéros non significatifs au nombre le
plus court, ou, de façon équivalente, d’aligner les deux nombres à droite et
d’interpréter les chiffres manquants comme des zéros.
La procédure générale d’addition est décrite à l’algorithme 2. Remarquons 
que l’algorithme d’addition utilise lui-même l’addition, ce qui paraît circulaire.
En fait, comme la retenue n’excède jamais 1, nous avons 0 ≤ r + xi + yi < 2b. Il
suffit donc de connaître une quantité finie de tables d’addition pour implémen-
ter l’addition générale.

Algorithme 2: Addition de deux nombres dans une base commune.


Entrées: deux nombres x et y décrits en base b ∈ N≥2
Sorties: x + y décrit en base b
m ← |x|; n ← |y| // nombre de chiffres de x et y
z ← [ ]; r ← 0 // séq. vide et retenue nulle
pour i de 0 à max(m, n) − 1
si i ≥ m alors xi ← 0
si i ≥ n alors yi ← 0
s ← r + xi + yi
z ← (s mod b) · z
si s ≥ b alors r ← 1 // retenue créée?
sinon r←0
si r = 1 alors z ← 1 · z // ajouter la dernière retenue
retourner z

1.4 Nombres fractionnaires


Le système de numération positionnelle peut être étendu naturellement afin de
représenter les fractions. Soit b une base. Un nombre fractionnaire dans la base
CHAPITRE 1. SYSTÈMES DE NUMÉRATION 8

b est constitué de deux séquences x et y de n et k chiffres respectivement, et


se dénote « x,y ». La séquence x est appelée la partie entière et la séquence y la
partie fractionnaire. La valeur du nombre x,y dans la base b est définie par:

n−1 ∑
k
(x,y)b := xi · bi + yi · b−i .
i=0 i=1

Exemple.

Nous avons:

(110,101)2 = 1 · 22 + 1 · 21 + 0 · 20 + 1 · 2−1 + 0 · 2−2 + 1 · 2−3


1 1
=4+2+ +
2 8
5
=6+
8
= 6,625.

En base 10, cette notation correspond au système décimal habituel. Notons


que l’ajout de zéros à droite de la partie fractionnaire d’un nombre ne change
pas sa valeur. Il s’agit donc de zéros non significatifs. Remarquons également
qu’un nombre fractionnaire, pouvant être représenté dans une certaine base,
n’est pas nécessairement représentable (de façon finie) dans une autre base.
Ainsi, les méthodes de conversion décrites à la section 1.2 ne s’appliquent pas
nécessairement. L’addition se fait telle que décrite pour les entiers à la sec-
tion 1.3, en alignant les deux nombres à leur virgule.

Exemple.

Le nombre 0,1 ne se représente pas de façon finie en base 2. Remarquons


que 1/10 = 16/160 = 10/160 + 5/160 + 1/160 = 1/16 + 1/32 + 1/160.
Ainsi, 0,1 = 2−4 + 2−5 + 2−4 · 0,1. Par conséquent:

0,1 = 2−4 + 2−5 + 2−4 · 0,1


= 2−4 + 2−5 + 2−4 · (2−4 + 2−5 + 2−4 · 0,1)
= 2−4 + 2−5 + 2−8 + 2−9 + 2−8 · 0,1
= 2−4 + 2−5 + 2−8 + 2−9 + 2−8 · (2−4 + 2−5 + 2−4 · 0,1)
= 2−4 + 2−5 + 2−8 + 2−9 + 2−12 + 2−13 + 2−12 · 0,1.

En poursuivant ce processus à l’infini, on obtient donc la représentation


biniaire infinie (0,0001100110011 · · · )2 , qui s’abrège par (0,00011)2 .
Un argument plus rigoureux, mais bien trop formel pour ce cours, est
donné en solution de l’exercice 1.9).
CHAPITRE 1. SYSTÈMES DE NUMÉRATION 9

1.5 Quiz récapitulatif


Vous pouvez tester votre compréhension en complétant ce court quiz . Il est
facultatif, anonyme et n’a aucune incidence sur votre note du cours.

1.6 Exercices
1.1) Expliquez comment effectuer la soustraction et la multiplication dans le 
système unaire.

1.2) Convertissez les nombres suivants: 


(a) 68301 de la base 9 vers la base 10,
(b) 103678 de la base 10 vers la base 16,
(c) 26654 de la base 7 vers la base 14,
(d) 111011010 de la base 2 vers la base 16,
(e) 865723 de la base 9 vers la base 3,
(f) ABCDEF de la base 16 vers la base 8.
⋆ OVERNINETHOUSAND de la base 32 vers la base 16

1.3) Effectuez ces additions dans la base indiquée: 


1101102
(a) +
101101012
7A2D5616
(b) +
B99C16

1.4) Quel est la plus petite quantité de bits nécessaire afin de représenter le 
nombre 2FEDCB16 en binaire?

1.5) Quel est le plus grand multiple de 5 pouvant être représenté par un nombre 
de 9 chiffres en base 4?

1.6) Quel est le plus grand nombre inférieur à 1 pouvant être représenté par 
un nombre fractionnaire binaire possédant jusqu’à 8 bits avant la virgule
et 6 bits après la virgule?

1.7) Dites s’il existe une base b dans laquelle le nombre 111b est pair. 
1.8) ⋆ Expliquez pourquoi il est impossible d’obtenir une retenue excédant 1 
lors de l’addition de deux nombres dans une base commune.

1.9) ⋆ Démontrez par l’absurde que 0,1 ne peut pas être représenté de façon 
finie en base 2.
CHAPITRE 1. SYSTÈMES DE NUMÉRATION 10

1.10) ⋆⋆ Le système ternaire équilibré, jadis exploité par l’ordinateur Setun, 


comporte trois chiffres {-, 0, +} qui représentent les valeurs {−1, 0, 1}.
Ce système permet de représenter des nombres positifs et négatifs. Par
exemple, +0-+ représente le nombre 1 · 33 + 0 · 32 + −1 · 31 + 1 · 30 = 25.
(a) Quel nombre est représenté par +-0-----?
(b) Représentez 255 dans le système ternaire équilibré.
(c) Représentez 5, 8, −5 et −8 dans le système ternaire équilibré. Quel
lien observez-vous entre 5 et −5, et 8 et −8? Déduisez-en une façon
générale de calculer −a à partir de la représentation de a. Expliquez
pourquoi votre procédure fonctionne.
(d) Expliquez comment additionner dans le système ternaire équilibré.
Calculez 5 + 8 avec votre procédure.
(e) Expliquez comment soustraire dans le système ternaire équilibré.
(f) Quels nombres peuvent être représentés avec n chiffres dans le sys-
tème ternaire équilibré?

1.11) L’outil chmod sur les systèmes de type UNIX permet d’accorder des droits 
en accès d’un fichier aux classes « user », « group » et « other ». Le format
est comme suit, où r, w et x correspondent aux droits de lecture (« read »),
d’écriture (« write »), et d’exécution (« execute »):

user group other


r w x r w x r w x

Par exemple, rwxr----- accorde tous les droits à « user », uniquement


le droit de lecture à « group », et aucun droit à « other ». Cette valeur se
représente en binaire par 1111000002 ou en octal par 7108 . L’outil chmod
supporte (sur certains systèmes d’exploitation) les valeurs octales. Ainsi,
« chmod 710 » accorde les droits décrits précédemment.
a) Donnez la valeur octale qui accorde tous les droits à toutes les classes.
b) Donnez la valeur octale qui accorde le droit de lecture à toutes les
classes; d’écriture à « user » et « group »; et d’exécution à « user ».
c) Quels sont les droits accordés par la valeur octale 6528 ?
2
Programmation en langage d’assemblage: ARMv8

Nous introduisons la programmmation en langage d’assemblage, donc la pro-


grammation de (très) bas niveau à l’aide du jeu d’instructions d’un processeur.
Nous utiliserons ARMv8-A, que nous dénoterons simplement ARMv8. Il s’agit
d’une architecture 64 bits de type RISC annoncée en 2011, parue en 2013, et
développée par la société britannique ARM. L’architecture ARMv8 est rétrocom-
patible avec l’architecture 32 bits ARMv7. Ces deux architectures se retrouvent
dans la plupart des téléphones intelligents, des tablettes numériques et des or-
dinateurs récents d’Apple, ainsi que dans plusieurs systèmes embarqués. Nous
faisons un premier suvol du jeu d’instruction à l’aide d’un programme simple
que nous écrirons progressivement.

2.1 Registres
L’architecture ARMv8 possède ces 32 registres de 64 bits [ARM15, Sect. 9.1.1]:

registres nom utilisation

x0 – x7 — registres d’arguments et de retour de sous-programmes


x8 xr registre pour retourner l’adresse d’une structure
x9 – x15 — registres temporaires sauvegardés par l’appelant
x16 – x17 ip0 – ip1 registres temporaires intra-procéduraux
x18 pr registre temporaire pouvant être réservé par le système
x19 – x28 — registres temporaires sauvegardés par l’appelé
x29 fp pointeur vers l’ancien sommet de pile (frame pointer)
x30 lr registre d’adresse de retour (link register)
xzr sp registre contenant la valeur 0, ou le
pointeur de pile (stack pointer)

11
CHAPITRE 2. PROGRAMMATION EN LANGAGE D’ASSEMBLAGE 12

Nous utiliserons principalement les registres du tableau ci-dessus qui ap-


paraissent sur des lignes non colorées, donc: x0 – x7 et x19 – x28 , et dans une
moindre mesure x9 – x15 . Nous n’utiliserons jamais les registres x16 – x18 .
Pour chaque indice k ∈ {0, 1, . . . , 30, zr}, il existe une version 32 bits du
registre xk nommée wk . Le sous-registre wk correspond aux 32 bits de poids
faible de xk . Autrement dit, si xk = b63 · · · b1 b0 , alors wk = b31 · · · b1 b0 .

2.2 Un premier programme


Afin d’explorer le jeu d’instructions de l’architecture ARMv8, ainsi que le fonc-
tionnement d’un programme en langage d’assemblage, nous allons écrire un
court programme basé sur un problème algorithmique et mathématique simple.

Algorithme 3: Calcul de la séquence de Collatz.


Entrées: n ∈ N
Sorties: —
tant que n ̸= 1
si n est pair alors
n←n÷2
sinon
n ← 3n + 1

Considérons l’algorithme 3. Il reçoit un nombre n ∈ N en entrée et applique


la fonction f : N → N suivante à répétition sur n jusqu’à l’atteinte de 1:
{
n ÷ 2 si n est pair,
f (n) :=
3n + 1 sinon.

La conjecture de Collatz affirme que cet algorithme se termine sur toute en-
trée; autrement dit, que la séquence sn définie par f (n), f (f (n)), f (f (f (n))), . . .
atteint éventuellement 1 peu importe la valeur n de départ. Par exemple, la sé-
quence s3 atteint 1 en sept étapes:
s3 = 10, 5, 16, 8, 4, 2, 1, . . . ,
et la séquence s14 atteint 1 en dix-sept étapes:
s14 = 7, 22, 11, 34, 17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2, 1, . . . .
Le nombre d’étapes afin d’atteindre 1 à partir de n est son temps de vol, que
nous dénotons tn . Par exemple, t3 = 7 et t14 = 17. Écrivons un programme qui
lit une entrée n ∈ N et affiche son temps de vol tn . Autrement dit, nous cher-
chons à calculer le nombre d’itérations de la boucle tant que de l’algorithme 3.
Plutôt que d’écrire le programme complet directement, nous présenterons
d’abord des segments de code qui accomplissent des sous-tâches, puis nous uni-
fierons et raffinerons ces segments de code afin d’obtenir un programme.
CHAPITRE 2. PROGRAMMATION EN LANGAGE D’ASSEMBLAGE 13

Remarque.

À ce jour, on ne sait pas si l’algorithme 3 termine toujours! En particulier,


il n’existe donc pas d’analyseur statique de code qui pourrait détecter s’il
boucle parfois à l’infini. Le célèbre mathématicien Paul Erdős a affirmé
que les mathématiques ne sont pas encore prêtes pour de tels problèmes!

2.2.1 Calcul de f (n)


Voyons d’abord comment calculer f (n) à partir d’un nombre n ∈ N. Nous devons
d’abord choisir des registres avec lesquels travailler. Les registres x19 – x28 sont
préférables car la convention nous permet de les utiliser librement. Supposons
que x19 contienne initialement n. Ce segment de code calcule f (n) et stocke sa
valeur dans le registre x19 :
tbnz x19, 0, impair // si x19[0] ≠ 0: aller à impair
pair:
mov x20, 2 // x20 ← 2
udiv x19, x19, x20 // x19 ← x19 ÷ x20
b fin // aller à fin
impair:
mov x20, 3 // x20 ← 3
mul x20, x20, x19 // x20 ← x20 * x19
add x19, x20, 1 // x19 ← x20 + 1
fin:

Décortiquons le code ci-dessus. Afin de calculer f , nous devons d’abord tester


si n est pair. Nous pourrions tester si n mod 2 = 0, mais le jeu d’instruction
d’ARMv8 n’offre pas d’instruction pour le calcul du modulo. Puisque n est stocké
en binaire, nous pouvons tester si n est pair grâce à l’observation suivante: n
est pair si et seulement si son bit de poids faible est égal à zéro.
L’instruction « tbnz xd, i, etiq » vérifie si le ième bit du registre xd diffère
de zéro. Si c’est le cas, elle effectue un saut vers l’étiquette « etiq: », sinon elle
ne fait rien. Ainsi, la première ligne de code teste si n est impair; si c’est le
cas, elle branche à l’étiquette « impair: »; sinon le programme passe à la ligne
suivante, donc à l’étiquette « pair: ».
Si n est pair, alors le programme calcule n ÷ 2 à l’aide de ces instructions:

mov xd, v assigne la valeur v au registre xd


udiv xd, xn, xm stocke le quotient de xn divisé par xm dans xd

L’instruction « b fin » effectue un saut vers la toute dernière ligne afin d’indi-
quer que le calcul de f (n) est complété. Les sauts sont aussi connus sous le nom
de branchement, d’où la lettre « b ».
Si n est impair, le programme calcule 3n + 1 à l’aide de ces instructions:
CHAPITRE 2. PROGRAMMATION EN LANGAGE D’ASSEMBLAGE 14

mov xd, v assigne la valeur v au registre xd


mul xd, xn, xm stocke le produit de xn et xm dans xd
add xd, xn, v stocke la somme de xn et la valeur v dans xd

2.2.2 Affichage du contenu d’un registre


Maintenant que nous savons calculer f (n), voyons comment afficher sa valeur,
c’est-à-dire le contenu du registre x19 . L’affichage se fait à l’aide du code suivant:

adr x0, msgRes // x0 ← adresse(msgRes)


mov x1, x19 // x1 ← x19
bl printf // printf(x0, x1)

.section ".rodata"
msgRes: .asciz "Résultat: %lu"

Comme l’affichage nécessiterait notamment d’exploiter les services du noyau


d’un système d’exploitation, nous utilisons plutôt les primitives d’entrée/sortie
du langage C. Ainsi, l’affichage se fait grâce à la fonction « printf » de la librairie
« stdio ». Le premier paramètre de cette fonction est l’adresse de la mémoire
principale où se situe la chaîne de caractères à afficher. Nous stockons donc une
chaîne de caractères msgRes sous forme de « constante ».
Les constantes sont définies dans le segment de données en lecture seule iden-
tifié par « .section ".rodata" ». Il est possible d’y déclarer une constante, de
type .t portant le nom etiq et contenant la valeur x, à l’aide de:
etiq: .t x

L’identifiant « .asciz » indique que nous désirons allouer une chaîne de ca-
ractères 1 . La chaîne de caractères "Résultat: %lu" contient le spécificateur de
format "%lu" qui spécifie un entier non négatif de 64 bits. Le deuxième argu-
ment de printf est donc, dans ce cas, un entier non négatif à afficher.
Comme brièvement mentionné à la section 2.1, les paramètres d’un sous-
programme sont passés via les registres x0 –x7 . Ainsi, le code suivant stocke
l’adresse de la chaîne msgRes et la valeur du registre x19 dans les registres x0 et
x1 respectivement, et appelle la fonction printf grâce à l’instruction bl:

adr x0, msgRes


mov x1, x19
bl printf

1. Plus précisément: une chaîne de caractères se terminant par le caractère nul. Nous discute-
rons de ce détail technique au chapitre 10.
CHAPITRE 2. PROGRAMMATION EN LANGAGE D’ASSEMBLAGE 15

2.2.3 Lecture d’une valeur dans un registre


Nous savons maintenant calculer et afficher f (n). Toutefois, nous n’avons pour
l’instant aucune façon de spécifier la valeur de n. Voyons comment une valeur
peut être lue et assignée au registre x19 . La lecture se fait grâce au code suivant:

adr x0, fmtEntree // x0 ← adresse(fmtEntree)


adr x1, temp // x1 ← adresse(temp)
bl scanf // scanf(x0, x1)

ldr x19, temp // x19 ← mem[temp]

.section ".bss"
.align 8
temp: .skip 8

.section ".rodata"
fmtEntree: .asciz "%lu"

Comme pour l’affichage, nous effectuons la lecture grâce à la librarie stdio


du langage C. Plus précisément, nous utilisons sa fonction scanf. Le premier pa-
ramètre de cette fonction est l’adresse à laquelle la valeur lue doit être stockée.
Le second paramètre est l’adresse du format de la valeur à lire.
Nous allouons un double mot nommé « temp: » dans le segment de données
non-initialisées identifié par « .section ".bss" ». Cette « variable » possède 8
octets (64 bits):
.section ".bss"
.align 8
temp: .skip 8

Ainsi, l’exécution de scanf lira un entier non négatif de 64 bits et stockera


sa valeur dans une « variable » temporaire. Afin de transférer son contenu vers
x19 , donc de la mémoire principale vers le processeur, nous utilisons ce code:

ldr x19, temp

Cette instruction charge le contenu stocké à l’adresse vers x19 :

ldr xd, etiq charge, dans xd , la valeur stockée à l’adresse de l’éti-


quette etiq: de la mémoire principale

2.2.4 Vers un premier programme


Jusqu’ici, nous avons vu comment lire un entier non négatif n, calculer f (n) et
afficher sa valeur. Le code qui accomplit ces trois tâches peut être réuni ainsi:
CHAPITRE 2. PROGRAMMATION EN LANGAGE D’ASSEMBLAGE 16

// Lecture de n
adr x0, fmtEntree // x0 ← adresse(fmtEntree)
adr x1, temp // x1 ← adresse(temp)
bl scanf // scanf(x0, x1)

ldr x19, temp // x19 ← mem[temp]

// Calcul de f(n)
tbnz x19, 0, impair // si x19[0] ≠ 0: aller à impair
pair:
mov x20, 2 // x20 ← 2
udiv x19, x19, x20 // x19 ← x19 ÷ x20
b fin // aller à fin
impair:
mov x20, 3 // x20 ← 3
mul x20, x20, x19 // x20 ← x20 * x19
add x19, x20, 1 // x19 ← x20 + 1
fin:
// Affichage de f(n)
adr x0, msgRes // x0 ← adresse(msgRes)
mov x1, x19 // x1 ← x19
bl printf // printf(x0, x1)

.section ".bss"
.align 8
temp: .skip 8

.section ".rodata"
fmtEntree: .asciz "%lu"
msgRes: .asciz "Résultat: %lu"

Nous sommes près d’avoir un programme complet. Néanmoins, nous n’avons


toujours pas accompli la tâche initiale qui était de calculer le temps de vol tn .

2.2.5 Calcul du temps de vol


Afin de calculer le temps de vol tn , nous générons la séquence sn itérativement,
et nous comptons le nombre d’itérations qui mènent à la valeur 1:
mov x21, 0 // x21 ← 0
boucle:
cmp x19, 1 //
b.eq finboucle // si n = 1: aller à finboucle
add x21, x21, 1 // sinon: incrémenter x21
CHAPITRE 2. PROGRAMMATION EN LANGAGE D’ASSEMBLAGE 17

/* calculer f(n) dans x19 ici */

b boucle // aller à boucle


finboucle:

Décortiquons ce code. La première instruction initialise un compteur à zéro


qui sera incrémenté afin de calculer tn . Rappelons que x19 contient n. L’instruc-
tion « cmp x19, 1 » compare le contenu de x19 et la constante 1. L’instruction
« b.eq finboucle » branche à l’étiquette « finboucle: » si la comparaison pré-
cédente résulte en une égalité. Autrement dit, si x19 = 1, alors nous avons ter-
miné de calculer tn . Si x19 ̸= 1, alors le compteur x21 est incrémenté, x19 est
remplacé par f (x19 ), et le programme branche vers le début de la boucle.

2.2.6 Programme complet


Nous pouvons finalement réunir tous nos segments de code. Afin d’exécuter
notre programme, nous devons lui donner un point d’entrée, ici « main: », et
quitter avec l’instruction « bl exit ». Ici, « exit » réfère à une fonction de la
librairie standard du langage C. Afin de quitter sans s’appuyer sur cette librairie,
il faudrait utiliser les services du noyau d’un système d’exploitation.
Plutôt que d’afficher x19 comme nous l’avions fait auparavant, nous devons
maintenant afficher x21 qui contient tn . Nous obtenons ce programme complet: 
.global main

main:
// Lecture de n
adr x0, fmtEntree // x0 ← adresse(fmtEntree)
adr x1, temp // x1 ← adresse(temp)
bl scanf // scanf(x0, x1)

ldr x19, temp // x19 ← mem[temp]

// Calcul du temps de vol


mov x21, 0 // x21 ← 0
boucle:
cmp x19, 1 //
b.eq finboucle // si n = 1: aller à finboucle
add x21, x21, 1 // sinon: incrémenter x21

// Calcul de f(x19)
tbnz x19, 0, impair // si x19[0] ≠ 0: aller à impair
pair:
mov x20, 2 // x20 ← 2
udiv x19, x19, x20 // x19 ← x19 ÷ x20
CHAPITRE 2. PROGRAMMATION EN LANGAGE D’ASSEMBLAGE 18

b fin // aller à fin


impair:
mov x20, 3 // x20 ← 3
mul x20, x20, x19 // x20 ← x20 * x19
add x19, x20, 1 // x19 ← x20 + 1
fin:
b boucle // aller à boucle
finboucle:

// Affichage du temps de vol


adr x0, msgRes // x0 ← adresse(msgRes)
mov x1, x21 // x1 ← x21
bl printf // printf(x0, x1)

mov x0, 0
bl exit // Quitter le programme

.section ".bss"
.align 8
temp: .skip 8

.section ".rodata"
fmtEntree: .asciz "%lu"
msgRes: .asciz "Résultat: %lu"

Remarque.

Une adaptation du programme ci-dessus, pour l’architecture x86-64, est


disponible sur le répertoire GitHub  du cours.

2.3 Détails pratiques

2.3.1 Normes de programmation


Organisation d’une ligne de code. Chaque ligne d’un programme en langage
d’assemblage est constituée d’au plus « 4 colonnes »:
etiquette: opcode opérandes // Commentaire

L’étiquette donne un nom symbolique à une ligne du programme, le code


d’opération (« opcode ») est le nom symbolique de l’instruction, les opérandes
sont les paramètres de l’instruction, et le commentaire donne des annotations sur
le code destinées à l’humain (elles sont ignorées par la machine). Pour faciliter
CHAPITRE 2. PROGRAMMATION EN LANGAGE D’ASSEMBLAGE 19

la lecture, il est préférable d’aligner les colonnes et de placer chaque étiquette


seule sur sa ligne; comme dans cet extrait de code de la section 2.2:
impair:
mov x20, 2 // x20 ← 2
udiv x19, x19, x20 // x19 ← x19 ÷ x20
b fin // aller à fin

Présentation. Comme les registres ne peuvent pas être renommés, il est pra-
tique d’associer implicitement des noms symboliques aux registres et de com-
menter chaque instruction par un commentaire décrivant l’effet de l’instruction,
en pseudocode ou dans un langage de plus haut niveau tel que C. Par exemple:

// Usage des registres:


// x19 -- n
// x20 -- tmp
tbnz x19, 0, impair // si n pas pair: aller à impair
pair:
mov x20, 2 // tmp ← 2
udiv x19, x19, x20 // n ← n ÷ tmp
b fin // aller à fin
impair:
mov x20, 3 // tmp ← 3
mul x20, x20, x19 // tmp ← tmp * n
add x19, x20, 1 // n ← tmp + 1
fin:

ou encore:

// Usage des registres:


// x19 -- n
// x20 -- tmp
tbnz x19, 0, impair // if (n % 2 == 0) goto impair
pair:
mov x20, 2 // tmp = 2
udiv x19, x19, x20 // n /= tmp
b fin // goto fin
impair:
mov x20, 3 // tmp = 3
mul x20, x20, x19 // tmp *= n
add x19, x20, 1 // n = tmp + 1
fin:

Comme dans les langages de haut niveau, il est recommandé de séparer


les blocs de code qui effectuent des tâches distinctes par un saut de ligne afin
CHAPITRE 2. PROGRAMMATION EN LANGAGE D’ASSEMBLAGE 20

« d’aérer » le code et d’en faciliter sa lecture. Il est également préférable d’utiliser


des noms d’étiquettes significatifs et courts, autant que possible.

Commentaires. Jusqu’ici, nous avons commenté chaque ligne en correspon-


dance une-à-une avec ce qu’elle accomplit précisément. Plus nous deviendrons
à l’aise avec le jeu d’instructions, plus il sera préférable de faire surgir les struc-
tures de contrôle de haut niveau afin de comprendre ce que le code effectue.
Par exemple, les commentaires ci-dessous évitent l’usage de « goto » au pro-
fit de « if { } else { } » et ignorent le détail technique de l’affectation des
constantes 2 et 3:
// Usage des registres:
// x19 -- n
// x20 -- tmp
tbnz x19, 0, impair // if (n % 2 == 0) {
pair: //
mov x20, 2 //
udiv x19, x19, x20 // n /= 2
b fin // }
impair: // else {
mov x20, 3 //
mul x20, x20, x19 // tmp = 3 * n
add x19, x20, 1 // n = tmp + 1
fin: // }

2.3.2 Segments de données


Il existe quatre types de segments de données pouvant être déclarés à l’aide des
directives suivantes:

directive contenu
.section ".text" instructions
.section ".rodata" données en lecture seule
.section ".data" données initialisées
.section ".bss" données non-initialisées

Si aucun segment n’est spécifié, alors ".text" est supposé par défaut.
Des données statiques et globales peuvent être déclarées et/ou initialisées
dans les segments autres que ".text" grâce aux mots-clés suivants:

.align k la donnée suivante est stockée à une adresse divisible par k


.skip k réserve k octets
.ascii s chaîne de caractères initialisée à s
.asciz s chaîne de caractères initialisée à s suivi du caractère nul
CHAPITRE 2. PROGRAMMATION EN LANGAGE D’ASSEMBLAGE 21

.byte v octet initialisé à v


.hword v demi-mot initialisé à v
.word v mot initialisé à v
.xword v double mot initialisé à v
.single f nombre en virgule flottante simple précision initialisé à f
.double f nombre en virgule flottante double précision initialisé à f

2.3.3 Spécificateurs de format


Les (principaux) formats de données des fonctions printf et scanf de la librai- 
rie stdio du langage C sont définis comme suit 2 :

famille format type

%d entier décimal
%u entier décimal non négatif
Nombres sur 32 bits
%X entier hexadécimal non négatif
%f nombre en virgule flottante

%ld entier décimal


%lu entier décimal non négatif
Nombres sur 64 bits
%lX entier hexadécimal non négatif
%lf nombre en virgule flottante

%c caractère (1 octet)
Caractères
%s chaîne de caractères

2. En général, le nombre de bits de chacun des formats dépend de l’architecture sur laquelle le
code est exécuté. Nous faisons ici référence à ARMv8.
CHAPITRE 2. PROGRAMMATION EN LANGAGE D’ASSEMBLAGE 22

— xkcd c
CHAPITRE 2. PROGRAMMATION EN LANGAGE D’ASSEMBLAGE 23

2.4 Quiz récapitulatif


Vous pouvez tester votre compréhension en complétant ce court quiz . Il est
facultatif, anonyme et n’a aucune incidence sur votre note du cours.

2.5 Exercices

2.1) Écrivez un programme ARMv8 qui lit quatre entiers non négatifs x, y, x′ , y ′ 
de 64 bits, et qui affiche l’aire et le périmètre du rectangle dont deux coins
opposés sont situés aux coordonnées (x, y) et (x′ , y ′ ) dans le plan.
(tiré de [SD11])

2.2) Écrivez un programme ARMv8 qui lit deux entiers non négatifs m et n de 
64 bits, l’un à la suite de l’autre, et qui affiche toutes les paires (i, j) telles
que 0 ≤ i < m et 0 ≤ j < n.

2.3) Écrivez un programme ARMv8 qui lit un entier non négatif n de 64 bits 
et qui affiche le nème terme de la suite de Fibonacci.

2.4) ⋆⋆ Tentez d’adapter vos programmes pour l’architecture x86-64 en vous


inspirant de ce programme  et/ou en fouillant sur le Web.
3
Architecture des ordinateurs

Dans ce chapitre, nous faisons un survol du fonctionnement de l’ordinateur mo-


derne. Celui-ci dépend de deux aspects: son architecture et son organisation.
L’architecture réfère aux services fournis par les composants de l’ordinateur,
comme le processeur et la mémoire principale:
— jeu d’instructions;
— types de données et leur représentation;
— modes d’adressage et accès aux données;
— mécanismes d’entrée/sortie.
L’organisation réfère quant à elle à la description physique des composants et
de leurs connexions:
— organisation interne du processeur;
— séquencement des instructions;
— gestions des conflits de ressources;
— interface entre processeur, mémoire et périphériques;
— organisation hiérarchique de la mémoire.
Autrement dit, l’architecture est la spécification de l’ordinateur, alors que l’or-
ganisation est une implémentation de cette spécification. Il peut exister plusieurs
implémentations d’une même architecture. Par exemple, les fabricants Intel et
AMD développent tous deux des processeurs x86-64. Nous mettrons l’emphase
sur l’architecture des ordinateurs.

3.1 Architecture de von Neumann


Le premier ordinateur d’usage général, l’ENIAC (Electronic Numerical Integrator
and Calculator), a été conçu vers la fin des années 1940 par J. Presper Eckert
et John Mauchly de l’Université de Pennsylvanie [PH17]. Cet ordinateur était
utilisé pour calculer des tables de tir d’artillerie. L’ENIAC était programmable à

24
CHAPITRE 3. ARCHITECTURE DES ORDINATEURS 25

Processeur

Unité arithmétique
Unité de contrôle et logique

Bus

Mémoire principale Unités d’entrée/sortie

Figure 3.1 – Architecture de von Neumann. (Figure reproduite à partir de [SD11, Fig. 2.1])

l’aide de câbles et d’interrupteurs, alors que les données étaient entrées à l’aide
de cartes perforées.
En 1944, Eckert et Mauchly cherchent déjà à simplifier l’entrée fastidieuse
des programmes dans l’ENIAC. John von Neumann, qui se joint entre temps au
groupe, écrit un mémo en 1945 sur une proposition d’ordinateur à programme
enregistré, où programmes et données sont stockés dans la même mémoire. Un
tel ordinateur, l’EDVAC (Electronic Discrete Variable Automatic Computer), sera
construit quelques anneés plus tard [PH17].
Le modèle général de l’EDVAC, connu sous le nom d’architecture de von Neu-
mann, est celui de la plupart des ordinateurs modernes. Dans cette architecture,
un ordinateur est composé:
— d’un processeur constitué de registres, d’une unité de contrôle, et d’une
unité arithmétique et logique;
— d’une mémoire principale qui stocke données et programmes;
— d’unités d’entrée/sortie.
Ces différents composants sont connectés par des systèmes de communica-
tion appelés bus. Le bus interne relie le processeur et la mémoire principale, alors
que les bus externes relient l’ordinateur aux unités d’entrée/sortie. Ces compo-
sants sont illustrés à la figure 3.1.

3.1.1 Mémoire principale


La mémoire principale, qui correspond à la mémoire vive en pratique, stocke les
programmes et leurs données. Elle peut être vue abstraitement comme une sé-
quence c0 , c1 , . . . , cn−1 de n cellules. Chacune de ces cellules contient une don-
née provenant d’un ensemble D. L’index i de chaque cellule ci est son adresse
qui permet de l’identifier uniquement. Dans la plupart des architectures mo-
dernes, les celulles contiennent 8 bits; autrement dit, D = {0, 1}8 . Une telle
CHAPITRE 3. ARCHITECTURE DES ORDINATEURS 26

séquence de 8 bits se nomme un octet. Les bits d’un octet n’ont à priori aucune
signification, leur interpétation est faite par un langage de programmation ou
par la personne qui écrit un programme. Par exemple, l’octet 01000001 peut
autant représenter le nombre 65 que le caractère A. Lors du chargement d’un
programme, son code est stocké dans les cellules de la mémoire principale aux
côtés de ses données (variables, constantes, etc.) On pourrait donc en théorie
imaginer un programme qui manipule son propre code, par ex. un virus. La
figure 3.2 illustre un contenu possible d’une mémoire principale.

0 00000000
1 01011000
2 01000000
3 00001111
4 00011000
5 01010101
6 11110000
7 00001111
.. .
.
. .
n−2 11111111
n−1 01100001

Figure 3.2 – Exemple de contenu d’une mémoire principale. Chaque celulle est
représentée par une case rectangulaire dont l’adresse apparaît à sa gauche.

Remarque.

Plusieurs outils, comme les débogueurs, affichent les adresses sous leur
valeur hexadécimale plutôt que décimale, souvent avec le préfixe « 0x ».
Par exemple, l’adresse 0x0000555548ee affichée par un tel outil corres-
pond à l’adresse 0000555548EE16 = 1431652590.

Granularité de l’accès mémoire. Bien qu’une adresse réfère typiquement à


un octet, les architectures modernes permettent aussi d’interpréter une adresse
comme faisant référence à plusieurs octets, souvent 2, 4 ou 8 octets. Par exemple,
sous l’architecture ARMv8, ces unités se nomment:

unité nombre de bits nombre d’octets

octet 8 bits 1 octet


demi-mot 16 bits 2 octets
mot 32 bits 4 octets
double mot 64 bits 8 octets
CHAPITRE 3. ARCHITECTURE DES ORDINATEURS 27

Cette terminologie et le nombre d’octets adressables diffèrent d’une archi-


tecture à l’autre. À moins d’avis contraire, nous utiliserons la terminologie du
tableau ci-dessus. Le contenu de l’octet, du demi-mot, du mot ou du double mot
situé à l’adresse i couvre respectivement les adresses suivantes:

unité adresses

octet i
demi-mot i, i + 1
mot i, i + 1, i + 2, i + 3
double mot i, i + 1, i + 2, i + 3, . . . , i + 7

Exemple.

Considérons la mémoire principale illustrée à la figure 3.2. Le contenu


de l’octet, du demi-mot, du mot et du double-mot situé à l’adresse 0 est
respectivement:

00000000,
00000000 01011000,
00000000 01011000 01000000 00001111,
00000000 01011000 01000000 00001111 00011000 01010101 11110000 00001111,

ou, de façon équivalente, en hexadécimal:


00,
00 58,
00 58 40 0F,
00 58 40 0F 18 55 F0 0F.

Ordre des octets. L’interpétation des valeurs situées en mémoire dépend de


l’ordre dans lequel les octets sont organisés sur l’architecture en question. Consi-
dérons une séquence d’octets x0 x1 · · · xn−1 obtenue à partir d’une adresse. Dans
le format dit « big-endian », aussi appelé gros-boutiste, les octets sont organisés
de gauche à droite, c.-à-d. de x0 vers xn−1 . À l’inverse, dans le format dit « little-
endian », aussi appelé petit-boutiste, les octets sont organisés de droite à gauche,
c.-à-d. de xn−1 vers x0 .

Exemple.

Considérons le mot x situé à l’adresse 0 de la mémoire principale illustrée


à la figure 3.2. Nous avons x = 0058400F. Ainsi, la valeur hexadécimale
du mot x dans le format « big-endian » est 0058400F16 , alors que dans
CHAPITRE 3. ARCHITECTURE DES ORDINATEURS 28

le format « little-endian » il s’agit plutôt de 0F40580016 .


Observons que la valeur se renverse au niveau des octets et non des
chiffres, ainsi la valeur dans le format « little-endian » n’est pas F004850016 .

L’architecture x86-64 utilise le format « little-endian », alors que ARMv8 sup-


porte les deux formats. Nous utiliserons le format « little-endian » lors de la
programmation sur l’architecture ARMv8. La plupart du temps, le format utilisé
n’est pas perceptible. Il peut néanmoins être observé, par exemple, lorsqu’un
programme lit une valeur octet par octet. 
Exemple.

Le code C suivant a produit une sortie différente selon le format de l’ar-


chitecture de la machine sur laquelle il est exécuté:
#include <stdint.h>
#include <stdio.h>

union Mot
{
uint32_t valeur;
uint8_t octets[4];
};

int main()
{
union Mot mot;

scanf("%X", &mot.valeur); // Sur entrée A1B2C3D4,


printf("%02X", mot.octets[0]); // affiche:
printf("%02X", mot.octets[1]); // A1B2C3D4 si big-endian
printf("%02X", mot.octets[2]); // D4C3B2A1 si little-endian
printf("%02X", mot.octets[3]);
}

a. Exemple basé sur un exemple d’un diaporama de Vincent Ducharme.

Alignement en mémoire. Certaines architectures imposent des contraintes


d’alignement en mémoire. Ces contraintes limitent les adresses auxquelles il est
possible d’adresser plus d’un octet: il est seulement possible d’adresser 2k octets
aux adresses qui sont des multiples de 2k . Par exemple, sous ces contraintes, on
peut seulement adresser un mot aux adresses: {0, 4, 8, 12, . . .}.
Remarquons qu’une adresse i est un multiple de 2k si et seulement si les k
bits de poids faible de sa représentation binaire sont égaux à zéro. Ainsi, une
adresse sous notation hexadécimale respecte la contrainte d’alignement pour
CHAPITRE 3. ARCHITECTURE DES ORDINATEURS 29

un double mot si elle se termine par 0 ou 8; pour un mot si elle se termine par
0, 4, 8 ou C; et pour un demi-mot si elle se termine par 0, 2, 4, 6, 8, A, C ou E.

Exemple.

L’adresse 0A5C16 = 1010010111002 satisfait les contraintes d’alignement


pour l’adressage d’un mot ou d’un demi-mot, mais pas d’un double mot.

Sur certaines architectures sans contrainte d’alignement, l’adressage de 2k


octets à une adresse non alignée ralentit l’accès mémoire. Par exemple, consi-
dérons l’adressage du mot situé à l’adresse 3 tel qu’illustré à la figure 3.3. La
valeur du mot est obtenue en deux accès: d’abord à l’adresse 0, puis à l’adresse
4. Les octets qui apparaissent dans la zone hachurée sont ensuite assemblés.

..
.

Figure 3.3 – Adressage d’un mot situé à l’adresse 3 (zone hachurée).

Remarque. 
L’ordre dans lequelles les données d’une structure sont déclarées en C/C++
a une incidence sur le nombres d’octets utilisés en raison des contraintes
d’alignement.

Taille de la mémoire. Les unités de mesure de la mémoire portent ces noms:


1 kilo-octet (Ko) = 10001 octets, 1 kibioctet (Kio) = 10241 octets,
1 mégaoctet (Mo) = 10002 octets, 1 mébioctet (Mio) = 10242 octets,
1 gigaoctet (Go) = 10003 octets, 1 gibioctet (Gio) = 10243 octets,
1 teraoctet (To) = 10004 octets, 1 tébioctet (Tio) = 10244 octets,
1 pétaoctet (Po) = 10005 octets, 1 pébioctet (Pio) = 10245 octets,
1 exaoctet (Eo) = 10006 octets, 1 exbioctet (Eio) = 10246 octets.
CHAPITRE 3. ARCHITECTURE DES ORDINATEURS 30

Remarque.

Dans le langage courant, on utilise souvent {kilo, méga, giga, tera, péta,
exa}octets pour référer aux puissances de 1024, bien qu’elles réfèrent
techniquement aux puissances de 1000.

Notons qu’une architecture, dont les adresses sont représentées sur k bits,
peut accéder jusqu’à 2k cellules de mémoire. Ainsi, une architecture 32 bits peut
accéder à un maximum de:

232 octets = 4 · 230 octets = 4 · (210 )3 octets = 4 · 10243 octets = 4 gibioctets.

En comparaison, une architecture 64 bits peut théoriquement accéder à plus


d’un milliard de fois plus d’octets qu’une architecture 32 bits:

264 octets = 16·260 octets = 16·(210 )6 octets = 16·10246 octets = 16 exbioctets.

3.1.2 Processeur
Le processeur est l’unité centrale de traitement de l’ordinateur, il s’agit donc en
quelque sorte du « cerveau » de l’ordinateur. Chaque processeur est associé à un
jeu d’instructions: un ensemble fini d’instructions machines formant les opéra-
tions élémentaires pouvant être exécutées par l’ordinateur. L’unité de contrôle
du processeur coordonne l’exécution des instructions machines. Son unité ari-
thmétique et logique performe les opérations arithmétique et logique nécessaires
à l’exécution des instructions. Le processeur peut communiquer avec la mémoire
principale via certaines instructions, et possède également une petite quantité
de mémoire interne connue sous le nom de registres.

Registres. Le processeur possède ses propres cellules de mémoire: les registres.


Ceux-ci servent à stocker les opérandes et le résultat des instructions machines.
L’accès aux registres ne dépend pas du bus interne et est donc beaucoup plus
rapide que l’accès à la mémoire principale. Toutefois, le processeur compte très
peu de registres; par ex. 4 registres sur l’architecture NMOS 6502 et 32 registres
sur l’architecture ARMv8. Le nombre de bits pouvant être stockés dans un re-
gistre varie d’une architecture à l’autre; par exemple, 8 bits sur l’architecture
NMOS 6502 et jusqu’à 64 bits sur les architectures ARMv8, RISC-V et x86-64.

Jeu d’instructions. Le jeu d’instructions d’une architecture décrit les instruc-


tions élémentaires de l’ordinateur. Chaque instruction possède cette forme:
Code d’opération opérande 1, opérande 2, …, opérande k

Le nombre d’opérandes varie d’une instruction à l’autre, et dans certain cas il


peut simplement n’y avoir aucun opérande.
CHAPITRE 3. ARCHITECTURE DES ORDINATEURS 31

Exemple.

L’instruction « add x, y, z » des architectures ARMv8 et RISC-V addi-


tionne le contenu des registres y et z, et socke leur somme dans le registre
x. Elle possède donc trois opérandes.
L’instruction nop n’effectue rien et ne possède aucun opérande.

Un jeu d’instructions possède normalement plusieurs instructions de diffé-


rents types: arithmétique, logique, contrôle, accès mémoire, etc. Nous verrons
plusieurs telles instructions aux chapitres subséquents.
Chaque instruction se traduit en code machine: une suite de bits interpré-
table par le processeur. Selon l’architecture, le nombre de bits requis afin de
représenter une instruction en code machine peut varier selon l’instruction.

Exemple.

Imaginons une architecture (fictive) qui possède huit registres, nommés


x0 , …, x7 , et ces quatre instructions:

foo a, b, c
bar a, b
baz a, b, c
qux

L’architecture pourrait représenter le code d’opération des instruc-


tions par 00, 01, 10 et 11 respectivement; et représenter chaque opé-
rande par les trois bits qui correspondent au numéro du registre. Ainsi,
« baz x2, x5, x7 » se traduirait vers « 10 010 101 111 » en code ma-
chine; alors que « qux » se traduirait simplement vers « 11 ».
Alternativement, l’architecture pourrait utiliser un code machine à
taille fixe, par ex. en utilisant toujours 2+3·3 = 11 bits et en ajoutant des
zéros au besoin. Ainsi, « baz x2, x5, x7 » se traduirait encore vers « 01
010 101 111 »; alors que « qux » se traduirait maintenant vers « 11 000
000 000 ». Sous ce codage à taille fixe, plusieurs codes seraient invalides,
par ex. « 11 111 111 11 ».

Sur les architecture ARMv8 et RISC-V, toutes les instructions sont de taille
fixe. Chaque instruction est traduite vers une suite de 32 bits dans un format de
bits bien précis.

Exemple.

Sur l’architecture ARMv8, « add x10, x11, x12 » se traduit vers le code
machine B0C016A16 , ou plus précisément en binaire:
CHAPITRE 3. ARCHITECTURE DES ORDINATEURS 32

1 0 0 01011 00 0 01100
| {z } 000000 01011
| {z } 01010
| {z }.
x12 x11 x10

Quinze bits indiquent les opérandes de l’instructions; le tout premier bit


indique si l’addition doit se faire en arithmétique 64 bits ou non; la sé-
quence « 0 0 01011 » est tout simplement fixée, et les autres bits repré-
sentent des arguments facultatifs que nous verrons plus tard [ARM18,
ADD (shifted register)].

Unité de contrôle. L’unité de contrôle coordonne le fonctionnement du pro-


cesseur. Considérons le programme suivant stocké dans la mémoire principale:
i add x10, x11, x12
i+4 add x10, x10, x13

Le processeur possède un registre spécial nommé compteur d’instruction; aussi


appelé « program counter » (pc) en anglais. Ce registre pointe initialement en
mémoire à la première ligne du programme, ici l’adresse i. L’unité de contrôle
exécute la ligne indiquée par le compteur d’instruction, puis incrémente ce
compteur. Lors de l’exécution de l’instruction « add x10, x11, x12 », l’unité
de contrôle indique à l’unité arithmétique et logique d’additionner les registres
x11 et x12 , puis s’occupe de stocker le résultat dans x10 . L’unité de contrôle in-
crémente ensuite le compteur d’instruction à i + 4, demande à l’unité arithmé-
tique et logique d’effectuer l’addition de x10 et x13 , et stocke la somme dans x10 .
Après l’exécution de ces deux instructions, le registre x10 contient la somme du
contenu des registres x11 , x12 et x13 .
En général, l’unité arithmétique et logique n’est pas le seul composant avec
lequel l’unité de contrôle interagit. Par exemple, l’unité de contrôle doit interagir
avec la mémoire principale lors d’une instruction de lecture ou d’écriture en
mémoire. De plus, comme nous le verrons à la section 3.2.1, l’unité de contrôle
fonctionne souvent en plusieurs étapes qui peuvent être parallélisées.

Remarque.

Le compteur d’instruction n’est pas toujours accessible. Par exemple, il


l’est sous le nom pc sur l’architecture RISC-V, mais ne l’est pas sur l’ar-
chitecture ARMv8.

Unité arithmétique et logique. L’unité arithmétique et logique (UAL) est le


composant du processeur en charge d’effectuer les calculs sur les:
— nombres entiers: addition, soustraction, multiplication, division entière et
comparaison;
CHAPITRE 3. ARCHITECTURE DES ORDINATEURS 33

— séquences de bits: ET logique, OU logique, OU exclusif, négation, décalages,


décalages circulaires, inversion, etc.
Le processeur peut également posséder d’autres unités de calcul comme une
unité de calcul en virgule flottante dédiée à l’arithmétique en virgule flottante.

3.1.3 Unités d’entrée/sortie


Les unités d’entrée/sortie contrôlent les périphériques comme le disque dur, le
moniteur, la souris, le clavier, les lecteurs, la webcam, etc. L’échange de données
entre le processeur et une unité d’entrée/sortie se fait grâce à un protocole de
communication via un bus externe. Cette communication est particulièrement
lente en comparaison à l’accès aux registres et à la mémoire principale. Nous
traiterons des entrées/sorties au chapitre 13.

3.1.4 Types d’architectures


Il existe deux grandes familles d’architectures modernes: RISC et CISC. Il n’existe
pas de définition claire de celles-ci, mais en général les architectures RISC sont
caractérisées par un jeu d’instructions constitué:
— de peu d’instructions;
— d’instructions relativement simples;
— d’instructions qui ne combinent pas les accès mémoire à d’autres types d’opé-
rations (comme l’arithmétique);
— d’instructions dont la traduction vers le code machine est de taille fixe.

Exemple.

Les architectures ARMv8 et RISC-V sont de type RISC, l’architecture x86-


64 est de type CISC, et on peut difficilement classifier NMOS 6502.

3.2 Organisation

3.2.1 Pipeline
L’exécution d’une instruction par le processeur se fait souvent en plusieurs étapes.
Par exemple, le processeur:
1. récupère l’instruction à partir de la mémoire principale;
2. décode le code d’opération et les opérandes de l’instruction;
3. charge les opérandes nécessaires à partir des registres ou de la mémoire;
4. exécute l’instruction;
5. stocke le résultat de l’instruction.
CHAPITRE 3. ARCHITECTURE DES ORDINATEURS 34

1 2 3 4 5 6 7 8 9

Récupération Décodage Chargement Exécution Stockage Récupération Décodage Chargement Exécution ···

Figure 3.4 – Exécution séquentielle d’instructions en cinq étapes.

Ce type d’exécution est illustré à la figure 3.4.


Afin d’augmenter la vitesse d’exécution de plusieurs instructions, les proces-
seurs utilisent souvent un pipeline tel qu’illustré à la figure 3.5. Cela permet de
paralléliser l’exécution des différentes étapes d’exécution, à la manière d’une
chaîne de production.

1 2 3 4 5 6 7 8 9

Récupération Décodage Chargement Exécution Stockage

Récupération Décodage Chargement Exécution Stockage

Récupération Décodage Chargement Exécution Stockage

Récupération Décodage Chargement Exécution Stockage

Récupération Décodage Chargement Exécution Stockage

..

Figure 3.5 – Pipeline à cinq étages. L’exécution de n instructions prend n + 4


cycles, plutôt que les 5n cycles d’une exécution purement séquentielle.

L’utilisation d’un pipeline peut créer plusieurs problèmes qui doivent être
résolus par le processeur. Par exemple, si une instruction x écrit une valeur en
mémoire à l’adresse i, et qu’une instruction subséquente y lit la valeur à la même
adresse i, alors la valeur lue par l’instruction y risque d’être erronée puisqu’elle
n’aura pas encore été mise à jour par x. Dans ce cas, le processeur peut, par
exemple, décider de retarder l’exécution de y, ce qui mitige les avantages du
pipeline.
Les instructions qui effectuent des sauts peuvent aussi causer des problèmes.
Par exemple, supposons que la première instruction x de la figure 3.5 effectue
un saut au 4ème cycle vers une instruction z située plus loin en mémoire. Au
5ème cycle, la seconde instruction y est exécutée. Cela ne devrait pas être le cas;
c’est plutôt l’instruction z qui devrait être exécutée après x. Le processeur doit
donc corriger la situation. Pour pallier à ce problème, les processeurs utilisent
différentes heuristiques de prédiction de branchement afin de « deviner » si un
saut sera effectué ou non.
CHAPITRE 3. ARCHITECTURE DES ORDINATEURS 35

3.3 Quiz récapitulatif


Vous pouvez tester votre compréhension en complétant ce court quiz . Il est
facultatif, anonyme et n’a aucune incidence sur votre note du cours.

3.4 Exercices
3.1) Considérons le contenu suivant de la mémoire principale: 
adresse contenu
0 0116
1 A416
2 BC16
3 4816
4 5F16
5 1116
6 FF16
7 4316
.. .
.
. .

Quelle est la valeur binaire du mot stocké à l’adresse 2 selon le format


« little-endian » et « big-endian »? Le mot stocké à l’adresse 2 est-il à une
adresse alignée? Répondez aux mêmes questions pour l’adresse 4.

3.2) À combien de tébioctets peut accéder une architecture 64 bits? À combien 


d’exbioctets pourrait accéder une architecture 128 bits?

3.3) Reconsidérons l’architecture fictive introduite en exemple à la section 3.1.2 


avec le jeu d’instructions:

foo a, b, c
bar a, b
baz a, b, c
qux

Traduisez ces instructions en code machine (d’abord sous le codage à taille


variable, puis à taille fixe):
— foo x6, x1, x3
— bar x5, x4
— baz x0, x7, x0
Traduisez le code machine « 010111011100000010111 » (sous codage à
taille variable) vers le programme qu’il représente.
CHAPITRE 3. ARCHITECTURE DES ORDINATEURS 36

3.4) Imaginons une architecture qui offre une instruction « addm x y a » qui 
additionne le contenu du registre y et la valeur de l’octet situé à l’adresse a
de la mémoire principale, et stocke le résultat dans le registre x. Pourrions-
nous qualifier cette architecture de RISC?
4
Nombres entiers

4.1 Représentation des entiers signés


Jusqu’ici, nous avons considéré l’arithmétique des nombres entiers non négatifs.
Nous considérons maintenant les entiers signés, c’est-à-dire non négatifs et néga-
tifs. Nous devons d’abord établir une représentation de ces nombres. Une façon
simple de représenter un entier signé a consiste à représenter sa valeur absolue
|a|, qui est toujours non négative, ainsi qu’un bit de signe, c’est-à-dire un bit
égal à 1 si et seulement si a < 0. Bien que cette représentation soit simple, elle
complique l’implémentation des opérations arithmétiques. Par exemple, pour
l’addition, il faut considérer les quatre valeurs possibles des deux bits de signe.
Une solution plus élégante consiste à utiliser le complément à deux. Dans
cette représentation, la valeur d’une suite de bits xn−1 · · · x1 x0 est définie par


n−2
−xn−1 · 2n−1 + xi · 2 i .
i=0

Exemple.

Pour n = 3 bits, les valeurs possibles sont:

bits valeur bits valeur


000 0 100 -4
001 1 101 -3
010 2 110 -2
011 3 111 -1

En général, la représentation par complément à deux sur n bits permet de


représenter les entiers de −2n−1 à 2n−1 − 1.

37
CHAPITRE 4. NOMBRES ENTIERS 38

Bien que cette représentation puisse paraître complexe à première vue, elle
jouit de plusieurs propriétés intéressantes. Premièrement, un nombre est né-
gatif si et seulement si son bit tout à gauche vaut 1. Deuxièmement, l’addition
s’effectue exactement de la même façon que l’addition d’entiers non signés.

Exemple.

La somme 2 + (−3) = −1 s’obtient ainsi:


010
+
101
111

Troisièmement, étant donné un entier a représenté par la suite de bits xn−1


· · · x1 x0 , il est possible de calculer −a en deux étapes simples:
— inverser tous les bits de a: (¬xn−1 ) · · · (¬x1 )(¬x0 ),
— additionner 1 au nombre obtenu à l’étape précédente.

Exemple.

Considérons a = 2 représenté par la suite 010. Nous obtenons la repré-


sentation de −2 ainsi:
complément +1
010 −−−−−−−→ 101 −−−−−−−→ 110.

Il est important de noter que le concept de bits non significatifs n’est pas
le même que pour les entiers non signés. Par exemple, considérons le nombre
−2 représenté par 110. L’ajout d’un zéro à gauche mène à la suite 0110, qui ne
représente pas la valeur −2. En effet, les nombres pouvant être représentés sur
quatre bits sont:

bits valeur bits valeur


0000 0 1000 -8
0001 1 1001 -7
0010 2 1010 -6
0011 3 1011 -5
0100 4 1100 -4
0101 5 1101 -3
0110 6 1110 -2
0111 7 1111 -1

Ainsi, la suite 0110 représente 6.


Afin d’étendre correctement le nombre de bits d’un nombre, il faut copier
son bit de signe (tout à gauche).
CHAPITRE 4. NOMBRES ENTIERS 39

Exemple.

La suite 110 qui représente −2 sur trois bits peut être étendue à 1110
sur quatre bits. Similairement, la suite 011 qui représente 3 sur trois bits
peut être étendue à 0011 sur quatre bits.

4.2 Addition
Comme annoncé, l’addition d’entiers signés s’effectue exactement de la même
façon que l’addition d’entiers non signés. Ainsi, l’unité arithmétique et logique
peut utiliser les mêmes circuits logiques (que nous couvrirons au chapitre 8).

4.2.1 Report
Lorsqu’une addition, en arithmétique signée ou non, engendre une retenue lors
de la somme des bits les plus à gauche, nous disons qu’il y a report. La détection
d’un report permet notamment d’identifier une erreur ou d’étendre l’addition
sur n bits à l’addition sur 2n bits.

Exemple.

Imaginons une architecture dont l’instruction d’addition opère sur des


registres de 4 bits, et que nous désirons effectuer la somme des nombres
−105 et 61, chacun stocké sur deux registres: 1001 0111 et 0011 1101.
Afin de réaliser cette addition, nous effectuons d’abord la somme des
registres de droite:
0111
+
1101
(report) 0100

Puisqu’il y a report, nous additionnons une retenue aux deux registres


de gauche:
0001
+
1001
0011
1101
En assemblant les deux sommes, nous obtenons 1101 0100 qui représente
bien −44 = −105 + 61.

4.2.2 Débordement
Lors de l’addition de deux nombres de n bits, nous disons qu’il y a débordement
si la somme ne peut pas être représentée sur n bits. Dans le cas de l’arithmé-
CHAPITRE 4. NOMBRES ENTIERS 40

tique non signée, il y a débordement précisément lorsqu’il y a report. Cela n’est


toutefois pas le cas en arithmétique signée.

Exemple.

Considérons l’addition des nombres 5 et 6 sur quatre bits. Nous avons:


0101 (5)
+
0110 (6)
1011 (-5)

Cette addition n’a pas de report, mais son résultat est erronné. En effet,
la suite 1011 représente −5 plutôt que la somme attendue de 11. Ce pro-
blème surgit lorsque la somme de deux nombres positifs (resp. négatifs)
excède la plus grande (resp. plus petite) valeur signée représentable.

Remarquons qu’il est impossible d’obtenir un débordement si deux nombres


de signes opposés sont additionnés, bien qu’il soit possible d’obtenir un report.

4.3 Soustraction
La soustraction a − b de deux entiers peut être effectuée en calculant le com-
plément à deux de b, puis en effectuant une addition standard.

Exemple.

Nous avons 3 − 5 = 3 + (−5) = 0011 + 1011:


0011 (3)
+
1011 (-5)
1110 (-2)

Un débordement peut se produire lorsque les termes de la soustraction sont


de signes différents, mais pas lorsqu’ils sont de même signe.

4.4 Multiplication

4.4.1 Multiplication non signée


Il est possible d’implémenter la multiplication de deux nombres a, b ∈ N à l’aide
de l’addition en exploitant l’identité:
a · b = b + b + ... + b.
| {z }
a−1 additions

Cette méthode a l’avantage d’être simple, mais elle est lente lorsque a est grand.
Il est possible de faire mieux en effectuant des multiplications par des puissances
de 2, et en exploitant le fait que cela correspond à des décalages de bits.
CHAPITRE 4. NOMBRES ENTIERS 41

Exemple.

Le produit 13 · 11 peut se calculer avec deux additions plutôt que douze:

13 · 11 = 13 · (23 + 21 + 20 )
= 13 · 23 + 13 · 21 + 20
= 11012 · 23 + 11012 · 21 + 11012 · 20
= 11010002 + 110102 + 11012
= 100011112
= 143.

Cette méthode correspond précisément à la méthode de multiplication


usuelle utilisée dans le système décimal:
1101 (13)
×
1011 (11)
1101
1101
+
0000
1101
10001111 (143)

Cet algorithme de multiplication peut être implémenté efficacement tel que


décrit à l’algorithme 4.

Algorithme 4: Algorithme pour multiplier deux entiers non signés.


Entrées: deux entiers non signés a et b de n bits
Sorties: un entier non signé de 2n bits égal à a · b
⟨hi, lo⟩ ← ⟨0, b⟩ // paire de deux nombres de n bits
répéter n fois
r←0
si lo0 = 1 alors r, hi ← hi + a // r = 1 s'il y a report
⟨hi, lo⟩ ← ⟨r hin−1 · · · hi2 hi1 , hi0 lon−1 · · · lo2 lo1 ⟩
retourner ⟨hi, lo⟩

Exemple.

Exécutons l’algorithme 4 sur les nombres a = 13 et b = 11 représentés


sur quatre bits, c.-à-d. 1101 et 1011. On obtient cette trace:
CHAPITRE 4. NOMBRES ENTIERS 42

itération hi lo
0 0000 1011
1 0110 1101
2 1001 1110
3 0100 1111
4 1000 1111

Notons que la multiplication de deux nombres non signés de n bits nécessite


au plus 2n bits. Il est donc toujours possible de multiplier, par exemple, deux
nombres de 32 bits et de stocker le résultat sur 64 bits. Toutefois, le résultat
peut nécessiter 64 bits, comme le démontre cette proposition plus générale:

Proposition 3. Soit n ∈ N≥2 et soit a le plus grand entier non signé de n bits. La 
représentation binaire de a · a requiert 2n bits.

4.4.2 Multiplication signée


La procédure de multiplication précédente s’applique seulement aux entiers non
signés. Une méthode simple afin de multiplier deux entiers signés a et b de n
bits consiste à
— étendre a et b à 2n bits avec les bons bits de signe;
— effectuer la multiplication standard (non signée) des deux nombres;
— garder les derniers 2n bits.

Exemple.

Nous pouvons calculer 5 · −7 = −35 ainsi:


00000101 (5)
×
11111001 (-7)
00000101
00000000
+
00000000
00000101
00000101
00000101
00000101
00000101
000010011011101 (-35)

ou bien ainsi:
CHAPITRE 4. NOMBRES ENTIERS 43

11111001 (-7)
×
00000101 (5)
11111001
00000000
+
11111001
10011011101 (-35)

Cet algorithme peut être implémenté de manière à ce que les bits addition- 
nels ne soient pas ajoutés explicitement, et ainsi que la multiplication se fasse
directement sur 2n bits.

4.5 Division

4.5.1 Division non signée


La division de deux entiers non signés de n bits se calcule comme en base 10.

Exemple.

La division entière 100102 ÷ 112 = 19 ÷ 3 = 6 reste 1 = 1102 reste 12 se


calcule ainsi:
10011 11
− 11 00110
111
− 11
1

Cette procédure s’implémente efficacement tel que décrit à l’algorithme 5.

Algorithme 5: Algorithme pour diviser deux entiers non signés.


Entrées: deux entiers non signés a et b de n bits
Sorties: deux entiers non signés q et r de n bits tels que a ÷ b = q et
a mod b = r
⟨q, r⟩ ← ⟨a, 0⟩ // paire de deux nombres de n bits
répéter n fois
⟨q, r⟩ ← ⟨qn−2 · · · q1 q0 0, rn−2 · · · r1 r0 qn−1 ⟩
si r ≥ b alors
r ←r−b
q0 ← 1
retourner ⟨q, r⟩
CHAPITRE 4. NOMBRES ENTIERS 44

Exemple.

Exécutons l’algorithme 5 sur les nombres a = 19 et b = 3 représentés sur


cinq bits, c.-à-d. 10011 et 00011. On obtient cette trace:

itération q r
0 10011 00000
1 00110 00001
2 01100 00010
3 11001 00001
4 10011 00000
5 00110 00001

4.5.2 Division signée


La division signée a ÷ b peut être effectuée en calculant |a| ÷ |b|, puis en ajustant
le signe du quotient:
— si a et b sont de même signe, alors le quotient est non négatif;
— si a et b sont de signe différent, alors le quotient est négatif.
Notons toutefois que la définition de division d’entiers signés varie selon l’archi-
tecture et le langage de programmation.

Exemple.

Nous avons −19 ÷ 3 = −6 en C++ et −19 ÷ 3 = −7 en Python. En langage


d’assemblage de l’architecture ARMv8, le résultat est −19 ÷ 3 = −6.

Remarquons également que la division du plus petit entier signé par −1 ne


donne pas le résultat attendu. En effet, −2n−1 ÷ −1 = 2n−1 , et ce dernier n’est
pas représentable sur n bits.

4.6 Particularités de l’architecture ARMv8

4.6.1 Codes de condition


L’architecture ARMv8 possède quatre codes de condition 1 :

N (négatif), Z (zéro), C (report) et V (débordement).

Certaines instructions mettent les codes de condition à jour ainsi:


— N est vrai ssi le résultat est négatif;
1. Les lettres C et V proviennent de l’anglais « carry » et « overflow ».
CHAPITRE 4. NOMBRES ENTIERS 45

— Z est vrai ssi le résultat vaut 0;


— C est vrai ssi il y a report;
— V est vrai ssi il y a débordement.
Lors de la comparaison de deux registres avec « cmp xd, xm », la soustrac-
tion xd − xm est effectuée, les codes de condition sont mis à jour selon la dif-
férence, et celle-ci est jetée. Les instructions adds, subs et negs mettent éga-
lement les codes de condition à jour, et se comportent respectivement comme
add, sub et neg.
Le code C peut être manipulé avec l’instruction « adc xd, xn, xm ». Celle-
ci stocke dans xd la somme de xn , xm , ainsi que 1 si C est vrai. L’instruction
« sbc » fonctionne similairement pour la soustraction. Remarquons que dans le
cas d’une addition, C indique la présence d’un report, alors que dans le cas d’une
soustraction, C indique l’absence d’emprunt.
Les codes de condition permettent également de faire des branchements
conditionnels à l’aide de l’instruction « b.condition etiq »:

Entiers non signés


Condition Signification Codes de condition
eq = Z
ne ̸ = ¬Z
hs ≥ C
hi > C ∧ ¬Z
ls ≤ ¬C ∨ Z
lo < ¬C

Entiers signés
Condition Signification Codes de condition
eq = Z
ne ̸ = ¬Z
ge ≥ N=V
gt > ¬Z ∧ (N = V)
le ≤ Z ∨ (N ̸= V)
lt < N ̸= V
vs débordement V
vc pas de débordement ¬V
mi négatif N
pl non négatif ¬N

4.6.2 Accès mémoire


Lors du chargement d’un entier signé — stocké dans un octet, un demi-mot ou
un mot — vers un registre, il est possible de prendre le bit de signe en compte
grâce aux instructions suivantes:
CHAPITRE 4. NOMBRES ENTIERS 46

nombre d’octets instruction


1 ldrsb xd, a
2 ldrsh xd, a
4 ldrsw xd, a

Par exemple, ldrsw charge un mot signé y dans les 32 bits de poids faible du
registre, et remplit les 32 bits de poids fort avec le bit de signe de y.
Remarquons que si « ldrsh xd, etiquette », par exemple, ne compile pas
en raison des détails spécifiques du code machine, il est possible d’utiliser:
adr xm, etiquette
ldrsh xd, [xm]

Remarque.

Des exemples de programmes C/C++ sont disponibles sur GitHub .


CHAPITRE 4. NOMBRES ENTIERS 47

4.7 Quiz récapitulatif


Vous pouvez tester votre compréhension en complétant ce court quiz . Il est
facultatif, anonyme et n’a aucune incidence sur votre note du cours.

4.8 Exercices

4.1) Écrivez les entiers signés 43, −43, 1, −1, 127, −128 sur 8 bits. 
4.2) Donnez le résultat des opérations signées suivantes sur 8 bits: 43 + 25, 
43 − 25 127 + 127, −128 − 128 et 127 − 128.

4.3) Calculez le produit a · b des entiers signés a = 6 et b = 7 de 4 bits. 


4.4) Donnez un exemple d’addition signée, sur 4 bits, où il y a débordement 
sans report, puis un exemple du scénario inverse.

4.5) Soient a = 1101011 et b = 100100 des entiers signés de 6 et 7 bits. 


— Effectuez la soustraction a − b.
— Donnez la valeur en base 10 du résultat de la soustraction précé-
dente.
— Supposons que le registre x19 contienne a (étendu sur 64 bits), et que
le registre x20 contienne b (étendu sur 64 bits). Donnez la valeur des
codes de condition N (négatif), Z (zéro) et V (débordement) après
l’exécution de l’instruction « cmp x19, x20 ».
— Dites si a > b.

4.6) ⋆⋆ Montrez que l’extension d’un entier signé par son bit de signe pré- 
serve bel et bien sa valeur.

4.7) ⋆⋆ Montrez que le complément à deux inverse bel et bien le signe d’un 
nombre.
5
Accès aux données

Dans ce chapitre, nous explorons les façons d’accéder et de référer aux données
de la mémoire principale ainsi que des registres du processeur. En particulier,
nous traitons d’adressage et des étapes de vie d’un programme.

5.1 Adresses
Nous avons indirectement vu deux façons de spécifier une adresse de la mémoire
principale. Une adresse numérique est une adresse spécifiée par un entier non
négatif. Par exemple, l’adresse 26 est numérique et réfère à la cellule c26 de la
mémoire principale. Les adresses numériques sont souvent écrites en notation
hexadécimale, par ex. 0000001A16 ou 0x0000001A plutôt que 26.

Remarque.

Une adresse de 64 bits s’écrit concisément avec 16 chiffres hexadécimaux.

Une adresse symbolique est un identifiant unique, typiquement une chaîne


de caractères, qui spécifie une cellule mémoire ca dont nous ne connaissons
(à priori) pas l’index a. En langage d’assemblage, les adresses symboliques se
manifestent sous forme d’étiquettes.
À titre d’exemple, considérons ce segment de code ARMv8 qui décrémente
itérativement le registre x19 jusqu’à ce qu’il atteigne zéro:
main: // faire {
sub x19, x19, 1 // x19 ← x19 - 1
cbnz x19, main // } tant que x19 ≠ 0

Rappelons que lors de l’exécution d’un programme, celui-ci vit dans la mé-
moire principale. Ainsi, lors du saut vers l’étiquette main:, le compteur d’instruc-
tion doit être déplacé vers l’adresse a de la mémoire qui contient l’instruction

48
CHAPITRE 5. ACCÈS AUX DONNÉES 49

sous cette étiquette. Rappelons que le code machine d’une instruction est gé-
néralement stocké sur plusieurs octets, par ex. 4 octets dans le cas d’ARMv8.
Ainsi, il s’agit ici de l’adresse a pointant vers la première celulle ca qui contient
une portion du code machine de l’instruction « sub x19, x19, 1 »:

.. .
.
. .
a
a+1
a+2 sub x19, x19, 1
a+3
a+4
a+5
a+6 cbnz x19, a
a+7
.. .
.
. .

Par conséquent, l’instruction « cbnz x19, main » correspond essentiellement


à l’instruction « cbnz x19, a ». Lors de l’écriture du code, nous ne connaissons
pas la valeur de a; elle peut être déterminée plus tard pour nous.

Remarque.

À l’interne, sur ARMv8, cet adressage se fera de façon relative et non de


façon absolue. Plutôt que de stocker a explicitement, on stocke −1 afin
d’indiquer que l’adresse a se situe à une distance de −1 · 4 adresses.

5.2 Adressage
Comme nous l’avons vu aux chapitres précédents, la plupart des instructions
d’un langage d’assemblage possèdent des opérandes. Il existe plusieurs façons
d’interpréter les opérandes afin de localiser la valeur qui leur est associée. Nous
appelons ces méthodes de localisation des modes d’adressage. Dans cette section,
nous décrivons quelques modes d’adressage répandus, notamment sur ARMv8.
Nous utiliserons i afin de dénoter une valeur immédiate, c’est-à-dire une
constante figée dans le code; n afin de référer au nom d’un registre; et a pour
dénoter une adresse de la mémoire principale. Nous écrivons reg[n] pour référer
au contenu du registre n, et nous écrivons mem[a] pour référer au contenu de
la mémoire principale à l’adresse a (sans se soucier pour l’instant du nombre
d’octets adressés).

5.2.1 Immédiat
Le mode d’adressage immédiat est le plus simple. Il associe simplement à l’opé-
rande i la valeur i elle-même:
i 7→ i.
CHAPITRE 5. ACCÈS AUX DONNÉES 50

Exemple.

Dans l’instruction suivante, la valeur immédiate i = 42 est tout simple-


ment interprétée comme la valeur 42:

mov x19, 42

5.2.2 Direct
Le mode d’adressage direct récupère, à partir d’une adresse a, la valeur contenue
à l’adresse a de la mémoire principale:

a 7→ mem[a].

Exemple.

Si a = FF16 , alors l’adressage direct récupère la valeur mem[FF16 ].

5.2.3 Par registre


Le mode d’adressage par registre, aussi appelé adressage direct par registre, ré-
cupère la valeur contenue dans un registre à partir de son nom:

n 7→ reg[n].

Exemple.

Dans l’instruction suivante, l’opérande x20 réfère au contenu situé dans


le registre x20 :

mov x19, x20

5.2.4 Indirect
Le mode d’adressage indirect récupère, à partir d’une adresse a, la valeur conte-
nue à l’adresse b, où b est la valeur contenue à l’adresse a:

a 7→ mem[mem[a]].
CHAPITRE 5. ACCÈS AUX DONNÉES 51

Exemple.

Si a = FF16 et mem[FF16 ] = 1A16 , alors l’adressage indirect récupére la


valeur contenue dans mem[1A16 ].

5.2.5 Indirect par registre


Le mode d’adressage indirect par registre récupère, à partir d’un nom de registre
n, la valeur contenue à l’adresse reg[n] de la mémoire principale:

n 7→ mem[reg[n]].

Exemple.

Supposons que le registre x20 contienne la valeur FF16 . Dans l’instruction


suivante, l’opérande « [x20] » réfère au contenu situé à l’adresse FF16 ;
autrement dit à mem[reg[20]] = mem[FF16 ]:

ldr x19, [x20] // charger mem[x20] dans x19

5.2.6 Indirect par registre indexé


Le mode d’adressage indirect par registre indexé récupère, à partir d’un nom de
registre n et d’une valeur immédiate i, la valeur contenue à l’adresse reg[n] + i
de la mémoire principale:

n, i 7→ mem[reg[n] + i].

Exemple.

Si n = 20, reg[20] = FA16 et i = 1, alors ce mode d’adressage récupère


la valeur mem[FB16 ].

Il existe une autre variante de ce mode d’adressage où le second paramètre


est un nom de registre m, plutôt qu’une valeur immédiate i. Dans ce cas, la
valeur récupérée est celle contenue à l’adresse reg[n] + reg[m]:

n, m 7→ mem[reg[n] + reg[m]].
CHAPITRE 5. ACCÈS AUX DONNÉES 52

5.2.7 Indirect par registre pré/post-incrémenté


Les modes d’adressage indirect par registre pré/post-incrémenté reçoivent un
nom de registre n ainsi qu’une valeur immédiate i. Dans le cas pré-incrémenté,
la valeur contenue dans le registre n est incrémentée par i, puis la valeur conte-
nue à l’adresse reg[n] de la mémoire principale est récupérée. Dans le cas post-
incrémenté, le registre n est incrémenté après l’accès mémoire:

pré-incrémenté: reg[n] ← reg[n] + i puis n 7→ mem[reg[n]],


post-incrémenté: n 7→ mem[reg[n]] puis reg[n] ← reg[n] + i.

Notons que ces deux modes d’adressage ont des effets de bord: le contenu du
registre n est modifié lors de l’interprétation de l’opérande.

Exemple.

Si n = 20, reg[20] = FA16 et i = 1, alors le mode pré-incrémenté récu-


père la valeur mem[FB16 ], et le mode post-incrémenté récupère la valeur
mem[FA16 ]. Dans les deux cas, reg[20] = FB16 à la fin de l’exécution.

5.2.8 Relatif
Le mode d’adressage relatif constitue un cas particulier de l’adressage indirect
par registre indexé, où le registre utilisé est le compteur d’instruction:

i 7→ mem[reg[pc] + i].

Exemple.

Lors du chargement de var dans le code suivant, l’accès est effectué de


façon relative au compteur d’instruction a , ici avec i = 8:
ldr x19, var
nop

.section ".data"
var: .xword 42

a. Comme une instruction est codée sur 4 octets sur ARMv8, i est un multiple de 4.

5.2.9 Sommaire des modes


Les modes d’adressage présentés sont répertoriés au tableau suivant:
CHAPITRE 5. ACCÈS AUX DONNÉES 53

Nom Valeur récupérée Exemple sur ARMv8

immédiat i 7→ i mov x19, 42

direct a 7→ mem[a] —

par registre n 7→ reg[n] mov x19, x20

indirect a 7→ mem[mem[a]] —

indirect par registre n 7→ mem[reg[n]] ldr x19, [x20]

n, i 7→ mem[reg[n] + i] ldr x19, [x20, i]


indirect par registre indexé
n, m 7→ mem[reg[n] + reg[m]] ldr x19, [x20, x21]

indirect par registre indexé reg[n] ← reg[n] + i, suivi de


pré-incrémenté n, i 7→ mem[reg[n]] ldr x19, [x20, i]!

indirect par registre indexé n, i 7→ mem[reg[n]], suivi de


post-incrémenté reg[n] ← reg[n] + i ldr x19, [x20], i

relatif i 7→ mem[reg[pc] + i] ldr x19, var

5.3 Particularités de l’architecture ARMv8


Soit d ∈ {0, 1, . . . , 31} l’index d’un registre, et soit a une adresse de l’architecture
ARMv8. Il est possible de charger/stocker un octet, un demi-mot, un mot ou un
double mot d’un/vers un registre grâce aux instructions suivantes:

nombre d’octets chargement stockage


1 ldrb wd, a strb wd, a
2 ldrh wd, a strh wd, a
4 ldr wd, a str wd, a
8 ldr xd, a str xd, a

L’adresse numérique associée à une étiquette etiq: peut être chargée dans
un registre r grâce à l’instruction:

adr r, etiq

Le contenu d’un registre xd , ou une valeur immédiate i, peut être chargée


dans un registre r grâce aux instructions:

mov r, xd // charge le contenu du registre xd dans r


mov r, i // charge la valeur immédiate i dans r
CHAPITRE 5. ACCÈS AUX DONNÉES 54

Remarque.

L’instruction mov permet de charger des valeurs immédiates de 12 bits;


au-delà de 12 bits, la possibilité du chargement n’est pas garantie et une
erreur peut être levée à la compilation.

5.4 Assemblage d’un programme


Afin d’exécuter un programme en langage d’assemblage, il est nécessaire de
l’assembler. L’assembleur est un outil qui traduit le code d’assemblage vers du
code machine; il effectue la traduction de chaque instruction vers sa représen-
tation binaire. La plupart des adresses symboliques sont également remplacées
par des adresses numériques lors de l’assemblage.
L’assembleur produit un fichier objet qui contient le code machine généré,
mais également certaines adresses symboliques qui dépendent de modules ex-
ternes comme des librairies. Le fichier objet contient une table de ces symboles
externes.
L’éditeur de liens est un outil qui combine les fichiers objets vers un fichier
exécutable. En particulier, l’éditeur de liens recalcule certaines adresses et trans-
forme les adresses symboliques encore présentes vers des adresses numériques.
Sur les systèmes de type UNIX, l’assemblage, l’édition des liens et l’exécution
peut se réaliser ainsi:

as foo.s -o foo.o # assemblage du code foo.s vers un objet foo.o


ld foo.o -o foo # édition de l'objet foo.o vers un exécutable foo
./foo # exécution du programme foo

L’option -e permet de spécifier le point d’entrée d’un programme, et l’option


-lc permet d’inclure les fichiers objets de la librairie standard du langage C (par
ex. pour utiliser printf et scanf). Ainsi, les programmes vus jusqu’ici peuvent
être exécutés grâce à:

as foo.s -o foo.o # assemblage


ld foo.o -o foo -e main -lc # édition des liens
./foo # exécution

Remarque.

Rappelons que le jeu d’instructions d’un ordinateur dépend de son pro-


cesseur. Ainsi, l’assemblage d’un programme ARMv8 échouera probable-
ment sur votre ordinateur. C’est pour cette raison que nous utilisons une
machine virtuelle qui émule un processeur ARMv8.
CHAPITRE 5. ACCÈS AUX DONNÉES 55

Remarque.

Des fichiers « makefile » seront fournis pour les laboratoires et les devoirs
afin d’automatiser l’assemblage.
CHAPITRE 5. ACCÈS AUX DONNÉES 56

5.5 Quiz récapitulatif


Vous pouvez tester votre compréhension en complétant ce court quiz . Il est
facultatif, anonyme et n’a aucune incidence sur votre note du cours.

5.6 Exercices
5.1) Considérons le contenu suivant de la mémoire principale: 
adresse contenu
0 0116
1 A416
2 BC16
3 4816
4 5F16
5 1116
6 FF16
7 0416
.. .
.
. .

Quelle est la valeur de x19 et x20 après l’exécution de ces instructions?

mov x19, 2
ldr x20, [x19], 3
sub x19, x19, 1
ldrb w20, [x19, 2]

5.2) Rappelons que le mode d’adressage indirect récupère mem[mem[a]] étant 


donné une adresse a. Supposons que l’on opère au niveau d’un seul oc-
tet. Par rapport à la mémoire illustrée à l’exercice 5.1), quelle valeur est
récupérée si l’on utilise ce mode d’adressage avec a = 0716 ?

5.3) Imaginons une instruction « load r, etiq » qui utilise le mode d’adres- 
sage indirect afin de charger dans le registre r le double mot stocké à
l’adresse b, où b est le mot stocké à l’adresse représentée par l’étiquette
etiq. Expliquez comment émuler cette instruction sur ARMv8.
6
Tableaux

Nous considérons maintenant un type élémentaire de données qui permet de


structurer d’autres données: les tableaux. Un tableau est une collection ordon-
née d’éléments identifiés par des indices numériques. Les éléments d’un tableau
possèdent tous la même taille et, dans notre contexte, sont stockés de façon
contigüe en mémoire.

Exemple.

Le tableau ci-dessous possède cinq éléments, tous stockés sur un octet,


et identifiés par les indices 0, 1, 2, 3 et 4.
0 01010101
1 11110000
2 01101101
3 11111111
4 11110101

En général, un tableau possède une dimension d ∈ N≥1 ainsi que d bornes


positives n0 , n1 , . . . , nd−1 ∈ N≥2 . Un indice est une collection ordonnée i d’arité
d (un « d-uplet ») telle que:

0 ≤ i0 < n0 ,
0 ≤ i1 < n1 ,
.. .. ..
. . .
0 ≤ id−1 < nd−1 .

Chaque indice i d’un tableau t est associé à un unique élément t[i] dont la valeur
est stockée sur un nombre fixe d’octets k. Ainsi, un tableau possède n = n0 ·
n1 · · · nd−1 éléments représentés globalement sur k · n octets.

57
CHAPITRE 6. TABLEAUX 58

Exemple.

Le tableau illustré ci-dessous est bidimensionnel (d = 2) et possède les


bornes n0 = 3 et n1 = 2. Il contient n = 3 · 2 = 6 éléments, tous stockés
sur un demi-mot (k = 2). Ses éléments sont identifiés par les indices
(0, 0), (0, 1), (1, 0), (1, 1), (2, 0) et (2, 1).
(0, 0) 2
(0, 1) 33
(1, 0) 65535
(1, 1) 73
(2, 0) 9000
(2, 1) 255

Un tel tableau 2D peut être interprété comme une représentation linéaire


d’une matrice:  
2 33
65535 73 
9000 255

Remarquons que la dimension d’un tableau, ainsi que ses indices, sont seule-
ment connus implicitement. Par exemple, ces trois tableaux peuvent être repré-
sentés exactement de la même façon en mémoire:
 
2 33 ( )
65535 73  2 33 65535
[2, 33, 65535, 73, 9000, 255]
73 9000 255
9000 255
Similairement, le type des éléments d’un tableau n’est qu’implicite et peut
être interprété différemment à l’exécution. Par exemple, considérons un tableau
initialisé avec m blocs de 8 octets. Ses éléments peuvent autant être vus comme
des entiers non signés que des entiers signés de 64 bits.

Observation.
En fait, ils pourraient même être vus comme 2m entiers de 32 bits (signés
ou non), ou n’avoir simplement aucun type.

6.1 Accès aux éléments


La manipulation des éléments d’un tableau requiert le calcul de leur adresse en
mémoire. Nous expliquons donc comment les calculer.
Considérons un tableau t de dimension d situé à l’adresse a de la mémoire
principale, et dont les éléments sont représentés sur k octets. L’adresse relative
CHAPITRE 6. TABLEAUX 59

à laquelle est stocké un élément de t se nomme son index. Autrement dit, l’index
correspond à la distance d’un élément rapport à l’adresse de base a.

6.1.1 Cas unidimensionnel


Nous écrivons memk [b] afin de référer au contenu stocké sur k octets à l’adresse
b de la mémoire principale. Pour un tableau unidimensionnel t, donc où d = 1,
nous avons:

t[0] = memk [a],


t[1] = memk [a + k],
t[2] = memk [a + 2 · k],
.. ..
. .
t[n0 − 1] = memk [a + (n0 − 1) · k].

ou de façon équivalente sous forme de diagramme:

.. .
.
. .
a
a+1
.. t[0]
.
a + (k − 1)
a+k
a + (k + 1)
.. t[1]
.
a + (2k − 1)
.. .
.
. .
a + n0 · k
a + (n0 · k + 1)
.. t[n0 − 1]
.
a + (n0 · k − 1)
.. .
.
. .

Ainsi, l’élément t[i] se situe à l’adresse absolue a + i · k et son index vaut i · k.


CHAPITRE 6. TABLEAUX 60

6.1.2 Cas bidimensionnel


Pour un tableau bidimensionnel, donc où d = 2, nous avons:

t[0, 0] = memk [a], t[1, 0] = memk [a + (n1 + 0) · k],


t[0, 1] = memk [a + k], t[1, 1] = memk [a + (n1 + 1) · k],
t[0, 2] = memk [a + 2 · k], t[1, 2] = memk [a + (n1 + 2) · k],
.. .. .. ..
. . . .
t[0, n1 − 1] = memk [a + (n1 − 1) · k], t[1, n1 − 1] = memk [a + (n1 + (n1 − 1)) · k],

t[2, 0] = memk [a + (2n1 + 0) · k],


t[2, 1] = memk [a + (2n1 + 1) · k],
.. ..
. .

Ainsi, l’élément identifié par l’indice (i, j) se situe à l’adresse:

a + (i · n1 + j) · k .
| {z }
index de l’élém. (i, j)

6.2 Particularités de l’architecture ARMv8

6.2.1 Allocation et initialisation


Considérons à nouveau le tableau bidimensionnel illustré à l’exemple précédent.
Rappelons que ses éléments sont stockés sur deux octets, donc des demi-mots.
Il est possible d’allouer statiquement le tableau en mémoire dans le segment de
données non initialisées:

.section ".bss"
.align 2
tab: .skip 12

Afin de faciliter la lecture du code, des macros peuvent aussi être utilisées
afin d’indiquer explicitement les bornes du tableau:

N0 = 3
N1 = 2

.section ".bss"
.align 2
tab: .skip N0*N1*2

Le tableau peut être rempli à l’aide de l’instruction strh (et non str, puisque
les éléments sont sur 2 octets): 
CHAPITRE 6. TABLEAUX 61

adr x19, tab //


mov w20, 2 //
strh w20, [x19], 2 // tab[0] = 2
mov w20, 33 //
strh w20, [x19], 2 // tab[1] = 33
mov w20, 65535 //
strh w20, [x19], 2 // tab[2] = 65535
mov w20, 73 //
strh w20, [x19], 2 // tab[3] = 73
mov w20, 9000 //
strh w20, [x19], 2 // tab[4] = 9000
mov w20, 255 //
strh w20, [x19] // tab[5] = 255

Notons que nous utilisons ici le mode d’adressage indirect par registre post-
incrémenté, ce qui évite d’incrémenter x19 manuellement.
Alternativement, les éléments du tableau peuvent être alloués et initialisés
directement dans le segment de données initialisées: 
.section ".data"
tab: .hword 2, 33, 65535, 73, 9000, 255

6.2.2 Parcours d’un tableau


Parcours linéaire. Les éléments d’un tableau peuvent être affichés linéaire-
ment avec le code ci-dessous. Ici, l’adresse du tableau est stockée dans x19 , l’in- 
dice courant dans x20 et l’index courant dans x21 . L’accès à un élément du ta-
bleau s’effectue ici avec le mode d’adressage indirect par registre indexé dénoté
par « [x19, x21] »:

N0 = 3
N1 = 2

main:
adr x19, tab //
mov x20, 0 // i = 0
afficher: // do {
adr x0, fmt //
mov x21, 2 //
mul x21, x21, x20 //
ldrh w1, [x19, x21] //
bl printf // afficher tab[i]
//
add x20, x20, 1 // i++
cmp x20, N0*N1 // }
CHAPITRE 6. TABLEAUX 62

b.lo afficher // while (i < N0*N1)

mov x0, 0
bl exit

.section ".data"
tab: .hword 2, 33, 65535, 73, 9000, 255

.section ".rodata"
fmt: .asciz "%u\n"

Il est également possible d’accéder aux éléments grâce au mode d’adressage


indirect par registre indexé post-incrémenté, dénoté par « [x19], 2 ». Cela sim- 
plifie le code puisqu’il n’est pas nécessaire de calculer explicitement l’index:

N0 = 3
N1 = 2

main:
adr x19, tab //
mov x20, 0 // i = 0
afficher: // do {
adr x0, fmt //
ldrh w1, [x19], 2 //
bl printf // afficher tab[i]
//
add x20, x20, 1 // i++
cmp x20, N0*N1 // }
b.lo afficher // while (i < N0*N1)

mov x0, 0
bl exit

.section ".data"
tab: .hword 2, 33, 65535, 73, 9000, 255

.section ".rodata"
fmt: .asciz "%u\n"

Parcours bidimensionnel. Le code ci-dessous permet d’afficher les éléments 


sous forme matricielle. Ici, l’adresse du tableau est stockée dans x19 , l’indice
courant (i, j) dans (x20 , x21 ), et l’index courant dans x22 . Ce dernier est calculé
à partir de l’expression (i·n1 +j)·2 puisque les éléments sont stockés sur 2 octets.
Nous utilisons ici le mode d’adressage indirect par registre indexé, dénoté par
« [x19, x22] »:
CHAPITRE 6. TABLEAUX 63

N0 = 3
N1 = 2

main:
adr x19, tab //
mov x20, 0 // i = 0
//
prochaineLigne: // do {
mov x21, 0 // j = 0
//
afficherLigne: // do {
adr x0, fmtElem //
//
// Calcul de l'index //
mov x22, N1 //
mul x22, x20, x22 //
add x22, x22, x21 //
add x22, x22, x22 // index = (i*N1 + j)*2
//
ldrh w1, [x19, x22] //
bl printf // afficher tab[i, j]
//
add x21, x21, 1 // j++
cmp x21, N1 // }
b.lo afficherLigne // while (j < N1)
//
adr x0, fmtSaut //
bl printf // afficher saut de ligne
//
add x20, x20, 1 // i++
cmp x20, N0 // }
b.lo prochaineLigne // while (i < N0)

mov x0, 0
bl exit

.section ".data"
tab: .hword 2, 33, 65535, 73, 9000, 255

.section ".rodata"
fmtElem: .asciz "%u "
fmtSaut: .asciz "\n"

Comme dans le cas linéaire, on peut accéder aux éléments du tableau à l’aide
du mode d’adressage indirect par registre indexé post-incrémenté « [x19], 2 », 
ce qui simplifie le code:
CHAPITRE 6. TABLEAUX 64

N0 = 3
N1 = 2

main:
adr x19, tab //
mov x20, 0 // i = 0
//
prochaineLigne: // do {
mov x21, 0 // j = 0
//
afficherLigne: // do {
adr x0, fmtElem //
ldrh w1, [x19], 2 //
bl printf // afficher tab[i, j]
//
add x21, x21, 1 // j++
cmp x21, N1 // }
b.lo afficherLigne // while (j < N1)
//
adr x0, fmtSaut //
bl printf // afficher saut de ligne
//
add x20, x20, 1 // i++
cmp x20, N0 // }
b.lo prochaineLigne // while (i < N0)

mov x0, 0
bl exit

.section ".data"
tab: .hword 2, 33, 65535, 73, 9000, 255

.section ".rodata"
fmtElem: .asciz "%u "
fmtSaut: .asciz "\n"

6.3 Autre exemple: tableaux de pointeurs


Voyons un exemple où les éléments d’un tableau ne sont pas des valeurs numé-
riques, mais plutôt une collection de procédures à sélectionner dynamiquement.
Considérons un programme qui, sur entrée i ∈ {0, 1, 2}, doit afficher x + y
si i = 0, x · y si i = 1, et x − y si i = 2. Nous pourrions effectuer une suite de
comparaisons et brancher selon la valeur de i. Il est toutefois possible d’éviter
de telles comparaisons en utilisant un tableau t de pointeurs, où t[0], t[1] et t[2]
CHAPITRE 6. TABLEAUX 65

contiennent respectivement l’adresse de segments de code calculant l’addition,


le produit et la différence.
Par exemple, voici une implémentation où les valeurs x = 5 et y = 7 sont 
figées afin de simplifier la présentation:

main:
// Initialiser tab //
adr x19, tab //
adr x20, somme //
str w20, [x19], 4 //
adr x20, produit //
str w20, [x19], 4 // tab = {&somme,
adr x20, diff // &produit,
str w20, [x19] // &diff}
//
// Lire chiffre //
adr x0, fmtEntree //
adr x1, temp //
bl scanf // scanf("%u", &temp)
ldr x21, temp // i = temp
//
// Évaluer f(7, 5) où f := tab[i] //
mov x20, 4 //
mul x22, x20, x21 //
adr x19, tab //
ldr w20, [x19, x22] // f = tab[i]
mov x27, 7 // x = 7
mov x28, 5 // y = 5
br x20 // f(x, y)
//
somme: // somme(x, y) {
add x1, x27, x28 // z = x + y
b afficher // }
produit: // produit(x, y) {
mul x1, x27, x28 // z = x * y
b afficher // }
diff: // diff(x, y) {
sub x1, x27, x28 // z = x - y
b afficher // }
//
// Afficher résultat //
afficher: //
adr x0, fmtSortie //
bl printf // printf("%lu\n", z)
//
CHAPITRE 6. TABLEAUX 66

mov x0, 0 //
bl exit //

.section ".bss"
tab: .skip 12 // tableau de 3 adresses
temp: .skip 4

.section ".rodata"
fmtEntree: .asciz "%u"
fmtSortie: .asciz "%lu\n"

Remarque.

Des implémentations en C/C++ sont également fournies sur GitHub .


CHAPITRE 6. TABLEAUX 67

6.4 Quiz récapitulatif


Vous pouvez tester votre compréhension en complétant ce court quiz . Il est
facultatif, anonyme et n’a aucune incidence sur votre note du cours.

6.5 Exercices
6.1) Considérons ce tableau bidimensionnel stocké à l’adresse a = FF00AB16 
et dont les éléments sont des mots. À quelle adresse se situe le mot qui
contient 999?
 
0 65536 −10
42 50 100 
 
1 999 1234
7 500 4321

6.2) Écrivez un programme qui affiche le produit des éléments de la diagonale 


principale d’une matrice.

6.3) Nous avons représenté les matrices par des tableaux bidimensionnels en 
stockant les lignes l’une à la suite des autres. Considérez la représentation
alternative où ce sont plutôt les colonnes qui sont stockées l’une à la suite
des autres. Revoyez la formule du calcul d’index et écrivez un programme
qui affiche une telle matrice.

6.4) Dites pourquoi un tableau de 23 éléments est forcément unidimensionnel. 


6.5) Considérons un tableau tridimensionnel où les éléments sont stockés sur 
k octets et où l’ordre des indices est

(0, 0, 0), (0, 0, 1), . . . , (0, 0, n2 − 1), (0, 1, 0), . . . ,


(0, n1 − 1, n2 − 1), (1, 0, 0), . . . , (n0 − 1, n1 − 1, n2 − 1).

Quel est l’index associé à l’indice (i0 , i1 , i2 )?

6.6) ⋆ Écrivez un programme qui implémente le jeu de la vie. Il doit lire une 
matrice binaire (où 0 représente une cellule morte, et 1 représente une
cellule vivante), puis retourner une matrice qui représente le nouvel état
des cellules après une application des règles.
7
Programmation structurée

Les langages d’assemblage appartiennent à la famille des langages de program-


mation impérative: on utilise des expressions qui indiquent comment mettre à
jour l’état d’un programme. La programmation structurée a émergé dans les an-
nées 60–70 afin de faciliter l’écriture, la lecture et la maintenabilité des pro-
grammes impératifs. L’un de ses ambassadeurs les plus influents, Edsger Dijks-
tra, plaidait à l’époque pour l’abandon de l’instruction goto au profit de struc-
tures de contrôle et de sous-routines. De nos jours, ces concepts se trouvent au
cœur de langages de programmation système comme C, C++ et Rust, et de lan-
gages de haut niveau comme Python, Java et C#.
Dans ce chapitre, nous expliquons comment les notions de programmation
structurée s’implémentent en langage d’assemblage.

7.1 Structures de contrôle


Voyons d’abord comment les structures de contrôle de haut niveau peuvent être
implémentées à l’aide de code de bas niveau. Les constructions sont illustrées
en C et C++ (haut niveau), ainsi que dans le langage d’assemblage de l’archi-
tecture ARMv8 (bas niveau). Nous considérons les trois types élémentaires de
structures de contrôle de la programmation structurée: la séquence, la sélection
et l’itération. Dans nos constructions, les termes « cond », « cond1 » et « cond2 »
dénotent des prédicats d’arité deux; autrement dit, des fonctions qui prennent
deux valeurs en entrée et qui retournent une valeur booléenne.

7.1.1 Séquence
La séquence consiste simplement à composer des instructions de façon séquen-
tielle. Cette structure est implémentée en traduisant chaque instruction vers du
code de bas niveau équivalent:

68
CHAPITRE 7. PROGRAMMATION STRUCTURÉE 69

Code de haut niveau (C/C++) Code de bas niveau (ARMv8)

// instruction 1 // code pour l'instruction 1


// instruction 2 // code pour l'instruction 2
// ... // ...
// instruction k // code pour l'instruction k

Remarquons qu’une instruction de haut niveau peut nécessiter plusieurs


instructions de bas niveau selon l’architecture. Par exemple, sur l’architecture
ARMv8, l’opération « x19 *= 7 » se traduit vers:

mov x20, 7
mul x19, x19, x20

7.1.2 Sélection
La sélection est une structure de contrôle qui permet d’exécuter certaines instruc-
tions conditionnellement à la validité d’un ou plusieurs prédicats; par exemple:
une exécution conditionnelle à l’égalité de deux variables. Voyons comment im-
plémenter les structures de sélection si/sinon–si/sinon et de type « switch ».

Si.

Code de haut niveau (C/C++) Code de bas niveau (ARMv8)

if (cond(xd, xn)) { si:


// code cmp xd, xn
} b.¬cond fin
// code
fin:

Si/sinon.

Code de haut niveau (C/C++) Code de bas niveau (ARMv8)

if (cond(xd, xn)) { si:


// code si cmp xd, xn
} b.¬cond sinon
else { // code si
// code sinon b fin
} sinon:
// code sinon
fin:
CHAPITRE 7. PROGRAMMATION STRUCTURÉE 70

Si/sinon–si/sinon.

Code de haut niveau (C/C++) Code de bas niveau (ARMv8)

if (cond1(xd, xn)) { si:


// code si cmp xd, xn
} b.¬cond1 sinonsi
else if (cond2(xd, xn)) { // code si
// code sinon si b fin
} sinonsi:
else { cmp xd, xn
// code sinon b.¬cond2 sinon
} // code sinon si
b fin
sinon:
// code sinon
fin:

Sélection de type « switch ».

Code de haut niveau (C/C++) Code de bas niveau (ARMv8)

switch (xd) { cas1:


case v1: cmp xd, v1
// code cas 1 b.ne cas2
break; // code cas 1
case v2: b fin
// code cas 2 cas2:
break; cmp xd, v2
/* ... */ b.ne cas3
case vk: // code cas 2
// code cas k b fin
break; /* ... */
default: cask:
// code par défaut cmp xd, vk
break; b.ne defaut
} // code cas k
b fin
defaut:
// code par défaut
fin:
CHAPITRE 7. PROGRAMMATION STRUCTURÉE 71

Remarque.

Lorsque les valeurs v1 , v2 , . . . , vk d’une structure « switch » sont rappro-


chées, il peut s’avérer plus efficace d’utiliser une « table de branchement »
qui indique l’adresse du code de chaque cas, à la façon de la section 6.3.

Sélection avec conditions multiples. Dans les langages de haut niveau, il


est normalement possible d’évaluer des prédicats sur plusieurs variables, et de
combiner ces prédicats à l’aide d’opérateurs logiques tels que ∧ et ∨. Cepen-
dant, la plupart des langages d’assemblage ne permettent de tester qu’une seule
condition sur un ou deux registres. Il est possible de traduire ces prédicats plus
complexes à l’aide de plusieurs branchements.
Nous donnons deux exemples, la conjonction et la disjonction de deux pré-
dicats: « cond1 (xd , xn ) ∧ cond2 (xm , xk ) » et « cond1 (xd , xn ) ∨ cond2 (xm , xk ) ». Ces
constructions se généralisent à un nombre arbitraire de prédicats et à d’autres
opérateurs logiques.

Code de haut niveau (C/C++) Code de bas niveau (ARMv8)

if (cond1(xd, xn) && si:


cond2(xm, xk)) { cmp xd, xn
// code si b.¬cond1 sinon
} cmp xm, xk
else { b.¬cond2 sinon
// code sinon // code si
} b fin
sinon:
// code sinon
fin:

Code de haut niveau (C/C++) Code de bas niveau (ARMv8)

if (cond1(xd, xn) || cmp xd, xn


cond2(xm, xk)) { b.cond1 si
// code si cmp xm, xk
} b.cond2 si
else { b sinon
// code sinon si:
} // code si
b fin
sinon:
// code sinon
fin:
CHAPITRE 7. PROGRAMMATION STRUCTURÉE 72

7.1.3 Itération
L’itération est une structure de contrôle qui répète l’exécution de certaines ins-
tructions en fonction de l’état du programme; par exemple, une répétition qui
dépend de l’égalité de deux variables. Voyons comment implémenter les struc-
tures tant que, faire/tant que et pour.

Tant que.

Code de haut niveau (C/C++) Code de bas niveau (ARMv8)

while (cond(xd, xn)) { boucle:


// code cmp xd, xn
} b.¬cond fin
// code
b boucle
fin:

Faire/tant que.

Code de haut niveau (C/C++) Code de bas niveau (ARMv8)

do { boucle:
// code // code
} while (cond(xd, xn)); cmp xd, xn
b.cond boucle
fin:

Boucle « pour ». L’un des cas les plus répandus de la boucle « pour » consiste
à incrémenter une variable, initialisée à 0, tant que sa valeur est inférieure ou
égale à la valeur d’une autre variable. Cette structure s’implémente ainsi:

Code de haut niveau (C/C++) Code de bas niveau (ARMv8)

for (xd = 0; xd <= xn; xd++) { mov xd, 0


// code boucle:
} cmp xd, xn
b.hi fin
// code
add xd, xd, 1
b boucle
fin:

Le cas général de la boucle « pour », où l’initialisation et l’incrémentation


sont arbitraires, s’implémente ainsi:
CHAPITRE 7. PROGRAMMATION STRUCTURÉE 73

Code de haut niveau (C/C++) Code de bas niveau (ARMv8)

for (init(xd); cond(xd, xn); // initialiser xd


modif(xd)) { boucle:
// code cmp xd, xn
} b.¬cond fin
// code
// modifier xd
b boucle
fin:

7.2 Sous-programmes
Les langages structurés offrent des primitives pour décrire des sous-routines qui
modularisent le code; par exemple, sous la forme de procédures, de fonctions
ou de méthodes. De telles primitives n’existent pas dans les langages de bas
niveau. Elles doivent être implémentées à l’aide de sous-programmes: des blocs
de code exécutables à l’aide de branchements, de passage d’arguments et de
sauvegarde de registres.
À titre d’exemple, nous allons implémenter une fonction qui reçoit deux va-
leurs entières et qui retourne le maximum de ces valeurs. En C/C++, cette fonc-
tion peut être implémentée ainsi:

long max(long a, long b)


{
if (a >= b) {
return a;
}
else {
return b;
}
}

Notons que les sous-programmes ne s’implémentent pas de la même façon


sur toutes les architectures, nous nous limitons ici à l’architecture ARMv8.

7.2.1 Paramètres et appel


Par convention, un sous-programme reçoit ses paramètres dans les registres x0
à x7 (dans cet ordre), et retourne sa sortie via le registre x0 1 . Ainsi, nous utili-
serons les registres x0 et x1 pour passer les valeurs a et b, et le registre x0 pour
retourner la valeur max(a, b).
1. Dans certains cas, la convention permet d’utiliser d’autres registres [ARM13].
CHAPITRE 7. PROGRAMMATION STRUCTURÉE 74

Afin de déclarer notre sous-programme « max », il suffit d’ajouter une éti-


quette « max: ». L’appel du sous-programme se réalise avec l’instruction de bran-
chement « bl » (branch with link), que nous avons déjà utilisée aux chapitres
précédents pour réaliser des entrées/sorties.
Si nous désirons stocker le maximum des registres x19 et x20 dans x21 , alors
nous pouvons utiliser le code suivant:
main:
mov x0, x19 // a = x19
mov x1, x20 // b = x20
bl max // m = max(a, b)
mov x21, x0 // x21 = m
//
mov x0, 0 // sans «bl exit»,
bl exit // l'exécution se
// poursuit dans «max»
max:
// code du sous-programme ici

Passage par adresse. Dans notre exemple, les arguments sont passés par va-
leur puisqu’il s’agit d’entiers de 64 bits stockables dans les registres. Les struc-
tures de données plus complexes et stockées dans la mémoire principale doivent
plutôt être passées par adresse. Autrement dit, plutôt que de passer le contenu
de la structure, son adresse en mémoire est passée via un registre. Par exemple,
un tableau peut être passé comme premier argument d’un sous-programme en
chargeant son adresse dans x0 ; le sous-programme peut ensuite accéder aux
éléments du tableau grâce aux instructions d’accès mémoire comme « ldr ».

7.2.2 Retour
L’instruction « ret » termine l’exécution d’un sous-programme et saute vers l’en-
droit où l’appel a été effectué. La valeur de retour du sous-programme doit être
stockée dans le registre x0 avant le retour. Ainsi, notre fonction « max » peut être
implémentée ainsi:

max:
cmp x0, x1 //
b.lt max_sinon // if (a >= b) {
mov x19, x0 // m = a
b max_retour // }
max_sinon: // else {
mov x19, x1 // m = b
max_retour: // }
mov x0, x19 //
ret // return m
CHAPITRE 7. PROGRAMMATION STRUCTURÉE 75

Puisqu’on peut altérer le contenu des registres de paramètres, notre code se


simplifie ainsi:

max:
cmp x0, x1 //
b.ge max_retour // if (a >= b) return a
mov x0, x1 // else return b
max_retour: //
ret //

Adresse de retour. Lors de l’appel d’un sous-programme à l’aide de l’instruc-


tion « bl », l’adresse de retour est stockée dans le registre spécial x30 . Puisque
les instructions de l’architecture ARMv8 sont toutes codées sur 4 octets, l’appel
« bl etiq » effectue d’abord l’assignation x30 ← pc + 4, puis branche vers l’éti-
quette « etiq: ». L’instruction « ret » effectue le retour en branchant vers x30 .

7.2.3 Sauvegarde des registres


Les registres sont partagés par le programme et tous les sous-programmes. Par
conséquent, l’appel d’un sous-programme peut détruire le contenu des registres
de l’appelant. Par convention, l’appelé peut altérer les registres x0 à x15 , mais est
en charge de rétablir le contenu des registres x19 à x28 , que nous avons utilisés
jusqu’ici, ainsi que les registres spéciaux x29 et x30 .
Un sous-programme peut sauvegarder et rétablir ces registres à l’aide des 
macros suivantes 2 :

Macro de sauvegarde Macro de restauration

.macro SAVE .macro RESTORE


stp x29, x30, [sp, -96]! ldp x27, x28, [sp, 16]
mov x29, sp ldp x25, x26, [sp, 32]
stp x27, x28, [sp, 16] ldp x23, x24, [sp, 48]
stp x25, x26, [sp, 32] ldp x21, x22, [sp, 64]
stp x23, x24, [sp, 48] ldp x19, x20, [sp, 80]
stp x21, x22, [sp, 64] ldp x29, x30, [sp], 96
stp x19, x20, [sp, 80] .endm
.endm

Ces macros utilisent une pile située dans la mémoire principale afin de sauve-
garder temporairement la valeur des registres. Nous discuterons du fonctionne-
ment de cette pile plus tard au chapitre 11. En particulier, la pile nous permettra
de mettre au point des sous-programmes récursifs.
CHAPITRE 7. PROGRAMMATION STRUCTURÉE 76

Exemple complet. En ajoutant l’appel de ces deux macros, nous obtenons ce 


code complet pour notre exemple:

main:
mov x0, x19 // a = x19
mov x1, x20 // b = x20
bl max // m = max(a, b)
mov x21, x0 // x21 = m
//
bl exit //
//
max: //
SAVE //
cmp x0, x1 //
b.lt max_sinon // if (a >= b) {
mov x19, x0 // m = a
b max_retour // }
max_sinon: // else {
mov x19, x1 // m = b
max_retour: // }
mov x0, x19 //
RESTORE //
ret // return m

7.3 Autres particularités de l’architecture ARMv8

7.3.1 Distance des adresses


L’instruction « bl » reçoit une adresse relative représentée sur 26 bits. Ainsi, elle
permet de brancher vers une étiquette dont l’adresse se situe à une distance de
±128Mio du compteur d’instruction. En effet:
4 · 226 octets = 2 · 27 · (210 )2 octets = 2 · 128 · 10242 octets = 2 · 128 Mio.
Afin de brancher vers une adresse située plus loin, il faut d’abord stocker cette
adresse dans un registre xd , puis brancher à l’aide de « blr xd ».

7.3.2 Assignation par sélection


L’instruction « csel » affecte une valeur à un registre de façon conditionnelle:

instruction effet
csel xd, xn, xm, cond si cond: xd ← xn , sinon: xd ← xm

Cette instruction permet, par exemple, de simplifier le code de max:


2. Macros tirées initialement d’un diaporama de Mikaël Fortin.
CHAPITRE 7. PROGRAMMATION STRUCTURÉE 77

max:
cmp x0, x1 //
csel x0, x0, x1, ge //
ret // return (a >= b) ? a : b

— xkcd c
CHAPITRE 7. PROGRAMMATION STRUCTURÉE 78

7.4 Quiz récapitulatif


Vous pouvez tester votre compréhension en complétant ce court quiz . Il est
facultatif, anonyme et n’a aucune incidence sur votre note du cours.

7.5 Exercices
7.1) Traduisez le code C/C++ ci-dessous en langage d’assemblage. Interprétez 
x19 , x20 et x21 comme des entiers signés.

if (x19 > x20 && (x20 != x21 || x21 == x19)) {


// A
}
else if (x19 < x20) {
// B
}
else {
// C
}

7.2) Comment un sous-programme, qui reçoit un tableau en argument, peut-il 


itérer à travers tous ses éléments?

7.3) Écrivez un sous-programme elem(tab, i) qui reçoit un tableau tab d’en- 


tiers signés et un indice i, puis qui retourne tab[i].

7.4) Écrivez un sous-programme renverser(tab, taille) qui reçoit un ta- 


bleau tab d’entiers signés ainsi que sa taille, puis qui renverse le contenu
du tableau en mémoire.

7.5) Modularisez des segments de code conçus précédemment. Par exemple, 


écrivez des sous-programmes pour le calcul du temps de vol du chapitre 2
et pour le programme obtenu à l’exercice 6.2).

7.6) ⋆ Écrivez un programme qui implémente le jeu Puissance 4. 


8
Circuits logiques

Maintenant que nous comprenons mieux le fonctionnement interne de l’ordina-


teur, nous faisons un survol de son implémentation au niveau le plus bas: les
circuits logiques. Nous considérons une version idéalisée des circuits logiques
afin de mieux cerner l’organisation de l’unité arithmétique et logique, de l’unité
de contrôle et de la mémoire principale.
À l’aide de transistors, il est possible de construire des portes logiques qui cal-
culent la négation (¬), la conjonction (∧), la disjonction (∨) et le OU exclusif
(⊕), où les entrées et sorties correspondent à des signaux numériques logiques
qui, à nos fins, ne sont simplement que des bits. Ces portes sont souvent illus-
trées comme à la figure 8.1.

x x y x y x y

¬x x∧y x∨y x⊕y

Figure 8.1 – De gauche à droite: portes NON, ET, OU, OU exclusif. Les bits
d’entrée et de sortie sont situés respectivement au-dessus et au bas des portes.

8.1 Arithmétique

8.1.1 Addition de deux bits


Considérons l’addition afin d’illustrer l’implémentation des opérations arithmé-
tiques. Rappelons que la somme de deux bits x et y mène à deux bits: un bit

79
CHAPITRE 8. CIRCUITS LOGIQUES 80

de poids faible nommé la somme et un bit de poids fort nommé la retenue. La


somme et la retenue correspondent respectivement aux valeurs x ⊕ y et x ∧ y:

x y x∧y x⊕y
(retenue) (somme)

0 0 0 0
0 1 0 1
1 0 0 1
1 1 1 0

Le circuit illustré à la figure 8.2 exploite cette observation afin d’implémen-


ter l’addition de deux bits. Ce type de circuit est connu sous le nom de demi-
additionneur.

x y

retenue somme

Figure 8.2 – Demi-additionneur.

Afin d’additionner deux suites de bits, il serait tentant de combiner plusieurs


demi-additionneurs. Toutefois, ils ne tiennent pas compte d’une retenue prove-
nant d’une addition précédente. Un additionneur complet, une généralisation du
demi-additionneur, prend également une telle retenue en entrée. Considérons
la table d’addition d’une retenue r et de deux bits x et y:

r x y r+x+y
(sur deux bits)

0 0 0 00
0 0 1 01
0 1 0 01
0 1 1 10
1 0 0 01
1 0 1 10
1 1 0 10
1 1 1 11

Nommons à nouveau le bit de poids faible la somme et le bit de poids fort la


retenue. Nous remarquons que la somme et la retenue correspondent aux com-
binaisons suivantes de portes ET, OU et OU exclusif:
CHAPITRE 8. CIRCUITS LOGIQUES 81

r x y (x ∧ y) ∨ (r ∧ (x ⊕ y)) r ⊕ (x ⊕ y)
(retenue) (somme)

0 0 0 0 0
0 0 1 0 1
0 1 0 0 1
0 1 1 1 0
1 0 0 0 1
1 0 1 1 0
1 1 0 1 0
1 1 1 1 1

Nous pouvons donc implémenter un additionneur complet sous forme de circuit


tel qu’illustré à la figure 8.3.

r x y

retenue somme

Figure 8.3 – Additionneur complet.

8.1.2 Addition de deux nombres


Afin d’additionner deux nombres x et y, chacun représenté par une suite de
n bits, il suffit de composer un demi-additionneur et n − 1 additionneurs tel
qu’illustré à la figure 8.4. Remarquons qu’une telle somme z peut engendrer 
une retenue r ∈ {0, 1}.

Remarque.

Des additionneurs plus sophistiqués, par ex. avec regard en avant (« carry-
lookahead »), permettent de calculer la somme plus rapidement.
CHAPITRE 8. CIRCUITS LOGIQUES 82

xn−1 yn−1 ··· x2 y2 x1 y1 x0 y0

Demi-additionneur

Additionneur complet

Additionneur complet

Additionneur complet

r zn ··· z2 z1 z0

Figure 8.4 – Additionneur de deux nombres de n bits, construit à partir d’un


demi-additionneur et de n − 1 additionneurs complets.

8.2 Décodage du jeu d’instructions


L’unité de contrôle combine plusieurs circuits afin d’entre autres décoder le jeu
d’instructions et d’invoquer l’unité arithmétique et logique.

x1 x0

y3 y2 y1 y0

Figure 8.5 – Décodeur de deux bits.

Exemple.

Considérons un jeu d’instruction simple constitué de quatre opérations


codées par les suites binaires 00, 01, 10 et 11. Lorsque l’unité de contrôle
reçoit une telle suite de deux bits x = x1 x0 , elle peut la décoder à l’aide
CHAPITRE 8. CIRCUITS LOGIQUES 83

du circuit illustré à la figure 8.5. Par exemple, sur entrée x = 10 nous


avons y2 = 1 et y3 = y1 = y0 = 0 à la sortie.

Un tel décodeur permet également d’accomplir des tâches plus complexes


comme la sélection de bits. Par exemple, le circuit illustré à la figure 8.6 est un 
multiplexeur qui sélectionne un bit yi parmi quatre bits, à partir de la représen-
tation binaire x de l’indice i.
y3 y2 y1 y0 x1 x0

Décodeur de deux bits

yx

Figure 8.6 – Multiplexeur à deux bits de sélection.

Exemple.

Sur entrée x = 11, la sortie de ce circuit est y3 , alors que sur entrée
x = 01, sa sortie est y1 .

En combinant des circuits comme les décodeurs et multiplexeurs, l’unité de


contrôle arrive notamment à décoder des instructions complètes et à sélection-
ner des résultats plus complexes provenant de l’unité arithmétique et logique.

Remarque.

Il devient difficile de spécifier graphiquement des circuits complexes. Il


existe donc des langages pour les décrire tels que Verilog et VHDL.
CHAPITRE 8. CIRCUITS LOGIQUES 84

8.3 Mémoire
Les circuits logiques présentés jusqu’ici sont dits combinatoires: ils transforment
des entrées en sorties, sans mémoriser d’état interne. Afin d’implémenter la mé-
moire principale et les registres, on peut plutôt exploiter des circuits logiques
dits séquentiels. Ceux-ci contiennent des cycles qui stockent des bits d’informa-
tion. Nous donnons un exemple simple et classique qui montre comment stocker
un bit. La figure 8.7 illustre un circuit à verrouillage (ou verrou). Ce circuit pos- 
sède deux bits d’entrée: s et r de l’anglais « set » et « reset ».

r s b (valeur stockée)

sortie

Figure 8.7 – Circuit à verrouillage stockant un bit b.

Considérons sa table de vérité:

r s bit interne sortie


(et nouveau bit interne)

0 0 b b
0 1 b 1
1 0 b 0
1 1 b 0

Lorsque r = s = 0, rien ne se produit et le circuit retourne tout simplement


le bit b qu’il mémorise. Lorsque r = 1, le bit mémorisé est remis à zéro. Lorsque
r = 0 et s = 1, le bit mémorisé est modifié à 1.
CHAPITRE 8. CIRCUITS LOGIQUES 85

8.4 Quiz récapitulatif


Vous pouvez tester votre compréhension en complétant ce court quiz . Il est
facultatif, anonyme et n’a aucune incidence sur votre note du cours.

8.5 Exercices
8.1) Quelles portes implémentent le maximum et le minimum de deux bits? 
8.2) Décrivez un circuit logique qui teste si un nombre de n bits vaut zéro. 
8.3) Décrivez un circuit logique qui teste si deux nombres sont égaux. 
8.4) Décrivez un circuit logique qui incrémente un nombre de 3 bits. 
8.5) Reproduisez le décodeur et le multiplexeur présentés, mais cette fois avec 
3 bits. Décrivez comment obtenir de tels circuits en général pour n bits.

8.6) Adaptez le multiplexeur présenté afin qu’il sélectionne parmi quatre suites
de n bits plutôt que quatre bits.

8.7) Donnez un circuit qui affiche un chiffre parmi {0, 1, 2, 3} sur un compteur 
digital à partir de deux bits d’entrées x1 x0 . Chaque segment du compteur
digital est associé à une variable booléenne yi . Par exemple, sur entrée
x = 10, nous avons:

y2

y1 y3
y6

y0 y4

y5

8.8) Donnez un circuit logique qui calcule le complément à deux d’un nombre, 
sans réutiliser les circuits précédents. Réfléchissez d’abord à une entrée
d’un bit, de deux bits, de trois bits, etc. puis généralisez à n bits.
9
Valeurs booléennes et chaînes de bits

9.1 Algèbre de Boole


L’algèbre de Boole, nommée en l’honneur du logicien et mathématicien George
Boole, constitue l’un des fondements de l’informatique et des ordinateurs. Cette
algèbre manipule des valeurs booléennes, c’est-à-dire les éléments vrai et faux,
à l’aide d’opérateurs logiques. Tel que discuté au chapitre 8, ces opérateurs per-
mettent notamment d’implémenter un processeur à l’aide de portes logiques.
Les valeurs booléennes peuvent être représentées par un ordinateur à l’aide
d’un bit: 1 représente vrai et 0 représente faux. Un opérateur logique f est donc
une fonction f : {0, 1}k → {0, 1} où k est le nombre d’arguments de l’opérateur.
La plupart des opérateurs utilisés afin de construire des circuits logiques ou des
conditions dans les langages de programmation sont unaires (k = 1) ou binaires
(k = 2). Nous rappelons certains de ces opérateurs à la figure 9.1.

x ¬x x y x∧y x y x∨y
0 1 0 0 0 0 0 0
1 0 0 1 0 0 1 1
1 0 0 1 0 1
1 1 1 1 1 1

x y x⊕y x y x→y x y x↔y


0 0 0 0 0 1 0 0 1
0 1 1 0 1 1 0 1 0
1 0 1 1 0 0 1 0 0
1 1 0 1 1 1 1 1 1

Figure 9.1 – Tables de vérité de la négation, de la conjonction (ET logique), de


la disjonction (OU logique), du OU exclusif, de l’implication et de l’équivalence.

86
CHAPITRE 9. VALEURS BOOLÉENNES ET CHAÎNES DE BITS 87

9.2 Représentation des valeurs booléennes


Bien qu’une valeur booléenne se représente par un seul bit, la plupart des ar-
chitectures ne permettent pas d’adresser un seul bit. Par exemple, la plus petite
unité de mémoire adressable sur l’architecture ARMv8 est l’octet. Il existe donc
plusieurs façons de stocker un bit en mémoire. Par exemple, on peut:
— considérer uniquement le bit de poids faible d’un octet, et figer les sept
autre bits à zéro: 000000002 = faux et 000000012 = vrai;
— répéter la valeur booléenne dans les huit bits de l’octet: 000000002 = faux
et 111111112 = vrai;
— considérer 000000002 comme faux et les 28 − 1 autres valeurs comme vrai.
Ces deux représentations « gaspillent » sept bits de mémoire, mais forment
néanmoins les représentations utilisées dans plusieurs langages de program-
mation. Historiquement, la troisième approche est celle employée par C qui ne
possède pas de type booléen. En C++, le code suivant retourne normalement 1,
ce qui signifie qu’une valeur booléenne se représente bel et bien par un octet:

#include <iostream>

int main()
{
std::cout << sizeof(bool) << std::endl;
}

Si l’on désire représenter plusieurs valeurs booléennes, on peut aussi utiliser


un tableau d’octets. Par exemple, 8n valeurs booléennes se représentent à l’aide
des bits de n octets. Cependant, puisque les bits ne sont pas adressables, ceux-ci
doivent être extraits à l’aide d’instructions de manipulation de bits.

9.3 Manipulation de bits


Puisque les valeurs booléennes se représentent numériquement par 0 et 1, on
pourrait envisager de les manipuler à l’aide d’opérations arithmétiques, par ex.:

¬x ≡ 1 − x
x ∧ y ≡ min(x, y) ≡ x · y
x ∨ y ≡ max(x, y)
x ⊕ y ≡ (x + y) mod 2

Toutefois, les architectures offrent normalement des instructions (plus efficaces)


dédiées à ces tâches.
Au-delà de la manipulation de valeurs booléennes, ces opérations sont aussi
essentielles à la programmation de bas niveau liée, par exemple, aux chaînes de
caractères, aux protocoles de communication, aux primitives cryptographiques
et à la manipulation d’images.
CHAPITRE 9. VALEURS BOOLÉENNES ET CHAÎNES DE BITS 88

9.3.1 Opérateurs logiques


Sous ARMv8, les opérations ¬, ∧, ∨ et ⊕ peuvent être calculées à l’aide des
instructions mvn, and, orr et eor. Ces opérations ne sont pas effectuées sur un
seul bit, mais bien « bit à bit » sur chacun des 64 bits.

Exemple.

Si x20 = 0 · · · 0 1011 et x21 = 1 · · · 1 1001, alors nous obtenons:

instruction contenu de x19 après l’exécution


mvn x19, x20 1 · · · 1 0100
and x19, x20, x21 0 · · · 0 1001
orr x19, x20, x21 1 · · · 1 1011
eor x19, x20, x21 1 · · · 1 0010

On obtient ces résultats en appliquant les opérateurs logiques « bit à bit »:

mvn x19, x20 and x19, x20, x21


¬ ··· ¬ ¬ ¬ ¬ ¬ 0 ··· 0 1 0 1 1
0 ··· 0 1 0 1 1 ∧ ··· ∧ ∧ ∧ ∧ ∧
1 ··· 1 0 1 0 0 1 ··· 1 1 0 0 1
0 ··· 0 1 0 0 1

orr x19, x20, x21 eor x19, x20, x21


0 ··· 0 1 0 1 1 0 ··· 0 1 0 1 1
∨ ··· ∨ ∨ ∨ ∨ ∨ ⊕ ··· ⊕ ⊕ ⊕ ⊕ ⊕
1 ··· 1 1 0 0 1 1 ··· 1 1 0 0 1
1 ··· 1 1 0 1 1 1 ··· 1 0 0 1 0

Échange de registres. L’opération OU exclusif permet d’échanger le contenu


de deux registres sans utiliser de registre temporaire. Le code suivant échange 
le contenu des registres x19 et x20 :

eor x19, x19, x20


eor x20, x19, x20
eor x19, x19, x20

Voyons pourquoi ce code fonctionne. Rappelons que ⊕ est commutatif, que


chaque bit est son propre inverse sous ⊕, et que 0 est l’identité de ⊕. Autrement
CHAPITRE 9. VALEURS BOOLÉENNES ET CHAÎNES DE BITS 89

dit, pour tous x, y ∈ {0, 1}, nous avons:

x ⊕ y = y ⊕ x,
x ⊕ x = 0,
x ⊕ 0 = x.

Ces propriétés sont égalements valables pour l’opération m ⊕ n étendue aux


chaînes de bits m, n ∈ {0, 1}k . Ainsi, si x19 et x20 contiennent initialement les
chaînes de 64 bits m et n, alors nous obtenons:

Contenu après l’exécution de l’opération


Opération
x19 x20
— m n
x19 ← x19 ⊕ x20 m⊕n n
x20 ← x19 ⊕ x20 m⊕n m⊕n⊕n=m⊕0=m
x19 ← x19 ⊕ x20 m⊕n⊕m=n⊕m⊕m=n⊕0=n m

Remarque.

Les opérations bit à bit s’avèrent aussi utiles pour la technique crypto-
graphique du masque jetable où on chiffre et déchiffre des données sim-
plement à l’aide de l’opérateur OU exclusif.

9.3.2 Décalages logiques


Un décalage logique d’une chaîne de bits déplace ses bits dans une certaine di-
rection. Puisque nous considérons des chaînes de taille fixe, certains bits sont
jetés lors du décalage, et les nouveaux bits sont mis à zéro.

Exemple.

Considérons un octet x dont le contenu binaire est 10110101. Un déca-


lage de 3 bits mène aux contenus suivants:

Octet x: 10110101

Décalage de x de 3 bits à droite: 00010110

Décalage de x de 3 bits à gauche: 10101000

La figure 9.2 illustre en général le résultat d’un décalage de j bits d’une


chaîne de n bits x = xn−1 · · · x1 x0
CHAPITRE 9. VALEURS BOOLÉENNES ET CHAÎNES DE BITS 90

xn−1 ··· xj+1 xj xj−1 ··· x0

0 ··· 0 xn−1 ··· xj+1 xj

xn−1 ··· xn−j xn−1−j ··· x1 x0

xn−1−j ··· x1 x0 0 ··· 0

Figure 9.2 – Décalage de j bits à droite (haut) et à gauche (bas).

Sur l’architecture ARMv8, ces opérations peuvent être effectuées sur 64 bits
à l’aide de ces instructions:

instructions effet
lsr xd, xn, j stocke dans xd le décalage de xn de j bits vers la droite
lsl xd, xn, j stocke dans xd le décalage de xn de j bits vers la gauche

Il existe également des variantes 32 bits qui manipulent les registres wd et wn .

Multiplication/division. Les décalages logiques permettent d’implémenter la


multiplication et la division entière non signée par une puissance de 2. En effet,
la multiplication (resp. division entière) par 2k correspond à un décalage logique
de k bits vers la gauche (resp. droite). Ainsi, l’instruction « lsl x19, x19, 3 »
multiplie le registre x19 par 8. Cela s’avère notamment pratique pour le calcul
d’index lors de la manipulation de tableaux. Plusieurs opérations arithmétiques
et modes d’adressage supportent directement les décalages.

9.3.3 Décalages circulaires


Les décalages circulaires se comportent comme les décalages logiques à une ex-
ception près: plutôt que de jeter les bits en trop, ceux-ci sont réinsérés de l’autre
côté de la chaîne.

Exemple.

Reconsidérons l’octet x dont le contenu binaire est 10110101. Un déca-


lage circulaire de 3 bits mène aux contenus suivants:
CHAPITRE 9. VALEURS BOOLÉENNES ET CHAÎNES DE BITS 91

Octet x: 10110101

Décalage de x de 3 bits à droite: 10110110

Décalage de x de 3 bits à gauche: 10101101

La figure 9.3 illustre en général le résultat d’un décalage circulaire de j bits


d’une chaîne de n bits x = xn−1 · · · x1 x0

xn−1 ··· xj+1 xj xj−1 ··· x0

xj−1 ··· x0 xn−1 ··· xj+1 xj

xn−1 ··· xn−j xn−1−j ··· x1 x0

xn−1−j ··· x1 x0 xn−1 ··· xn−j

Figure 9.3 – Décalage circulaire de j bits à droite (haut) et à gauche (bas).

L’architecture ARMv8 ne supporte que les décalages circulaires à droite:

instructions effet
ror xd, xn, j stocke dans xd le décalage circulaire de xn de
j bits vers la droite

Il existe également une variante 32 bits qui manipule les registres wd et wn .

9.3.4 Décalages arithmétiques


Les décalages arithmétiques sont une forme de décalage qui manipulent des en-
tiers signés. Ils se comportent essentiellement comme les décalages logiques,
mais le bit de signe est propagé lors d’un décalage vers la droite.

Exemple.

Considérons l’octet x dont le contenu binaire est 11100101. Un décalage


arithmétique de 2 bits mène aux contenus suivants:
CHAPITRE 9. VALEURS BOOLÉENNES ET CHAÎNES DE BITS 92

Octet x: 11100101

Décalage de x de 2 bits à droite: 11111001

Décalage de x de 2 bits à gauche: 10010100

La figure 9.4 illustre en général le résultat d’un décalage arithmétique de j


bits d’une chaîne de n bits x = xn−1 · · · x1 x0 . Remarquons que vers la gauche il
n’y a pas de distinction entre décalage arithmétique et décalage logique.

xn−1 ··· xj+1 xj xj−1 ··· x0

xn−1 ··· xn−1 xn−1 ··· xj+1 xj

xn−1 ··· xn−j xn−1−j ··· x1 x0

xn−1−j ··· x1 x0 0 ··· 0

Figure 9.4 – Décalage arithmétique de j bits à droite (haut) et à gauche (bas).

L’architecture ARMv8 possède une instruction pour les décalages arithmé-


tiques à droite:

instructions effet
asr xd, xn, j stocke dans xd le décalage arithmétique de xn
de j bits vers la droite

Il existe également une variante 32 bits qui manipule les registres wd et wn .

Multiplication/division. Les décalages logiques permettent d’implémenter la


multiplication et la division entière signée par une puissance de 2. En effet, la
division entière par 2k correspond à un décalage arithmétique de k bits vers la
droite où les nouveaux bits demeurent égaux au bit de signe. Ainsi, l’instruction
« asr x19, x19, 2 » divise le registre x19 par 4.
CHAPITRE 9. VALEURS BOOLÉENNES ET CHAÎNES DE BITS 93

9.4 Masquage
Les opérateurs logiques permettent également d’isoler certains bits d’une chaîne.
Par exemple, l’instruction « and x19, x19, 4 » met tous les bits de xx19 à zéro,
à l’exception du troisième bit de poids faible qui demeure inchangé. En effet,
nous avons:

b63 ··· b3 b2 b1 b0
∧ ··· ∧ ∧ ∧ ∧
0 ··· 0 1 0 0
0 ··· 0 b2 0 0

Dans notre exemple, le nombre 4 est appelé le masque. Un masque permet


de spécifier les bits à isoler. Si nous changeons 4 pour le masque 9 = 10012 dans
l’exemple précédent, alors tous les bits sont mis à zéro à l’exception du premier
et quatrième bits de poids faible:

b63 ··· b3 b2 b1 b0
∧ ··· ∧ ∧ ∧ ∧
0 ··· 1 0 0 1
0 ··· b3 0 0 b0

L’effet du masque varie selon les opérateurs logiques utilisés. Voici certaines
variantes pratiques:

nom opération effet

sélection r∧m met à 0 les bits de r non spécifiés par le masque m


activation r∨m met à 1 les bits de r spécifiés par le masque m
désactivation r ∧ ¬m met à 0 les bits de r spécifiés par le masque m
basculement r⊕m inverse la valeur des bits de r spécifiés par le masque m

Sur l’architecture ARMv8, ces opérations peuvent être effectuées à l’aide des
instructions and, orr, bic et eor respectivement.

Remarque.

En C++, les opérateurs logiques et bit à bit s’écrivent ainsi:


CHAPITRE 9. VALEURS BOOLÉENNES ET CHAÎNES DE BITS 94

Opérateur Logique Bit à bit


négation ¬ ! ~
ET ∧ && &
OU ∨ || |
OU exclusif ⊕ != ^
équivalence ↔ ==
implication → <= (rare)
décalage logique à gauche <<
décalage logique à droite >> (sur type « unsigned »)
décalage arithmétique à droite >> (sur type « signed », dé-
pend de l’implémentation)

9.5 Exemple: cryptographie visuelle


Afin de mettre les concepts en pratique, voyons un exemple simple de chiffre-
ment d’images par manipulation de bits. Par exemple, considérons cette image
A de 4 × 5 pixels, chacun noir ou blanc, qu’on cherche à chiffrer:

Chiffrement. On convertit A vers deux nouvelles images B et C en appliquant


ce processus itérativement pour chaque pixel (i, j):
— si le pixel A[i, j] est blanc, alors on choisit une couleur aléatoire, puis on
assigne cette couleur à B[i, j] et C[i, j];
— si le pixel A[i, j] est noir, alors on choisit une couleur aléatoire, puis on
assigne cette couleur à B[i, j] et la couleur inverse à C[i, j].
Par exemple, ce processus pourrait mener à ces deux images:

image B image C
Si l’on considère noir comme le bit 1 et blanc comme le bit 0, alors on obtient
A[i, j] = B[i, j] ⊕ C[i, j] pour chaque pixel (i, j). En effet, cela découle du fait
que 0 = 0 ⊕ 0 = 1 ⊕ 1 et 1 = 0 ⊕ 1 = 1 ⊕ 0. De plus, les images B et C sont
individuellement indistinguables d’une image aléatoire.
CHAPITRE 9. VALEURS BOOLÉENNES ET CHAÎNES DE BITS 95

Déchiffrement. Si deux personnes (ou deux machines) possèdent B et C res-


pectivement, alors elles détiennent chacune une clé secrète permettant de re-
construire A conjointement. En effet, il suffit de calculer B[i, j] ⊕ C[i, j] pour
chaque pixel (i, j) afin de retrouver les pixels de départ. Autrement dit, on « su-
perpose » les images B et C, puis on applique un OU exclusif à chaque position.

9.5.1 Format PBM


Cette procédure s’implémente assez aisément pour le format d’image PBM. Sous
ce format, une image est codée par une en-tête et un corps. L’en-tête débute par
la chaîne de caractères « P4 » qui spécifie qu’il s’agit d’une image PBM, suivi du
nombre de pixels apparaissant sur une ligne et sur une colonne de l’image. Par
exemple, l’en-tête de l’image A précédente est:

P4
5 4

Le reste du fichier décrit les pixels. Plus précisément, chaque ligne du fichier
dicte les pixels d’une ligne de l’image. Dans notre exemple, nous voulons donc
représenter:
01010
00000
10001
01110

Le format PBM stocke chaque bloc de 8 pixels d’une ligne dans un octet, et,
au besoin, ajoute des zéros afin de compléter le dernier octet d’une ligne. Dans
notre cas, il faut donc ajouter 3 bits à zéro sur chaque ligne, ce qui mène aux
octets suivants:
0x50
0x00
0x88
0x70

9.5.2 Implémentation
Ce programme  implémente la procédure de déchiffrement. Autrement dit, 
il lit consécutivement le contenu de deux images B et C au format PBM, puis
affiche le contenu de l’image A obtenue en calculant A[i, j] = B[i, j] ⊕ C[i, j]
pour chaque pixel (i, j). Afin de manipuler une image, on lit son en-tête:

P4
n m
CHAPITRE 9. VALEURS BOOLÉENNES ET CHAÎNES DE BITS 96

Cela permet d’identifier le nombre d’octets à consommer: m · ((n + 7) ÷ 8).


Ici, la division par 8 correspond au fait que chaque octet contient 8 pixels, et
l’ajout de 7 permet de tenir compte des bits à zéro qui remplissent possiblement
le dernier octet au bout d’une ligne:
— si n est de la forme n = 8k (donc un multiple de huit), alors (n + 7) ÷ 8 =
(8k + 7) ÷ 8 = (8k ÷ 8) + (7 ÷ 8) = k + 0 = k;
— sinon n est de la forme n = 8k + r où 0 < r < 8, et ainsi (n + 7) ÷ 8 =
(8k + r + 7) ÷ 8 = (8k ÷ 8) + (r + 7) ÷ 8 = k + 1.
CHAPITRE 9. VALEURS BOOLÉENNES ET CHAÎNES DE BITS 97

9.6 Quiz récapitulatif


Vous pouvez tester votre compréhension en complétant ce court quiz . Il est
facultatif, anonyme et n’a aucune incidence sur votre note du cours.

9.7 Exercices
9.1) Considérons les chaînes de six bits a = 101101 et b = 101010. Calculez 
¬a, a ∧ b, a ∨ b et a ⊕ b bit à bit. Répétez l’exercice avec d’autres chaînes.

9.2) Expliquez comment effectuer un décalage circulaire vers la gauche à l’aide 


d’un décalage circulaire vers la droite.

9.3) À partir d’un registre xd , comment peut-on obtenir une valeur booléenne 
qui indique si xd contient un nombre pair?

9.4) Supposons que 8n bits soient représentés par un tableau de n octets. Ex- 
pliquez comment extraire le ième bit.

9.5) ⋆⋆ Montrez que le décalage arithmétique à droite implémente bien la 


division par une puissance de 2.

9.6) Si le registre x19 contient l’entier signé −488410 , alors quelle est sa valeur 
après l’exécution de l’instruction « asr x19, x19, 2 »?

9.7) Deux des programmes ci-dessous terminent avec une même valeur n dans 
x19 , alors que l’autre programme termine avec une valeur différente de n
dans x19 . Quelle est la valeur décimale de n?

Programme A Programme B Programme C

mov x19, 5 mov x19, 5 mov x19, 5


ror x19, x19, 1 eor x19, x19, 7 lsl x19, x19, 1
orr x19, x19, 1 lsr x19, x19, 1 and x19, x19, 3
and x19, x19, 7 orr x19, x19, 4 orr x19, x19, 1

9.8) Chaque schéma ci-dessous représente une opération de masquage. Trou- 


vez des opérateurs et masques qui mènent aux résultats. Plus précisément,
remplacez chaque occurrence de ? par un opérateur logique, et chaque
occurrence de ? par 0 ou 1.
CHAPITRE 9. VALEURS BOOLÉENNES ET CHAÎNES DE BITS 98

b4 b3 b2 b1 b0 b4 b3 b2 b1 b0
? ?
? ? ? ? ? ? ? ? ? ?
b4 0 b2 b1 0 ¬b4 b3 b2 b1 ¬b0

b4 b3 b2 b1 b0
?
? ? ? ? ?
b4 1 1 b1 b0
10
Chaînes de caractères

La lecture et l’affichage de symboles lisibles par un humain requiert la manipu-


lation de caractères: lettres, chiffres, signes de ponctuation, émojis, etc. Dans ce
chapitre, nous expliquons comment les représenter et manipuler.
De façon générale, jusqu’à 2n caractères peuvent être représentés à l’aide de
n bits. Par exemple, nous pourrions représenter les lettres de l’alphabet français
à l’aide de 6 bits en choisissant (arbitrairement) que 000000 = a, 000001 = b,
000010 = c, . . ., 011001 = z, 011010 = à, etc. Un tel système se nomme un
codage de caractères. Il existe une myriade de codages de caractères, dont UTF-8
et ISO 8859-1, tous deux basés sur un ancêtre commun: ASCII.

10.1 ASCII
Le codage ASCII (American Standard Code for Information Interchange) utilise
7 bits afin de représenter 128 caractères: les lettres de l’alphabet latin (en mi-
nuscules et majuscules), les chiffres (indo-)arabes, certains symboles mathé-
matiques et de ponctuation, ainsi que des caractères spéciaux. Les caractères
graphiques du codage ASCII sont répertoriés à la figure 10.1. Par exemple, la
lettre « m » est représentée par le code 109 = 6D16 = 11011012 , et le chiffre
« 6 » est représenté par 54 = 3616 = 01101102 .
Les autres caractères qui n’apparaissent pas à la figure 10.1 sont des ca-
ractères spéciaux non graphiques. Par exemple, le code 9 représente une tabu-
lation; le code 10 représente un saut de ligne (\n); le code 13 représente un
retour de chariot (\r); et le code 32 représente une espace.
Notons que le code d’une lettre minuscule se situe à 32 positions de la même
lettre en majuscule. En fait, le codage d’une lettre majuscule et minuscule ne dif-
fère qu’au 6ème bit de poids faible. Par exemple, a = 11000012 et A = 10000012 .

99
CHAPITRE 10. CHAÎNES DE CARACTÈRES 100

code code code


carac. carac. carac.
déc. hex. déc. hex. déc. hex.
33 21 ! 65 41 A 97 61 a
34 22 " 66 42 B 98 62 b
35 23 # 67 43 C 99 63 c
36 24 $ 68 44 D 100 64 d
37 25 % 69 45 E 101 65 e
38 26 & 70 46 F 102 66 f
39 27 ' 71 47 G 103 67 g
40 28 ( 72 48 H 104 68 h
41 29 ) 73 49 I 105 69 i
42 2A * 74 4A J 106 6A j
43 2B + 75 4B K 107 6B k
44 2C , 76 4C L 108 6C l
45 2D - 77 4D M 109 6D m
46 2E . 78 4E N 110 6E n
47 2F / 79 4F O 111 6F o
48 30 0 80 50 P 112 70 p
49 31 1 81 51 Q 113 71 q
50 32 2 82 52 R 114 72 r
51 33 3 83 53 S 115 73 s
52 34 4 84 54 T 116 74 t
53 35 5 85 55 U 117 75 u
54 36 6 86 56 V 118 76 v
55 37 7 87 57 W 119 77 w
56 38 8 88 58 X 120 78 x
57 39 9 89 59 Y 121 79 y
58 3A : 90 5A Z 122 7A z
59 3B ; 91 5B [ 123 7B {
60 3C < 92 5C \ 124 7C |
61 3D = 93 5D ] 125 7D }
62 3E > 94 5E ̂ 126 7E ̃
63 3F ? 95 5F _
64 40 @ 96 60 `

Figure 10.1 – Caractères graphiques du codage ASCII. Chaque caractère est


représenté par un code numérique dans sa forme décimale et hexadécimale.

10.2 ISO 8859-1 (Latin-1)


Bien que l’ASCII permette de représenter l’anglais, ce n’est pas entièrement le
cas pour le français en raison de ses lettres accentuées. Puisque la plus petite
unité de mémoire de la plupart des architectures est l’octet, il est possible d’uti-
liser le 8ème bit inutilisé par l’ASCII afin de représenter 128 caractères régio-
CHAPITRE 10. CHAÎNES DE CARACTÈRES 101

naux supplémentaires. Historiquement, une foule de codages ont été créés afin
d’étendre l’ASCII. En particulier, le codage ISO 8859-1 (Latin-1) étend l’ASCII
avec suffisamment de caractères pour la représentation du français et de plu-
sieurs langues indo-européennes dont le système d’écriture se base sur le latin.
Les caractères graphiques additionnels du codage ISO 8859-1 sont répertoriés
à la figure 10.2.

code code code


carac. carac. carac.
déc. hex. déc. hex. déc. hex.
161 A1 ¡ 192 C0 À 224 E0 à
162 A2 ¢ 193 C1 Á 225 E1 á
163 A3 £ 194 C2 Â 226 E2 â
164 A4 ¤ 195 C3 Ã 227 E3 ã
165 A5 ¥ 196 C4 Ä 228 E4 ä
166 A6 ¦ 197 C5 Å 229 E5 å
167 A7 § 198 C6 Æ 230 E6 æ
168 A8 ¨ 199 C7 Ç 231 E7 ç
169 A9 © 200 C8 È 232 E8 è
170 AA ª 201 C9 É 233 E9 é
171 AB « 202 CA Ê 234 EA ê
172 AC ¬ 203 CB Ë 235 EB ë
173 AD 204 CC Ì 236 EC ì
174 AE ® 205 CD Í 237 ED í
175 AF ¯ 206 CE Î 238 EE î
176 B0 ° 207 CF Ï 239 EF ï
177 B1 ± 208 D0 Ð 240 F0 ð
178 B2 ² 209 D1 Ñ 241 F1 ñ
179 B3 ³ 210 D2 Ò 242 F2 ò
180 B4 ´ 211 D3 Ó 243 F3 ó
181 B5 µ 212 D4 Ô 244 F4 ô
182 B6 ¶ 213 D5 Õ 245 F5 õ
183 B7 214 D6 Ö 246 F6 ö
184 B8 ¸ 215 D7 × 247 F7 ÷
185 B9 ¹ 216 D8 Ø 248 F8 ø
186 BA º 217 D9 Ù 249 F9 ù
187 BB » 218 DA Ú 250 FA ú
188 BC ¼ 219 DB Û 251 FB û
189 BD ½ 220 DC Ü 252 FC ü
190 BE ¾ 221 DD Ý 253 FD ý
191 BF ¿ 222 DE Þ 254 FE þ
223 DF ß 255 FF ÿ

Figure 10.2 – Caractères graphiques du codage ISO 8859-1. Chaque caractère


est représenté par un code numérique dans sa forme décimale et hexadécimale.
CHAPITRE 10. CHAÎNES DE CARACTÈRES 102

10.3 UTF-8
Bien que le format ISO 8859-1 soit suffisant pour plusieurs langues européennes,
il ne permet pas de représenter d’autres langues comme le grec, l’arabe, l’hé-
breu, le japonais et les langues chinoises. Afin de palier ce problème, le standard
Unicode spécifie plus de 143 859 caractères (et permet d’en accommoder plus
d’un million). Chaque caractère est associé à un code numérique, appelé « code
point » en anglais. Le codage UTF-8 utilise jusqu’à 21 bits afin de représenter
les caractères spécifiés par Unicode. Contrairement aux codages ASCII et ISO
8859-1, le codage UTF-8 code les caractères avec un nombre variable d’octets:
1, 2, 3 ou 4 selon le caractère.
Pour des raisons de rétrocompatibilité et d’économie de mémoire, les 128
premiers caractères de l’UTF-8 sont précisément ceux de l’ASCII codés sur un
seul octet. Les 128 caractères suivants sont ceux de l’ISO 8859-1 codés sur deux
octets. Par exemple, le caractère « é » codé par E916 = 111010012 sous le codage
ISO 8859-1, est codé par C3A916 = 11000011 101010012 sous le codage UTF-8.
Le format général de l’UTF-8 se décrit ainsi [Yer03]:

plage de codes 1 format binaire des octets


# bits
début fin octet 1 octet 2 octet 3 octet 4
7 00000016 00007F16 0******* — — —
11 00008016 0007FF16 110***** 10****** — —
16 00080016 00FFFF16 1110**** 10****** 10****** —
21 01000016 10FFFF16 11110*** 10****** 10****** 10******

Exemple.

Les caractères a, é, et 𒐍 possèdent les codes 6116 , E916 , 30B116 et


1240D16 respectivement. Leur codage UTF-8 s’obtient donc ainsi:

car. code codage


a 6116 = 11000012 011000012
é E916 = 000 111010012 11000011 101010012
30B116 = 00110000 101100012 11100011 10000010 101100012
𒐍 1240D16 = 00001 00100100 000011012 11110000 10010010 10010000 100011012

Remarque.

Il existe des codages alternatifs pour le standard Unicode, par ex. UTF-16
et UTF-32. L’UTF-32 code tous les caractères sur 4 octets, ce qui permet
notamment d’accéder rapidement au ième caractère d’une suite de carac-

1. Pour des raisons techniques, les codes D80016 à DFFF16 sont considérés invalides et ne re-
présentent donc aucun caractère.
CHAPITRE 10. CHAÎNES DE CARACTÈRES 103

tères. L’UTF-16 propose un codage à taille variable sur 2 et 4 octets, ce


qui offre un compromis entre UTF-8 et UTF-32.

10.4 Chaînes de caractères


Une chaîne de caractères est une suite finie de caractères. Dans les codages pré-
sentés plus tôt, on indique normalement la fin d’une chaîne par le caractère nul
spécifié par le code 0016 . Ainsi, la chaîne "Allo" est représentée par la suite
d’octets 0x41 0x6C 0x6C 0x6F 0x00
Le parcours d’une chaîne de n caractères ASCII ou ISO 8859-1 s’effectue
similairement à celle d’un tableau, puisqu’une telle chaîne correspond précisé-
ment à un tableau de n+1 octets. Cependant, dans le cas d’UTF-8, la position du
ième caractère d’une chaîne ne se calcule pas directement, puisque chacun des
caractères peut prendre 1 à 4 octets en mémoire. Il faut donc, pour chaque ca-
ractère, vérifier s’il débute par 0, 110, 1110 ou 11110 afin de déterminer s’il faut
consommer 1, 2, 3 ou 4 octets. Cette vérification peut s’implémenter à l’aide de
décalages de bits ou d’opérations de masquage.
Sur l’architecture ARMv8, une chaîne de caractères (statique) se déclare à
l’aide de « .asciz », alors qu’une suite de caractères, sans caractère nul de fin,
se déclare à l’aide de « .ascii ».
Remarque.

Dans notre cas, une chaîne de caractères lue dans un terminal avec scanf
est codée sous UTF-8. Si seules des lettres du codage ASCII sont entrées,
alors on obtient une chaîne sous codage ASCII (avec un 0 au 8ème bit).

Remarque.

On peut lire/afficher une chaîne avec scanf/printf à l’aide du format


"%s". Si l’on désire lire une chaîne avec des espaces, alors on peut utiliser
le format "%[^\n]".

— xkcd c
CHAPITRE 10. CHAÎNES DE CARACTÈRES 104

10.5 Quiz récapitulatif


Vous pouvez tester votre compréhension en complétant ce court quiz . Il est
facultatif, anonyme et n’a aucune incidence sur votre note du cours.

10.6 Exercices
10.1) Donnez le codage UTF-8 des caractères associés à ces codes numériques: 
0006AB16 , 00007316 , 01234516 et 00A0B116 .

10.2) Écrivez un sous-programme qui reçoit une chaîne de caractères sous co- 
dage ASCII et qui retourne sa taille (excluant son caractère nul).

10.3) Écrivez un sous-programme qui reçoit une chaîne de caractères sous co- 
dage ASCII, ainsi que sa taille, et qui retourne une valeur booléenne indi-
quant si la chaîne est un palindrome.

10.4) Écrivez un sous-programme qui reçoit une chaîne de caractères et qui re- 
tourne une valeur booléenne indiquant si son premier caractère fait partie
du codage ASCII.

10.5) Supposons que x19 contienne le code ASCII d’une lettre. Donnez une seule
instruction qui inverse la casse de x19 ; autrement dit, une lettre minuscule
doit être mise en majuscule et vice-versa.

10.6) Considérons le codage de caractères fictif IZO-IFT209. Ce codage permet 


de représenter les 2048 premiers caractères du standard Unicode. En par-
ticulier, les 256 premiers caractères codés par l’IZO-IFT209 sont respecti-
vement ceux de l’ASCII et de l’ISO 8859-1 (Latin-1). Les caractères sont
codés sur un à deux octets selon le format suivant:

plage de codes numériques codage IZO-IFT209


début fin octet 1 octet 2
00016 07F16 1******* —
08016 7FF16 01****** 001*****

Rappelons que chaque caractère possède un code numérique et un codage.


Par exemple, le code numérique du caractère « a » est 6116 , et son codage
est donc « 11100001 »; le code numérique du caractère « é » est E916 , et
son codage est donc « 01000111 00101001 ».
— Donnez le codage IZO-IFT209 du caractère « ü » dont le code numé-
rique est FC16 .
— Combien de caractères sont contenus dans la chaîne de caractères
IZO-IFT209 ci-dessous?
CHAPITRE 10. CHAÎNES DE CARACTÈRES 105

10100011 01111101 00101011 01111111 00101111 10001001 10101101 11000000

— Écrivez un sous-programme qui accomplit la tâche suivante:

Entrée: codage IZO-IFT209 d’un caractère c


Sortie: code numérique de c

Puisqu’un caractère IZO-IFT209 est codé sur au plus 16 bits et que


les registres contiennent 64 bits, nous supposons que les bits excé-
dentaires de poids fort sont égaux à 0. Voici des exemples d’entrées
et de sorties du sous-programme:

Entrée (64 bits) Valeur de retour (64 bits)


00 · · · 0 00000000 111000012 00 · · · 0 00000000 011000012
00 · · · 0 01000111 001010012 00 · · · 0 00000000 111010012

Rappel: il est possible de spécifier une valeur hexadécimale avec le préfixe « 0x »,


par ex.: « mov x19, 0x3FA ».

10.7) ⋆ Supposons que x19 contienne l’adresse d’une chaîne de caractères s 


sous codage UTF-32, et que x20 contienne un indice i. Donnez une seule
instruction qui charge le ième caractère de s dans x21 .
11
Sous-programmes et mémoire

Tel que discuté au chapitre 7, les programmes sont normalement modularisés en


sous-routines. En langage d’assemblage, une sous-routine est implémentée sous
forme de sous-programme: un segment de code appelé avec un branchement.
Dans ce chapitre, nous revisitons leur fonctionnement plus en détail.

11.1 Pile d’exécution

11.1.1 Appels de sous-programmes


Sur l’architecture ARMv8, les arguments d’un sous-programme sont passés dans
les registres x0 à x7 . Cependant, certaines architectures ne possèdent pas de
registres dédiés à cette tâche. De plus, un sous-programme pourrait technique-
ment posséder plus de huit paramètres.
Rappelons également que certains registres, comme les registres x19 à x29
sous ARMv8, doivent être préservés lors de l’appel d’un sous-programme. De
plus, l’adresse de retour doit être préservée d’une certaine façon. Sur ARMv8,
le registre x30 joue ce rôle, mais ne peut stocker qu’une seule adresse à la fois.
Ainsi, une séquence d’appels de sous-programmes peut engendrer une quan-
tité arbitraire de données qui doivent être stockées temporairement en dehors
des registres. Ces données sont stockées dans un segment de la mémoire prin-
cipale nommé la pile d’exécution. Ce segment porte ce nom puisqu’il est utilisé
comme une pile: des données y sont empilées et dépilées.

11.1.2 Disposition de la mémoire


Afin de comprendre le fonctionnement de la pile d’exécution, voyons d’abord
brièvement l’organisation de la mémoire d’un programme. Lors du chargement
d’un programme, ses instructions et ses données statiques sont stockées dans un
segment fixe de la mémoire principale. Un segment de données est également
alloué pour la manipulation de données dynamiques, c’est-à-dire des données

106
CHAPITRE 11. SOUS-PROGRAMMES ET MÉMOIRE 107

00000 · · · 000016

Instructions (text)

Données statiques Initialisées, en lecture seule (rodata)

Initialisées (data)

Non initialisées (bss)

Tas
Données dynamiques

↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓

↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ sp (pointeur de pile)

Pile

FFFF · · · FFFF16

Figure 11.1 – Disposition de la mémoire d’un programme.

dont la taille demeure inconnue à l’assemblage. Ce segment se divise en deux


sous-segments:
— le tas: qui contient les données allouées dynamiquement afin de stocker
des structures de données, des objets, etc.;
— la pile d’exécution: qui contient les données temporaires des appels de
sous-programmes.
Par convention, lors de l’ajout de données, les adresses du tas croissent, alors
que celles de la pile décroissent. Cette organisation de la mémoire est illustrée
à la figure 11.1.
CHAPITRE 11. SOUS-PROGRAMMES ET MÉMOIRE 108

Remarque.

Les variables locales d’une fonction en C/C++ sont généralement stockées


sur la pile d’exécution. On peut allouer de la mémoire dynamiquement
sur le tas avec « new » en C++ et avec « malloc » en C.

11.1.3 Fonctionnement de la pile


À tout moment, le pointeur de pile sp contient l’adresse de l’élément situé au
sommet de la pile. Initialement, la pile est vide et ainsi sp pointe vers la dernière
cellule mémoire de la pile, par ex. l’adresse FFFF · · · FFFF16 .
Par convention, le pointeur de pile décroît vers 0000 · · · 000016 lorsque des
données y sont ajoutées. Ainsi, afin d’empiler k octets sur la pile d’exécution,
sp est décrémenté de k adresses, puis les octets sont stockés à l’adresse sp.
Similairement, afin de dépiler k octets, ceux-ci sont lus à l’adresse sp, puis sp
est incrémenté de k adresses.

11.1.4 Sauvegarde et restauration


Nous sommes maintenant en mesure d’expliquer le fonctionnement des macros
de sauvegarde et de restauration de registres ARMv8 présentées au chapitre 7.

Sauvegarde. Considérons d’abord la macro de sauvegarde:

.macro SAVE
stp x29, x30, [sp, -96]!
mov x29, sp
stp x27, x28, [sp, 16]
stp x25, x26, [sp, 32]
stp x23, x24, [sp, 48]
stp x21, x22, [sp, 64]
stp x19, x20, [sp, 80]
.endm

L’instruction « stp xd, xn, a » sauvegarde le contenu de xd et xn consécu-


tivement à l’adresse a. Il s’agit donc essentiellement de deux appels consécutifs
à str. Toutefois, l’architecture ARMv8 requiert que sp soit un multiple de 16 en
tout temps. Ainsi, l’usage de stp garantit la satisfaction de cette contrainte.
Décortiquons la première ligne de SAVE: « stp x29, x30, [sp, -96]! ».
Cette instruction décrémente d’abord la valeur de sp de 96. Cela correspond à
l’allocation de 12 double mots afin de stocker le contenu des 12 registres: x19
à x30 . L’instruction stocke ensuite le contenu de x29 et x30 à l’adresse sp, donc
au-dessus de la pile. Le contenu des autres registres est stocké dans les double
mots suivants. Ainsi, après l’exécution de SAVE, la pile est organisée ainsi:
CHAPITRE 11. SOUS-PROGRAMMES ET MÉMOIRE 109

sp
x29
sp + 8
x30
sp + 16
x27
sp + 24
x28
.. ..
. .
sp + 80
x19
sp + 88
x20
(ancien sommet) sp + 96

contenu antérieur
Notons que l’instruction « mov x29, sp » a pour but de stocker un pointeur
vers l’information qui permet de restaurer l’ancien sommet de la pile. Cela est
requis par la convention d’appel d’ARMv8 [ARM13, Sec. 5.2.3]. La portion de
la mémoire située entre x29 et l’ancien sommet se nomme le bloc d’activation
de l’appel. En particulier, l’adresse stockée dans x29 permet de rétablir la pile.

Restauration. La macro de restauration est symétrique à la macro de sauve-


garde:

.macro RESTORE
ldp x27, x28, [sp, 16]
ldp x25, x26, [sp, 32]
ldp x23, x24, [sp, 48]
ldp x21, x22, [sp, 64]
ldp x19, x20, [sp, 80]
ldp x29, x30, [sp], 96
.endm

L’instruction « ldp xd, xn, a » charge le contenu des adresses a et a + 8


dans xd et xn respectivement. Ainsi, les registres sont restaurés et la dernière
instruction incrémente sp de 96 adresses afin de libérer les 96 octets au sommet
de la pile.

11.2 Récursion
La pile d’exécution peut être utilisée afin d’implémenter des sous-programmes
récursifs: c’est-à-dire des sous-programmes qui s’appellent eux-mêmes. À titre
CHAPITRE 11. SOUS-PROGRAMMES ET MÉMOIRE 110

d’exemple, considérons la suite de Fibonacci F0 , F1 , F2 , . . . définie par:




0 si n = 0,
Fn = 1 si n = 1,


Fn−1 + Fn−2 si n ≥ 2.
Les premiers termes de cette suite sont: 0, 1, 1, 2, 3, 5, 8, 13, 21, . . ..
Le sous-programme suivant calcule la valeur de Fn à partir de l’argument n: 
fib: // fib(n)
SAVE // {
mov x19, x0 //
cmp x19, 2 // if (n >= 2)
b.lo fin // {
//
sub x0, x19, 1 //
bl fib //
mov x20, x0 // r = fib(n - 1)
//
sub x0, x19, 2 //
bl fib //
add x0, x20, x0 // n = r + fib(n - 2)
fin: // }
RESTORE //
ret // return n
// }

Le code ci-dessus utilise les macros SAVE et RESTORE. Ainsi, chaque ap-
pel ajoute 96 octets à la pile d’exécution. Plusieurs de ces octets sont inutiles
puisque la plupart des registres sont inutilisés.
Le code suivant utilise trois fois moins de mémoire en ajoutant 32 octets 
plutôt que 96 octets:
fib: // fib(n)
// Sauvegarder environnement // {
stp x29, x30, [sp, -32]! //
mov x29, sp //
stp x19, x20, [sp, 16] //
//
// Calculer fib(n) //
mov x19, x0 //
cmp x19, 2 // if (n >= 2)
b.lo fin // {
//
sub x0, x19, 1 //
bl fib //
mov x20, x0 // r = fib(n - 1)
CHAPITRE 11. SOUS-PROGRAMMES ET MÉMOIRE 111

//
sub x0, x19, 2 //
bl fib //
add x0, x20, x0 // n = r + fib(n - 2)
fin: // }
// Restaurer environnement //
ldp x29, x30, [sp], 16 //
ldp x19, x20, [sp], 16 //
ret // return n
// }

11.3 Limitations
La taille de la pile d’exécution est bornée par la taille de la mémoire principale,
et elle est souvent restreinte à une taille plus petite par le système d’exploitation.
Ainsi, une récursion trop profonde peut mener à une erreur de segmentation, et
plus précisément à un débordement de pile. En effet, si la pile se remplit lors
d’un appel, l’appel suivant tentera d’écrire en mémoire en dehors de la pile.

Remarque.

Le célèbre site Web de questions et réponses Stack Overflow tire son nom
du terme anglais signifiant « débordement de pile ».
CHAPITRE 11. SOUS-PROGRAMMES ET MÉMOIRE 112

11.4 Quiz récapitulatif


Vous pouvez tester votre compréhension en complétant ce court quiz . Il est
facultatif, anonyme et n’a aucune incidence sur votre note du cours.

11.5 Exercices

11.1) Écrivez un sous-programme qui reçoit un entier n ≥ 0 en entrée et qui 


calcule récursivement la somme 1+2+. . .+n, où par convention la somme
vaut 0 si n = 0.

11.2) Si sp contient AF0916 et que l’on empile le contenu de quatre registres sur 
la pile d’exécution, que devient sp? Puis, que devient sp si l’on en dépile
deux?

11.3) L’instruction « stp » empile le contenu de deux registres, ce qui nous force 
à empiler un nombre pair de double mots. Comment peut-on gérer l’em-
pilement d’un nombre impair de double mots?

11.4) Implémentez l’algorithme récursif suivant sous forme de sous-programme: 

Entrée: entier non signé n de 64 bits


Retour: entier non signé de 64 bits valant 2n
exp(n):
si n = 0 alors
retourner 1
sinon
r ← exp(n ÷ 2) · exp(n ÷ 2)
si n est pair alors
retourner r
sinon
retourner 2 · r

Vous n’avez pas à gérer les débordements. Vous pouvez utiliser les macros
SAVE et RESTORE.

11.5) Supposons que vous n’ayez pas accès aux macros SAVE et RESTORE. Consi- 
dérons un sous-programme qui effectue des appels récursifs et qui utilise
uniquement les registres x19 , x20 , x23 et x25 . Complétez le code des macros
suivantes afin d’implémenter la sauvegarde et la restauration des registres
spécifiquement pour ce sous-programme:
CHAPITRE 11. SOUS-PROGRAMMES ET MÉMOIRE 113

.macro SAUVEGARDER .macro RESTAURER


// Code ici /*
mov x29, sp Code ici
/* */
Code ici .endm
*/
.endm

11.6) Le programme ci-dessous affiche les nombres de 1 à 10, puis boucle à l’in- 
fini plutôt que de terminer. Expliquez pourquoi le programme ne termine
pas. Plus précisément, identifiez les étiquettes qui sont atteintes infini-
ment souvent et la raison pour laquelle elles le sont.

.global main

main:
main0: bl foo
main1: bl exit

foo:
foo0: mov x19, 0
foo1: add x19, x19, 1
foo2: adr x0, fmt
foo3: mov x1, x19
foo4: bl printf
foo5: cmp x19, 10
foo6: b.lo foo1
foo7: ret

.section ".rodata"
fmt: .asciz "%lu\n"

11.7) Implémentez cet algorithme sous forme de sous-programme sans les ma-
cros SAVE et RESTORE: 
Entrée: tableau d’entiers signés de 64 bits spécifié par une adresse t et
un nombre d’éléments n; un entier signé de 64 bits x
Retour: 1 si le tableau contient x, 0 sinon
elem(t, n, x):
si n = 0 alors
retourner 0 // tableau vide, donc forcément faux
sinon
u ← adresse de l’élément à l’indice 1 du tableau
retourner (t[0] = x) ∨ elem(u, n − 1, x)
CHAPITRE 11. SOUS-PROGRAMMES ET MÉMOIRE 114

11.8) Implémentez cet algorithme sous forme de sous-programme sans les ma- 
cros SAVE et RESTORE:

Entrée: tableau d’entiers signés de 64 bits spécifié par une adresse t et


un nombre d’éléments n; un entier signé de 64 bits x
Retour: 1 si le tableau contient x, 0 sinon
elem-rev(t, n, x):
si n = 0 alors
retourner 0 // tableau vide, donc forcément faux
sinon
retourner (t[n − 1] = x) ∨ elem-rev(t, n − 1, x)
12
Nombres en virgule flottante

Nous avons vu jusqu’ici comment manipuler les entiers sur un ordinateur. Bien
que ce type élémentaire numérique soit suffisant pour une foule d’applications,
il ne l’est pas nécessairement pour d’autres comme la simulation, le calcul scien-
tifique, l’infographie, le traitement de signal, l’apprentissage automatique et les
méthodes numériques de la recherche opérationnelle. Dans ce chapitre, nous
considérons donc la représentation et la manipulation de nombres réels. Soient
a, b ∈ R des nombres réels tels que a < b. L’intervalle [a, b] contient une in-
finité de nombres. Ainsi, il est impossible de représenter tous les nombres de
l’intervalle [a, b] à l’aide d’un ordinateur, comme nous l’avons fait pour N et
Z. Nous étudions donc la représentation en nombres en virgule flottante qui
permet d’approximer raisonnablement les nombres réels.

12.1 Représentation
Un nombre en virgule flottante est un nombre de la forme
exposant
signe
z}|{ z}|{
± d0 ,d1 d2 · · · dn−1 × β e
| {z } |{z}
mantisse base

où chaque composante est un entier non négatif, et où la mantisse est un nombre


fractionnaire en base β ≥ 2.

Exemple.

Ces nombres en virgule flottante:


−0, 5142 × 103 1, 0567 × 10−2 1, 011 × 22
possèdent respectivement ces valeurs décimales:
−514,2 0,010567 5,5.

115
CHAPITRE 12. NOMBRES EN VIRGULE FLOTTANTE 116

Il existe généralement plusieurs représentations d’une même valeur. Par


exemple, 0,123 × 102 et 1,23 × 101 représentent tous deux la valeur 12,3. Nous
disons qu’un nombre en virgule flottante est normalisé si d0 ̸= 0; autrement dit,
si le premier chiffre de sa mantisse est non nul. Ainsi, 0,123 × 102 n’est pas nor-
malisé, alors que 1,23×101 est normalisé. La forme normalisée d’un nombre non
nul offre une représentation unique par rapport à une base β. Notons cependant
que 0 ne peut pas être normalisé.
Si emin ≤ e ≤ emax , alors la quantité de nombres normalisés représentables
avec une certaine base β et une mantisse de taille n est:
2 · (β − 1) · β n−1 · (emax − emin + 1) .
|{z} | {z } | {z } | {z }
± d0 d1 ,...,dn−1 e

La plus petite valeur absolue xmin et la plus grande valeur absolue xmax repré-
sentables par un nombre normalisé sont:
xmin = 1,0 · · · 0 × β emin xmax = (β − 1),(β − 1) · · · (β − 1) × β emax
= β emin , = (β − 1)(β − 1) · · · (β − 1),0 × β emax −(n−1)
= (β n − 1) · β emax −n+1
= β emax +1 − β emax +1−n .

Exemple.

Pour β = 2, n = 3 et −1 ≤ e ≤ 1, il y a 24 nombres normalisés représen-


tables:

−3 ⁄2
7
−2 −3⁄2 ⁄4
7 ⁄2
5
−1 −3⁄4 −1⁄2 5
⁄8 ⁄8
7 ⁄4
5

−5⁄4 −7⁄8 −5⁄8 ⁄2


1
⁄4 1
3
−5⁄2
−7⁄4 ⁄2
3
2
−7⁄2 3

Remarquons que plus les nombres s’approchent de 0, plus la distance entre


ceux-ci est petite. En effet, les nombres positifs de l’intervalle de l’exemple ci-
dessus se réécrivent ainsi:
4
⁄8, 5⁄8, 6⁄8, 7⁄8, 4⁄4, 5⁄4, 6⁄4, 7⁄4, 4⁄2, 5⁄2, 6⁄2, 7⁄2 .
| {z } | {z } | {z }
bonds de 1⁄8 bonds de 1⁄4 bonds de 1⁄2

Les nombres en virgule flottante permettent donc de représenter des valeurs de


différents ordres de grandeur.

12.2 Précision
Puisque la vaste majorité 1 des nombres réels ne sont pas représentables par
un nombre en virgule flottante, ceux-ci doivent être approximés. Par exemple,
1. En fait, il y en a une infinité non dénombrable!
CHAPITRE 12. NOMBRES EN VIRGULE FLOTTANTE 117

considérons la base décimale (β = 10) et une mantisse de n = 4 chiffres. Le


nombre x = 1,54163 ne peut pas être représenté exactement. Il existe plusieurs
façons d’arrondir x. Par exemple, x peut être arrondi au nombre en virgule
flottante le plus près (1,542) ou à sa troncation (1,541).
Sous la première méthode, x peut se situer à distance égale de deux nombres.
Cela se produit lorsqu’on prend une décision selon le chiffre β/2 suivi d’aucun
chiffre ou de zéros non significatifs. Dans ce cas, la façon la plus répandue de
briser l’égalité consiste à arrondir au nombre le plus près dont le dernier chiffre
est pair, par ex. 1,9565000 est arrondi à 1,956, et non à 1,957. Cette approche
a pour avantage de ne pas favoriser les approximations vers le haut à chaque
bris d’égalité, ce qui pourrait autrement amplifier les erreurs lors de plusieurs
calculs successifs. Voici d’autres exemples de cette méthode pour n = 4:

décimal binaire

1,9565 → 1,956 1,0101 → 1,010


1,9555 → 1,956 1,0011 → 1,010
1,95607 → 1,956 1,01001 → 1,010
1,95670 → 1,957 1,01011 → 1,011

12.2.1 Erreur d’approximation


Fixons emin , emax , n et β. Pour tout nombre x ∈ R \ {0}, nous écrivons x afin
de dénoter le nombre en virgule flottante normalisé arrondi au nombre le plus
près de x, et où un bris d’égalité se fait vers le nombre dont le dernier chiffre
est pair. Autrement dit, x est l’approximation de x selon la méthode d’arrondi
que nous venons d’introduire. L’erreur absolue de x est x − x, et l’erreur relative
de x est:
x−x
err(x) := .
x
L’erreur absolue donne la différence entre une valeur réelle et son approxi-
mation en nombre en virgule flottante. L’erreur relative décrit le rapport entre
l’erreur absolue et la valeur, elle indique donc l’importance de l’erreur.
Posons ε := β2 · β −n . Nous appelons la constante ε l’epsilon machine. L’erreur
relative d’un nombre est toujours d’au plus ±ε. En effet, nous avons:
Proposition 4. |err(x)| ≤ ε pour tout x ∈ R \ {0} tel que xmin ≤ |x| ≤ xmax . 
−n n
Dans le cas particulier de β = 2, l’epsilon machine vaut ε = 2 = 1/2 . Ainsi,
l’erreur relative est exponentiellement plus petite que la taille de la mantisse.

Exemple.

Reconsidérons le cas où β = 2, n = 3 et −1 ≤ e ≤ 1. L’epsilon machine


vaut ε = 2−3 = 1/8. Ainsi, l’erreur relative d’une approximation est d’au
plus ±1/8. Par exemple, considérons x = 1,1010011×21 . Ce nombre n’est
pas représentable avec une mantisse de taille n = 3. Sa valeur décimale
CHAPITRE 12. NOMBRES EN VIRGULE FLOTTANTE 118

est 3,296875, ce qui est compris entre 3 et 3,5. Ainsi, x = 1,11 × 21 .


Autrement dit, x est arrondi à 3,5 et par conséquent:

3,296875 − 3,5 0,25


|err(x)| = ≤ = 1/13 ≤ 1/8.
3,296875 3,25

Remarque.

Les erreurs se définissent de la même façon pour la troncation. Il serait


aussi possible d’obtenir une borne semblable à celle de la proposition 4.

12.3 Arithmétique
Voyons comment calculer la somme et le produit de deux nombres normalisés:

x = 1,u × β e ,
y = 1,v × β f .

Nous ne couvrirons pas la gestion des signes. Elle s’accomplit en inspectant le


bit de signe et en effectuant des compléments à deux au besoin.

12.3.1 Addition
Supposons que e ≤ f . Si ce n’est pas le cas, on peut simplement inverser x et y.
Observons que:

x + y = (1,u × β e ) + (1,v × β f )
= (1,u · β e−f × β f ) + (1,v × β f )
= (1,u · β e−f + 1,v) × β f .

L’addition se calcule donc en mettant les exposants en commun à β f , puis en


additionant les mantisses. La mise en commun des exposants peut dénormaliser
le nombre avec le plus petit exposant. Il faut donc renormaliser après l’addition.
Ainsi, pour obtenir la somme, il faut:
1. Mettre les exposants en commun en décalant la première mantisse de f −e
positions;
2. Additionner les mantisses;
3. Normaliser le résultat en décalant la mantisse tout en incrémentant ou
décrémentant l’exposant selon la direction;
4. Arrondir le résultat.
CHAPITRE 12. NOMBRES EN VIRGULE FLOTTANTE 119

Exemple.

Reconsidérons à nouveau le cas où β = 2, n = 3 et −1 ≤ e ≤ 1. Addi-


tionnons 1⁄2 et 7⁄4. La représentation normalisée de ces deux nombres est
x = 1,00 × 2−1 et y = 1,11 × 20 . Nous avons donc:

x + y = (1,00 × 2−1 ) + (1,11 × 20 )


= (0,100 × 20 ) + (1,11 × 20 )
= (0,100 + 1,11) × 20
= 10,010 × 20
= 1,0010 × 21
≈ 1,00 × 21 .

Ainsi, l’addition mène à 2,0 comme approximation de la valeur exacte


1
⁄2 + 7⁄4 = 0,5 + 1,75 = 2,25.

12.3.2 Multiplication
Observons que:

x · y = (1,u × β e ) · (1,v × β f )
= (1,u · 1,v) × β e+f .

Le produit s’obtient donc en multipliant les mantisses et en additionnant les


exposants. Toutefois, la multiplication des mantisses peut engendrer un nombre
non normalisé. Il faut donc procéder ainsi:
1. Additionner les exposants;
2. Multiplier les mantisses;
3. Normaliser le résultat en décalant la mantisse tout en incrémentant ou
décrémentant l’exposant selon la direction;
4. Arrondir le résultat.

Exemple.

Reconsidérons à nouveau le cas où β = 2, n = 3 et −1 ≤ e ≤ 1. Mul-


tiplions 3⁄4 et 7⁄2. La représentation normalisée de ces deux nombres est
CHAPITRE 12. NOMBRES EN VIRGULE FLOTTANTE 120

x = 1,10 × 2−1 et y = 1,11 × 21 . Nous avons donc:

x · y = (1,10 × 2−1 ) · (1,11 × 21 )


= (1,10 · 1,11) × 20
= 10,101 × 20
= 1,0101 × 21
≈ 1,01 × 21 .

Ainsi, la multiplication mène à 2,5 comme approximation de la valeur


exacte 3⁄4 · 7⁄2 = 0,75 · 3,5 = 2,625.

12.4 Norme IEEE 754


La norme IEEE 754 définit des formats de nombres en virgule flottante en base 2
et en base 10 utilisés par la grande majorité des architectures modernes. Nous
nous concentrons sur les formats binaires de précision simple (32 bits) et préci-
sion double (64 bits) 2 :

format β n emin emax


simple 2 24 −126 127
double 2 53 −1022 1023

Leurs plus petits et plus grands nombres positifs normalisés sont donc:

format xmin xmax


−126
simple 2 2128
− 2104 ≈ 2128
double 2−1022 21024 − 2971 ≈ 21024

Selon la méthode d’arrondi avec bris d’égalité, l’epsilon machine de chacun de


ces formats est:

format ϵ
simple 2−24 = 0,000000059604644775390625
double 2−53 = 0,00000000000000011102230246251565404236316680908203125

2. Dans la version 2008 de la norme IEEE 754, ces deux formats sont officiellement nommés
binary32 et binary64, respectivement. Nous utilisons plutôt la nomenclature traditionnelle simple
et double afin de référer à ces deux formats.
CHAPITRE 12. NOMBRES EN VIRGULE FLOTTANTE 121

12.4.1 Codage des formats


Les formats de précisions simple et double sont codés avec 32 et 64 bits, res-
pectivement. Leurs bits sont répartis ainsi en mémoire:

format signe exposant mantisse


simple 1 bit 8 bits 23 bits (+1 bit caché)
double 1 bit 11 bits 52 bits (+1 bit caché)

Nombres normalisés. Décortiquons la valeur d’un nombre en virgule flot-


tante de précision simple (le format double est analogue):
bit de signe
z}|{
b31 b30 b29 · · · b23 b22 b21 · · · b0 .
| {z }| {z }
exposant mantisse

Le bit de signe b31 vaut 1 si le nombre est négatif, et 0 sinon.


Les bits b22 b21 · · · b0 représentent les bits situés après la virgule de la man-
tisse, autrement dit: 1,b22 b21 · · · b0 . Ainsi, la mantisse possède 24 bits, mais le
premier bit, nommé le bit caché, vaut implicitement 1 et n’est donc pas stocké.
Les bits b30 b29 · · · b23 codent e+127 sous forme d’entier non signé; autrement
dit, l’exposant plus un biais de 127. Les chaînes de bits 00000000 et 11111111
ne sont pas permises pour représenter un exposant, elles sont plutôt réservées
à d’autres fins.
Exemple.

Voici trois exemples de représentation d’exposant:

00000001 représente e = 1 − 127 = −126,


00100011 représente e = 35 − 127 = −92,
11111110 représente e = 254 − 127 = 127.

Zéro. Le nombre 0, qu’on ne peut pas normaliser, est représenté par un bit de
signe suivi de tous les autres bits assignés à 0:
b31 00 · · · 000 · · · 0.
Ainsi, il existe deux zéros: −0 et +0 qui valent tous deux 0.

Infini. La norme IEEE 754 permet de représenter l’infini en assignant tous les
bits de l’exposant à 1 et les bits de la mantisse à 0:
b31 11 · · · 100 · · · 00.
Le bit de signe détermine s’il s’agit de −∞ ou +∞.
CHAPITRE 12. NOMBRES EN VIRGULE FLOTTANTE 122

Valeurs indéterminées. La norme IEEE 754 permet également de représenter


une valeur spéciale NaN qui n’est pas un nombre (« Not a Number »).√Cette valeur
est obtenue lors d’opérations comme 0/0, ∞ − ∞, ∞/∞, 0 · ∞ et −x.
Cette valeur se représente en assignant tous les bits de l’exposant à 1 et la
mantisse à une valeur non nulle:

b31 11 · · · 1 e 00 · · · 01.

Le bit e indique si une erreur doit être lancée lors de l’obtention de NaN.

Nombres dénormalisés. La norme IEEE 754 supporte également des nom-


bres en virgule flottante dénormalisés, c’est-à-dire des nombres dont le premier
chiffre de la mantisse est 0. Ceux-ci permettent de représenter des nombres plus
près de zéro. Nous n’entrerons pas dans ces détails techniques.

Approximation. Plusieurs méthodes d’approximation sont supportées. Par dé-


faut, la norme IEEE 754 utilise l’arrondi avec bris d’égalité vers le nombre dont
le dernier chiffre est pair.

Observation.
Certains entiers de 64 bits (ou 32 bits) ne sont pas représentables par des
nombres en virgule flottante de précision double (ou simple).
Par exemple, 253 + 1 est clairement représentable en tant qu’entier
de 64 bits, mais pas en tant que nombre en virgule flottante double pré-
cision. En effet, 253 + 1 s’écrit de cette façon:

1, 0| ·{z
· · 0} 1 × 253 .
52 fois

Ce nombre requiert donc une mantise de 54 chiffres, alors qu’un nombre


en virgule flottante double précision en possède 53.
La même observation est vraie dans l’autre direction. Par exemple,

l’entier 264 se représente par 1,0 · · · 0×264 en nombre en virugle flottante
double précision, mais n’est pas représentable en tant qu’entier de 64 bits.

Observation.
Des calculs successifs peuvent créer des comportements surprenants. Par
exemple, le résultat de l’addition d’une liste de nombres en virgule flot-

tante dépend de l’ordre dans lequel les valeurs sont sommées.
CHAPITRE 12. NOMBRES EN VIRGULE FLOTTANTE 123

Remarque.

En C++, std::numeric_limits permet d’inspecter certains aspects de la


représentation interne des nombres en virgule flottante. Par exemple,
is_iec559 permet de vérifier si la norme IEEE 754 est utilisée; epsilon()

retourne l’epsilon machine a ; et round_style indique le mode d’approxi-
mation utilisé.
a. En fait, deux fois la valeur de ce que nous avons appelé l’epsilon machine.

12.5 Particularités de l’architecture ARMv8

12.5.1 Registres
L’architecture ARMv8 possède 32 registres de nombres en virgule flottante de
64 bits [ARM13, Sect. 5.1.2] dont l’usage est comme suit:

registres utilisation
d0 – d7 registres d’arguments et de retour de sous-programmes
d8 – d15 registres sauvegardés par l’appelé
d16 – d31 registres sauvegardés par l’appelant

Chaque registre dn possède un sous-registre de 32 bits nommé sn .


Lors de l’appel d’un sous-programme, il faut faire attention à la numérota-
tion des arguments. Par exemple, si un sous-programme reçoit un entier, un
nombre en virgule flottante et une adresse, on doit passer ces arguments dans
(x0 , d0 , x1 ), et non pas dans (x0 , d1 , x2 ).

12.5.2 Instructions
Le jeu d’instruction des nombres en virgule flottante ressemble à celui des en-
tiers. Les conditions de branchement sont les mêmes que pour les entiers et sont
déterminées à partir de codes de condition mis à jour par fcmp. Voici quelques-
unes des instructions:
CHAPITRE 12. NOMBRES EN VIRGULE FLOTTANTE 124

code op. syntaxe effet exemple


ldr dn, a charge un nombre en virgule ldr d8, [x19]
ldr
flottante double précision de
l’adresse a vers dn (8 octets)
ldr sn, a charge un nombre en virgule ldr s8, [x19]
flottante simple précision de
l’adresse a vers sn (4 octets)
str dn, a stocke un nombre en virgule str d8, [x19]
str
flottante double précision de
dn vers l’adresse a (8 octets)

str sn, a stocke un nombre en virgule str s8, [x19]


flottante simple précision de
sn vers l’adresse a (4 octets)
fmov vd, vm vd ← vm fmov d8, d9
fmov
fmov vd, i vd ← i fmov d8, 1.5
fcmp vd, vm compare vd et vm fcmp d8, d9
fcmp
fcmp vd, i compare vd et i fcmp d8, 0.0
fadd fadd vd, vn, vm vd ← vn + vm fadd d8, d9, d10
fsub fsub vd, vn, vm vd ← vn − vm fsub d8, d9, d10
fmul fmul vd, vn, vm v d ← vn · vm fmul d8, d9, d10
fdiv fdiv vd, vn, vm vd ← vn /vm fdiv d8, d9, d10
fmax fmax vd, vn, vm vd ← max(vn , vm ) fmax d8, d9, d10
fmin fmin vd, vn, vm vd ← min(vn , vm ) fmin d8, d9, d10

fsqrt fsqrt vd, vn vd ← vn fsqrt d8, d9
fabs fabs vd, vn vd ← |vn | fabs d8, d9
convertit l’entier non signé ucvtf d8, x19
dans rn vers un nombre en ucvtf d8, w19
ucvtf ucvtf vd, rn virgule flottante dans vd (selon ucvtf s8, x19
le mode d’approximation configuré dans ucvtf s8, w19
le registre de contrôle FPCR)

convertit l’entier signé dans scvtf d8, x19


rn vers un nombre en virgule scvtf d8, w19
scvtf scvtf vd, rn flottante dans vd (selon le mode scvtf s8, x19
d’approximation configuré dans le scvtf s8, w19
registre de contrôle FPCR)

convertit le nombre en
virgule flottante dans vn vers
fcvt fcvt vd, vn un nombre en virgule fcvt d8, s9
flottante d’une autre
précision dans vd
CHAPITRE 12. NOMBRES EN VIRGULE FLOTTANTE 125

Remarque.

Il existe d’autres instructions, que nous ne couvrirons pas, qui permettent


d’effectuer d’autres conversions de types, notamment selon des modes
d’approximation précis.

12.5.3 Exemple de programme



Le programme suivant calcule la norme euclidienne ∥(x, y)∥ := x2 + y 2 d’un 
vecteur (x, y) ∈ R2 à l’aide d’un sous-programme:

main: // main()
// Lire x // {
adr x0, fmtEntree
//
adr x1, temp //
bl scanf // scanf("%lf", &temp)
ldr d8, temp // x = temp
//
// Lire y //
adr x0, fmtEntree //
adr x1, temp //
bl scanf //
ldr d9, temp // scanf("%lf", &temp)
// y = temp
// Calculer ||(x, y)|| //
fmov d0, d8 //
fmov d1, d9 //
bl norme //
fmov d8, d0 // n = norme(x, y)
//
// Afficher ||(x, y)|| //
adr x0, fmtSortie //
fmov d0, d8 //
bl printf // printf("%lf\n", n)
//
// Quitter //
mov x0, 0 //
bl exit // return 0
// }
/***************************************************************
Entrée: nombres x, y en virgule flottante précision double
Sortie: norme euclidienne du vecteur (x, y)
***************************************************************/
CHAPITRE 12. NOMBRES EN VIRGULE FLOTTANTE 126

norme: // double norme(double x, double y) {


fmul d16, d0, d0 //
fmul d17, d1, d1 //
fadd d18, d16, d17 //
fsqrt d0, d18 //
ret // return √(x² + y²)
// }

.section ".rodata"
fmtEntree: .asciz "%lf"
fmtSortie: .asciz "%lf\n"

.section ".bss"
.align 8
temp: .skip 8
CHAPITRE 12. NOMBRES EN VIRGULE FLOTTANTE 127

12.6 Quiz récapitulatif


Vous pouvez tester votre compréhension en complétant ce court quiz . Il est
facultatif, anonyme et n’a aucune incidence sur votre note du cours.

12.7 Exercices
12.1) Normalisez ces nombres et donnez leur valeur décimale: 
123,456 × 102 − 0,0909 × 10−1 0,000101 × 2−3 − 11101,11 × 24

12.2) Ce programme C++ additionne x = 240 et y = 2−13 . Puisque x et y sont des 


puissances de 2, ils sont représentables en base 2. Pourtant, le programme
affiche « OK » et « !? ». Autrement dit, bien que y ̸= 0, on obtient x+y = x, 
ce qui ne fait pas de sens mathématiquement. Expliquez ce qui se produit.

double x = 1099511627776.0; // 2⁴⁰


double y = 0.0001220703125; // 2⁻¹³
double z = x + y;

std::cout << (y != 0 ? "OK" : "!?") << std::endl


<< (z != x ? "OK" : "!?") << std::endl;

12.3) (a) Considérons le système de nombres en virgule flottante où β = 2, 


n = 3 et −2 ≤ e ≤ 2. Autrement dit, la base est 2, la mantisse possède
3 bits, et l’exposant varie entre −2 et 2. Effectuez cette addition:

(1,11 × 20 ) + (1,01 × 21 ).

Votre somme doit être normalisée et approximée par troncation.


(b) Donnez la valeur décimale (en base 10) de l’approximation que vous
avez obtenue en (a), ainsi que de la valeur exacte de la somme.
(c) Quelle est l’erreur relative de votre approximation?

12.4) Considérons le système de nombres en virgule flottante où β = 2, n = 5 


et −3 ≤ e ≤ 3. Autrement dit, la base est 2, la mantisse possède 5 bits, et
l’exposant varie entre −3 et 3. Effectuez cette multiplication:

(1,11 × 20 ) · (1,01 × 21 ).

Votre somme doit être normalisée et approximée par arrondi.

12.5) Écrivez un sous-programme qui reçoit l’adresse d’un tableau de nombres 


en virgule flottante double précision, ainsi que sa taille, et qui retourne la
moyenne des éléments du tableau.
CHAPITRE 12. NOMBRES EN VIRGULE FLOTTANTE 128

12.6) Ces deux sous-questions portent sur les nombres en virgule flottante simple 
précision IEEE 754.
— Expliquez pourquoi la chaîne 00001101 représente l’exposant −114.
— Une chaîne de 8 bits permet de représenter 256 exposants différents.
Un nombre simple précision est pourtant limité à 254 exposants dif-
férents. Pourquoi y a-t-il 254 exposants possibles plutôt que 256?

12.7) Quel est le codage binaire du nombre décimal 42,5 en tant que nombre 
en virgule flottante simple précision (IEEE 754)?

12.8) ⋆ Considérons deux nombres positifs normalisés x et y en virgule flot- 


tante simple précision (IEEE 754). Écrivons vx et vy afin de dénoter la
valeur entière associée au codage binaire de x et y. Par exemple, x =
1,101×22 est représenté par « 1 10000001 10100000000000000000000 »,
et ainsi vx = 231 + 230 + 223 + 222 + 220 = 3234856960. Expliquez pourquoi
x < y ssi vx < vy . Autrement dit, expliquez pourquoi on peut comparer
x et y en interprétant leur codage binaire comme des entiers non signés
(ce qui peut paraître surprenant). Indice: pensez à l’ordre lexico.

12.9) ⋆⋆ Afin de générer un entier de 32 bits de façon aléatoire et uniforme, 


il suffit de tirer les 32 bits à pile ou face. Montrez que cette approche ne
fonctionne pas si on cherche à générer un nombre en virgule flottante
simple précision.

12.10) ⋆⋆ Démontrez une variante de la proposition 4 pour la troncation, c.-à- 


d. que |err(x)| ≤ 1/β n−1 pour tout x ∈ R \ {0} tel que xmin ≤ |x| ≤ xmax .

12.11) ⋆ Considérons ce programme C++: 


float x = 0.0;

while (x != 1.0) {
x += 0.1;
}

(a) On pourrait s’attendre à ce que la la boucle while soit exécutée dix


fois. Expliquez pourquoi ce n’est pas le cas.
(b) Expliquez pourquoi la boucle while ne termine jamais. Pensez à l’im-
pact d’additionner un petit nombre à un grand nombre.
13

Introduction aux entrées/sorties: NES

Dans les chapitres précédents, nous n’avons considéré qu’une seule forme d’en-
trée/sortie: celles d’un terminal. De plus, celles-ci se réalisaient avec des fonc-
tions de la librairie standard du langage C. Dans ce chapitre, nous voyons comme
réaliser des entrées/sorties de bas niveau. Afin d’illustrer ces concepts, nous sur-
volons l’architecture de la console de jeux vidéo Nintendo Entertainment System
(NES) illustrée aux figures 13.1 et 13.2. Cela permettra aussi de mettre en pra-
tique l’ensemble de nos connaissances sur une autre architecture qu’ARMv8.

Figure 13.1 – Console de jeux Nintendo Entertainment System (NES).

13.1 Architecture du NES


Le processeur du NES est une variante du célèbre MOS 6502 utilisant le même
jeu d’instructions. La mémoire principale de la console contient 2 Kio. Le code
d’un programme (jeu vidéo) est stocké sur une cartouche insérée dans la console.
Une telle cartouche peut optionnellement contenir une mémoire de sauvegarde.

129
CHAPITRE 13. INTRODUCTION AUX ENTRÉES/SORTIES: NES 130

Le NES possède également un second processeur, dit « picture processing unit


(PPU) ». Celui-ci se dédie à l’affichage des graphiques et possède sa propre mé-
moire appelée mémoire vidéo. Ces différents composants sont reliés par plusieurs
bus de données.

Figure 13.2 – Carte mère du NES.

13.1.1 Organisation de la mémoire


Nous décrivons l’organisation des deux mémoires de la console, telles qu’illus-
trées à la figure 13.3.

Mémoire principale. La mémoire principale, donc adressable directement par


le processeur, se divise en trois segments:

■ mémoire primaire: possède une portion de mémoire tout usage (dite zero-
page), une pile d’exécution, ainsi qu’une autre portion généralement des-
tinée au stockage temporaire de tuiles;
■ mémoire d’entrée/sortie: contient des registres permettant d’effectuer des
entrées/sorties notamment avec le processeur d’images et les manettes (il
ne s’agit pas de registres au sens usuel puisqu’ils se situent en mémoire principale);
■ cartouche de jeu: contient le code du programme (donc du jeu vidéo), et
d’autres segments de mémoire optionnels (pour sauvegarde et expansions).

Remarquons que certaines portions de la mémoire sont simplement des mirroirs.


Par exemple, l’adresse 080016 pointe en fait vers l’adresse 000016 , et l’adresse
200816 pointe en fait vers l’adresse 200016 .
CHAPITRE 13. INTRODUCTION AUX ENTRÉES/SORTIES: NES 131

Mémoire vidéo. La mémoire du processeur d’images se divise en trois parties:

■ tuiles: contient deux tables des tuiles du jeu: sprites (personnages, objets,
etc.) et de l’arrière-plan (collines, nuages, etc.);
■ arrière-plan: quatre tables spécifiant les tuiles qui forment l’arrière-plan
actuel (tuiles, positions, couleurs, orientations, etc.);
■ palettes de couleur: décrit les couleurs disponibles pour les tuiles (permet,
par exemple, de spécifier la couleur des personnages dans un monde sur terre et
dans un monde sous-terrain).
Comme dans le cas de la mémoire principale, certaines portions sont simple-
ment des mirroirs. Par exemple, l’adresse 300016 pointe en fait vers l’adresse
200016 , et l’adresse 3F2016 pointe en fait vers l’adresse 3F0016 .
Notons que le processeur d’images a également accès à une mémoire de
sprites de 256 octets située en dehors de la mémoire vidéo.

000016
Mémoire générale 000016
Tuiles
Table des sprites
Mémoire primaire

(zero-page) 100016
010016 Table d’arrière-plan
Pile Table de tuiles 0 200016
23C016
Mémoire générale 020016 Table d’attributs 0
240016
(stockage de tuiles) Table de tuiles 1
080016 27C016
Mirroir des registres Table d’attributs 1
Arrière-plan

000016 à 07FF16 280016


Table de tuiles 2
200016 2BC016
Registres Table d’attributs 2
Entrées/sorties

d’entrée/sortie Table de tuiles 3


2C0016
200816
Mirroir des registres Table d’attributs 3
2FC016
200016 à 200716 300016
400016 Mirroir des registres
Registres 200016 à 2EFF16
d’entrée/sortie 3F0016
Palettes de couleur

402016 Palettes
Mémoire
Cartouche de jeu

d’arrière-plan
d’expansion 3F1016
600016 Palettes des
Mémoire
sprites
de sauvegarde 3F2016
800016 Mirroir des registres
Mémoire
3F0016 à 3F1F16
de programme 400016
1000016

Figure 13.3 – Mémoire principale (gauche) et mémoire vidéo (droite) du NES.


CHAPITRE 13. INTRODUCTION AUX ENTRÉES/SORTIES: NES 132

13.2 Registres
Le processeur du NES possède quatre registres d’un octet (8 bits) chacun:

registres utilisation principale

a accumulateur, utilisé comme opérande et valeur de retour des opé-


rations arithmétiques et logiques

x utilisé comme compteur ou comme index pour l’adressage indexé

y utilisé comme compteur ou comme index pour l’adressage indexé

s pointeur de la pile d’exécution (pointe vers l’adresse 010016 + s)

Il existe également deux registres internes:


— p: registre d’état qui contient des états et codes de conditions dont le re-
port/emprunt d’une instruction arithmétique (1 octet);
— pc: compteur d’instruction qui contient l’adresse de la prochaine instruc-
tion à exécuter (2 octets).
Il n’y a pas de registres de nombres en virgule flottante.

13.3 Jeu d’instructions


Le jeu d’instructions possède une centaine d’instructions. Contrairement à l’ar-
chitecture ARMv8, plusieurs instructions permettent de manipuler directement
la mémoire sans devoir explicitement effectuer un chargement/stockage. Nous
présentons un sous-ensemble du jeu d’instructions.

13.3.1 Valeurs immédiates


Les préfixes $ et % indiquent que la valeur qui suit est hexadécimale et binaire,
respectivement. L’absence de préfixe indique une valeur décimale. Les valeurs
précédées d’un # représentent des valeurs numériques, alors que celles sans #
représentent une adresse.

Exemple.

expression valeur
#5 510
#$FF FF16
#%00010011 000100112
$FF adresse FF16
CHAPITRE 13. INTRODUCTION AUX ENTRÉES/SORTIES: NES 133

Les valeurs numériques possèdent 8 bits, alors que les adresses peuvent par-
fois posséder jusqu’à 16 bits.

13.3.2 Modes d’adressage

nom syntaxe adresse exemple


absolu i i lda $D010

i, x i+x lda $D010, x


indexé par x
etiq, x etiq + x lda tab, x

i, y i+y lda $D010, y


indexé par y
etiq, y etiq + y lda tab, y

13.3.3 Accès mémoire


Écrivons mem1 [a] afin de dénoter l’octet à l’adresse a de la mémoire principale.

code d’op. syntaxe effet exemple

lda #i a←i lda #42


lda
lda adr a ← mem1 [adr] lda var

ldx #i x←i ldx #42


ldx
ldx adr x ← mem1 [adr] ldx var

ldy #i y←i ldy #42


ldy
ldy adr y ← mem1 [adr] ldy var

sta sta adr mem1 [adr] ← a sta var

stx stx adr mem1 [adr] ← x stx var

sty sty adr mem1 [adr] ← y sty var

txa txa a←x txa

tax tax x←a tax

tya tya a←y tya

tay tay y←a tay

txs txs s←x txs

tsx tsx x←s tsx


CHAPITRE 13. INTRODUCTION AUX ENTRÉES/SORTIES: NES 134

13.3.4 Arithmétique

code d’op. syntaxe effet exemple

adc #i a ← a + i + report lda #1


adc
adc adr a ← a + mem1 [adr] + report adc var

sbc #i a ← a − i − emprunt sbc #1


sbc
sbc adr a ← a − mem1 [adr] − emprunt sbc var

clc clc report ← 0 (utile avant adc) clc

sec sec emprunt ← 0 (utile avant sbc) sec

inx inx x←x+1 inx

iny iny y←y+1 iny

inc inc adr mem1 [adr] ← mem1 [adr] + 1 inc var

dec dec adr mem1 [adr] ← mem1 [adr] − 1 dec var

13.3.5 Logique

code d’op. syntaxe effet exemple


asl asl adr décalage logique de mem1 [adr] asl var
d’un bit à gauche (directement en
mémoire)

lsr lsr adr décalage logique de mem1 [adr] lsr var


d’un bit à la droite (directement en
mémoire)

and #i a←a∧i and #%00100011


and
and adr a ← a ∧ mem1 [adr] and var

ora #i a←a∨i ora #%00100011


ora
ora adr a ← a ∨ mem1 [adr] ora var

eor #i a←a⊕i eor #%00100011


eor
eor adr a ← a ⊕ mem1 [adr] eor var

13.3.6 Comparaisons et branchements


CHAPITRE 13. INTRODUCTION AUX ENTRÉES/SORTIES: NES 135

code d’op. syntaxe effet exemple

cmp #i compare a et i cmp #0


cmp
cmp adr compare a et mem1 [adr] cmp var

cpx #i compare x et i cpx #0


cpx
cpx adr compare x et mem1 [adr] cpx var

cpy #i compare y et i cpy #0


cpy
cpy adr compare y et mem1 [adr] cpy var

beq beq etiq branche à etiq: si = beq boucle

bne bne etiq branche à etiq: si ̸= bne boucle

jmp jmp etiq branche à etiq: jmp boucle

jsr jsr etiq branche au sous-programme etiq: jsr func


et empile l’adresse de retour
rts rts branche à l’adresse de retour d’un rts
sous-programme
rti rti branche à l’adresse de retour d’une rti
interruption

13.4 Sorties graphiques

13.4.1 Tuiles
L’image à l’écran est constituée de tuiles de 8 × 8 pixels. Chacune de ces tuiles
est stockée dans la cartouche de jeu et chargée dans une table de la mémoire
vidéo. Afin d’afficher une tuile, il faut spécifier sur 4 octets (dans cet ordre):
— sa position verticale comprise entre 0 et 255;
— son identifiant (sa position dans la table de tuiles);
— ses attributs tels qu’une palette de couleur;
— sa position horizontale comprise entre 0 et 255.
Les attributs d’une tuile sont déterminés par ces huit bits:
n?
?
le
e?

r
la
ta

eu
al

-p
on
ic

ul
re
rt

riz

és

co
è
ve

i
rr

s
ho

ili

de
l’a
on

ut
on
xi

te
re

in
xi
fle

t

le
fle

ts
rr

Pa

De

Bi

b7 b6 b5 b4 à b2 b1 à b0
CHAPITRE 13. INTRODUCTION AUX ENTRÉES/SORTIES: NES 136

Exemple.

La séquence de 4 octets (33, 5, 010000102 , 160) spécifie d’afficher la tuile 5


à la coordonnée (160, 33), de transformer la tuile par une réflexion hori-
zontale, et d’utiliser la palette de couleurs 2.

13.4.2 Affichage de tuiles


Il y a plusieurs façons d’effectuer l’affichage. Par exemple, pour afficher un per-
sonnage constitué de n tuiles, nous pouvons:
— stocker la description de chacune de ses tuiles (4n octets) consécutive-
ment aux adresses 020016 à 02FF16 de la mémoire principale;
— signaler au processeur d’images d’afficher le contenu de ces adresses en
stockant 0216 à l’adresse 401416 de la mémoire principale.
L’adresse 401416 réfère à un registre d’entrée/sortie. Si on y stocke un nombre
de la forme 0X16 , alors cela signale au bus de données de transférer le contenu
des adresses 0X0016 à 0XFF16 vers la mémoire de sprites. Cette technique d’accès
direct à la mémoire (DMA) sera couverte plus en détails au chapitre 14.

Exemple.

Ce code transfère les tuiles spécifiées aux adresses 020016 à 02FF16 de la


mémoire principale vers la mémoire de sprites:

lda #$02
sta $4014

Afin d’éviter des incohérences visuelles, l’affichage ne doit se faire que durant
l’intervalle de rafraîchissement vertical (VBLANK). Cet intervalle correspond à la
période de temps où le canon a électron se repositionne au haut de l’écran avant
d’effectuer un nouvel affichage. Afin d’en être informé, il est possible de spécifier
une sous-routine qui est appelée chaque fois que cet intervalle se produit. Durant
l’appel de cette sous-routine, le processeur met en suspens ce qu’il était en train
d’exécuter, puis reprend lorsque la sous-routine est complétée. Un jeu possède
donc normalement une boucle infinie qui alterne entre traitement et affichage.

13.5 Entrées à partir des manettes


Le processeur peut lire les boutons enfoncés d’une manette à l’aide d’un proto-
cole de communication via le port de manette. Par exemple, pour l’état de la
première manette, il faut (dans cet ordre):
— stocker 1 à l’adresse 401616 ;
CHAPITRE 13. INTRODUCTION AUX ENTRÉES/SORTIES: NES 137

— stocker 0 à l’adresse 401616 ;


— effectuer huit fois: une lecture de 401616 et récupérer le bit de poids faible.
Les huit lectures consécutives correspondent, dans l’ordre, aux boutons A, B,
select, start, haut, bas, gauche, droite:

SELECT START

A B

Pour chacune des lectures, le bit de poids faible vaut 1 si (et seulement si)
le bouton était enfoncé lors de l’initialisation du protocole. Par exemple, le code
suivant vérifie si le bouton A est enfoncé:

lda #1 ;
sta $4016 ;
lda #0 ;
sta $4016 ; demander une lecture des boutons
;
lda $4016 ;
and #%00000001 ; obtenir l'état du bouton A

On pourrait préserver l’état des huit boutons dans un seul octet en accumu-
lant chacun des bits à l’aide de décalages et d’opérations de masquages.

13.6 Exemple de programme simple


Concevons un programme simple qui: 
— affiche un sprite à l’écran qui se déplace continuellement vers la droite;
— fait alterner le sprite entre les tuiles représentant les caractères 0, 1, . . . , 9;
— fait descendre le sprite lorsqu’on enfonce le bouton A.

Un aperçu animé du programme est disponible sur GitHub .


On alloue d’abord statiquement quatre octets qui représentent respective-
ment la position horizontale, la position verticale, l’identifiant de la tuile à affi-
cher, ainsi que le nombre d’itérations d’une animation:

posX: .rs 1 ; pos. horizontale du chiffre


posY: .rs 1 ; pos. verticale du chiffre
chiffre: .rs 1 ; tuile du chiffre entre 1 et 10
iter: .rs 1 ; nombre d'itér. à effectuer

Le point d’entrée « main: » effectue quelques initialisations techniques (que


nous expliquerons au prochain chapitre), ainsi que l’initialisation des variables:
CHAPITRE 13. INTRODUCTION AUX ENTRÉES/SORTIES: NES 138

main: ; main()
; initialisation technique ; {
;
jsr init_variables ; init_variables()
;
; autres init. technique ; }

Les tuiles 1 à 10 représentent les caractères 0 à 9 respectivement. Ceci est


spécifique à la table de tuiles fournie dans ce cours, ce pourrait donc être d’autres
identifiants pour un autre jeu.
Ainsi, on place initialement le sprite à la position (0, 100) et on lui assigne la
tuile 1. De plus, on initialise le nombre d’itérations d’animation à 24:

init_variables: ; init_variables()
lda #0 ; {
sta posX ; posX = 0
lda #100 ;
sta posY ; posY = 100
lda #1 ;
sta chiffre ; chiffre = 1 (no. de tuile)
lda #24 ;
sta iter ; iter = 24
rts ; }

À chaque intervalle de rafraîchissement vertical (VBLANK), la sous-routine


« update: » est automatiquement appelée. Celle-ci signale au bus de données
de transférer l’état des sprites vers la mémoire de sprites, puis déplace et met à
jour notre unique sprite:

update: ; update()
lda #$02 ; {
sta $4014 ; copier tuiles 0x0200 à
; 0x02FF vers PPU
;
jsr deplacer_chiffre ; deplacer_chiffre()
jsr update_chiffre ; update_chiffre()
;
rti ; }

Afin de déplacer notre sprite, on incrémente d’abord la position horizontale


posX, on demande une lecture des boutons, puis on lit le bit d’état du bouton A
qu’on additionne à posY. Ainsi, si le bouton est enfoncé, alors la position verti-
cale est incrémentée, et autrement elle demeure la même.

deplacer_chiffre: ; deplacer_chiffre() {
inc posX ; posX++
CHAPITRE 13. INTRODUCTION AUX ENTRÉES/SORTIES: NES 139

;
lda #1 ;
sta $4016 ;
lda #0 ;
sta $4016 ; demander lecture des boutons
;
lda $4016 ; lire bit b de poids
and #%00000001 ; faible du bouton A
;
clc ;
adc posY ;
sta posY ; posY += b (incrémente posY
; si A est enfoncé)
rts ; }

Finalement, la sous-routine « update_chiffre: » met le sprite à jour:


— on décrémente iter;
— si ce-dernier a atteint 0, alors on modifie le sprite et on remet iter à 24;
sinon, on ne fait rien (cela permet de ralentir l’animation, en choisissant 48 on
aurait par ex. une animation deux fois plus lente);
— si le sprite doit être modifié, alors on passe au chiffre suivant en incrémen-
tant l’identifiant de tuile chiffre;
— on stocke les quatre octets (posY, chiffre, 0, posX) à partir de l’adresse
020016 en vue du prochain affichage.

update_chiffre: ; update_chiffre()
; Position verticale ; {
lda posY ;
sta $0200 ; mem[0x0200] = posY
;
; Choix de la tuile ;
lda chiffre ; mem[0x0201] = chiffre
sta $0201 ;
;
lda iter ;
cmp #0 ; if (iter != 0) {
beq prochain_chiffre ;
;
prochaine_iteration: ;
sec ;
dec iter ; iter--
jmp continuer ; }
prochain_chiffre: ; else {
lda #24 ;
sta iter ; iter = 24
CHAPITRE 13. INTRODUCTION AUX ENTRÉES/SORTIES: NES 140

inc chiffre ; chiffre++


lda chiffre ;
cmp #11 ;
bne continuer ; if (chiffre == 11) {
lda #1 ;
sta chiffre ; chiffre = 1
; }
continuer: ; }
; Attributs de la tuile ;
lda #%00000000 ;
sta $0202 ; mem[0x0202] = 0
;
; Position horizontale ;
lda posX ;
sta $0203 ; mem[0x0203] = posX
;
rts ; }

Remarque.

Le mot-clé « .rs » alloue un certain nombre d’octets comme « .skip »


sur ARMv8. On peut aussi utiliser « .byte » afin d’allouer des octets ini-
tialisés. Par exemple, « .byte $8C, $01, $00, $00 » alloue les octets
(8C16 , 0116 , 0016 , 0016 ) consécutivement en mémoire.
CHAPITRE 13. INTRODUCTION AUX ENTRÉES/SORTIES: NES 141

13.7 Quiz récapitulatif


Vous pouvez tester votre compréhension en complétant ce court quiz . Il est
facultatif, anonyme et n’a aucune incidence sur votre note du cours.

13.8 Exercices
13.1) Complétez le sous-programme « lecture: » ci-dessous afin qu’il assigne 
la valeur 1 à la variable appuye si les boutons select et start de la première
manette sont tous deux appuyés, et 0 autrement.

appuye: .rs 1 ; variable d'un octet

lecture:
/* code ici */
rts

Rappel:
— l’adresse $4016 du NES est liée au port de sa première manette;
— pour initier une lecture, il faut envoyer 1, puis 0, vers son port;
— l’ordre des boutons est: A, B, select, start, haut, bas, gauche, droite.

13.2) Écrivez un sous-programme qui décrémente la variable « posX: » si la flèche 


gauche est enfoncée.

13.3) Comment peut-on afficher les tuiles stockées de 030016 à 03FF16 ? 


13.4) La variable booléenne « dir: » du programme ci-dessous indique les direc- 
tions gauche et droite respectivement à l’aide des valeurs 0 et 1. Complétez
le sous-programme « renverser: » afin qu’il applique une réflexion hori-
zontale sur la tuile lorsque la direction indique la droite.

dir: .rs 1 ; vaut 0 (gauche) ou 1 (droite)


tuile: .rs 4 ; (posY, identifiant, attributs, posX)

renverser:
/* code ici */
rts
14
Entrées/sorties

Un ordinateur est normalement constitué de périphériques d’entrée/sortie qui


lui permettent d’interagir avec le monde extérieur (clavier, souris, moniteur,
mémoire externe, webcam, imprimante, pavé tactile, capteurs, etc.) Ces péri-
phériques ne sont généralement pas synchronisés avec le processeur. Ainsi, cer-
tains mécanismes sont nécessaires afin que le processeur traite les différentes
entrées et sorties. Dans ce chapitre, nous décrivons certains de ces mécanismes.
La plupart des concepts seront illustrés avec l’architecture du NES introduite au
chapitre 13; à l’exception des appels système qui seront illustrés avec ARMv8.

14.1 Attente active


L’un des mécanismes les plus simples afin d’interagir avec un contrôleur d’en-
trée/sortie consiste à interroger certains bits d’états. Par exemple, le processeur
d’images du NES possède un registre d’état en lecture seule accessible à l’adresse
$2002. En particulier, le bit de poids fort de ce registre indique si l’intervalle de
rafraîchissement vertical (VBLANK) est en cours. Ainsi, on peut programmer
l’affichage de tuiles de cette façon:
— lire continuellement $2002 jusqu’à ce que le bit de poids fort égale 1;
— envoyer les tuiles vers le processeur d’images.
Cette attente active s’implémente ainsi:

attendre_vblank: ; attendre_vblank()
lda $2002 ; {
and #%10000000 ; while (!VBLANK) {
cmp #%10000000 ; // ne rien faire
bne attendre_vblank ; }
rts ; }

Ainsi, la routine principale du programme attend, effectue l’affichage lorsque


l’attente se débloque, puis recommence:

142
CHAPITRE 14. ENTRÉES/SORTIES 143

main: ; while (true) {


jsr attendre_vblank ; attendre activement le VBLANK
jsr afficher_tuiles ; afficher les tuiles
jmp main ; }

En général, cette méthode est connue sous le nom d’attente active puisqu’elle
attend en répétant constamment une opération.

14.2 Interruptions
L’attente active se démarque par sa simplicité, mais elle monopolise les cycles
du processeur et empêche toute autre instruction d’être exécutée. Ainsi, elle
fonctionne relativement bien sur le NES puisque l’intervalle de rafraîchissement
se produit aux 16,6 millisecondes, mais elle est peu adaptée aux systèmes où les
entrées/sorties se produisent rarement ou à des fréquences variables.
Les interruptions offrent une solution élégante à cette problématique. Plutôt
que d’attendre incessamment qu’un événement se produise, un signal est lancé
lorsqu’il se produit. Le processeur s’interrompt alors et lance une sous-routine
qui traite l’événement. Lorsque la sous-routine se termine, le processeur reprend
ses activités. Ainsi, un programme qui lit une touche au clavier, par exemple,
n’a pas à bloquer l’ordinateur jusqu’à ce que l’on appuie sur une touche.

14.2.1 Gestionnaires d’interruption


Le NES possède trois types d’interruptions:
— NMI: lancée lors de l’intervalle de rafraîchissement vertical (VBLANK);
— RESET: lancée au démarrage de la console (bouton « POWER » enfoncé)
ou lorsque la console est redémarrée (bouton « RESET » appuyé);
— IRQ: ne possède pas d’usage particulier, mais peut, par exemple, être lan-
cée par une puce électronique d’une cartouche de jeu.
Les six derniers octets de la mémoire principale contiennent l’adresse des
sous-routines qui doivent être appelées afin de gérer ces interruptions:
000016

..
.
FFFA16
NMI
FFFC16
RESET
FFFE16
IRQ

En général, une sous-routine appelée lors d’une interruption se nomme un


gestionnaire d’interruption, et le segment de mémoire qui contient l’adresse de
CHAPITRE 14. ENTRÉES/SORTIES 144

chaque gestionnaire se nomme table d’interruptions ou vecteur d’interruptions. Il


s’agit d’un tableau de pointeurs similaire à une table de branchement.
Ainsi, dans le cas du NES, la sous-routine d’affichage et le point d’entrée du
jeu peuvent être assignés respectivement comme gestionnaires des interruptions
NMI et RESET dans la table d’interruptions. Cela permet de retirer la boucle
d’attente active:
main: ; main()
; initialisation du jeu ; {
main_boucle: ; while (true)
jmp main_boucle ; rien faire
rti ; }
;
mise_a_jour: ; mise_a_jour() {
jsr traitement ; traitement()
jsr afficher_tuiles ; afficher_tuiles()
rti ; }
;
.org $FFFA ; Table d'interruptions (à 0xFFFA)
.word mise_a_jour ; NMI
.word main ; RESET
.word 0 ; IRQ (aucune sous-routine)

14.2.2 Traitement des interruptions


Lorsqu’un gestionnaire d’interruption est appelé, l’exécution actuelle du proces-
seur doit être mise en suspens. Typiquement, l’instruction en cours d’exécution
est complétée, puis le gestionnaire est appelé essentiellement comme le serait
un sous-programme. Cependant, contrairement aux sous-programmes, l’exécu-
tion d’un gestionnaire d’interruption ne doit pas altérer l’état du processeur.

Exemple.

Considérons ce programme:
foo: ; Sous-programme
cmp #0
beq foo
rts

bar: ; Gestionnaire d'interruption


cmp #1
rti

Ici, « foo » et « bar » désignent, respectivement, un sous-programme


et le gestionnaire d’une interruption i. Supposons que foo soit appelé
CHAPITRE 14. ENTRÉES/SORTIES 145

et que l’accumulateur a contienne la valeur 1. Puisque a ̸= 0, aucun


branchement ne se produira et foo retournera à son appelant.
Considérons un scénario alternatif où foo exécute « cmp #0 » alors
qu’une interruption i est lancée. Le processeur complète la comparaison
en cours, puis exécute bar. Celui-ci effectue la comparaison « cmp #1 »
et se termine. Le processeur reprend donc l’exécution de foo en exé-
cutant « beq foo ». Or, bar a modifié les codes de condition lors de sa
comparaison, et ainsi un branchement est effectué dans foo, ce qui ne
correspond pas au comportement attendu.

Un mécanisme doit donc être mis en place afin d’éviter ce comportement


problématique. En fait, sur l’architecture du NES, et plus généralement sur MOS
6502, ce problème ne se produit pas. En effet, lors du traitement d’une interrup-
tion, les opérations suivantes sont effectuées automatiquement:
— l’instruction en cours d’exécution est complétée;
— l’octet de poids fort de l’adresse de retour est empilé;
— l’octet de poids faible de l’adresse de retour est empilé;
— le contenu du registre d’état p est empilé;
— l’octet de poids fort du gestionnaire d’interruption est récupéré;
— l’octet de poids faible du gestionnaire d’interruption est récupéré.
Remarquons qu’un gestionnaire d’interruption ne se termine pas par l’ins-
truction « rts », mais bien par « rti ». Cette instruction effectue la séquence
d’opérations inverse. En particulier, elle rétablit le contenu de p à partir de la
pile. Ainsi, bien que bar modifie les codes de condition, ceux-ci sont rétablis à
la fin de son exécution, et foo n’est pas affecté.
Les registres a, x et y ne sont cependant pas sauvegardés automatiquement.
Ainsi un gestionnaire d’interruption doit manuellement rétablir leur contenu.
L’architecture du NES possède une instruction qui permet d’empiler a, ainsi
qu’une instruction qui permet de dépiler vers a. Ainsi, nous pouvons implémen-
ter la sauvegarde et la restauration de registres similairement aux macros SAVE
et RESTORE mises au point pour l’architecture ARMv8:
; Sauvegarde des registres
pha ; empiler a
txa ; a = x
pha ; empiler a
tya ; a = y
pha ; empiler a

; Restauration des registres


pla ; dépiler vers a
tay ; y = a
pla ; dépiler vers a
CHAPITRE 14. ENTRÉES/SORTIES 146

tax ; x = a
pla ; dépiler vers a

En ajoutant ce code respectivement au début et à la fin d’un gestionnaire d’in-


terruption, on rétablit donc entièrement l’environnement.

14.2.3 Niveaux de priorité


Le NES possède peu d’interruptions. Toutefois, un ordinateur moderne peut en
posséder une dizaine, voire des centaines 1 . Ainsi, un périphérique peut lancer
une interruption alors qu’un gestionnaire d’interruption est déjà en cours d’exé-
cution. Lorsque cela se produit, le processeur doit faire un choix en fonction de
l’importance de l’interruption.
Par exemple, chaque type d’interruption peut posséder une priorité spéci-
fiée par une valeur numérique comprise entre 0 et n. Dans ce cas, plus la valeur
numérique est petite, plus la priorité est importante. Lors de la gestion d’une
interruption de niveau i, les interruptions moins prioritaires, donc de i + 1 à n,
sont ignorées. Supposons qu’un gestionnaire G d’une interruption de niveau i
soit en cours d’exécution. Si une interruption de priorité égale ou supérieure,
donc de 0 à i, est lancée, alors l’état de G est sauvegardé (par ex. sur une pile),
et un gestionnaire d’interruption G' est exécuté afin de traiter la nouvelle inter-
ruption. Lorsque G' termine, G reprend le contrôle.
Les interruptions de niveau 0 sont dites non masquables car elles ne peuvent
pas être ignorées. Par exemple, l’interruption RESET du NES est non masquable.
Les interruptions qui sont ignorées par le processeur ne sont pas nécessai-
rement oubliées. Elles peuvent être mises en attente en « allumant » un certain
bit d’un registre d’interruption. Lorsque le niveau de priorité actuel le permet, le
processeur peut ainsi servir une interruption en attente en lançant son gestion-
naire.
Certaines interruptions qui ne peuvent pas être masquées automatiquement
par le processeur, peuvent être masquées manuellement. Par exemple, l’inter-
ruption NMI du NES est non masquable, mais elle peut être désactivée en met-
tant le bit de poids fort du registre de contrôle $2000 à zéro. Cela s’avère pra-
tique afin d’initialiser un jeu avant de débuter l’affichage:

main: ;
lda #%00000000 ;
sta $2000 ; désactiver les interruptions NMI

; Initialisation ici

lda #%10000000 ;
sta $2000 ; activer les interruptions NMI

; Début de l'affichage

1. Du moins, au niveau logiciel.


CHAPITRE 14. ENTRÉES/SORTIES 147

14.2.4 Interruptions logicielles


Les interruptions réfèrent typiquement à des signaux lancés par des dispositifs
d’entrée/sortie. Cependant, plusieurs architectures offrent des interruptions lo-
gicielles. Contrairement aux interruptions matérielles qui sont asynchrones puis-
qu’elles dépendent de facteurs externes, les interruptions logicielles sont lancées
par une instruction (logicielle).
Par exemple, sur le NES, et plus généralement sur l’architecture MOS 6502,
l’instruction « brk » lance une interruption IRQ. Cela permet notamment d’im-
plémenter un débogueur puisque « brk » interrompt l’exécution du programme
à une ligne précise.

14.3 Accès direct à la mémoire


Afin d’afficher un sprite sur le NES, ses tuiles doivent être stockées dans la mé-
moire de sprites. Rappelons que cette mémoire comporte 256 octets. Comme
le processeur n’a pas accès à cette mémoire, il doit y accéder indirectement. Si
nous désirons écrire la valeur v à l’adresse a ∈ [0..255] de la mémoire de sprites,
il est possible de procéder en deux étapes:
— écrire a à l’adresse $2003,
— écrire v à l’adresse $2004.

Après ces deux étapes d’initialisation, on écrit v à l’adresse a et le contenu


de $2003 est automatiquement incrémenté par le processeur d’images. Ainsi,
une tuile décrite par les quatre octets (100, 1, 0, 127) peut être affichée ainsi:

lda #$00 ; débuter l'écriture dans la


sta $2003 ; mémoire de sprites à 0x00
;
; Position verticale ;
lda #100 ;
sta $2004 ; mem_sprite[0x00] = 100
;
; Numéro de la tuile ;
lda #1 ;
sta $2004 ; mem_sprite[0x01] = 1
;
; Attributs de la tuile ;
lda #%00000000 ;
sta $2004 ; mem_sprite[0x02] = 0
;
; Position horizontale ;
lda #127 ;
sta $2004 ; mem_sprite[0x03] = 127
CHAPITRE 14. ENTRÉES/SORTIES 148

Cette méthode est simple, mais requiert 4n accès mémoire afin de stocker
n tuiles, ce qui accapare plusieurs cycles du processeur. Le NES offre un autre
mécanisme plus efficace afin de transférer plusieurs tuiles: l’accès direct à la
mémoire (DMA). Celui-ci permet au processeur d’initier un transfert de données,
puis de laisser un contrôleur effectuer lui-même le transfert.
Pour ce faire, il faut:
— stocker la description des tuiles dans un segment contigu de la mémoire
principale, par exemple de $0200 à $02FF;
— écrire l’octet de poids fort du segment à l’adresse $4014.
Ainsi, les tuiles peuvent être envoyées comme ceci:

; Position verticale ;
lda #100 ;
sta $0200 ; mem[0x0200] = 100
;
; Numéro de la tuile ;
lda #1 ;
sta $0201 ; mem[0x0201] = 1
;
; Attributs de la tuile ;
lda #%00000000 ;
sta $0202 ; mem[0x0202] = 0
;
; Position horizontale ;
lda #127 ;
sta $0203 ; mem[0x0203] = 127
;
; Stocker les autres tuiles ; ...
;
; Initier un transfert ;
lda #$02 ; copier mem[0x0200, 0x02FF]
sta $4014 ; vers la mémoire de sprites

14.4 Appels système


Les programmes exécutés par l’intermédiaire d’un système d’exploitation n’ont
généralement pas un accès direct aux périphériques d’entrée/sortie pour des
raisons de sécurité. Le noyau du système d’exploitation offre plutôt des services
qui doivent être appelés via une interruption logicielle nommée appel système.
Par exemple, sur les systèmes de type UNIX, les appels système « write »
et « read » permettent d’effectuer des écritures/lectures vers/à partir d’une res-
source du système. Les fonctions de haut-niveau « printf » et « scanf », que
nous avons utilisé jusqu’ici, sont implémentées à l’aide de ces appels système.
Sur ARMv8, pour effectuer un appel système, il faut:
CHAPITRE 14. ENTRÉES/SORTIES 149

— indiquer le code du service dans x8 ;


— passer les arguments dans x0 à x5 ;
— utiliser l’instruction « svc 0 ».
Par exemple, les appels système « write » et « read » de UNIX sont décrits
par ces codes et paramètres:

service code paramètre 0 paramètre 1 paramètre 2

write 64 flux de sortie adresse de la chaîne nombre d’octets


à afficher de la chaîne

read 63 flux d’entrée adresse où stocker la nombre d’octets


chaîne lue (mémoire à lire
tampon)

Le programme ci-dessous lit une chaîne de 10 octets au clavier et affiche 


cette chaîne à l’aide de sous-programmes qui utilisent des appels système plutôt
que « scanf » et « printf »:

main: // main()
adr x0, temp // {
mov x1, 10 //
bl lire // lire(&temp, 10)
//
adr x0, temp //
mov x1, 10 //
bl afficher // afficher(&temp, 10)
//
mov x0, 0 //
bl exit // }
//
afficher: // afficher(chaine, taille)
mov x9, x0 // {
mov x10, x1 //
//
mov x8, 64 // /* write = 64
mov x0, 1 // stdout = 1 */
mov x1, x9 //
mov x2, x10 //
svc 0 // write(stdout, chaine, taille)
//
ret // }
//
lire: // lire(tampon, taille)
mov x9, x0 // {
mov x10, x1 //
CHAPITRE 14. ENTRÉES/SORTIES 150

//
mov x8, 63 // /* read = 63
mov x0, 0 // stdin = 0 */
mov x1, x9 //
mov x2, x10 //
svc 0 // read(stdin, tampon, taille)
//
ret // }

.section ".bss"
temp: .skip 10

Remarque.

Nous avons toujours utilisé de la mémoire allouée statiquement (sauf lors


de la manipulation de la pile). Cela peut sembler irréaliste en comparai-
son aux programmes de haut niveau.
Cependant, il n’y a pas de différence fondamentale. En effet, afin d’al-
louer n octets dynamiquement sur un système de type UNIX, on pourrait
simplement:
— utiliser le service « brk » afin d’obtenir l’adresse a du dessus du tas;
— utiliser le service « brk » afin de demander au système d’exploita-
tion d’incrémenter cette adresse de n octets.
Par la suite, la région de la mémoire principale comprise dans l’intervalle
[a, a + n) pourrait être utilisée librement par notre programme, exacte-
ment de la même façon que pour la mémoire allouée statiquement.
Un programme, qui dépasse légèrement le cadre du cours, est dispo-
nible sur GitHub . Celui-ci effectue la lecture d’une chaîne de caractères

de taille arbitaire, et ce sans utiliser « scanf ».

Remarque.

Les différents types de systèmes d’exploitation et d’appels systèmes, le


fonctionnement d’un noyau, ainsi que la gestion de la mémoire dyna-
mique, seront couverts dans le cours IFT320 – Systèmes d’exploitation.
CHAPITRE 14. ENTRÉES/SORTIES 151

14.5 Quiz récapitulatif


Vous pouvez tester votre compréhension en complétant ce court quiz . Il est
facultatif, anonyme et n’a aucune incidence sur votre note du cours.

14.6 Exercices

14.1) Écrivez un sous-programme pour le NES qui attend activement que le bou- 
ton start soit enfoncé afin d’appeler le sous-programme « faire_pause ».

14.2) Le processeur du NES peut accéder, indirectement, à la mémoire vidéo 


du processeur d’images (PPU). Afin d’accéder à l’adresse XYZW16 de la
mémoire vidéo, il faut:
— envoyer XY16 à l’adresse 200616 de la mémoire principale;
— envoyer ZW16 à l’adresse 200616 de la mémoire principale;
— effectuer l’opération désirée:
• lecture: lire l’octet à l’adresse 200716 de la mémoire principale
pour obtenir celui à l’adresse XYZW16 de la mémoire vidéo;
• écriture: écrire à l’adresse 200716 de la mémoire principale afin
d’écrire à l’adresse XYZW16 de la mémoire vidéo.
Écrivez un programme qui lit l’octet à l’adresse 200016 de la mémoire vidéo
et le copie à l’adresse 2C0016 de la mémoire vidéo.

14.3) Sur le NES, pourquoi complète-t-on l’exécution d’un gestionnaire d’inter- 


ruption à l’aide de « rti » plutôt que « rts »?

14.4) Nos programmes ARMv8 se terminent par l’appel exit(0) afin de deman- 
der la terminaison du programme sans erreur. Nous utilisions jusqu’ici la
fonction offerte par la librairie standard C. Comment peut-on plutôt ap-
peler le service exit du noyau de Linux (sachant que son code est 93)?
A
Fiches récapitulatives

Les fiches des pages suivantes résument le contenu de chacun des chapitres.
Elles peuvent être imprimées recto-verso, ou bien au recto seulement afin d’être
découpées et pliées en deux. À l’ordinateur, il est possible de cliquer sur la plu-
part des puces « ▶ » pour accéder à la section du contenu correspondant.

152
1. Systèmes de numération
Conversions
Système unaire n fois
z }| { ▶ b à 10: x0 + b · (x1 + b · (x2 + b · (. . . + b · xn−1 )))
▶ Chaque nombre n ∈ N se représente par 1 · · · 11
▶ 10 à b: diviser à répétition par b et concaténer les restes de
▶ L’addition correspond à la concaténation droite à gauche, par ex. 62 = 110:
▶ Pas concis 6 ÷ 2 = 3 reste 0, 3 ÷ 2 = 1 reste 1, 1 ÷ 2 = 0 reste 1
▶ b à bm : remplacer chaque bloc de taille m par sa valeur en
Représentation positionnelle base bm , par ex. si bm = 23 : 10110 → 26
▶ Généralisation du système décimal à une base b ∈ N≥2 ▶ bm à b: éclater chaque symbole vers sa représentation de
▶ Systèmes particuliers: binaire (b = 2), octal (b = 8), décimal taille m en base b, par ex. si bm = 23 : 73 → 111011
(b = 10), hexadécimal (b = 16)
Addition
▶ Chiffres: éléments de {0, 1, . . . , b − 1}
▶ Comme en base 10: additionner chiffre à chiffre en base b et
▶ Chiffres au-delà de 9: A = 10, B = 11, . . . , F = 15, . . . propager une retenue vers la gauche
▶ Valeur de x en base b: xb = xn−1 · bn−1 + . . . + x1 · b1 + x0 · b0
Fractions
▶ Exemple: 8B516 = 8 · 162 + 11 · 161 + 5 · 160
▶ Exemple: (11,01)2 = 1 · 21 + 1 · 20 + 0 · 2−1 + 1 · 2−2 = 3,25
▶ Les zéros tout à gauche ne changent rien: (0 · · · 0x)b = xb
▶ Chiffres non significatifs: (0 · · · 0x,y0 · · · 0)b = (x,y)b

2. Programmation en langage d’assemblage: ARMv8


Données statiques
Registres
▶ Adresse divisible par k: .align k
▶ Registres: x0 –x30 (64 bits) ou w0 –w30 (sous-registres 32 bits)
▶ Alloue k octets consécutifs: .skip k
▶ Usage libre: x0 –x7 (arguments) et x19 –x28 (sauveg. par l’appelé)
▶ 1, 2, 4, 8 octets: .byte v, .hword v, .word v, .xword v
▶ Usage semi-libre: x9 –x15 (sauvegardés par l’appelant)
▶ Chaîne de car.: .asciz s
Organisation du code
Segments de données
▶ Ligne: etiquette: opcode operandes // Commentaire
▶ Instructions: .section ".text"
▶ Étiquette: nom symbolique d’une ligne de code
▶ Données en lecture seule: .section ".rodata"
▶ Exemple: impair:
mov x20, 3 // tmp = 3 ▶ Données initialisées: .section ".data"
mul x20, x20, x19 // tmp = tmp * n ▶ Données non-initialisées: .section ".bss"
add x19, x20, 1 // n = tmp + 1
Quelques instructions Entrée/sortie (de haut niveau via C)
mov xd, v xd ←v où v est regis. ou const. ▶ Affichage: printf(&format, val1 , val2 , . . .)
add xd, xn, v xd ← xn + v où v est regis. ou const. ▶ Lecture: scanf(&format, &var1 , &var2 , . . .)
mul xd, xn, xm xd ← xn · xm
▶ Format nombres: int32 (%d), uint32 (%u), uint32-hex (%X),
udiv xd, xn, xm xd ← xn ÷ xm
64 bits via préfixe l, par ex. int64 (%ld)

3. Architecture des ordinateurs


Architecture et organisation
▶ Big-endian: [00, 58, 40, 0F] vaut 0058400F
▶ Architecture: spécification des services des composants Little-endian: [00, 58, 40, 0F] vaut 0F405800
▶ Organisation: description physique des composants ▶ Alignement: adresser 2k octets à une adresse qui n’est pas un
multiple de 2k — parfois: interdit, souvent: ralentit l’accès
Architecture de von Neumann
▶ Mémoire principale: stocke les programmes et leurs données Processeur code d’opér. opérandes
z}|{ z
}| {
▶ Processeur: unité centrale de traitement de l’ordinateur ▶ Jeu d’instructions élémentaires, par ex: add x10, x11, x12
▶ Unités d’entrée/sortie: contrôlent les périphériques ▶ Registres: cellules de mémoire interne, très rapide d’accès
▶ Bus: systèmes de communication entre les composants ▶ Code machine: traduction des instructions en suite de bits
▶ Compteur d’instruction: pointe vers prochaine instruction
Mémoire principale
▶ Unité de contrôle: coordonne l’exécution des instructions
▶ Suite de cellules d’octets identifiées par des adresses uniques
▶ Unité arithmétique et logique: calculs sur Z et chaînes de bits
▶ Une adresse peut référer à: 1 octet (8 bits), 2 octets (demi-
▶ Pipeline: parallélisation des étapes d’exécution
mot), 4 octets (mot), 8 octets (double mot)
▶ RISC: instructions simples, taille fixe, mémoire–ou–autre
▶ Quantité de mémoire utilisable limitée par taille des adresses
4. Nombres entiers
Représentation des entiers signés
∑n−2 ▶ Multiplication et division non signées: comme en base 10:
▶ Compl. à 2: val(xn−1 · · · x1 x0 ) = −xn−1 · 2n−1 + i=0 xi · 2i
101 (5) 10011 11
×
bits 000 001 010 011 100 101 110 111 11 (3) − 11 00110
101 111
valeur 0 1 2 3 -4 -3 -2 -1 − 11
101
+ 1
1111 (15)
▶ Représentables sur n bits: [−2n−1 , 2n−1 − 1]
▶ Mult. signée: étendre opérandes à 2n bits et garder 2n bits
▶ Bit de signe: négatif ssi bit de gauche = 1 faibles du résultat (s’implémente sans extension explicite)
▶ Ajout de bits: répéter bit de signe à gauche: 101 → 1 · · · 101 ▶ Division signée: calculer |a| ÷ |b| et ajuster signe
complément +1
▶ Changement de signe: 010 −−−−−−−→ 101 −−−−−−−→ 110 Codes de condition bit de retenue résultat trop grand/petit
z
}| { z }| {
▶ Codes: N (négatif), Z (zéro), C (report), V (débordement)
Opérations arithmétiques
▶ Codes modifiés par: cmp, adds, subs, negs, adcs, sbcs
▶ Addition: comme les entiers non signés
▶ Comparaison: codes mis à jour via soustraction bidon
▶ Soustraction: addition/changement de signe: a−b = a+(−b)
▶ Accès aux codes: avec b.condition etiq
▶ Report: lors d’une retenue sur la somme des bits de poids fort
▶ Accès au report: « adc rd, rn, rm » ≡ rd ← rn + rm + C
▶ Débordement: lorsque le résultat ne peut pas être représenté

5. Accès aux données


Adresses
Accès mémoire sur ARMv8
▶ Numérique: entier non négatif, souvent en hexadécimal
▶ Chargement et stockage:
▶ Symbolique: chaîne représentant une adresse à déterminer
# octets chargement stockage
Modes d’adressage 1 ldrb wd, a strb wd, a
▶ Mode: méthode pour récupérer la valeur d’un opérande 2 ldrh wd, a strh wd, a
4 ldr wd, a str wd, a
▶ Récapitulatif des modes: 8 ldr xd, a str xd, a

Nom Valeur récupérée Exemple


▶ Autres instructions:
immédiat i 7→ i mov x0, 42 adr r, etiq // charge adr(etiq) dans reg. r
direct a 7→ mem[a] — mov r, s // charge reg. s dans reg. r
par registre n 7→ reg[n] mov x0, x1 mov r, i // charge valeur i dans reg. r
indirect a 7→ mem[mem[a]] —
indirect par registre n 7→ mem[reg[n]] ldr x0, [x1]
Assemblage
indir. par reg. indexé n, i 7→ mem[reg[n] + i] ldr x0, [x1, i]
indir. par reg. indexé reg[n] ← reg[n] + i, suivi de ▶ Assembleur: instructions → code machine; la plupart des
7→ ldr x0, [x1, i]!
pré-incrémenté n, i mem[reg[n]] adresses symboliques → adresses numériques
indir. par reg. indexé n, i 7→ mem[reg[n]], suivi de
post-incrémenté reg[n] ← reg[n] + i ldr x0, [x1], i ▶ Éditeur de liens: fichiers objets → fichier exécutable; recalcule
relatif i 7→ mem[reg[pc] + i] ldr x0, var certaines adresses; adresses symboliques → numériques

6. Tableaux
Généralités Calcul d’adresse
▶ Tableau: collection d’éléments identifiés par des indices ▶ Index: adresse relative à laquelle est stocké un élément
▶ Éléments: tous de même taille, contigus en mémoire ▶ Calcul: si a = adresse du tableau et k = nombre d’octets d’un
▶ Indice: d-uplet i où d ≥ 1 est la dimension élément, alors l’adresse d’un élément correspond à:
▶ Bornes: 0 ≤ ij < nj pour chaque dimension j
i·k
a + |{z} a + (i · n1 + j) · k
▶ Taille: n0 · n1 · · · nd−1 éléments | {z }
index élém. i (tableau 1D) index élém. (i, j) (tableau 2D)
▶ Types: le type des éléments est implicite
▶ Exemples de tableau 1D et tableau 2D: Allocation/accès mémoire
(0, 0) 2 ▶ Tableau non initialisé:
0 01010101
(0, 1) 33 .section ".bss"
1 11110000
(1, 0) 65535 .align 2
2 01101101 tab: .skip 3*2*2 // n0 * n1 * # octets
(1, 1) 73
3 11111111
4 11110101
(2, 0) 9000 ▶ Tableau initialisé:
(2, 1) 255
.section ".data"
tab: .hword 2, 33, 65535, 73, 9000, 255 // six demi-mots
n0 = 5
n0 = 3, n1 = 2
5 éléments ▶ Accès: avec str / ldr (ou variantes) + modes d’adressage
6 éléments
7. Programmation structurée
Séquence Itération
▶ Composition séquentielle d’instructions ▶ Exécution répétée d’instructions (while, do while, for, ...)
▶ Une instruction de haut niveau peut nécessiter plusieurs ins- ▶ Implémentation: branchements arrière, et parfois avant:
tructions de bas niveau; par ex. « x19 *= 7 » devient:
while (cond(xd, xn)) { boucle:
mov x20, 7 // code cmp xd, xn
mul x19, x19, x20 } b.¬cond fin
// code
Sélection b boucle
fin:
▶ Exécution conditionnelle d’instructions (if, switch, ...)
▶ Implémentation: branchements avant: Sous-programmes
if (cond(xd, xn)) { si: ▶ Permettent de modulariser le code en sous-routines
// code si cmp xd, xn
}
▶ Registres partagés par programme et sous-programmes
b.¬cond sinon
else { // code si ▶ Arguments: passés par valeur ou adresse dans x0 –x7 (en ordre)
// code sinon b fin
} sinon: ▶ Appel: « bl sprog » assigne x30 ← pc+4 et branche à sprog:
// code sinon
fin: ▶ Retour: « ret » branche vers l’adresse de retour x30
▶ Conditions multiples: obtenues avec plusieurs sélections ▶ Sauvegarde: l’appelé doit rétablir les registres x19 à x30

8. Circuits logiques
Circuits Décodage
▶ « Blocs » de base constitués de portes logiques qui permettent ▶ Décodeur: sur entrée x, sortie: yx = 1 et yj = 0 pour j ̸= x
d’implémenter l’ordinateur:
▶ Multiplexeur: sur entrée x, sélectionne le bit yx
x xy xy xy
▶ Instructions: décodables/exécutables à l’aide de tels circuits
x1 x0 y3 y2 y1 y0 x1 x0
Décodeur
¬x x∧y x∨y x⊕y
Arithmétique
▶ Demi-additionneur: somme de deux bits
▶ Additionneur complet: somme de deux bits et d’une retenue
▶ Addition: somme sur n bits avec un demi-additionneur et une y3 y2 y1 y0
cascade de n − 1 additionneurs complets r x y r sb
Mémoire yx
x y
▶ Circuits séquentiels: peuvent mémoriser des bits
▶ Verrou: stocke un bit b,
remise à 0 avec r, et mise à 1 avec s
retenue somme
retenue somme sortie

9. Valeurs booléennes et chaînes de bits


Valeurs booléennes ▶ Bit de signe copié lors d’un décalage arithmétique à droite:
▶ Correspond à un bit: 1 = vrai, 0 = faux 3 bits vers la droite
11000101 −−−−−−−−−−−→ 11111000 asr xd, xn, 3
▶ Représentation: sur un octet, puisque bits non adressables
▶ Multiplication/division: par 2k correspond à un décalage de
Opérateurs logiques k bits vers la gauche/droite
▶ Opérations: ¬, ∧, ∨, ⊕ « bit à bit » étendues aux chaînes: Décalages circulaires
mvn x19, x20 and x19, x20, x21 orr x19, x20, x21 eor x19, x20, x21 ▶ Comme un décalage logique, mais les bits « perdus » sont ré-
¬ ··· ¬ ¬ ¬ 0 ··· 1 0 1 0 ··· 1 0 1 0 ··· 1 0 1 insérés de l’autre côté:
0 ··· 1 0 1 ∧ ··· ∧ ∧ ∧ ∨ ··· ∨ ∨ ∨ ⊕ ··· ⊕ ⊕ ⊕
3 bits vers la gauche
1 ··· 0 1 0 1 ··· 1 0 0 1 ··· 1 0 0 1 ··· 1 0 0 11000101 −−−−−−−−−−−→ 00101110 n’existe pas sur ARMv8
0 ··· 1 0 0 1 ··· 1 0 1 1 ··· 0 0 1 3 bits vers la droite
11000101 −−−−−−−−−−−→ 10111000 ror xd, xn, 3
▶ Échange de valeurs: se fait sans registre temporaire avec eor Masquage
▶ Permet d’isoler certains bits à manipuler:
Décalages logiques et arithmétiques
sélection r∧m met à 0 les bits de r non spécifiés par m
▶ Décale les bits de j positions vers la gauche/droite:
activation r∨m met à 1 les bits de r spécifiés par m
3 bits vers la gauche
11000101 −−−−−−−−−−−→ 00101000 lsl xd, xn, 3 désactivation r ∧ ¬m met à 0 les bits de r spécifiés par m
3 bits vers la droite
11000101 −−−−−−−−−−−→ 00011000 lsr xd, xn, 3 basculement r⊕m inverse les bits de r spécifiés par m
10. Chaînes de caractères
UTF-8
Généralités ▶ Représente > 1 000 000 caractères sur 1 à 4 octets
▶ Caractère: symbole représenté par une chaîne de bits ▶ Caractères 0 à 127: ASCII
▶ Chaîne de caractères: suite finie de caractères, normalement ▶ Caractères 128 à 255: ISO 8859-1, mais codés différemment
terminée par un caractère nul
▶ Format général:
ASCII # bits
plage de codes 1 format binaire des octets
début fin octet 1 octet 2 octet 3 octet 4
▶ Représente 128 caractères codés sur 7 bits
7 00000016 00007F16 0******* — — —
▶ Lettre minuscule mise en majuscule en assignant le 6ème bit 11 00008016 0007FF16 110***** 10****** — —
16 00080016 00FFFF16 1110**** 10****** 10****** —
de poids faible à 0, par ex. a = 11000012 et A = 10000012 21 01000016 10FFFF16 11110*** 10****** 10****** 10******

ISO 8859-1 (Latin-1) ▶ Exemples:


▶ Représente 256 caractères codés sur 8 bits
car. code codage
▶ Caractères 0 à 127: ASCII a 11000012 011000012
▶ Caractères 128 à 255: lettres accentuées et autres caractères é 000 111010012 11000011 101010012
00110000 101100012 11100011 10000010 101100012
𒐍 00001 00100100 000011012 11110000 10010010 10010000 100011012

11. Sous-programmes et mémoire


Disposition de la mémoire. Tas.
00000 · · · 000016

Instructions (text)
▶ Contient les données allouées dynamiquement: structures de
données, objets, etc.
Données statiques

Initialisées, en lecture seule (rodata)


Pile d’exécution.
Initialisées (data)
▶ Stocke les données temporaires lors d’appel de sous-prog.
Non initialisées (bss) ▶ Données empilées à l’appel et dépilées au retour
▶ Pointeur de pile: sp contient l’adresse du sommet de la pile
Données dynamiques

Tas
▶ Empiler: décrémenter sp + stocker avec stp xd, xn, a
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ▶ Dépiler: incrémenter sp + charger avec ldp xd, xn, a

Récursion.
sp (pointeur de pile)
↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑
▶ Implémentée par: appels de sous-prog. + usage de la pile
Pile ▶ Récursion trop profonde: erreur car la pile est bornée
FFFF · · · FFFF16 ▶ Solution (partielle): empiler le moins de données possibles

12. Nombres en virgule flottante


Représentation. Précision.
▶ Nombre en virgule flottante: ▶ Approximations de nombres réels:
exposant (a) arrondir (égalité: dernier chiffre pair): 1,9565 −
→ 1,956
signe
z}|{ z}|{
± d0 ,d1 d2 · · · dn−1 × β e (b) troncation: 1,5416 −
→ 1,541
| {z } |{z}
mantisse en base β base ▶ Erreur relative: err(x) := x−x
x où x est l’approximation

▶ Normalisé: si d0 ̸= 0 ▶ Borne pour mode (a): |err(x)| ≤ (β/2) · β −n


| {z }
Norme IEEE 754. ε machine
▶ Représente différents ordres de grandeur:
⁄2
▶ format signe exposant mantisse
−3 ⁄2
7
−2 −3⁄2 ⁄4
7
5
−1 −3⁄4 −1⁄2 5
⁄8 ⁄8
7 5
⁄4
simple 1 bit 8 bits 23 bits (+1 bit caché)
−5⁄4 −7⁄8 −5⁄8 1
⁄2 ⁄4 1
3
−5⁄2
−7⁄4 ⁄2
3
2
−7⁄2 3 double 1 bit 11 bits 52 bits (+1 bit caché)
Arithmétique. ▶ Repr. avec biais: 1000011011100 · · · 0 = −1,11 × 213−127
▶ Addition: (1) mettre exposants en commun; (2) additionner ▶ ±0 (s0 · · · 00 · · · 0); ±∞ (s1 · · · 10 · · · 00); NaN (s1 · · · 1 e 0 · · · 01)
mantisses; (3) normaliser; (4) arrondir
ARMv8.
▶ Multiplication: (1) additionner exposants; (2) multiplier ▶ Registres: dn (64 bits) et sn (32 bits)
mantisses; (3) normaliser; (4) arrondir
▶ Instructions: ldr, str, fmov, fcmp, fadd, fmul, fsqrt, etc.
13. Introduction aux entrées/sorties : NES
Architecture. Tuiles.
▶ Pas RISC: possible de manipuler la mémoire directement ▶ Images: constituées de tuiles de 8 × 8 pixels
▶ Processeurs: proc. principal + proc. d’images (PPU) ▶ Tuiles: stockées dans la cartouche, transférées vers le PPU
▶ Mémoire principale: primaire + registres d’E/S + programme ▶ Tuile: spécifiée par 4 octets (y, i, a, x): position verticale y,
▶ Mémoire vidéo: stocke les tuiles et palettes de couleurs numéro de tuile i, attributs a, position horizontale x
▶ Attributs: 8 bits pour réflexions, profondeur et couleurs
Jeu d’instructions.
▶ Registres: a (accumulateur), x (index), y (index), s (pile) Sorties (graphiques).
▶ Valeurs imm.: # (numérique), $ (hexadécimal), % (binaire) ▶ L’affichage se fait lors du rafraîchissement vertical
▶ Accès mémoire: lda, ldx, ldy (chargement d’octet); sta, stx, ▶ Sortie: stocker tuiles de 0X0016 à 0XFF16 en mém. principale
sty (stockage d’octet); txa, tax, tya, etc. (copie) ▶ Affichage: transférer au PPU en écrivant #$0X à $4014
▶ Arithmétique: adc (addition avec report); sbc (soustraction avec
Entrées (manettes).
emprunt); inc, inx, iny, dec (inc/décrémentation)
▶ Entrée: protocole de communication via port de manettes
▶ Logique: asl (<< 1), lsr (>> 1), and (∧), ora (∨), eor (⊕)
▶ Demande de lecture: envoyer #1, puis #0, via $4016
▶ Contrôle: cmp, cpx, cpy (comparaison); beq, bne (branch. condi-
tionnel), jmp (branch. incond.), jsr/rts (sous-prog.) ▶ Lecture: lire bit de poids faible à $4016 pour chaque bouton

14. Entrées/sorties
Mécanismes d’entrée/sortie. Accès direct à la mémoire (DMA).
▶ Attente active: interrogation continue d’un registre d’état jus- ▶ DMA: permet au processeur d’initier un accès mémoire et de
qu’à un événement (ex. VBLANK) laisser un contrôleur effectuer le transfert de données
▶ Interruption: signal lancé vers le processeur lors d’un événe- ▶ Sur le NES: envoi des tuiles mem[0x0200, 0x02FF] vers la
ment (ex. NMI, RESET, IRQ) mémoire de sprites via DMA:
lda #$02
Interruptions. sta $4014
Appels système.
▶ Gestionnaire: sous-routine qui traite une interruption ▶ Accès E/S: empêché par le système d’exploitation (sécurité)
▶ Table d’interruptions: contient l’adresse des gestionnaires ▶ Appel système: service offert par le noyau du système d’ex-
▶ Traitement: sauvegarder l’état du processeur; appeler le ges- ploitation; appelé via une interruption logicielle
tionnaire; restaurer l’état ▶ Exemples UNIX + ARMv8: // Afficher chaine
▶ Priorité: valeur numérique assignée à une interruption mov x8, 64
code appel système
▶ Gestion des priorités: interruption ignorée si une interruption mov x0, 1
64 write(flux, chaine, #octets) adr x1, chaine
de priorité > est en cours; gestionnaire en exécution mis en
read(flux, tampon, #octets) mov x2, 10
attente si une interruption de priorité ≥ est lancée 63
svc 0
▶ Non masquable: top priorité, ne peut pas ignorer (ex. RESET) Flux d’entrée standard = 0
Flux de sortie standard = 1
B
Solutions des exercices

Cette section présente des solutions à certains des exercices du document. Dans
certains cas, il ne peut s’agir que d’ébauches de solutions.

158
ANNEXE B. SOLUTIONS DES EXERCICES 159

Chapitre 1
1.1) Si l’on suppose que x ≥ y, alors on peut calculer x − y en retirant en alter- 
nance un symbole de la représentation de x et y jusqu’à ce que ce dernier
devienne vide, auquel cas le premier nombre contient la différence.
Pour calculer x × y, on: (1) initialise une séquence vide z; (2) retourne z
si x est vide; (3) retire un symbole de x; (4) concatène y à z; (5) répète
l’étape 2.

1.2) En appliquant les procédures du chapitre, nous obtenons: 


(a)45442
(b)194FE
(c)289B
(d)1DA
(e)222012210210
(f)
52746757
⋆ C7DDBBCAEEEC71EE2AED
1.3) En appliquant la procédure décrite dans le chapitre, nous obtenons: 
(a) 111010112
(b) 7AE6F216

1.4) 22 bits: deux bits pour le chiffre 2, et quatre bits pour chaque autre chiffre 
hexadécimal.

1.5) Le plus grand nombre pouvant être représenté par 9 chiffres en base 4 
est: 49 − 1 = 262143. Ainsi, le plus grand multiple de 5 représentable est
262140. Autrement dit, il s’agit de ((49 − 1) ÷ 5) · 5 = 262140.

1.6) Un nombre fractionnaire est inférieur à 1 ssi il ne possède que des zéros à 
gauche de la virgule. Ainsi, la plus grande valeur est 00000000,1111112 =
0,1111112 = 1/2 + 1/4 + 1/8 + 1/16 + 1/32 + 1/64 = 63/64 = 0, 984375.

1.7) Non, il n’en existe pas. Le nombre 111b = b2 + b + 1 est pair ssi b2 + b est 
impair. Or, b2 + b = b(b + 1) est forcément pair comme il s’agit du produit
de deux nombres consécutifs.

1.8) ⋆ Soit r ∈ {0, 1} une retenue et soient x et y deux chiffres en base b. Lors 
de l’addition r + x + y, la retenue suivante est r′ := (r + x + y) ÷ b. Cette
nouvelle retenue n’excède pas 1 car:
r′ = (r + x + y) ÷ b
≤ (1 + (b − 1) + (b − 1)) ÷ b (car x, y ∈ {0, 1, . . . , b − 1})
= (2b − 1) ÷ b
=1 (car b entre 1 fois dans 2b − 1).
ANNEXE B. SOLUTIONS DES EXERCICES 160

1.9) ⋆ En bref, si 1/10 se représentait en base 2, alors il serait le ratio de deux 


entiers non nuls m/2k , ce qui est impossible car 10 est divisible par 5.
Formellement, supposons par l’absurde que 0,110 se représente en base 2.
Cela signifie que 0,110 s’écrit de la forme (x,y)2 , où x et y sont respective-
ment formés de n ≥ 1 et k ≥ 1 bits. Comme 0,110 est inférieur à 0, nous
avons forcément que des zéros à la gauche de la virgule. Ainsi:
1
= 0,110
10
= (0,y)2

k
yi
= (par définition d’un nombre fractionnaire)
i=1
2i

k
yi · 2k−i
= (car 2k−i /2k = 1/2i )
i=1
2k
∑k
i=1 yi · 2k−i
= (car sous dénominateur commun).
2k
∑k
i=1 yi · 2
k−i
Par conséquent, en posant m := , nous obtenons 1/10 =
m/2 , ce qui signifie que 2 = 10m et ainsi que 2k−1 = 5m. Remarquons
k k

que m est un entier non nul puisque y contient au moins un bit non nul.
Cela signifie que l’entier 2k−1 > 0 se divise par 5, ce qui est impossible.

1.10) ⋆⋆ 
(a) 1337
(b) +00++0
(c) On a 5 = +--, 8 = +0-, −5 = -++ et −8 = -0+. En général, pour
obtenir −a, il suffit d’intervertir les chiffres + et -. Montrons pourquoi
cela fonctionne. Soit a un nombre qui s’écrit de la forme xn−1 ·3n−1 +
. . . + x1 · 31 + x0 · 30 . Nous avons:
−a = −(xn−1 · 3n−1 + . . . + x1 · 31 + x0 · 30 )
= (−xn−1 ) · 3n−1 + . . . + (−x1 ) · 31 + ·(−x0 ) · 30 .
Puisque −(−1) = 1, −(0) = 0 et −(1) = −1, cela montre qu’il suffit
de remplacer - par +, 0 par 0, et + par - pour passer de a à −a.
(d) On utilise l’approche présentée dans le chapitre, c.-à-d. qu’on addi-
tionne de droite à gauche en propageant une retenue. Il faut cepen-
dant changer les règles d’addition. Premièrement, la table d’addition
de deux chiffres est:
+ - 0 +
- -+ - 0
0 - 0 +
+ 0 + +-
ANNEXE B. SOLUTIONS DES EXERCICES 161

Il faut donc tenir compte de trois types de retenue. La table d’addition


d’une retenue r ∈ {-, 0, +} avec un nombre s ∈ {-+, -, 0, +, +-} est:

retenue + somme intermédiaire = retenue somme


- -+ - 0
- - - +
- 0 0 -
- + 0 0
- +- 0 +
0 -+ - +
0 - 0 -
0 0 0 0
0 + 0 +
0 +- + -
+ -+ 0 -
+ - 0 0
+ 0 0 +
+ + + -
+ +- + 0

Appliquons cette procédure pour calculer 5 + 8 = (+--) + (+0-):


- -
+--
+
+0-
+++

(e) Afin de calculer b − a, on obtient d’abord −a à partir de a, puis on


effectue l’addition b + (−a).
(f) Le plus grand nombre est

+ · · · ++ = 3n−1 + . . . + 31 + 30 = (3n − 1)/2,

et le plus petit nombre est

- · · · -- = −3n−1 − . . . − 31 − 30 = −(3n − 1)/2.

Ainsi, on peut représenter l’intervalle [−(3n − 1)/2, (3n − 1)/2].

1.11) 
a) 7778
b) rwxrw-r-- ≡ 1111101002 = 7648
c) 6528 = 1101010102 ≡ rw-r-x-w-
ANNEXE B. SOLUTIONS DES EXERCICES 162

Chapitre 2
2.1) Voir sur GitHub . 
2.2) Voir sur GitHub . 
2.3) Voir sur GitHub . 
ANNEXE B. SOLUTIONS DES EXERCICES 163

Chapitre 3
3.1) Le mot (4 octets) stocké à l’adresse 2 est: BC16 4816 5F16 1116 . Sa 
valeur est donc 115F48BC16 dans le format « little-endian » et BC485F1116
dans le format « big-endian ». Cette adresse ne respecte pas les contraintes
d’alignement pour un mot puisqu’un mot contient 4 octets et que l’adresse
2 n’est pas un multiple de 4.
Le mot stocké à l’adresse 4 est: 5F16 1116 FF16 4316 . Sa valeur est
donc 43FF115F16 dans le format « little-endian » et 5F11FF4316 dans le
format « big-endian ». En binaire, ces deux valeurs correspondent respec-
tivement à

10000111111111100010001010111112 , et
10111110001000111111111010000112 .

L’adresse respecte les contraintes d’alignement pour un mot puisque l’adresse


4 est un multiple de 4.

3.2) Une architecture de 64 bits peut accéder à 264 octets = 224 · 240 octets = 
16 777 216·(210 )4 octets = 16 777 216·10244 octets = 16 777 216 tébioctets.
Une architecture de 128 bits peut accéder à 2128 octets = 268 ·260 octets =
268 · (210 )6 octets = 268 · 10246 octets = 295 147 905 179 352 825 856 exbi-
octets.

3.3) On obtient ce code machine: 

instruction code machine (variable) code machine (fixe)


foo x6, x1, x3 00 110 001 011 00 110 001 011
bar x5, x4 01 101 100 01 101 100 000
baz x0, x7, x0 10 000 111 000 10 000 111 000

Le code machine représente ce programme:

01 011 101 bar x3, x5


11 qux
00 000 010 111 foo x0, x2, x7

3.4) Non, car elle possède une instruction qui effectue à la fois un accès à la 
mémoire principale et une opération arithmétique.
ANNEXE B. SOLUTIONS DES EXERCICES 164

Chapitre 4

4.1)

43 → 00101011
−43 → 11010101
1 → 00000001
−1 → 11111111
127 → 01111111
−128 → 10000000


4.2)

43 + 25 = 00101011 + 00011001 = 01000100 (68)


43 − 25 = 00101011 + 11100111 = 00010010 (18)
127 + 127 = 01111111 + 01111111 = 11111110 (−2)
−128 − 128 = 10000000 + 10000000 = 00000000 (0)
127 − 128 = 01111111 + 10000000 = 11111111 (−1)


4.3)

00000110 (6)
×
00000111 (7)
00000110
00000110
+
00000110
000000000101010 (42)

4.4) L’addition 0111 (7) + 0001 (1) = 1000 (−8) mène à un débordement sans 
report. L’addition 1111 (−1) +1111 (−1) = 1110 (−2) mène à un report sans
débordement.

4.5)
— Mettons d’abord les deux nombres sur 7 bits: a = 1101011 et b =
1100100. Afin de calculer a − b, on peut calculer a + (−b). La valeur de
−b s’obtient par complément à deux: −b = 0011100. Nous obtenons
donc: a + (−b) = 1101011 + 0011100 = 0000111
— 4 + 2 + 1 = 7.
ANNEXE B. SOLUTIONS DES EXERCICES 165

— L’instruction « cmp x19, x20 » calcule a − b. Comme a et −b sont de


signes différents, il n’y a pas de débordement, ainsi V = 0. Comme
a − b = 7 > 0, on a N = 0 et Z = 0.
— Par la réponse précédente, on a a − b > 0 et ainsi a > b.

4.6) ⋆⋆ Soit x = xn−1 · · · x0 un entier signé de n bits. Soit ext(x) l’extension 


de x avec son bit de signe. Nous avons:

val(ext(x)) = val(xn−1 x) (déf. de ext)



n−1
= −xn−1 · 2n + xi · 2i (déf. de val)
i=0

n−2
= −xn−1 · 2n + xn−1 · 2n−1 + xi · 2i
i=0

n−2
= −xn−1 · (2n − 2n−1 ) + xi · 2 i
i=0

n−2
= −xn−1 · 2n−1 + xi · 2i
i=0
= val(x) (déf. de val).

4.7) ⋆⋆ Soit x = xn−1 · · · x0 un entier signé de n bits. Soit compl(x) le com- 


plément de x bit-à-bit. Nous avons:

val(compl(x))

= val((1 − xn−1 ) · · · (1 − x1 )(1 − x0 )) (car ¬xi = 1 − xi )



n−2
= −(1 − xn−1 ) · 2n−1 + (1 − xi ) · 2i (par déf. de val)
i=0

n−2 ∑
n−2
= −2n−1 + 2i + xn−1 · 2n−1 − xi · 2 i (en distribuant)
i=0 i=0

n−2
= −2n−1 + 2i − val(x) (par déf. de val)
i=0
= −2n−1 + (2 n−1
− 1) − val(x)

= −val(x) − 1.

Nous obtenons donc −val(x) = val(compl(x)) + 1 tel que recherché.


ANNEXE B. SOLUTIONS DES EXERCICES 166

Chapitre 5
5.1) x19 = 4 et x20 = FF16 , puisque: 

Valeur après l’exécution de l’instruction


Instruction
x19 x20
mov x19, 2 2 0x????????????????
ldr x20, [x19], 3 5 0x????04FF115F48BC
sub x19, x19, 1 4 0x????04FF115F48BC
ldrb w20, [x19, 2] 4 0x00000000000000FF

5.2) 5F16 car mem[mem[a]] = mem[mem[0716 ]] = mem[0416 ] = 5F16 . 


5.3) En supposant que l’on désire utiliser le registre r = x19 , on pourrait émuler 
l’instruction avec celles-ci:
adr x19, etiq // a = adr(etiq)
ldr x19, [x19] // b = mem₈[a]
ldr w19, [x19] // r = mem₄[b]

Par exemple, ce programme  utilise cette approche pour charger indi-


rectement une donnée.
ANNEXE B. SOLUTIONS DES EXERCICES 167

Chapitre 6
6.1) Rappelons que dans notre contexte, un mot correspond à 4 octets. Le mot 
qui contient 999 est associé à l’indice (2, 1). Son index est donc ℓ = (2 · 3 +
1) · 4 = 28. Le mot se situe ainsi à l’adresse a + ℓ = FF00AB16 + 1C16 =
FF00C716 .

6.2)

main:
adr x19, tab // a = &tab
mov x20, N // n = N
//
// Calculer le produit //
mov x21, 0 // i = 0
mov x22, 1 // acc = 1
mov x24, 8 //
add x23, x20, 1 //
mul x23, x23, x24 // k = (n+1)*8
//
boucle: // do {
mul x25, x21, x23 // index = i*k
ldr x26, [x19, x25] // d = mem[a+index]
mul x22, x22, x26 // acc *= d
add x21, x21, 1 // i++
cmp x21, x20 // }
b.lo boucle // while (i < n)
//
// Afficher le produit //
adr x0, fmtSortie //
mov x1, x22 //
bl printf // printf("%ld\n", acc)
//
// Quitter //
mov x0, 0 //
bl exit // exit(0)

N = 5

.section ".data"
tab: .xword 10, 3, 4, 5, 6
.xword 2, 1, 2, 9, 5
.xword 1, 4, 5, 8, 7
.xword 1, 1, 1, 1, 1
.xword 0, 2, 6, 0, -2
ANNEXE B. SOLUTIONS DES EXERCICES 168

.section ".rodata"
fmtSortie: .asciz "%ld\n"

6.3) Soit k le nombre d’octets de chaque élément. L’index associé à l’indice 


(i, j) est (j · n0 + i) · k.

6.4) Comme 23 est premier, il n’est un multiple d’aucun autre nombre que 1 et 
23. Le tableau est donc forcément unidimensionnel.

6.5) (i0 · n1 · n2 + i1 · n2 + i2 ) · k. 
6.6) ⋆ Voir sur GitHub . 
ANNEXE B. SOLUTIONS DES EXERCICES 169

Chapitre 7

7.1)

cmp x19, x20


b.lt sinonsi
b.eq sinon
cmp x20, x21
b.ne si
cmp x21, x19
b.ne sinon
si:
// A
b fin
sinonsi:
// B
b fin
sinon:
// C
fin:

7.2) Il doit recevoir le tableau par son adresse ainsi que sa taille. Autrement, il 
n’y a aucune façon d’identifier la fin du tableau.

7.3) On suppose ici qu’il s’agit d’un tableau d’entiers de 64 bits: 


elem: // long elem(long tab[], unsigned long i)
mov x9, 8 // {
mul x1, x1, x9 //
ldr x0, [x0, x1] // return *(tab + i) // return tab[i]
// }

7.4) Voir sur GitHub . 


7.5) Voir sur GitHub . 
7.6) ⋆ Voir sur GitHub . 
ANNEXE B. SOLUTIONS DES EXERCICES 170

Chapitre 8

8.1) max(x, y) = x ∨ y et min(x, y) = x ∧ y.

8.2) Afin de tester si un un nombre de n bits ne vaut pas zéro, il suffit d’utili- 
ser une « cascade » de portes OU. Ainsi, en ajoutant une porte NON, on
obtient le circuit recherché. Autrement dit, on construit un circuit pour
l’expression booléenne

n−1
¬ xi .
i=0

8.3) On peut tester si deux bits xi et yi sont égaux grâce à ¬(xi ⊕ yi ). Afin de 
tester x = y, il suffit donc de comparer x0 avec y0 , x1 avec y1 , . . ., puis de
combiner ces résultats à l’aide d’une cascade de portes ET. Autrement dit,
on construit un circuit pour l’expression booléenne


n−1
¬(xi ⊕ yi ).
i=0

8.4) Étant donné un nombre binaire x de deux bits, la valeur x + 1 correspond 


au nombre binaire y de deux bits défini par:

y0 = ¬x0 ,
y1 = x0 ⊕ x1 ,
y2 = (x0 ∧ x1 ) ⊕ x2 .

Par exemple, si x = 011, alors on obtient y = 100. Cela mène à un circuit


de quatre portes. 
8.5) Il suffit de répéter la même approche: on assigne une porte NON à chaque 
bit d’entrée xi ; on crée 2n sorties y2n −1 , . . . , y1 , y0 ; et on utilise 2n blocs
de portes ET afin d’« allumer » le bon bit. Autrement dit, yk = 1 ssi l’entrée
x correspond à la représentation binaire du nombre k.

8.7) Nous décrivons les formules booléennes: 

x1 ∨ ¬x0 
¬x1 ∧ ¬x0 vrai
x1

¬x0 ¬x1 ∨ x0

x1 ∨ ¬x0

8.8) Soient x l’entrée de n bits. On doit d’abord complémenter x, puis lui addi- 
ANNEXE B. SOLUTIONS DES EXERCICES 171

tionner 1. Soit yi le ième bit en sortie et soit ri la retenue créée par l’addition
à la position i. On a:

y0 = x0 ,
r0 = ¬x0 ,
yi = ri−1 ⊕ ¬xi pour 1 ≤ i < n,
ri = ri−1 ∧ ¬xi pour 1 ≤ i < n.

On peut traduire ces identités en un circuit. Par exemple, pour n = 3, on 


obtient:

x3 x2 x1 x0

y3 y2 y1 y0
ANNEXE B. SOLUTIONS DES EXERCICES 172

Chapitre 9

9.1) ¬a = 010010, a ∧ b = 101000, a ∨ b = 101111, a ⊕ b = 000111

9.2) Pour effectuer un décalage circulaire d’une chaîne de n bits de k bits vers
la gauche, on effectue un décalage circulaire de n − k bits vers la droite.

9.3) and xd, xd, 1
eor xd, xd, 1

9.4) Voir sur GitHub . 


9.5) ⋆⋆ Soit x = xn−1 · · · x1 x0 un entier signé de n bits. Soit x′ le résultat 
d’un décalage arithmétique de x d’un bit vers la droite. Remarquons que
x′ = xn−1 xn−1 · · · x1 . Par définition, nous avons:

n−2
val(x) = xn−1 · −2n−1 + xi · 2i + x0 .
i=1

Ainsi, la division entière par deux mène à:



n−2
val(x) ÷ 2 = xn−1 · −2n−2 + xi · 2i−1 . (B.1)
i=1

Montrons que val(x′ ) = val(x) ÷ 2:



n−1
val(x′ ) = xn−1 · −2n−1 + xi · 2i−1 (déf. de val(x′ ))
i=1

n−2
= xn−1 · −2n−1 + xn−1 · 2n−2 + xi · 2i−1
i=1

n−2
= xn−1 · (−2n−1 + 2n−2 ) + xi · 2i−1
i=1

n−2
= xn−1 · −2n−2 + xi · 2i−1
i=1
= val(x) ÷ 2 (par (B.1)).
9.6) −122110 
9.7) 3 
b4 b3 b2 b1 b0 b4 b3 b2 b1 b0 
9.8) ∧ ⊕
1 0 1 1 0 1 0 0 0 1
b4 0 b2 b1 0 ¬b4 b3 b2 b1 ¬b0

b4 b3 b2 b1 b0

0 1 1 0 0
b4 1 1 b1 b0
ANNEXE B. SOLUTIONS DES EXERCICES 173

Chapitre 10
10.1) En inspectant la table des plages de code, on voit que ces codes donnent 
lieu à des codages de 2, 1, 4 et 3 octets, respectivement. Après conversion
des codes numériques de l’hexadécimal vers le binaire, on obtient:
0006AB16 → 11010101011 → 11011010 10101011
00007316 → 1110011 → 01110011
01234516 → 10010001101000101 → 11110000 10010010 10001101 10000101
00A0B116 → 1010000010110001 → 11101010 10000010 10110001

10.2) Voir sur GitHub  


10.3) Voir sur GitHub  
10.4) Voir sur GitHub  
10.6) — FC16 → 111111002 → 01000111 00111100

— 6 caractères: 1 octet 2 octets 2 octets 1 octet 1 octet 1 octet
— On détermine d’abord s’il s’agit d’un caractère d’un ou deux octets avec une
opération de masquage, puis on extrait le code numérique à l’aide d’un déca-
lage et d’opérations de masquage:

codage:
SAVE
and x19, x0, 0xFF00
cmp x19, 0
b.eq un_octet
deux_octets:
and x19, x0, 0x3000
and x20, x0, 0x0F00
orr x19, x19, x20
lsr x19, x19, 3
and x20, x0, 0x1F
orr x19, x19, x20
b fin
un_octet:
and x19, x0, 0x7F
fin:
mov x0, x19
RESTORE
ret

10.7) ⋆ « ldr w21, [x19, x20, lsl 2] » 


ANNEXE B. SOLUTIONS DES EXERCICES 174

Chapitre 11
11.1) Voir sur GitHub . 
11.2) AEE916 (décrémenté de 4 · 8 = 32), AEF916 (incrémenté de 2 · 8 = 16). 
11.3) On peut empiler xzr , ce qui gaspille 8 octets mis à zéro. Lorsque l’on dépile 
les quatre double mots, on dépile ces 8 octets et les ignore.

11.4)

exp: // exp(n)
SAVE // {
mov x19, x0 //
mov x20, 1 // r = 1
//
cbz x19, fin // if (n != 0)
// {
lsr x0, x19, 1 //
bl exp // k = exp(n / 2)
mul x20, x0, x0 // r = k * k
//
tbz x19, 0, fin // if (n est pair)
lsl x20, x20, 1 // r *= 2
fin: // }
mov x0, x20 //
RESTORE //
ret // return r
// }


11.5)

.macro SAUVEGARDER .macro RESTAURER


stp x29, x30, [sp, -48]! ldp x19, x20, [sp, 16]
mov x29, sp ldp x23, x25, [sp, 32]
stp x19, x20, [sp, 16] ldp x29, x30, [sp], 48
stp x23, x25, [sp, 32] .endm
.endm

11.6) Les étiquettes « foo5: », « foo6: » et « foo7: » sont atteintes infiniment 


souvent. En effet, puisque le registre x30 n’est pas stocké sur la pile d’exé-
cution, l’appel à « printf » détruit l’adresse de retour de « foo: » et ainsi
« ret » branche à « foo5: ».

11.7)
ANNEXE B. SOLUTIONS DES EXERCICES 175

elem: // elem(long tab[], ulong n,


stp x29, x30, [sp, -32]! // long x)
mov x29, x30 // {
stp x19, x20, [sp, 16] //
//
mov x19, x0 //
mov x0, 0 // bool r = false
//
cbz x1, elem_ret // if (n != 0) {
ldr x20, [x19] //
cmp x20, x2 // if (tab[0] == x) {
b.ne elem_rec //
mov x0, 1 // r = true
b elem_ret // }
elem_rec: // else {
add x0, x19, 8 //
sub x1, x1, 1 // r = elem(tab + 1,
bl elem // n - 1, x)
// }
elem_ret: // }
ldp x19, x20, [sp, 16] //
ldp x29, x30, [sp], 32 //
ret // return r
// }


11.8)

elem_rev: // elem_rev(long tab[],


stp x29, x30, [sp, -48]! // ulong n, long x)
mov x29, x30 // {
stp x19, x20, [sp, 16] //
stp x21, xzr, [sp, 32] //
//
mov x19, x0 //
mov x0, 0 // bool r = false
//
cbz x1, elem_rev_ret // if (n != 0) {
sub x20, x1, 1 //
lsl x20, x20, 3 //
ldr x21, [x19, x20] //
cmp x21, x2 // if (tab[n-1] == x) {
b.ne elem_rev_rec //
mov x0, 1 // r = true
b elem_rev_ret // }
elem_rev_rec: // else {
ANNEXE B. SOLUTIONS DES EXERCICES 176

mov x0, x19 //


sub x1, x1, 1 // r = elem_rev(tab,
bl elem_rev // n - 1, x)
// }
elem_rev_ret: // }
ldp x19, x20, [sp, 16] //
ldp x21, xzr, [sp, 32] //
ldp x29, x30, [sp], 48 //
ret // return r
// }
ANNEXE B. SOLUTIONS DES EXERCICES 177

Chapitre 12
12.1) Nombres normalisés: 
1,23456 × 104 − 9,09 × 10−3 1,01 × 2−7 − 1,110111 × 28

Valeurs décimales:

12345,6 −0,00909 5/512 = 1012 ×2−9 −476 = 119·4 = −11101112 ×22

12.2) Les deux variables sont représentées par des nombres en virgule flottante 
double précision de la norme IEEE 754. Nous avons:

x = 1,0 × 240 y = 1,0 × 2−13

Calculons x + y. Mettons les deux nombres sous un exposant commun:

x = 1,0 × 240 · · 0} 1 × 240


y = 0, 0| ·{z
52 fois

En ajoutant des zéros non signicatifs à droite de la mantisse de x, puis en


effectuant la somme bit à bit à partir de la droite, on obtient:
52 fois
z }| {
1, 0 · · · 0 0 × 240
+
0, 0| ·{z
· · 0} 1 × 240
52 fois

1, 0| ·{z
· · 0} 1 × 240
52 fois

Notons que le résultat est déjà normalisé. Cependant, la mantisse possède


trop de bits: 54 bits plutôt que les 53 de la norme IEEE 754. On doit donc
arrondir, ce qui mène à 1, |0 ·{z
· · 0} ×240 . Ainsi, x + y = 240 = x.
23 fois

12.3) (a) 1,11 × 20 + 1,01 × 21 = 0,111 × 21 + 1,010 × 21
= 10,001 × 21
= 1,0001 × 22
≈ 1,00 × 22

(b) 4,0 (approximation) et 4,25 (valeur exacte)


(c) 1/16 obtenu ainsi:
(4,25 − 4,0)/4,0 = 0,25/4,0
= (1/4)/4
= 1/16
ANNEXE B. SOLUTIONS DES EXERCICES 178

12.4) Nous avons: 


(1,11 × 2 ) · (1,01 × 2 ) = (1,11 · 1,01) × 2
0 1 0+1

= (1,11 · 1,01) × 21 .

Évaluons le produit des mantisses en binaire:


1,11
×
1,01
0,0111
+ 0,000
1,11
10,0011

Ainsi, nous obtenons 10,0011 × 21 et après normalisation: 1,00011 × 22 . Il


faut arrondir la mantisse 1,00011. On arrondit vers le haut afin d’obtenir
1,0010. Ainsi, le résultat de la multiplication est 1,0010 × 22 .
Bien que ce ne soit pas demandé, notons que la valeur obtenue est 4,5 alors
que la valeur exacte serait 4,375. Ainsi, l’erreur relative est de (4,375 −
4,5)/4,5 = −0,125/4,5 = −(1/8)/(9/2) = −2/72 = −1/36 = −0,027.

12.5) Voir sur GitHub . 



12.6) — Parce que la représentation a un biais de −127: 13 − 127 = −114.
— Pour représenter des valeurs spéciales: ±0, ±∞, NaN.

12.7) Le nombre décimal 42,5 correspond au nombre binaire 101010,1. En nor- 


malisant, on obtient 1,010101 × 25 , ce qui est équivalent à +1,010101 ×
2132−127 . Son codage binaire est donc

« 0 10000100 01010100000000000000000 ».

12.8) ⋆ Remarquons d’abord que les nombres x et y sont codés par des chaînes 
de 32 bits cx et cy . Les entiers vx et vy correspondent simplement à l’in-
terprétation de cx et cy en tant qu’entiers non signés. Écrivons ≺ afin
de dénoter l’ordre lexicographique. Rappelons que vx < vy ssi cx ≺ cy .
Puisque x et y sont positifs et normalisés, ils sont de la forme x = 1,u × 2e
et y = 1,v × 2f . Notons que
x < y ⇐⇒ (e < f ) ∨ (e = f ∧ u < v).
Autrement dit, on peut comparer x et y, en comparant d’abord leurs ex-
posants, puis leurs mantisses en cas d’égalité. De plus, tester e < f est
équivalent à tester e + 127 < f + 127. Ainsi, puisque les codages cx et cy
sont de la forme « 0 (exposant+127) mantisse », tester cx ≺ cy correspond
précisément à comparer les exposants, puis les mantisses en cas d’égalité.
On a donc x < y ssi cx ≺ cy ssi vx < vy .
ANNEXE B. SOLUTIONS DES EXERCICES 179

12.9) ⋆⋆ Il y a une probabilité de 1/2 que le bit de poids fort de l’exposant 


soit 0. Dans ce cas, on obtient un nombre de la forme 1,x × 2e où e ≤ 0,
car 011111112 − 127 = 0. De plus, la mantisse représente toujours un
nombre (strictement) inférieur à 2. Cela signifie qu’avec probabilité au
moins 1/2, on génère un nombre dont la valeur absolue est inférieure à
2. On obtient donc une distribution (très) loin d’être uniforme comme on
peut représenter des valeurs absolues qui excèdent 2127 .

12.10) ⋆⋆ Soit x ∈ R tel que xmin ≤ |x| ≤ xmax . Le nombre x s’écrit de la forme 
d0 ,d1 d2 · · · dn−1 dn · · · × β e où 0 < d0 < β, 0 ≤ (0,d1 d2 · · · dn−1 · · · )β < 1
et emin ≤ e ≤ emax . Observons que la mantisse de x est obtenue à partir
de la mantisse de x en retirant au plus

(0, |0 ·{z
· · 0} (β − 1)(β − 1) · · · )β .
n − 1 fois

Soient u et v la valeur des mantisses de x et x respectivement. Par l’ob-


servation précédente, nous avons:

|u − v| ≤ β · β −n . (B.2)

Par conséquent:

|err(x)| = |(x − x)/x| (par déf. de err(x))


= |(u · β ) − (v · β )|/(u · β )
e e e
(par déf. de u, v et e)
= |(u − v) · β e |/(u · β e )
= (|u − v| · β e )/(u · β e )
≤ β e−n+1 /(u · β e ) (par (B.2))
= β/(u · β ) n

≤ β/β n . (car u ≥ 1 par d0 > 0)


n−1
= 1/β .

12.11) ⋆ 
(a) Comme nous l’avons vu au chapitre 1, il est impossible de représenter
0,1 de façon finie en binaire. Ainsi, 0.1 est un nombre en virgule flot-
tante simple précision qui approxime 0,1. Plus précisément, il s’agit
de
0,100000001490116119384765625.
Après la dixième itération, x excède donc 1 et ainsi le critère d’arrêt
n’est pas atteint.
(b) Après avoir excédé 10, x continue de croître. À un certain point, la
valeur de x est si grande que l’incrément est négligeable. À partir
de ce moment, la valeur de x ne change plus jamais et la boucle se
répète à l’infini.
ANNEXE B. SOLUTIONS DES EXERCICES 180

Plus précisément, à un certain point, x prend la valeur a := 2097152.


Rappelons que l’incrément est de

b := 0,100000001490116119384765625.

Remarquons que

a + b = 2097152 + 0,100000001490116119384765625
= 1,0 × 221 + 1,0011001100110011001101 × 2−4
21 fois
z }| {
= 1 0 · · · 0 ,0 × 20 + 0,00010011001100110011001101 × 20
21 fois
z }| {
= (1 0 · · · 0 ,0 + 0,00010011001100110011001101) × 20
21 fois
z }| {
= 1 0 · · · 0 ,00010011001100110011001101) × 20 .

Rappelons que les nombres en virgule flottante simple précision pos-


sèdent une mantisse de 24 bits. Ainsi, peu importe la façon d’arron-
dir, on obtient
21 fois
z }| {
1 0 · · · 0 ,00 × 20 .
Ce nombre est égal à 1,0 × 221 qui est égal à a. Ainsi, a + b ≈ a.
La trace de l’exécution du code est comme suit:
x = 0.000000000000000000000000000
x = 0.100000001490116119384765625
x = 0.200000002980232238769531250
x = 0.300000011920928955078125000
x = 0.400000005960464477539062500
x = 0.500000000000000000000000000
x = 0.600000023841857910156250000
x = 0.700000047683715820312500000
x = 0.800000071525573730468750000
x = 0.900000095367431640625000000
x = 1.000000119209289550781250000
x = 1.100000143051147460937500000
...
x = 2097152.000000000000000000000000000
x = 2097152.000000000000000000000000000
x = 2097152.000000000000000000000000000
...
ANNEXE B. SOLUTIONS DES EXERCICES 181

Chapitre 13
13.1) On établit une connexion avec le port de manette; on ignore les boutons 
A et B ; puis on combine l’état des deux boutons suivants avec un ET:

appuye: .rs 1

lecture: ; lecture()
; Demander lecture boutons ; {
lda #1 ; envoyer 1
sta $4016 ; au port de manette 1
lda #0 ; envoyer 0
sta $4016 ; au port de manette 1
;
; Déterminer si SELECT ;
; et START sont appuyés ;
lda $4016 ; lire et ignorer A
lda $4016 ; lire et ignorer B
;
lda $4016 ;
and #%00000001 ; lire état x de SELECT
sta appuye ; appuye = x
;
lda $4016 ;
and #%00000001 ; lire état y de START
and appuye ;
sta appuye ; appuye &= y
;
rts ; }

Remarquons que la lecture du bouton start est inutile lorsque select n’est
pas appuyé. On pourrait donc l’éviter à l’aide d’un branchement.

13.2) On établit une connexion avec le port de manette; on ignore les six pre- 
miers boutons; puis on décrémente posX: si le bouton suivant est enfoncé:

avancer: ; avancer()
; Demander lecture boutons ; {
lda #1 ; envoyer 1
sta $4016 ; au port de manette 1
lda #0 ; envoyer 0
sta $4016 ; au port de manette 1
;
; Ignorer six 1er boutons ;
ldx #0 ; x = 0
ANNEXE B. SOLUTIONS DES EXERCICES 182

;
avancer_lire: ; do {
lda $4016 ; lire/ignorer bouton
inx ; x++
cpx #6 ; }
bne avancer_lire ; while (x != 6)
;
; Déplacer selon flèche ;
lda $4016 ;
and #%00000001 ; a = bit d'état de <--
cmp #0 ;
beq avancer_fin ; if (a != 0)
dec posX ; posX--
avancer_fin: ;
rts ; }

13.3) En indiquant l’octet de poids fort 0316 au bus de données: 


lda #$03 ; envoyer 0x03
sta $4014 ; au registre d'E/S 0x4014

13.4) Si la direction indique la gauche, alors on éteint le bit 6 de l’octet 2 de la 


tuile, et sinon on l’allume:

dir: .rs 1 ; vaut 0 (gauche) ou 1 (droite)
tuile: .rs 4 ; (posY, identifiant, attributs, posX)

renverser: ; renverser()
ldx #2 ; {
lda dir ;
cmp #0 ;
bne renverser_un ; if (dir == 0)
renverser_zero: ; {
lda tuile, x ;
and #%10111111 ; a = tuile[2] & 0xBF
jmp renverser_fin ; }
renverser_un: ; else
lda tuile, x ; {
ora #%01000000 ; a = tuile[2] | 0x40
renverser_fin: ; }
sta tuile, x ; tuile[2] = a
rts ; }

Autre solution (inutilement compliquée) qui illustre d’autres concepts: 


ANNEXE B. SOLUTIONS DES EXERCICES 183

dir: .rs 1 ; vaut 0 (gauche) ou 1 (droite)


tuile: .rs 4 ; (posY, identifiant, attributs, posX)

renverser: ; renverser()
; Créer masque (dir <<= 6) ; {
ldx 0 ; x = 0
renverser_decal_gauche: ; do {
asl dir ; dir <<= 1
inx ; x++
cpx #6 ; }
bne renverser_decal_gauche; while (x != 6)
;
; Renverser tuile selon dir ;
ldx #2 ;
lda tuile, x ; a = tuile[2]
and #%10111111 ; a &= 0xBF
ora dir ; a |= dir
sta tuile, x ; tuile[2] = a
;
; Restaurer dir (dir >>= 6) ;
ldx 0 ; x = 0
renverser_decal_droite: ; do {
lsr dir ; dir >>= 1
inx ; x++
cpx #6 ; }
bne renverser_decal_droite ; while (x != 6)
;
rts ; }
ANNEXE B. SOLUTIONS DES EXERCICES 184

Chapitre 14

14.1)
attendre_start: ; attendre_start()
lda #1 ; {
sta $4016 ; do {
lda #0 ;
sta $4016 ; demander lecture boutons
;
lda $4016 ; ignorer A
lda $4016 ; ignorer B
lda $4016 ; ignorer SELECT
lda $4016 ;
and #%00000001 ; obtenir état a de START
cmp #0 ; }
beq attendre_start ; while (a == 0)
;
jsr faire_pause ; faire_pause()
rts ; }


14.2)
; Lire mem_video[0x2000]
lda #$20
sta $2006
lda #$00
sta $2006
lda $2007
tax

; Copier vers mem_video[0x2C00]


lda #$2C
sta $2006
lda #$00
sta $2006
txa
sta $2007

Notons qu’on pourrait aussi utiliser la pile plutôt que le registre x afin de
mettre le contenu de l’accumulateur a temporairement de côté.

14.3) Afin d’indiquer au processeur de restaurer le registre d’état (donc les codes 
de condition) avant de reprendre son exécution.

14.4)
ANNEXE B. SOLUTIONS DES EXERCICES 185

mov x8, 93
mov x0, 0
svc 0
C
Matériel additionnel

Proposition 1. Soit b une base et soit n ∈ N≥1 . Le plus grand nombre pouvant 
être représenté en base b avec n chiffres est bn − 1.

Démonstration. Soit mb,n le plus grand nombre représentable en base b avec n


chiffres. Nous montrons que mb,n = bn − 1 par induction sur n.
Cas de base. Si n = 1, alors le plus grand nombre est la valeur du plus grand
chiffre, c’est-à-dire b − 1. Nous avons donc bien mb,n = b − 1 = b1 − 1.
Étape d’induction. Supposons que n > 1 et mb,n = bn − 1. Par définition de
valeur en base b, le plus grand nombre formé de n + 1 chiffres est obtenu en
concaténant (b − 1) à gauche du plus grand nombre formé de n chiffres. Ainsi:

mb,n+1 = (b − 1) · bn + mb,n
= (b − 1) · bn + (bn − 1) (par hypothèse d’induction)
=b n+1
−b +b −1
n n

=b n+1
− 1.

Proposition 2. La plus courte représentation de x ∈ N≥1 en base b contient exac- 


tement ⌊logb x⌋ + 1 chiffres.

Démonstration. Soit n le nombre de chiffres dans la plus courte représentation


de x en base b. Cette représentation n’a forcément pas de zéro non significatif.
Nous avons donc bn > x ≥ bn−1 et ainsi n > logb (x) ≥ n − 1.
Cela signifie que n ≥ ⌊logb (x)⌋ + 1 et logb (x) + 1 ≥ n. En combinant ces
deux expressions, nous obtenons logb (x) + 1 ≥ n ≥ ⌊logb (x)⌋ + 1. Comme n est
un entier, nous avons forcément n = ⌊logb (x)⌋ + 1.

Proposition 3. Soit n ∈ N≥2 et soit a le plus grand entier non signé de n bits. La 
représentation binaire de a · a requiert 2n bits.

186
ANNEXE C. MATÉRIEL ADDITIONNEL 187

Démonstration. Nous devons démontrer que a est plus grand que le plus grand
entier non signé pouvant être représenté sur 2n − 1 bits. Nous avons:

a · a = (2n − 1)(2n − 1) (par la proposition 1)


=2 2n
−2 n+1
+1 (en distribuant)
>2 2n
−2 n+1

≥2 2n
− 22n−1 (car 2n − 1 ≥ n + 1 pour tout n ≥ 2)
=2·2 2n−1
−2 2n−1

2n−1
=2
> 22n−1 − 1.

L’algorithme de multiplication de nombres signés du chapitre 4 peut être 


implémenté de manière à ce que les bits additionnels ne soient pas ajoutés
explicitement, et ainsi que la multiplication se fasse directement sur 2n bits.
L’implémentation est décrite à l’algorithme 6.

Algorithme 6: Algorithme pour multiplier deux entiers signés.


Entrées: deux entiers signés a et b de n bits
Sorties: un entier signé de 2n bits égal à a · b
⟨hi, lo⟩ ← ⟨0, b⟩ // paire de deux nombres de n bits
répéter n fois
n ← 0; v ← 0
si dernière itération alors a ← −a
si lo0 = 1 alors n, v, hi ← hi + a // n = nég., v = débord.
⟨hi, lo⟩ ← ⟨(n ⊕ v) hin−1 · · · hi2 hi1 , hi0 lon−1 · · · lo2 lo1 ⟩
retourner ⟨hi, lo⟩

Proposition 4. |err(x)| ≤ ε pour tout x ∈ R \ {0} tel que xmin ≤ |x| ≤ xmax . 
Démonstration. Soit x ∈ R tel que xmin ≤ |x| ≤ xmax . Le nombre x s’écrit de la
forme d0 ,d1 d2 · · · dn−1 dn · · · × β e où 0 < d0 < β, 0 ≤ (0,d1 d2 · · · dn−1 · · · )β < 1
et emin ≤ e ≤ emax . Observons que la mantisse de x est obtenue en arrondissant
la mantisse de x, donc en lui ajoutant ou en retirant au plus

(0, 0| ·{z
· · 0} (β/2))β .
n − 1 fois

Soient u et v la valeur des mantisses de x et x respectivement. Par l’observation


précédente, nous avons:

|u − v| ≤ (β/2) · β −n . (C.1)
ANNEXE C. MATÉRIEL ADDITIONNEL 188

Par conséquent:

|err(x)| = |(x − x)/x| (par déf. de err(x))


= |(u · β ) − (v · β )|/(u · β )
e e e
(par déf. de u, v et e)
= |(u − v) · β |/(u · β )
e e

= (|u − v| · β e )/(u · β e )
≤ β e−n+1 /(2 · u · β e ) (par (C.1))
= β/(2 · u · β n )
≤ β/(2 · β n ). (car u ≥ 1 par d0 > 0)
= ε. (par déf. de ε).
D
Architecture ARMv8: sommaire

Cette annexe dresse un sommaire de l’architecture ARMv8 et plus particulière-


ment de son jeu d’instructions. L’annexe contient également quelques rappels
utiles comme les formats d’entrée/sortie du langage C, et les commandes de
débogage de GDB.

189
Registres. [22 février 2024]

— Chaque registre xn possède 64 bits: b63 b62 · · · b1 b0


— Notation: xn ⟨i⟩ := bi , xn ⟨i, j⟩ := bi bi−1 · · · bj , rn réfère au registre xn ou wn
— Chaque sous-registre wn possède 32 bits et correspond à xn ⟨31, 0⟩
— Le compteur d’instruction pc n’est pas accessible
— Conventions:
Registres Nom Utilisation
x0 – x7 — registres d’arguments et de retour de sous-programmes
x8 xr registre pour retourner l’adresse d’une structure
x9 – x15 — registres temporaires sauvegardés par l’appelant
x16 – x17 ip0 – ip1 registres temporaires intra-procéduraux
x18 pr registre temporaire pouvant être réservé par le système
x19 – x28 — registres temporaires sauvegardés par l’appelé
x29 fp pointeur vers l’ancien sommet de pile (frame pointer)
x30 lr registre d’adresse de retour (link register)
xzr sp registre contenant la valeur 0, ou pointeur de pile (stack pointer)

Arithmétique (entiers).
— Les codes de condition sont modifiés par cmp, adds, adcs, subs, sbcs et negs
— À cette différence près, adds, adcs, subs, sbcs et negs se comportent respectivement comme add, adc, sub, sbc et neg
— Instructions, où i est une valeur immédiate de 12 bits et j est une valeur immédiate de 6 bits:

Code d’op. Syntaxe Effet Exemple


cmp rd, rm compare rd et rm cmp x19, x21
cmp cmp rd, i compare rd et i cmp x19, 42
cmp rd, rm, decal j compare rd et rm decal j cmp x19, x21, lsl 1
add rd, rn, rm rd ← rn + rm add x19, x20, x21
add add rd, rn, i rd ← rn + i add x19, x20, 42
add rd, rn, rm, decal j rd ← rn + (rm decal j) add x19, x20, x21, lsl 1
adc adc rd, rn, rm rd ← rn + rm + C adc x19, x20, x21
sub rd, rn, rm rd ← rn − rm sub x19, x20, x21
sub sub rd, rn, i rd ← rn − i sub x19, x20, 42
sub rd, rn, rm, decal j rd ← rn − (rm decal j) sub x19, x20, x21, lsl 1
sbc sbc rd, rn, rm rd ← rn − rm − 1 + C sbc x19, x20, x21
neg rd, rm rd ← −rm neg x19, x21
neg
neg rd, rm, decal j rd ← −(rm decal j) neg x19, x21, lsl 1
mul mul rd, rn, rm rd ← rn · rm mul x19, x20, x21
udiv udiv rd, rn, rm rd ← rn ÷ rm (non signé) udiv x19, x20, x21
sdiv sdiv rd, rn, rm rd ← rn ÷ rm (signé) sdiv x19, x20, x21
madd madd rd, rn, rm, ra rd ← ra + (rn · rm ) madd x19, x20, x21, x22
msub msub rd, rn, rm, ra rd ← ra − (rn · rm ) msub x19, x20, x21, x22
Accès mémoire.
— ldrsw, ldrsh et ldrsb se comportent respectivement comme ldr (4 octets), ldrh et ldrb à l’exception du fait qu’ils effec-
tuent un chargement dans xd où les bits excédentaires sont le bit de signe de la donnée chargée, plutôt que des zéros
— Instructions, où a est une adresse et memb [a] réfère aux b octets à l’adresse a de la mémoire principale:

Code d’op. Syntaxe Effet Exemple


mov rd, rm rd ← rm mov x19, x21
mov
mov rd, i rd ← i mov x19, 42
ldr xd, a charge 8 octets: xd ⟨63, 0⟩ ← mem8 [a] ldr x19, [x20]
ldr
ldr wd, a charge 4 octets: xd ⟨31, 0⟩ ← mem4 [a]; xd ⟨63, 32⟩ ← 0 ldr w19, [x20]
ldrh ldrh wd, a charge 2 octets: xd ⟨15, 0⟩ ← mem2 [a]; xd ⟨63, 16⟩ ← 0 ldrh w19, [x20]
ldrb ldrb wd, a charge 1 octet: xd ⟨ 7, 0⟩ ← mem1 [a]; xd ⟨63, 8⟩ ← 0 ldrb w19, [x20]
str xd, a stocke 8 octets: mem8 [a] ← xd ⟨63, 0⟩ str x19, [x20]
str
str wd, a stocke 4 octets: mem4 [a] ← xd ⟨31, 0⟩ str w19, [x20]
strh strh wd, a stocke 2 octets: mem2 [a] ← xd ⟨15, 0⟩ str w19, [x20]
strb strb wd, a stocke 1 octet: mem1 [a] ← xd ⟨ 7, 0⟩ strb w19, [x20]
ldp ldp xd, xn, a charge 16 octets: xd ⟨63, 0⟩ ← mem8 [a], xn ⟨63, 0⟩ ← mem8 [a + 8] ldp x19, x20, [sp]
stp stp xd, xn, a stocke 16 octets: mem8 [a] ← xd ⟨63, 0⟩, mem8 [a + 8] ← xn ⟨63, 0⟩ stp x19, x20, [sp]

Conditions de branchement.
— Codes de condition: N (négatif), Z (zéro), C (report), V (débordement)
— C indique aussi l’absence d’emprunt lors d’une soustraction
— Conditions de branchement:

Entiers non signés Entiers signés


Code Signification Codes de condition Code Signification Codes de condition
eq = Z eq = Z
ne ̸= ¬Z ne ̸= ¬Z
hs ≥ C ge ≥ N=V
hi > C ∧ ¬Z gt > ¬Z ∧ (N = V)
ls ≤ ¬C ∨ Z le ≤ Z ∨ (N ̸= V)
lo < ¬C lt < N ̸= V
vs débordement V
vc pas de débordement ¬V
mi négatif N
pl non négatif ¬N

Branchement.
— Instructions de branchement, où j est une valeur immédiate de 6 bits:

Code d’op. Syntaxe Effet Exemple


b. b.cond etiq branche à etiq: si cond b.eq main100
b b etiq branche à etiq: b main100
cbz cbz rd, etiq branche à etiq: si rd = 0 cbz x19 main100
cbnz cbnz rd, etiq branche à etiq: si rd ̸= 0 cbnz x19 main100
tbz tbz rd, j, etiq branche à etiq: si rd ⟨j⟩ = 0 tbz x19, 1, main100
tbnz tbnz rd, j, etiq branche à etiq: si rd ⟨j⟩ ̸= 0 tbnz x19, 1, main100
bl bl etiq branche à etiq: et x30 ← pc + 4 bl printf
blr blr xd branche à xd et x30 ← pc + 4 blr x20
br br xd branche à xd br x20
ret ret branche à x30 (retour de sous-prog.) ret
Adressage.
— Modes d’adressages, où k est une valeur immédiate de 7 bits:

Nom Syntaxe Adresse Effet Exemple


adresse d’une étiquette adr xd, etiq — xd ← adresse de etiq: adr x19, main100
indirect par registre [xd] xd — [x20]
[xd, xn] xd + xn — [x20, x21]
indirect par registre indexé [xd, k] xd +k — [x20, 1]
[xd, xn, decal k] xd + (xn decal k) — [x20, x21, lsl 1]
ind. par reg. indexé pré-inc. [xd, k]! xd +k xd ← xd + k avant calcul [x20, 1]!
ind. par reg. indexé post-inc. [xd], k xd xd ← xd + k après calcul [x20], 1
relatif etiq adresse de etiq — main100

Autres instructions.
Code d’op. Syntaxe Effet Exemple
csel csel rd, rn, rm, cond si cond: rd ← rn , sinon: rd ← rm csel x19, x20, x21, eq

Logique et manipulation de bits.


— Les instructions lsl, lsr, asr et ror possèdent également une variante de 32 bits utilisant les registres wd , wn et wm (dans
ce cas, les 32 bits de poids fort sont mis à 0)
— Instructions, où i est une valeur immédiate de 12 bits et j est une valeur immédiate de 6 bits:

Code d’op. Syntaxe Effet Exemple


mvn mvn rd, rn rd ← ¬rn mvn x19, x20
and rd, rn, rm rd ← rn ∧ rm and x19, x20, x21
and and rd, rn, i rd ← rn ∧ i and x19, x20, 4
and rd, rn, rm, decal j rd ← rn ∧ (rm decal j) and x19, x20, x21, lsl 1
orr rd, rn, rm rd ← rn ∨ rm orr x19, x20, x21
orr orr rd, rn, i rd ← rn ∨ i orr x19, x20, 4
orr rd, rn, rm, decal j rd ← rn ∨ (rm decal j) orr x19, x20, x21, lsl 1
eor rd, rn, rm rd ← rn ⊕ rm eor x19, x20, x21
eor eor rd, rn, i rd ← rn ⊕ i eor x19, x20, 4
eor rd, rn, rm, decal j rd ← rn ⊕ (rm decal j) eor x19, x20, x21, lsl 1
bic rd, rn, rm rd ← rn ∧ ¬rm bic x19, x20, x21
bic bic rd, rn, i rd ← rn ∧ ¬i bic x19, x20, 4
bic rd, rn, rm, decal j rd ← rn ∧ ¬(rm decal j) bic x19, x20, x21, lsl 1
décalage de j bits vers la gauche:
lsl lsl xd, xn, j lsl x19, x20, 1
xd ⟨63, j⟩ ← xn ⟨63 − j, 0⟩; xd ⟨j − 1, 0⟩ ← 0
décalage de j bits vers la droite:
lsr lsr xd, xn, j lsr x19, x20, 1
xd ⟨63 − j, 0⟩ ← xn ⟨63, j⟩; xd ⟨63, 64 − j⟩ ← 0
décalage arithmétique de j bits vers la droite:
asr asr xd, xn, j asr x19, x20, 1
xd ⟨63 − j, 0⟩ ← xn ⟨63, j⟩; xd ⟨63, 64 − j⟩ ← xn ⟨63⟩
décalage circulaire de j bits vers la droite:
ror ror xd, xn, j ror x19, xn, 1
xd ← xn ⟨j − 1, 0⟩ xn ⟨63, j⟩
Registres (nombres en virgule flottante).
— Possède 32 registres double précision (64 bits) de la forme dn
— Chaque registre dn possède un sous-registre simple précision (32 bits) sn
— vn réfère au registre dn ou sn
— Conventions:
Registres Utilisation
d0 – d7 registres d’arguments et de retour de sous-programmes
d8 – d15 registres sauvegardés par l’appelé
d16 – d31 registres sauvegardés par l’appelant

Manipulation et arithmétique (nombres en virgule flottante).


— Les conditions de branchement sont les mêmes que pour les entiers et sont déterminées à partir de codes de condition mis
à jour par fcmp

Code d’op. Syntaxe Effet Exemple


ldr dn, a charge un nombre en virgule flottante double ldr d8, [x19]
ldr
précision de l’adresse a vers dn (8 octets)
ldr sn, a charge un nombre en virgule flottante simple ldr s8, [x19]
précision de l’adresse a vers sn (4 octets)
str dn, a stocke un nombre en virgule flottante double str d8, [x19]
str
précision de dn vers l’adresse a (8 octets)
str sn, a stocke un nombre en virgule flottante simple str s8, [x19]
précision de sn vers l’adresse a (4 octets)
fmov vd, vm vd ← vm fmov d8, d9
fmov
fmov vd, i vd ← i fmov d8, 1.5
fcmp vd, vm compare vd et vm fcmp d8, d9
fcmp
fcmp vd, i compare vd et i fcmp d8, 0.0
fadd fadd vd, vn, vm vd ← vn + vm fadd d8, d9, d10
fsub fsub vd, vn, vm vd ← vn − vm fsub d8, d9, d10
fmul fmul vd, vn, vm vd ← vn · vm fmul d8, d9, d10
fdiv fdiv vd, vn, vm vd ← vn /vm fdiv d8, d9, d10

fsqrt fsqrt vd, vn vd ← vn fsqrt d8, d9
fabs fabs vd, vn vd ← |vn | fabs d8, d9
convertit l’entier non signé dans rn vers un ucvtf d8, x19
nombre en virgule flottante dans vd (selon le ucvtf d8, w19
ucvtf ucvtf vd, rn
mode d’approximation configuré dans le registre de contrôle ucvtf s8, x19
FPCR) ucvtf s8, w19
convertit l’entier signé dans rn vers un scvtf d8, x19
nombre en virgule flottante dans vd (selon le scvtf d8, w19
scvtf scvtf vd, rn
mode d’approximation configuré dans le registre de contrôle scvtf s8, x19
FPCR) scvtf s8, w19
convertit le nombre en virgule flottante dans
fcvt fcvt vd, vn vn vers un nombre en virgule flottante d’une fcvt d8, s9
autre précision dans vd
Appels système.
— x8 : code numérique du service
— x0 à x5 : arguments
— svc 0: appel du service

Données statiques.

Segments de données Données


Pseudo-instruction Contenu .align k donnée suivante stockée à une adresse divisible par k
.section ".text" instructions .skip k réserve k octets
.section ".rodata" données en lecture seule .ascii s chaîne de caractères initialisée à s
.section ".data" données initialisées .asciz s chaîne de caractères initialisée à s suivi du carac. nul
.section ".bss" données non-initialisées .byte v octet initialisé à v
.hword v demi-mot initialisé à v
.word v mot initialisé à v
.xword v double mot initialisé à v
.single f nombre en virg. flottante simple précision initialisé à f
.double f nombre en virg. flottante double précision initialisé à f

Entrées/sorties (haut niveau).


— Affichage: printf(&format, val1 , val2 , . . .)
— Lecture: scanf(&format, &var1 , &var2 , . . .)
— Spécificateurs de format:

Famille Format Type


%ld entier décimal signé
%lu entier décimal non signé
Nombres sur 64 bits
%lX entier hexadécimal non signé
%lf nombre en virgule flottante
%d entier décimal signé
%u entier décimal non signé
Nombres sur 32 bits
%X entier hexadécimal non signé
%f nombre en virgule flottante
%hd entier décimal signé
Nombres sur 16 bits %hu entier décimal non signé
%hX entier hexadécimal non signé
%c caractère (1 octet)
Caractères
%s chaîne de caractères
Débogage avec GDB.
Commande Effet
Commandes de base
gdb exec Charge l’exécutable ./exec en mode débogage
break etiq Ajoute un point d’interruption à l’étiquette etiq:
run Débute l’exécution en mode débogage
continue Continue l’exécution jusqu’au prochain point d’interruption
stepi Exécute la prochaine instruction
nexti Exécute la prochaine instruction (sans entrer dans les sous-programmes)
info reg Affiche le contenu des registres
x &etiq Affiche le contenu de la mémoire à l’adresse associée à l’étiquette etiq:
quit Quitter le débogueur
Commandes avancées
run < fichier Débute l’exécution en mode débogage avec l’entrée contenue dans fichier
p/s $xd Affiche le contenu du registre dans le format s parmi l’un de ces choix:
u = entier non signé, d = entier signé,
x = valeur hexadécimale, t = valeur binaire,
f = nombre en virgule flottante, c = caractère.

Par exemple, p/t $x19 affiche le contenu du registre x19 en binaire


p/s var Affiche le contenu de la variable var dans le format s
set var = val Assigne la valeur val à var; ce-dernier peut être un registre $xd ou une variable
x 0xABCDFE Affiche le contenu de la mémoire à l’adresse hexadécimale ABCDEF
x/nsu adr Affiche le contenu de n unités de mémoire à partir de l’adresse adr dans le format s.
L’unité de mémoire est défini par l’un des choix suivants de u:
b = octet, h = demi-mot,
w = mot, g = double mot.

Par exemple, x/10ug &tab affiche les 10 premiers éléments de 64 bits non signés d’un
tableau tab
E
Architecture du NES: sommaire

Cette annexe dresse un sommaire de l’architecture du NES et plus particulière-


ment de son jeu d’instructions.

196
Registres. [22 février 2024]

— Possède 4 registres d’un octet


— Registre interne: p (registre d’état), contient des états et codes de conditions dont report/emprunt (1 octet)
— Registre interne: pc (compteur d’instruction), contient l’adresse de la prochaine instruction (2 octets)

Nom Utilisation principale


a accumulateur, utilisé comme opérande et valeur de retour des opé-
rations arithmétiques et logiques
x utilisé comme compteur ou comme index pour l’adressage indexé
y utilisé comme compteur ou comme index pour l’adressage indexé
s pointeur de pile (pointe vers 010016 + s)

Valeurs immédiates.
— #: valeur numérique, sans #: adresse
— $: valeur hexadécimale
expression valeur
— %: valeur binaire
#5 510
— Exemples: #$FF FF16
#%00010011 000100112
$FF adresse FF16

Modes d’adressage.
Nom. Syntaxe Adresse Exemple
absolu i i lda $D010
i, x i+x lda $D010, x
indexé par x
etiq, x etiq + x lda tab, x
i, y i+y lda $D010, y
indexé par y
etiq, y etiq + y lda tab, y

Accès mémoire.
— Instructions, où mem1 [a] dénote l’octet situé à l’adresse a de la mémoire principale:

Code d’op. Syntaxe Effet Exemple


lda #i a←i lda #42
lda
lda adr a ← mem1 [adr] lda var
ldx #i x←i ldx #42
ldx
ldx adr x ← mem1 [adr] ldx var
ldy #i y←i ldy #42
ldy
ldy adr y ← mem1 [adr] ldy var
sta sta adr mem1 [adr] ← a sta var
stx stx adr mem1 [adr] ← x stx var
sty sty adr mem1 [adr] ← y sty var
txa txa a←x txa
tax tax x←a tax
tya tya a←y tya
tay tay y←a tay
txs txs s←x txs
tsx tsx x←s tsx
pha pha empile a sur la pile pha
pla pla dépile le premier octet de la pile vers a pla
Arithmétique.
Code d’op. Syntaxe Effet Exemple
adc #i a ← a + i + report lda #1
adc
adc adr a ← a + mem1 [adr] + report adc var
sbc #i a ← a − i − emprunt sbc #1
sbc
sbc adr a ← a − mem1 [adr] − emprunt sbc var
clc clc report ← 0 (utile avant adc) clc
sec sec emprunt ← 0 (utile avant sbc) sec
inx inx x←x+1 inx
iny iny y←y+1 iny
inc inc adr mem1 [adr] ← mem1 [adr] + 1 inc var
dec dec adr mem1 [adr] ← mem1 [adr] − 1 dec var

Logique.
Code d’op. Syntaxe Effet Exemple
asl asl adr décalage logique de mem1 [adr] d’un asl var
bit à gauche (directement en mémoire)
lsr lsr adr décalage logique de mem1 [adr] d’un lsr var
bit à droite (directement en mémoire)
and #i a←a∧i and #%00100011
and
and adr a ← a ∧ mem1 [adr] and var
ora #i a←a∨i ora #%00100011
ora
ora adr a ← a ∨ mem1 [adr] ora var
eor #i a←a⊕i eor #%00100011
eor
eor adr a ← a ⊕ mem1 [adr] eor var

Comparaisons et branchements.
Code d’op. Syntaxe Effet Exemple
cmp #i compare a et i cmp #0
cmp
cmp adr compare a et mem1 [adr] cmp var
cpx #i compare x et i cpx #0
cpx
cpx adr compare x et mem1 [adr] cpx var
cpy #i compare y et i cpy #0
cpy
cpy adr compare y et mem1 [adr] cpy var
beq beq etiq branche à etiq: si = beq boucle
bne bne etiq branche à etiq: si ̸= bne boucle
jmp jmp etiq branche à etiq: jmp boucle
jsr jsr etiq branche au sous-programme etiq: jsr func
et empile l’adresse de retour
rts rts branche à l’adresse de retour d’un rts
sous-programme
rti rti branche à l’adresse de retour d’une rti
interruption
Bibliographie

[ARM13] ARM Limited. Procedure Call Standard for the ARM 64-bit Architecture
(AArch64), 2013. Version 1.0.
[ARM15] ARM Limited. ARM® Cortex®-A Series: Programmer’s Guide for
ARMv8-A, 2015. Version 1.0.
[ARM18] ARM Limited. ARM® Architecture Reference Manual: ARMv8, for
ARMv8-A architecture profile, 2018. Version D.a.
[PH17] David Patterson and John Hennessy. Computer Organization and De-
sign RISC-V Edition. Elsevier, 2017.
[SD11] Richard St-Denis. L’architecture du processeur SPARC et sa program-
mation en langage d’assemblage. Éditions GGC, 2011.
[Yer03] F. Yergeau. UTF-8, a transformation format of ISO 10646. RFC 3629,
2003.

199
Index

accès direct à la mémoire, 147 bits, 86


accès mémoire, 26, 45 borne, 57
addition, 6, 39, 79, 118 boucle, 72
adr, 53
adressage, 26, 49 caractère, 99
adresse, 48 changement de base, 4
de retour, 75 chaîne de caractères, 99
numérique, 48 circuit
symbolique, 48 combinatoire, 84
affichage, 14, 136 séquentiel, 84
algèbre de Boole, 86 à verrouillage, 84
alignement, 28 circuit logique, 79
allocation, 60 CISC, 33
appel, 73 code de condition, 44
appel système, 148 commentaire, 19
architecture, 24 commutativité, 88
de von Neumann, 24 complément à deux, 37
argument, 73 compteur d’instruction, 32
arithmétique, 118 conjonction, 86
ARM, 11 contraintes d’alignement, 28
arrondi, 116 conversion, 4
ASCII, 99 cryptographie, 89
assembleur, 54
assignation demi-additionneur, 79
par sélection, 76 Dijkstra, Edsger, 68
attente active, 142 dimension, 57
disjonction, 86
bidimensionnel, 60 division, 43
big-endian, 27 DMA, 147
binaire, 3 double, 120
bit de signe, 37 décalage arithmétique, 91

200
INDEX 201

décalage circulaire, 90 Latin-1, 100


décalage logique, 89 ldr, 53
dépiler, 109 ldrb, 53
ldrh, 53
E/S, 142 lecture, 15
éditeur de liens, 54 ligne de code, 18
EDVAC, 24 big-endian, 27
empiler, 108
ENIAC, 24 manette, 136
entiers, 37 manipulation de bits, 86, 87
entiers négatifs, 37 masquage, 93
entiers signés, 37 masque jetable, 89
entrée/sortie, 129, 142 matrice, 57
ET logique, 86 mode d’adressage, 49
étiquette, 19 MOS6502, 129
exaoctet, 29 mov, 53
exbioctet, 29 multiplication, 40, 119
mébioctet, 29
fichier objet, 54 mégaoctet, 29
fraction, 7 mémoire, 106
principale, 25
George Boole, 86 vive, 25
gestionnaire d’interruption, 143
gibioctet, 29 NaN, 122
gigaoctet, 29 NES, 129
granularité, 26 nombre en virgule flottante, 115
gros-boutiste, 27 nombre fractionnaire, 7
norme IEEE 754, 120
hexadécimal, 3 normes de programmation, 18
négation, 86
IEEE 754, 120
implication, 86 octal, 4
implémentation, 24 organisation, 24, 33
index, 58 OU
indice, 57 exclusif, 86
initialisation, 60 logique, 86
interruption, 143
logicielle, 147 paramètre, 73
ISO 8859-1, 100 parcours de tableau, 61
itération, 72 passage
par adresse, 74
jeu de la vie, 67 par valeur, 74
petit-boutiste, 27
kibioctet, 29
pile d’exécution, 106
kilooctet, 29
pipeline, 33
langage d’assemblage, 11 pointeur, 64
INDEX 202

porte logique, 79 unaire, 1


printf, 14, 21 sélection, 69
priorité, 146 séquence, 68
processeur, 30 de Collatz, 12
program counter, 32
programmation tableau, 57
impérative, 68 taille, 29
structurée, 68 tas, 106
précision, 116 troncation, 116
double, 120 tuile, 135
simple, 120 type, 57
prédiction de branchement, 33
Puissance 4, 78 UAL, 32
pébioctet, 29 Unicode, 102
pétaoctet, 29 unidimensionnel, 59
unité arithmétique et logique, 32
registre, 11, 30 unité de contrôle, 32
représentation, 115 UTF-16, 102
restauration, 75, 109 UTF-32, 102
retour, 74 UTF-8, 102
RISC, 33
récursion, 109 valeur de retour, 74
verrou, 84
sauvegarde, 75, 108 von Neumann, 24
scanf, 15, 21
segment de données, 20 zéro non significatif, 2, 7
simple, 120
équivalence, 86
sous-programme, 73, 106
sous-routine, 73
soustraction, 40
spécificateur de format, 21
spécification, 24
stdio, 15
str, 53
strb, 53
strh, 53
structure de contrôle, 68
switch, 69
systèmes de numération, 1
binaire, 3
décimal, 2
hexadécimal, 3
notation positionnelle, 2
notation unaire, 1
numération romaine, 2
octal, 4

Vous aimerez peut-être aussi