Masquer et filtrer
Ce que ce chapitre apporte
- Décrire ce que font and, or et xor bit par bit, et nommer l'usage propre à chacun.
- Construire un masque pour isoler un champ, forcer un drapeau à un ou l'effacer sans toucher aux bits voisins.
- Distinguer le décalage logique du décalage arithmétique, et choisir celui qui convient à la donnée traitée.
- Extraire un champ situé au milieu d'un mot en combinant un décalage et un masque.
- Reconnaître les limites de la forme immédiate, et passer par li quand un masque dépasse douze bits signés.
Un registre contient trente-deux bits, et il arrive très souvent que ces trente-deux bits ne représentent pas un nombre. Un registre d'état d'automate y range trente-deux réponses par oui ou par non. Une adresse réseau y range quatre octets qui ne s'additionnent jamais. Un mot reçu d'un capteur y range une valeur sur douze bits, un code d'erreur sur trois, et le reste sans signification. Dans tous ces cas, la question n'est pas de calculer, mais de découper : garder certains bits, en mettre d'autres à un, en basculer un seul, et déplacer un champ jusqu'à l'endroit où il se lit. Trois opérations logiques et trois décalages y suffisent, et rien de tout cela n'a d'exotique.
Les instructions de ce chapitre
Trois opérations logiques et trois décalages, chacun en une seule ligne. C'est le chapitre du module qui emploie le plus d'instructions différentes, mais la moitié vient des précédents, et les six premières lignes du tableau ne sont que trois opérations écrites de deux façons.
| Instruction | Forme | Ce qu'elle fait |
|---|---|---|
and | and rd, rs1, rs2 | Ne garde dans rd que les bits portés à un par rs1 et par rs2 : le reste tombe à zéro. |
andi | andi rd, rs, imm | La même chose, le masque étant écrit dans l'instruction. Déjà croisée au chapitre précédent. |
or | or rd, rs1, rs2 | Met à un dans rd tout bit qu'au moins un des deux registres porte à un. |
ori | ori rd, rs, imm | La même chose, le masque étant écrit dans l'instruction. |
xor | xor rd, rs1, rs2 | Met à un dans rd les seuls bits qui diffèrent entre les deux registres. |
xori | xori rd, rs, imm | La même chose, le masque étant écrit dans l'instruction. |
slli | slli rd, rs, n | Pousse les bits de rs de n positions vers la gauche, en comblant par des zéros à droite. Déjà vue au premier chapitre. |
srli | srli rd, rs, n | Pousse les bits de n positions vers la droite, en comblant par des zéros à gauche. Déjà vue au chapitre précédent. |
srai | srai rd, rs, n | Pousse les bits de n positions vers la droite, en recopiant le bit de poids fort à gauche. |
Deux formes existent donc pour chaque opération logique : celle à trois registres, qui prend son masque dans un registre, et celle dite immédiate, reconnaissable au i final, qui porte son masque dans l'instruction. La seconde est plus courte à écrire et plus limitée, ce dont la dernière section de ce chapitre tire les conséquences. Les programmes s'appuient par ailleurs sur li, la, lb et sb, déjà connues.
Trois opérations, appliquées trente-deux fois
and, or et xor travaillent bit par bit : chacune aligne les deux registres source et applique la même règle à chaque paire de bits, indépendamment des voisins. Il n'y a ni retenue, ni propagation : trente-deux opérations élémentaires menées en parallèle. and donne un si les deux bits valent un, or si au moins l'un des deux vaut un, xor si les deux bits diffèrent.
De ces trois règles découlent trois usages, et c'est ainsi qu'il faut les mémoriser. Un bit passé à and avec 0 devient 0, et avec 1 reste inchangé : and efface là où le masque porte des zéros, et laisse passer là où il porte des uns. Un bit passé à or avec 1 devient 1, et avec 0 reste inchangé : or force à un. Un bit passé à xor avec 1 change de valeur, et avec 0 reste inchangé : xor bascule.
Un masque est une valeur choisie non pour ce qu'elle vaut, mais pour la position de ses bits. Un masque d'extraction porte des uns là où l'information intéresse et des zéros partout ailleurs, et s'emploie avec and. Un masque de forçage porte un un à la seule position à établir, et s'emploie avec or. Écrire un masque en binaire ou en hexadécimal est presque obligatoire : 0b00001000 se relit, et 8 ne dit rien de la position concernée.
Isoler un champ
Le cas le plus courant consiste à ne garder qu'une partie d'une valeur.
programme
- 0x0andi t0, a0, 0b1111
registres
| t0 | 0 | |
| a0 | 181 |
Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.
a0 vaut 181, dont l'écriture binaire est 10110101. Le masque 0b1111 porte des uns sur les quatre positions de droite et des zéros ailleurs. Le résultat est 0101, soit 5 : les quatre bits de gauche ont été effacés, les quatre de droite recopiés tels quels.
Le nom andi se lit comme au chapitre 1 : and avec un immédiat, c'est-à-dire une constante écrite dans l'instruction plutôt que prise dans un registre. La contrainte des douze bits signés s'applique, et sera reprise à la fin de ce chapitre.
Un cas particulier de masque mérite d'être retenu tel quel.
programme
- 0x0andi t0, a0, 1
- 0x4andi t1, a1, 1
registres
| t0 | 0 | |
| t1 | 0 | |
| a0 | 7 | |
| a1 | 12 |
Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.
Le bit de droite d'un nombre binaire vaut un pour un nombre impair et zéro pour un nombre pair, exactement comme le dernier chiffre décimal renseigne sur la divisibilité par dix. Tester la parité revient donc à un andi avec 1, et c'est ce qu'écrit tout compilateur là où le code source demandait un reste de division par deux. L'opération est d'autant plus intéressante que RISC-V de base ne sait pas diviser.
Forcer, effacer, sans toucher aux voisins
Un registre d'état rassemble des drapeaux indépendants. Modifier l'un d'eux sans perturber les autres est le geste quotidien du pilotage de matériel.
programme
- 0x0ori t0, a0, 0b00001000
- 0x4li t1, -33addi t1, zero, -33
- 0x8and t2, t0, t1
registres
| t0 | 0 | |
| t1 | 0 | |
| t2 | 0 | |
| a0 | 33 |
Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.
a0 vaut 33, soit 100001 : les drapeaux de rang 0 et de rang 5 sont levés. Le ori ajoute le drapeau de rang 3 et donne 41, soit 101001. Les deux drapeaux précédents sont intacts, ce que or garantit puisqu'il laisse passer tout bit déjà à un.
L'effacement demande un détour. Il n'existe pas d'instruction qui mette un bit à zéro : il faut un and avec un masque qui porte un zéro à la position visée et des uns partout ailleurs. Pour effacer le rang 5, le masque est donc l'inverse de 0b100000, soit -33 en écriture décimale. li t1, -33 le pose dans un registre, et and t2, t0, t1 l'applique : c'est la forme à trois registres, celle qui prend son masque là où il se trouve au lieu de le porter dans l'instruction. Le résultat vaut 9, soit 1001 : le rang 5 a disparu, les rangs 0 et 3 sont restés.
Que -33 serve de masque déconcerte au premier regard. Son motif de bits est fait de trente et un uns et d'un seul zéro, en position 5, ce qui est très exactement le masque voulu ; Décider expliquera pourquoi un nombre négatif s'écrit ainsi. La pseudo-instruction not produit ce complément à partir d'un masque positif, et l'assembleur la remplace par un xori avec -1, ce qui bascule les trente-deux bits d'un coup.
Le tout se combine dans une figure qui résume à peu près tout le pilotage de matériel.
programme
- 0x0la t0, etatlui t0, 3
- 0x4addi t0, t0, 256
- 0x8lb t1, 0(t0)
- 0xcori t2, t1, 0b1000
- 0x10li t3, -3addi t3, zero, -3
- 0x14and t4, t2, t3
- 0x18sb t4, 0(t0)
registres
| sp | 0x2000 | |
| t0 | 0 | |
| t1 | 0 | |
| t2 | 0 | |
| t3 | 0 | |
| t4 | 0 |
mémoire
| 0x3100 | etat | 38 |
Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.
Le la charge l'adresse du registre d'état et le lb en rapporte l'octet, qui vaut 38, soit 100110. Le ori lève le drapeau de rang 3 et donne 46, soit 101110. Le and avec -3, dont le motif de bits porte un seul zéro en position 1, efface ce drapeau et donne 44, soit 101100. Le sb final renvoie l'octet au périphérique.
Cette suite de trois temps porte un nom, et c'est le motif fondamental de tout dialogue avec un registre matériel. Écrire directement la valeur voulue effacerait les drapeaux qu'un autre morceau de code avait levés ; il faut donc partir de ce qui s'y trouve. Ce même motif, exécuté par deux programmes en même temps sur la même case, est exactement ce qui produit les pertes de mise à jour qu'étudie le cours sur la concurrence.
Basculer, et remettre à zéro
xor occupe une place à part. Appliqué deux fois avec le même masque, il ramène à la valeur de départ.
programme
- 0x0xori t0, a0, 0b0010
- 0x4xori t1, t0, 0b0010
registres
| t0 | 0 | |
| t1 | 0 | |
| a0 | 10 |
Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.
a0 vaut 10, soit 1010. Le premier xori bascule le bit de rang 1 et donne 8, soit 1000. Le second le bascule de nouveau et rend 10. C'est ce qui permet de faire clignoter une sortie sans jamais savoir dans quel état elle se trouvait : le masque dit quel bit changer, pas quelle valeur lui donner.
Une conséquence de la règle de xor mérite un programme à elle seule.
programme
- 0x0xor t0, t0, t0
- 0x4xor t1, t1, t1
registres
| t0 | 123456 | |
| t1 | -1 |
Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.
Un bit ne diffère jamais de lui-même : xor d'un registre avec lui-même donne donc zéro, quelle que soit la valeur de départ. L'idiome a une raison d'être historique, sur les architectures où une constante coûte plusieurs octets dans l'instruction, et il apparaît dans presque tout code désassemblé. En RISC-V, li t0, 0 fait aussi bien, mais l'idiome se reconnaît dans les autres alphabets, ce qui est le sujet du dernier chapitre du module.
Le même raisonnement fournit une comparaison : xor de deux valeurs donne zéro si, et seulement si, elles sont identiques. Tester une égalité sans instruction de comparaison a un usage très concret dans les réseaux, deux sections plus bas.
Déplacer un champ
Les décalages complètent les masques. slli pousse les bits vers la gauche en comblant par des zéros à droite, srli les pousse vers la droite en comblant par des zéros à gauche, srai les pousse vers la droite en comblant par des copies du bit de poids fort.
La différence entre les deux décalages à droite ne se voit pas sur un nombre positif, dont le bit de poids fort vaut zéro. Il faut un négatif.
programme
- 0x0srli t0, a0, 1
- 0x4srai t1, a0, 1
- 0x8slli t2, a0, 1
registres
| t0 | 0 | |
| t1 | 0 | |
| t2 | 0 | |
| a0 | -8 |
Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.
L'écart est spectaculaire. srai rend -4, ce qui est la moitié de -8 : le décalage arithmétique préserve le signe, et divise par deux. srli rend 2 147 483 644, un nombre positif énorme : le décalage logique a fait entrer un zéro par la gauche, à la place du bit de signe, et la valeur a changé de nature.
Aucune des deux n'est correcte dans l'absolu. Le bon choix dépend de ce que le registre contient. Une mesure signée, une température, un écart se décalent avec srai. Un motif de bits, une adresse, un octet de trame se décalent avec srli, parce que leur bit de poids fort est un bit comme les autres et non un signe.
Le décalage vers la gauche, lui, ne pose pas cette question : il n'existe qu'en une seule version, et slli de -8 donne -16, soit un doublement, comme il donnerait un doublement sur un positif.
L'écart entre les deux décalages à droite se fait disparaître en une frappe. Ouvrir « Modifier le programme », remplacer -8 par 8 sur la ligne registres: et relancer : srli et srai rendent alors tous deux 4, et seule slli continue de se distinguer, avec 16. Sur un nombre dont le bit de poids fort vaut zéro, il n'y a rien à recopier, donc rien qui sépare les deux instructions.
srai divise par deux, mais arrondit toujours vers le bas, et non vers zéro. Sur -7, un décalage arithmétique d'une position rend -4, là où un quotient tronqué vers zéro vaudrait -3. L'écart ne concerne que les nombres négatifs, et les langages eux-mêmes ne s'accordent pas sur la règle d'arrondi, ce qui rend ce détail d'autant plus facile à ne pas remarquer.
Un champ au milieu du mot
Un champ qui n'est ni tout à droite ni tout à gauche demande les deux outils : un masque pour l'isoler, un décalage pour l'amener en position de lecture.
programme
- 0x0li t1, 0xFF000lui t1, 255
- 0x4addi zero, zero, 0
- 0x8and t0, a0, t1
- 0xcsrli t2, t0, 12
registres
| t0 | 0 | |
| t1 | 0 | |
| t2 | 0 | |
| a0 | 305419896 |
Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.
La valeur 0x12345678 contient, sur les bits 12 à 19, l'octet 0x45, et le masque 0xFF000 porte des uns exactement sur ces huit positions. Le and ne garde que ce champ, mais le laisse là où il était : 282 624, un nombre qui ne veut rien dire. Le srli de douze positions le ramène à droite et donne 69, soit 0x45 : c'est seulement à ce moment que le champ devient lisible et comparable. L'ordre inverse, décaler puis masquer, fonctionne aussi bien, et les deux formes se rencontrent dans le code produit par les compilateurs.
Une adresse réseau, découpée
Le cas d'usage le plus parlant réunit tout ce chapitre. Une adresse IPv4 est un nombre de trente-deux bits, que la notation pointée présente comme quatre octets. Découper l'un en l'autre est une affaire de décalages et de masques.
programme
- 0x0li t0, 0xC0A80D07lui t0, -259455
- 0x4addi t0, t0, -761
- 0x8srli t1, t0, 24
- 0xcsrli t2, t0, 16
- 0x10andi t2, t2, 255
- 0x14srli t3, t0, 8
- 0x18andi t3, t3, 255
- 0x1candi t4, t0, 255
registres
| t0 | 0 | |
| t1 | 0 | |
| t2 | 0 | |
| t3 | 0 | |
| t4 | 0 |
Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.
Le registre t0 affiche -1 062 728 441, ce qui est parfaitement absurde pour une adresse, et parfaitement exact pour un motif de bits dont la position de poids fort vaut un. Le chapitre 1 l'annonçait : un registre ne porte aucun type, et rien n'oblige la figure à deviner qu'il s'agit d'une adresse.
Le premier octet s'obtient par un srli de vingt-quatre positions, sans masque, puisqu'il ne reste rien à sa gauche ; les deux du milieu demandent un décalage puis un masque, le dernier un masque seul. Le résultat est 192.168.13.7. Le choix du décalage logique n'est pas indifférent, et l'erreur inverse se voit tout de suite.
programme
- 0x0li t0, 0xC0A80D07lui t0, -259455
- 0x4addi t0, t0, -761
- 0x8srai t1, t0, 24
- 0xcsrli t2, t0, 24
registres
| t0 | 0 | |
| t1 | 0 | |
| t2 | 0 |
Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.
srai recopie le bit de poids fort, qui vaut un dans cette adresse, et rend -64. srli comble par des zéros et rend 192. Une adresse n'a pas de signe : le décalage logique est le seul correct ici, et toute bibliothèque réseau l'emploie.
Vient enfin le masque de sous-réseau, qui est littéralement un masque au sens de ce chapitre.
programme
- 0x0li t0, 0xC0A80D07lui t0, -259455
- 0x4addi t0, t0, -761
- 0x8li t1, 0xC0A80D2Alui t1, -259455
- 0xcaddi t1, t1, -726
- 0x10xor t2, t0, t1
- 0x14li t3, 0xFFFFFF00lui t3, 0
- 0x18addi t3, t3, -256
- 0x1cand t4, t2, t3
registres
| t0 | 0 | |
| t1 | 0 | |
| t2 | 0 | |
| t3 | 0 | |
| t4 | 0 |
Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.
Le programme compare 192.168.13.7 et 192.168.13.42. Le xor donne 45, soit 0x2d : les seuls bits qui diffèrent sont dans le dernier octet, celui de la machine. Le and avec le masque /24 efface ce dernier octet, et le résultat est nul. Deux adresses sont dans le même sous-réseau si, et seulement si, leur différence bit à bit s'annule sous le masque, et c'est très exactement ce calcul que fait une table de routage, des millions de fois par seconde.
La figure révèle au passage une curiosité dans la colonne des instructions réelles. li t3, 0xFFFFFF00 produit lui t3, 0 puis addi t3, t3, -256, parce que ce motif de bits, lu comme un nombre signé, vaut -256. La même constante s'écrit donc de deux façons opposées selon qu'on la regarde comme un masque ou comme un nombre, et la machine ne fait pas la différence.
Le masque qui ne tient pas dans l'instruction
Une dernière contrainte encadre la forme immédiate. andi, ori et xori logent leur constante dans l'instruction, et il n'y a de place que pour douze bits signés, soit les valeurs de -2048 à 2047. Un masque plus large ne passe pas.
Écrire andi t0, a0, 0xFF000 fait échouer l'assemblage, avec un message qui nomme le coupable : l'immédiat ne tient pas sur douze bits signés, 1 044 480 n'étant pas entre -2048 et 2047. La marche à suivre est celle de la figure précédente : poser le masque dans un registre avec li, qui sait produire deux instructions quand la constante l'exige, puis employer la forme à trois registres, and.
La limite mérite d'être lue précisément. Un masque comme 0xFFF, qui vaut 4095, ne passe pas davantage que 0xFF000, alors qu'il tient pourtant sur douze bits : la borne haute est 2047, parce que le douzième bit sert de signe. En revanche, andi t0, a0, -256 est accepté, et efface les huit bits de droite, parce que -256 tient dans l'intervalle alors que son motif de bits, 0xffffff00, compte bien plus de douze positions. Un masque à bits de poids fort passe donc plus facilement sous sa forme négative que sous sa forme hexadécimale.
1.Quel masque isole les quatre bits de droite d'une valeur ?
2.Comment lever un drapeau sans modifier les autres bits ?
3.Que donne xor d'un registre avec lui-même ?
4.Un registre contient -8. Que rend srli d'une position, et pourquoi ?
5.Pourquoi extraire le premier octet d'une adresse IPv4 avec srai donne-t-il un résultat faux ?
6.Deux adresses passées à xor donnent un résultat qui s'annule sous le masque /24. Que peut-on en conclure ?
7.Pourquoi andi t0, a0, 0xFF000 est-il refusé ?
La méthode
- Décider d'abord ce que le registre contient : un nombre signé, un motif de bits ou une adresse. Tout le reste en découle, à commencer par le choix entre
srlietsrai. - Écrire le masque en binaire ou en hexadécimal, jamais en décimal, pour que la position des bits reste lisible dans le code.
- Choisir l'opération d'après l'intention :
andpour garder,orpour forcer à un,andavec un masque inversé pour effacer,xorpour basculer. - Isoler puis décaler quand un champ se trouve au milieu d'un mot, et vérifier la valeur obtenue en la comparant à l'écriture hexadécimale de la source.
- Appliquer le motif « lire, modifier, réécrire » sur tout registre matériel, plutôt que d'y ranger une valeur construite de zéro.
- Vérifier la taille du masque avant d'employer une forme immédiate, et passer par
lipuis la forme à trois registres dès que la constante dépasse 2047. - Contrôler le résultat sur un cas négatif chaque fois qu'un décalage à droite intervient, puisque c'est le seul cas où les deux variantes divergent.
Synthèse
and,oretxorappliquent la même règle à chacun des trente-deux bits, sans retenue ni propagation. Leurs usages respectifs sont garder ou effacer, forcer à un, et basculer.- Un masque se choisit pour la position de ses bits, non pour sa valeur.
andi t0, a0, 1teste la parité, et c'est ce qu'écrit un compilateur là où le code source demandait un reste de division par deux. - Effacer un bit demande un
andavec le masque inversé : sur un état valant 41, unandavec -33 rend 9, sans toucher aux drapeaux de rang 0 et 3. xorappliqué deux fois avec le même masque ramène à la valeur de départ, etxord'un registre avec lui-même donne toujours zéro, ce qui en fait un idiome reconnaissable dans tout code désassemblé.srlietsraidivergent sur les valeurs négatives : sur -8, la première rend 2 147 483 644 et la seconde -4. Le choix dépend de ce que le registre contient, pas de ce qui est commode.- Un champ au milieu d'un mot s'extrait par un masque puis un décalage. C'est ce que fait le découpage d'une adresse IPv4 en quatre octets, et le masque de sous-réseau est littéralement un masque appliqué par
and. - Les formes immédiates logent douze bits signés, soit de -2048 à 2047. Au-delà, le masque se pose dans un registre avec
li, puis s'applique avec la forme à trois registres.
Tous les programmes de ce chapitre suivent une ligne droite, du premier pas au dernier. Un bit extrait ne sert pourtant à rien tant qu'il ne change pas la suite des instructions. Décider donne cette capacité, et explique enfin pourquoi -33 porte le motif de bits qui en fait un masque.