Tableaux et adresses
Ce que ce chapitre apporte 5 points
- Charger l'adresse d'une donnée dans un registre avec la, puis lire la case visée avec lw.
- Calculer l'adresse d'un élément à partir de l'adresse de base, de l'indice et de la taille d'un élément.
- Parcourir un tableau de mots en déplaçant un registre d'adresse de quatre en quatre.
- Parcourir une chaîne d'octets terminée par un zéro, en déplaçant l'adresse de un en un.
- Expliquer pourquoi lire au-delà de la fin d'un tableau ne provoque aucune erreur, et ce que la lecture rapporte alors.
En Python, t[3] désigne le quatrième élément d'une liste, et la question de savoir où il se trouve ne se pose jamais. Le processeur, lui, ne dispose d'aucune notion de tableau, d'élément ou d'indice. Il a une mémoire, qui est une longue suite d'octets numérotés, et des instructions qui lisent quatre octets à une adresse. Un tableau n'est donc rien de plus qu'une convention : des valeurs de même taille rangées les unes derrière les autres, et une adresse qui désigne la première. Tout le reste, y compris l'indice, est une addition. Ce chapitre installe ce calcul d'adresse et montre ce qui arrive quand il sort du tableau, ce qui est précisément la différence entre un langage qui protège et un langage qui obéit.
Les dix instructions employées ici
Ce chapitre n'introduit aucune instruction nouvelle : les dix qu'il emploie ont toutes été rencontrées avant. Elles reviennent en revanche toutes les trois ou quatre lignes, et un lecteur qui les a croisées il y a quelques jours a le droit de ne plus se souvenir de leur forme exacte. Les voici rassemblées une fois pour toutes, à garder sous les yeux le temps du chapitre.
| Instruction | Ce qu'elle fait |
|---|---|
la t0, notes | met dans t0 l'adresse de la donnée déclarée notes, et rien d'autre |
lw t1, 0(t0) | lit les quatre octets rangés à l'adresse contenue dans t0 |
sw t1, 0(t0) | écrit les quatre octets de t1 à l'adresse contenue dans t0 |
lb t2, 0(t0) | lit un seul octet à cette adresse, au lieu de quatre |
li t1, 5 | met la constante 5 dans t1 |
add t1, t0, t1 | range dans t1 la somme de t0 et de t1 |
addi t0, t0, 4 | ajoute la constante 4 à t0, sans toucher à la mémoire |
slli t1, a0, 2 | décale a0 de deux bits vers la gauche, ce qui multiplie par quatre |
beqz t1, fin | saute à l'étiquette fin si t1 vaut zéro |
j boucle | saute à l'étiquette boucle, sans condition |
Le détail de chaque famille, avec les variantes que ce chapitre n'emploie pas, se trouve dans le mémento du jeu d'instructions.
Un mot enfin sur les figures, qui ne sont pas seulement à regarder tourner. Le bouton « Modifier le programme » ouvre le texte du programme affiché : chaque frappe le réassemble et le relance, et un autre bouton ramène à la version d'origine. Les consignes qui demandent de changer une valeur et de relancer se suivent donc sans quitter la page, et une instruction dont le rôle reste flou se comprend souvent mieux en la supprimant pour voir ce qui manque ensuite.
Charger une adresse, puis lire à travers elle
Une déclaration données: pose des valeurs en mémoire et donne un nom à l'adresse de la première. Ce nom n'est pas une variable : c'est une adresse, et elle doit passer par un registre avant que quoi que ce soit ne soit lu. Le geste tient en deux temps, la puis lw, et il ne changera plus de tout le module.
programme
- 0x0la t0, noteslui t0, 1
- 0x4addi zero, zero, 0
- 0x8lw t1, 0(t0)
- 0xcaddi t0, t0, 4
- 0x10lw t2, 0(t0)
registres
| Registre | Valeur | Valeur précédente |
|---|---|---|
| sp | 0x2000 | |
| t0 | 0 | |
| t1 | 0 | |
| t2 | 0 |
mémoire
| Adresse | Valeur | Nom |
|---|---|---|
| 0x1000 | notes | 12 |
| 0x1004 | 5 | |
| 0x1008 | -3 | |
| 0x100c | 40 |
Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.
Cinq pas, t1 à 12 et t2 à 5. La figure signale les deux accès, à l'adresse 0x1000 puis à l'adresse 0x1004 : quatre octets d'écart pour un élément d'écart. Entre les deux, addi t0, t0, 4 a fait passer t0 de 4096 à 4100, c'est-à-dire de 0x1000 à 0x1004. Le registre t0 ne contient pas une note : il contient l'endroit où en chercher une.
La colonne des instructions réelles mérite un regard. La ligne la t0, notes y donne lui t0, 1, suivie d'un addi zero, zero, 0 qui ne fait rien : l'assembleur réserve deux instructions pour charger une adresse, parce qu'une adresse ne tient pas toujours sur douze bits, et comble avec une instruction neutre quand une seule suffit.
lw t1, notes est refusé, et c'est volontaireLe moteur de ce module rejette l'écriture qui mélange le nom et la lecture, alors qu'un assembleur professionnel l'accepterait en la développant en deux instructions. Le refus force le geste juste : une adresse se charge dans un registre, puis se lit à travers lui. C'est ce geste, et non la commodité d'écriture, qui rend lisible tout ce qui suit, du parcours de tableau jusqu'à la pile.
Que contient un registre après la t0, notes ?
L'indice n'existe pas, il se calcule
L'accès t[i] d'un langage de haut niveau se décompose en quatre choses, dont aucune n'est cachée ici : une adresse de base, un indice, une taille d'élément, et une addition. L'adresse cherchée vaut base plus indice multiplié par la taille de l'élément. Pour des mots de quatre octets, multiplier par quatre revient à décaler de deux bits vers la gauche, ce que slli fait en une instruction, faute de multiplication dans RV32I.
programme
- 0x0la t0, noteslui t0, 1
- 0x4addi zero, zero, 0
- 0x8slli t1, a0, 2
- 0xcadd t1, t0, t1
- 0x10lw t2, 0(t1)
registres
| Registre | Valeur | Valeur précédente |
|---|---|---|
| sp | 0x2000 | |
| t0 | 0 | |
| t1 | 0 | |
| t2 | 0 | |
| a0 | 2 |
mémoire
| Adresse | Valeur | Nom |
|---|---|---|
| 0x1000 | notes | 12 |
| 0x1004 | 5 | |
| 0x1008 | -3 | |
| 0x100c | 40 |
Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.
Le registre t1 vaut 8 après le décalage, 4104 après l'addition, et la lecture rapporte -3, le troisième élément. Les trois instructions du milieu sont exactement ce qu'un compilateur produit pour t[i], et elles expliquent d'un coup plusieurs mystères : pourquoi un tableau est indexé à partir de zéro, puisque l'élément zéro est celui qui se trouve à l'adresse de base plus rien ; pourquoi la taille d'élément doit être connue à la compilation ; et pourquoi l'accès à un élément quelconque coûte le même temps quel que soit l'indice.
Quelle est l'adresse de l'élément d'indice 3 d'un tableau de mots dont la base vaut 0x1000 ?
Parcourir plutôt que recalculer
Rien n'oblige à repartir de la base à chaque tour. Un parcours complet se fait plus simplement en déplaçant le registre d'adresse, une boucle du chapitre précédent tenant le compte des éléments restants.
programme
- 0x0la t0, noteslui t0, 1
- 0x4addi zero, zero, 0
- 0x8li t1, 5addi t1, zero, 5
- 0xcli t2, 0addi t2, zero, 0
- 0x10beqz t1, finbeq t1, zero, fin
- 0x14lw t3, 0(t0)
- 0x18add t2, t2, t3
- 0x1caddi t0, t0, 4
- 0x20addi t1, t1, -1
- 0x24j bouclejal zero, boucle
registres
| Registre | Valeur | Valeur précédente |
|---|---|---|
| sp | 0x2000 | |
| t0 | 0 | |
| t1 | 0 | |
| t2 | 0 | |
| t3 | 0 |
mémoire
| Adresse | Valeur | Nom |
|---|---|---|
| 0x1000 | notes | 12 |
| 0x1004 | 5 | |
| 0x1008 | -3 | |
| 0x100c | 40 | |
| 0x1010 | 7 |
Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.
Trente-cinq pas, t2 à 61, et cinq lectures aux adresses 0x1000, 0x1004, 0x1008, 0x100c et 0x1010. Deux registres suffisent à tenir le parcours : t0 dit où lire, t1 dit combien de fois encore. Le registre t0 finit à 4116, une case au-delà du dernier élément, ce qui est normal et mérite d'être noté : après un parcours, le pointeur désigne la fin, pas le dernier élément.
Écrire dans le tableau se fait avec la même adresse, et l'instruction symétrique.
programme
- 0x0la t0, noteslui t0, 1
- 0x4addi zero, zero, 0
- 0x8li t1, 4addi t1, zero, 4
- 0xcbeqz t1, finbeq t1, zero, fin
- 0x10lw t2, 0(t0)
- 0x14add t2, t2, t2
- 0x18sw t2, 0(t0)
- 0x1caddi t0, t0, 4
- 0x20addi t1, t1, -1
- 0x24j bouclejal zero, boucle
registres
| Registre | Valeur | Valeur précédente |
|---|---|---|
| sp | 0x2000 | |
| t0 | 0 | |
| t1 | 0 | |
| t2 | 0 |
mémoire
| Adresse | Valeur | Nom |
|---|---|---|
| 0x1000 | notes | 12 |
| 0x1004 | 5 | |
| 0x1008 | -3 | |
| 0x100c | 40 |
Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.
Trente-deux pas, et le tableau contient à la fin 24, 10, -6 et 80. Chaque tour lit, calcule, range au même endroit. La figure signale les écritures avec la valeur précédente, ce qui rend visible le fait qu'un tableau modifié sur place perd ses valeurs d'origine sans laisser de trace.
Une chaîne est un tableau d'octets
Un texte suit exactement la même convention, avec deux différences. La taille d'un élément vaut un octet et non quatre, donc l'adresse avance de un en un. Et la longueur n'est écrite nulle part : la fin est signalée par un octet nul posé après le dernier caractère. Le parcours ne compte donc pas, il cherche.
programme
- 0x0la t0, messagelui t0, 1
- 0x4addi zero, zero, 0
- 0x8li t1, 0addi t1, zero, 0
- 0xclb t2, 0(t0)
- 0x10beqz t2, finbeq t2, zero, fin
- 0x14addi t1, t1, 1
- 0x18addi t0, t0, 1
- 0x1cj bouclejal zero, boucle
registres
| Registre | Valeur | Valeur précédente |
|---|---|---|
| sp | 0x2000 | |
| t0 | 0 | |
| t1 | 0 | |
| t2 | 0 |
mémoire
| Adresse | Valeur | Nom |
|---|---|---|
| 0x1000 | message | 1702064993 |
| 0x1004 | 1701601901 | |
| 0x1008 | 29301 |
Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.
Cinquante-cinq pas, et t1 finit à 10, la longueur du mot. Les valeurs prises par t2 sont 97, 115, 115, 101, 109, 98, 108, 101, 117, 114, puis 0 : les codes des lettres, lus un octet à la fois par lb, et le zéro terminal trouvé à l'adresse 0x100a. Le décalage est de 1 à chaque tour, jamais de 4, parce que la taille d'un élément est ici d'un octet.
La taille d'élément n'est donc pas une propriété de la mémoire, c'est un choix de lecture. Rien n'empêche de lire quatre octets d'un texte d'un seul coup.
programme
- 0x0la t0, messagelui t0, 1
- 0x4addi zero, zero, 0
- 0x8lb t1, 0(t0)
- 0xclb t2, 3(t0)
- 0x10lw t3, 0(t0)
registres
| Registre | Valeur | Valeur précédente |
|---|---|---|
| sp | 0x2000 | |
| t0 | 0 | |
| t1 | 0 | |
| t2 | 0 | |
| t3 | 0 |
mémoire
| Adresse | Valeur | Nom |
|---|---|---|
| 0x1000 | message | 1145258561 |
| 0x1004 | 0 |
Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.
Les deux lectures d'octet donnent 65 et 68, les codes de A et de D. La lecture de mot, à la même adresse de base, donne 1 145 258 561. Ce nombre n'a aucun sens en tant que nombre, et il est pourtant la lecture exacte des mêmes quatre octets, assemblés dans l'ordre où le processeur les range. Les octets ne changent pas : c'est l'instruction qui décide de les voir comme un caractère ou comme un nombre de trente-deux bits.
Noter au passage le décalage écrit dans l'instruction, 3(t0), qui évite de déplacer le registre. Les deux écritures sont équivalentes, et la première est préférable quand l'adresse de base doit être conservée.
1.Pourquoi un parcours de chaîne avance-t-il de un en un, alors qu'un parcours de tableau de mots avance de quatre en quatre ?
2.Comment un programme sait-il où se termine une chaîne ?
Sortir du tableau ne provoque rien
Voici la différence la plus nette entre ce module et un langage de haut niveau. En Python, lire t[7] dans une liste de trois éléments lève une exception qui arrête tout. Ici, il ne se passe rien du tout.
programme
- 0x0la t0, noteslui t0, 1
- 0x4addi zero, zero, 0
- 0x8lw t1, 8(t0)
- 0xclw t2, 12(t0)
- 0x10lw t3, 16(t0)
registres
| Registre | Valeur | Valeur précédente |
|---|---|---|
| sp | 0x2000 | |
| t0 | 0 | |
| t1 | 0 | |
| t2 | 0 | |
| t3 | 0 |
mémoire
| Adresse | Valeur | Nom |
|---|---|---|
| 0x1000 | notes | 12 |
| 0x1004 | 5 | |
| 0x1008 | -3 | |
| 0x100c | seuil | 777 |
Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.
Le tableau notes occupe douze octets, de 0x1000 à 0x100b. La première lecture, au décalage 8, atteint son dernier élément et donne -3. La deuxième, au décalage 12, sort du tableau et donne 777 : c'est la valeur de seuil, déclarée juste après et posée à l'adresse 0x100c. La troisième, au décalage 16, atteint une case où rien n'a jamais été écrit et donne 0. Aucune des trois n'est signalée comme une faute, et le programme se termine normalement.
La mémoire n'a pas de bord. Un tableau n'existe pas pour le processeur, donc sa fin n'existe pas davantage. Lire au-delà rapporte ce qui se trouve à côté : une autre donnée, un reste d'exécution précédente, ou des octets jamais écrits. Le programme continue avec une valeur fausse, et la faute se manifeste bien plus loin, sous une forme qui n'a plus aucun rapport avec sa cause. C'est le mécanisme de fond des vulnérabilités de lecture hors limites, et l'explication de pourquoi les langages qui vérifient l'indice acceptent d'en payer le prix.
Une erreur est bien signalée, mais elle porte sur autre chose : l'alignement. Un mot de quatre octets se lit à une adresse multiple de quatre, et le moteur refuse toute autre adresse.
programme
- 0x0la t0, noteslui t0, 1
- 0x4addi zero, zero, 0
- 0x8lw t1, 1(t0)
registres
| Registre | Valeur | Valeur précédente |
|---|---|---|
| t0 | 0 | |
| t1 | 0 |
mémoire
| Adresse | Valeur | Nom |
|---|---|---|
| 0x1000 | notes | 12 |
| 0x1004 | 5 | |
| 0x1008 | -3 |
Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.
L'exécution s'interrompt avec le message qui désigne 0x1001 comme non multiple de quatre. La contrainte vient du matériel, qui va chercher la mémoire par blocs alignés, et elle explique une part des règles de disposition que les langages système imposent aux structures de données. Il est donc parfaitement possible de sortir d'un tableau sans être inquiété, et parfaitement impossible de lire un mot une case trop loin dans le mauvais sens.
Un programme lit un mot situé huit octets après la fin d'un tableau. Que se passe-t-il ?
Deux dimensions dans une seule suite de cases
Une grille n'a pas d'existence propre non plus. Elle est rangée ligne après ligne dans la même suite de cases, et l'élément de coordonnées ligne et colonne occupe l'indice ligne × largeur + colonne. La formule ajoute une multiplication à celle qui convertit l'indice en octets.
programme
- 0x0la t0, grillelui t0, 1
- 0x4addi zero, zero, 0
- 0x8slli t1, a0, 2
- 0xcadd t1, t1, a1
- 0x10slli t1, t1, 2
- 0x14add t1, t0, t1
- 0x18lw t2, 0(t1)
registres
| Registre | Valeur | Valeur précédente |
|---|---|---|
| sp | 0x2000 | |
| t0 | 0 | |
| t1 | 0 | |
| t2 | 0 | |
| a0 | 2 | |
| a1 | 1 |
mémoire
| Adresse | Valeur | Nom |
|---|---|---|
| 0x1000 | grille | 10 |
| 0x1004 | 11 | |
| 0x1008 | 12 | |
| 0x100c | 13 | |
| 0x1010 | 20 | |
| 0x1014 | 21 | |
| 0x1018 | 22 | |
| 0x101c | 23 | |
| 0x1020 | 30 | |
| 0x1024 | 31 | |
| 0x1028 | 32 | |
| 0x102c | 33 |
Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.
Sept pas, et t2 reçoit 31. Le registre t1 passe par 8, la ligne multipliée par la largeur, puis 9, l'indice dans la suite, puis 36, le même indice converti en octets, puis 4132, soit 0x1024, l'adresse finale. Les deux slli valent tous deux « multiplier par quatre », mais pour des raisons distinctes : le premier parce que la grille a quatre colonnes, le second parce qu'un mot occupe quatre octets. Les deux coïncident ici, et il serait imprudent d'en tirer une règle. Une grille de trois colonnes demanderait une multiplication par trois, donc des additions répétées, puisque RV32I ne multiplie pas.
Dans une grille de quatre colonnes, quel indice occupe la case de la ligne 2 et de la colonne 1 ?
Chapitre révisé en octobre 2026 · Une erreur dans ce chapitre ?
Mettre en pratique
L'adresse d'un élément, le parcours sans multiplication, et ce que rapporte une lecture hors bornes.
Tous les exercices sur tableaux et adresses