Aller au contenu principal

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.

InstructionFormeCe qu'elle fait
andand rd, rs1, rs2Ne garde dans rd que les bits portés à un par rs1 et par rs2 : le reste tombe à zéro.
andiandi rd, rs, immLa même chose, le masque étant écrit dans l'instruction. Déjà croisée au chapitre précédent.
oror rd, rs1, rs2Met à un dans rd tout bit qu'au moins un des deux registres porte à un.
oriori rd, rs, immLa même chose, le masque étant écrit dans l'instruction.
xorxor rd, rs1, rs2Met à un dans rd les seuls bits qui diffèrent entre les deux registres.
xorixori rd, rs, immLa même chose, le masque étant écrit dans l'instruction.
sllislli rd, rs, nPousse les bits de rs de n positions vers la gauche, en comblant par des zéros à droite. Déjà vue au premier chapitre.
srlisrli rd, rs, nPousse les bits de n positions vers la droite, en comblant par des zéros à gauche. Déjà vue au chapitre précédent.
sraisrai rd, rs, nPousse 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.

Masque

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.

RISC-V RV32Ipas 0 sur 1pc = 0x0

programme

  1. 0x0andi t0, a0, 0b1111

registres

t00
a0181

Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.

Exécuter l'unique instruction et relever t0, qui vaut 5. Comparer l'écriture binaire de la valeur de départ, 10110101, à celle du résultat, 101 : seuls les quatre bits de droite ont survécu.

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.

RISC-V RV32Ipas 0 sur 2pc = 0x0

programme

  1. 0x0andi t0, a0, 1
  2. 0x4andi t1, a1, 1

registres

t00
t10
a07
a112

Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.

Relever les deux résultats, 1 pour a0 et 0 pour a1, et vérifier que le second correspond bien au nombre pair des deux.

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.

RISC-V RV32Ipas 0 sur 3pc = 0x0

programme

  1. 0x0ori t0, a0, 0b00001000
  2. 0x4li t1, -33addi t1, zero, -33
  3. 0x8and t2, t0, t1

registres

t00
t10
t20
a033

Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.

Suivre les trois valeurs, 41 puis -33 puis 9, et lire leur écriture binaire : le bit de rang 3 apparaît au premier pas, le bit de rang 5 disparaît au dernier, et le bit de rang 0 traverse les deux sans bouger.

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.

RISC-V RV32Ipas 0 sur 7pc = 0x0

programme

  1. 0x0la t0, etatlui t0, 3
  2. 0x4addi t0, t0, 256
  3. 0x8lb t1, 0(t0)
  4. 0xcori t2, t1, 0b1000
  5. 0x10li t3, -3addi t3, zero, -3
  6. 0x14and t4, t2, t3
  7. 0x18sb t4, 0(t0)

registres

sp0x2000
t00
t10
t20
t30
t40

mémoire

0x3100etat38

Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.

Suivre la valeur lue, 38, puis les deux transformations, 46 et 44, et vérifier au dernier pas que la case 0x3100 contient bien 44 à la place de 38.

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.

Lire, modifier, réécrire

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.

RISC-V RV32Ipas 0 sur 2pc = 0x0

programme

  1. 0x0xori t0, a0, 0b0010
  2. 0x4xori t1, t0, 0b0010

registres

t00
t10
a010

Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.

Relever les deux résultats, 8 puis 10, et constater que le second est égal à la valeur de départ de a0. Le bit de rang 1 s'éteint au premier pas et se rallume au second.

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.

RISC-V RV32Ipas 0 sur 2pc = 0x0

programme

  1. 0x0xor t0, t0, t0
  2. 0x4xor t1, t1, t1

registres

t0123456
t1-1

Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.

Exécuter les deux instructions et relever les valeurs d'avant signalées par la figure, 123456 et -1, ainsi que les deux valeurs d'après, toutes deux nulles.

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.

RISC-V RV32Ipas 0 sur 3pc = 0x0

programme

  1. 0x0srli t0, a0, 1
  2. 0x4srai t1, a0, 1
  3. 0x8slli t2, a0, 1

registres

t00
t10
t20
a0-8

Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.

Comparer les trois résultats obtenus à partir de la même valeur : 2147483644 pour srli, -4 pour srai, -16 pour slli. Reculer et avancer entre les deux premiers pas pour bien voir que la source n'a pas changé entre eux.

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.

Un décalage à droite n'est pas exactement une division

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.

RISC-V RV32Ipas 0 sur 4pc = 0x0

programme

  1. 0x0li t1, 0xFF000lui t1, 255
  2. 0x4addi zero, zero, 0
  3. 0x8and t0, a0, t1
  4. 0xcsrli t2, t0, 12

registres

t00
t10
t20
a0305419896

Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.

Relever les trois valeurs, 1044480 pour le masque, 282624 pour le champ isolé, 69 pour le champ ramené à droite. Comparer 69 à l'écriture hexadécimale de la valeur de départ.

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.

RISC-V RV32Ipas 0 sur 8pc = 0x0

programme

  1. 0x0li t0, 0xC0A80D07lui t0, -259455
  2. 0x4addi t0, t0, -761
  3. 0x8srli t1, t0, 24
  4. 0xcsrli t2, t0, 16
  5. 0x10andi t2, t2, 255
  6. 0x14srli t3, t0, 8
  7. 0x18andi t3, t3, 255
  8. 0x1candi t4, t0, 255

registres

t00
t10
t20
t30
t40

Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.

Relever les quatre valeurs finales, 192, 168, 13 et 7, et les comparer à la valeur brute de t0, -1062728441, affichée juste au-dessus.

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.

RISC-V RV32Ipas 0 sur 4pc = 0x0

programme

  1. 0x0li t0, 0xC0A80D07lui t0, -259455
  2. 0x4addi t0, t0, -761
  3. 0x8srai t1, t0, 24
  4. 0xcsrli t2, t0, 24

registres

t00
t10
t20

Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.

Comparer les deux valeurs obtenues à partir de la même source, -64 et 192, et retenir laquelle des deux est le premier octet de l'adresse.

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.

RISC-V RV32Ipas 0 sur 8pc = 0x0

programme

  1. 0x0li t0, 0xC0A80D07lui t0, -259455
  2. 0x4addi t0, t0, -761
  3. 0x8li t1, 0xC0A80D2Alui t1, -259455
  4. 0xcaddi t1, t1, -726
  5. 0x10xor t2, t0, t1
  6. 0x14li t3, 0xFFFFFF00lui t3, 0
  7. 0x18addi t3, t3, -256
  8. 0x1cand t4, t2, t3

registres

t00
t10
t20
t30
t40

Rien n'a encore été exécuté. Le compteur ordinal pointe la première instruction, en 0x0.

Suivre le résultat du xor, 45, puis le résultat final, 0. Cette valeur nulle est la réponse : les deux adresses sont dans le même sous-réseau.

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.

Douze bits signés, pas douze bits

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.

Vérification rapideon peut se reprendre

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

  1. 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 srli et srai.
  2. Écrire le masque en binaire ou en hexadécimal, jamais en décimal, pour que la position des bits reste lisible dans le code.
  3. Choisir l'opération d'après l'intention : and pour garder, or pour forcer à un, and avec un masque inversé pour effacer, xor pour basculer.
  4. 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.
  5. Appliquer le motif « lire, modifier, réécrire » sur tout registre matériel, plutôt que d'y ranger une valeur construite de zéro.
  6. Vérifier la taille du masque avant d'employer une forme immédiate, et passer par li puis la forme à trois registres dès que la constante dépasse 2047.
  7. 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, or et xor appliquent 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, 1 teste 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 and avec le masque inversé : sur un état valant 41, un and avec -33 rend 9, sans toucher aux drapeaux de rang 0 et 3.
  • xor appliqué deux fois avec le même masque ramène à la valeur de départ, et xor d'un registre avec lui-même donne toujours zéro, ce qui en fait un idiome reconnaissable dans tout code désassemblé.
  • srli et srai divergent 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.