Opérateurs bit à bit en C/C++ Ce document présente des techniques de programmation qui sont utiles non seulement dans le cadre d’analyse d’adresses réseau mais aussi dans de nombreux autres domaines. Ces techniques de programmation font appel aux opérateurs bit à bit (ou binaires) qui sont proposés par la totalité des langages de programmation dont bien sûr le langage C/C++. Vous allez donc vous familiariser dès maintenant avec ces opérateurs. Introduction Le langage C/C++ est un langage relativement “bas niveau” c’est-à-dire proche de la machine. À travers les fonctionnalités standards de ce langage — dont les opérateurs bit à bit mais aussi les pointeurs — il est ainsi possible, dans certains cas, d’accéder directement au matériel. Ces fonctionnalités font que ce langage est très utilisé en informatique industrielle. Comme leur nom l’indique, les opérateurs bit à bit ou bitwise en anglais vont permettre de lire ou de modifier certains bits d’une valeur stockée dans une variable. Jusqu’à présent, vous avez appris qu’une variable était un emplacement mémoire dans lequel était stocké une valeur — codée en binaire — qui représente une information en relation avec le problème à traiter par un programme informatique. Ex. : une température, une adresse mail, le résultat d’un calcul… Vous avez aussi appris que, selon le type de la variable (nombre entier ou décimal, caractère…), l’emplacement mémoire qui lui était attribué pouvait s’étendre sur 1 ou plusieurs octets constitués chacun de 8 bits (contraction de Binary digIT). Or, on peut rencontrer des situations dans lesquelles on désire mémoriser des informations pouvant être codées sur un nombre restreint de bits (en l’occurence inférieur à 8). Dès lors, pourquoi “gâcher” un octet de mémoire pour mémoriser seulement 1 ou 2 bits d’information ? Bien sûr, les ordinateurs récents disposent d’énormément de mémoire et le fait de “gâcher” quelques octets n’est pas génant. Cependant, cette mémoire a un coût et occasionne une consommation de courant. Il est donc fréquent de trouver des systèmes informatiques — même modernes — dans lesquels les ressources mémoire sont très limitées. Dans ces systèmes, on va alors optimiser l’utilisation de la mémoire en stockant dans un seul octet plusieurs informations codées sur quelques bits. Toutefois, le langage C standard ne propose pas d’accès direct aux bits d’un octet [1]. Dans ces conditions, il va falloir “jongler” avec les opérateurs bit à bit mis à notre disposition pour pouvoir lire ou modifier ces bits. C’est ce qui va vous être présenté dans ce qui suit. Si l’optimisation de la mémoire et donc le regroupement de plusieurs informations dans un seul octet est parfois superflu, l’accès au niveau bit est par contre obligatoire lorsqu’on désire accéder à certaines zones mémoire spéciales, appelées registres situées dans le microprocesseur ou certains de ses circuits périphériques. L’écriture d’un ou plusieurs bits dans ces registres provoque souvent une action au niveau du comportement du système informatique contrairement à l’écriture dans une mémoire conventionnelle dont le rôle se limite exclusivement à la mémorisation de l’information. Généralités Le langage C propose 6 opérateurs qui travaillent au niveau du bit : le NON bit à bit noté ~ le ET bit à bit noté & le OU bit à bit noté | le OU EXCLUSIF bit à bit noté ^ le DÉCALAGE À GAUCHE noté << le DÉCALAGE À DROITE noté >> Notez que les 4 premiers opérateurs bit à bit portent le nom de fonctions logiques booléennes (c’est-à-dire relatives à l’algèbre de Boole qui opère sur des expressions VRAIES OU FAUSSES). Bien qu’ils présentent des similitudes, le contexte d’utilisation est totalement différent : agir sur 1 bit (0 ou 1) d’une valeur binaire n’a rien à voir avec le fait d’évaluer une expression booléenne (VRAI ou FAUX). Souvenez-vous en !! La confusion entre les 2 types d’opérateur est d’autant plus facile que leur notation est très proche en langage C. Exemple : & pour le ET bit-à-bit et && pour le ET logique. Dans la suite du document, des exemples d’opérations bit à bit vont être présentés. Ces opérations vont agir sur des nombres binaires. Le langage C/C++ accepte les nombres saisis en décimal (base 10), hexadécimal (base 16), octal (base 8) et enfin binaire (base 2) depuis sa version “C++14”. Ainsi, un nombre précédé de 0x (chiffre 0 suivie de la lettre x) sera considéré comme de l’hexadécimal. Ex. : 0x1E. De même, un nombre précédé de 0 (zéro) sera considéré comme de l’octal. Ex. : 07. Un nombre en base 10 s’exprime quant à lui sans préfixe. Ex. : 96. On veillera dans le cas d’un nombre en base 10 à ne pas mettre de 0 non significatifs à sa gauche sous peine qu’il soit interprété comme de l’octal. Ex. : Le nombre 017 exprimé en base 8 donne 15 en base 10 (017 → 1 x 81 + 7 x 80 → 8 + 7 → 15). Enfin, le préfixe 0b sera utilisé pour les nombres exprimés en binaire. Ex : 0b01101001 Etant donné que le langage C++ ne supporte que de depuis peu l’expression en binaire des nombres dans les codes source et du fait que cette représentation est plus longue à saisir (10 caractères pour exprimer un nombre sur 8 bits), il est très fréquent d’utiliser l’hexadécimal pour représenter les nombres utilisés dans un contexte binaire. La méthode pour passer d’une représentation binaire à une représentation hexadécimale consiste à : constituer des groupes de 4 bits dans la valeur binaire de départ coder en hexadécimal chacun des groupes de 4 bits avec les correspondances suivantes : binaire hexadécimal 0000 0 0001 1 0010 2 0011 3 0100 4 0101 5 0110 6 0111 7 1000 8 1001 9 1010 A 1011 B 1100 C 1101 D 1110 E 1111 F préfixer le nombre obtenu par 0x Exemple 0110100101011010 → 0110 1001 0101 1010 → 6 9 5 A → 0x695A (ou 0x695a avec un 'a' en minuscule) 🖮 Exercice n° 1 Bases décimale, hexadécimale, binaire Exprimer en hexadécimal puis en décimal les nombres binaires suivants : 0b00000100, 0b00001010, 0b00010101, 0b00100000, 0b00111100, 0b10110110 Exprimer en binaire puis en hexadécimal les nombres décimaux suivants : 5, 10, 13, 15, 16, 48, 127, 166, 255 Exprimer en binaire et en décimal les nombres hexadécimaux suivants : 0x10, 0x0F, 0x1F, 0xA4, 0x4A Vous pouvez vérifier vos résultats avec la calculatrice de Windows ou Linux configurée avec un affichage de type “Programmeur”. Le NON bit à bit Cet opérateur inverse les bits de son opérande unique : les ‘0’ sont transformés en ‘1’ et les ‘1’ en ‘0’ En langage C, il est noté avec un tilde : ~ b ~b 0 1 1 0 On l’appelle aussi le complément à 1. Exemple : // définition d'un octet initialisé avec la valeur binaire 0b10101111 unsigned char octet = 0xAF; // définition d'une variable qui contient le complément à 1 de 'octet' unsigned char resultat = ~octet; // => 'resultat' contient la valeur binaire 0b01010000 (0x50 en hexadécimal) // ~ 10101111 // ---------- // 01010000 Lorsqu’un opérateur n’admet qu’un seul opérande — comme pour l’opérateur ~ — il est qualifié d’opérateur Unaire. Lorsqu’il en admet 3, c’est un opérateur TERnaire. Il n’en existe qu’un de ce type en langage C (a ? b : c). Lorsqu’un opérateur admet 2 opérandes, comme dans a + b, c’est un opérateur … BInaire. Remarquez toutefois que dans ce contexte, “binaire” ne signifie pas que l’opérateur agit directement au niveau des bits (bitwise) mais simplement qu’il est “BI-opérandes”. 🖮 Exercice n° 2 Complément à 1 Exprimer en binaire et en hexadécimal le complément à 1 des valeurs suivantes : 0b00001111 0b00111100 0x5A 0xF0 0x41 Le ET bit à bit Cet opérateur effectue un ET binaire entre les bits de même rang de ses 2 opérandes c’est-à-dire entre le bit 0 du 1ier opérande et le bit 0 du 2ème opérande PUIS entre le bit 1 du 1ier opérande et le bit 1 du 2ième opérande etc… En langage C, il est noté avec 1 seul “et commercial” : &. Le ET logique se note && (2 “ET commerciaux” successifs) en C/C++. Le risque est donc assez grand de le confondre avec le ET binaire. b1 b2 b1 & b2 0 0 0 0 1 0 1 0 0 1 1 1 Exemple : unsigned char octet1 = 0xA5; // 0b10100101 unsigned char octet2 = 0xF0; // 0b11110000 unsigned char resultat = octet1 & octet2; // => 'resultat' contient la valeur binaire 0b10100000 (0xA0 en hexadécimal) // 10100101 // & 11110000 // ---------- // 10100000 Une même variable peut constituer le résultat et l’opérande d’un opérateur bit à bit à 2 opérandes. C’est par exemple le cas lorsqu’on désire la mettre à jour à partir de sa valeur précédente. Dans ce cas, il existe une forme abrégée pour noter l’opération. Ainsi, pour l’opérateur ET binaire, les 2 écritures suivantes sont équivalentes : resultat = resultat & 0xF0; // Écriture normale resultat &= 0xF0; // Écriture abrégée 🖮 Exercice n° 3 ET binaire Poser les opérations suivantes et exprimer leurs résultats en binaire et en hexadécimal : 0xBB & 0x44 0x12 & 0x34 0xAA & 0xFF 0x5C & 0x0F 0x61 & 0xDF 0x61 & ~0x20 0xC5 & ~0x0F Le OU bit à bit Cet opérateur effectue un OU binaire entre les bits de même rang de ses 2 opérandes. En langage C, il est noté avec 1 seule barre verticale : | Le OU logique se note || (2 barres verticales successives) en C/C++. Le risque est donc assez grand de le confondre avec le OU binaire. b1 b2 b1 | b2 0 0 0 0 1 1 1 0 1 1 1 1 Exemple : unsigned char octet1 = 0xA5; // 0b10100101 unsigned char octet2 = 0xF0; // 0b11110000 unsigned char resultat = octet1 | octet2; // => 'resultat' contient la valeur binaire 0b11110101 (0xF5 en hexadécimal) // 10100101 // | 11110000 // ---------- // 11110101 🖮 Exercice n° 4 OU binaire Poser les opérations suivantes en binaire et exprimer leurs résultats en binaire et en hexadécimal : 0xBB | 0x44 0x12 | 0x34 0xAA | 0xFF 0x5C | 0x0F 0x41 | 0x20 0x61 | ~0x20 0xC5 | ~0x0F 0x29 | (~0x29 & 0x0F) Le OU EXCLUSIF bit à bit Cet opérateur effectue un OU EXCLUSIF binaire entre les bits de même rang de ses 2 opérandes. En langage C, il est noté avec l’accent circonflexe : ^ b1 b2 b1 ^ b2 0 0 0 0 1 1 1 0 1 1 1 0 Exemple : unsigned char octet1 = 0xA5; // 0b10100101 unsigned char octet2 = 0xF0; // 0b11110000 unsigned char resultat = octet1 ^ octet2; // => 'resultat' contient la valeur binaire 0b01010101 (0x55 en hexadécimal) // 10100101 // ^ 11110000 // ---------- // 01010101 🖮 Exercice n° 5 OU Exclusif Poser les opérations suivantes et exprimer leurs résultats en binaire et en hexadécimal : 0xBB ^ 0x44 0x12 ^ 0x34 0xAA ^ 0xFF 0x55 ^ 0xFF 0x41 ^ 0x20 0x61 ^ ~0x20 0xC5 ^ ~0x0F 0x29 ^ (~0x29 & 0x0F) Le DÉCALAGE À GAUCHE Cet opérateur permet de faire “glisser” vers la gauche tous les bits d’une valeur binaire. Le 1er opérande spécifie la valeur sur laquelle le décalage doit être effectué et le 2ème opérande indique son amplitude. En langage C, il est noté avec un double chevrons orienté vers la gauche : << Le “vide” de droite occasionné par le déplacement est comblé de 0. Les bits de gauche qui “sortent” de la valeur sont, quant à eux, perdus. Exemple : unsigned char octet1 = 0xA5; // 0b10100101 unsigned char resultat = octet1 << 3; // => 'resultat' contient la valeur binaire 0b00101000 (0x28 en hexadécimal) // // 10100101 << 3 provoque : // <- [10100101] <- 000 // 1 <- [01001010] <- 00 // 10 <- [10010100] <- 0 // 101 <- [00101000] <- // \ / +-+--> 3 bits 0 insérés pour combler le vide // v //3 bits perdus Noter que dans le code précédent, la variable octet1 n’est pas modifiée suite au décalage. Pour la modifier, il faudrait écrire : octet1 = octet1 << 3. 🖮 Exercice n° 6 Décalage à gauche Déterminer le résultat des opérations suivantes en se basant sur le fait que les valeurs sont toutes codées sur un seul octet. Les résultats seront exprimés dans la base utilisée pour spécifier la valeur à décaler dans la question. 1 << 0 1 << 1 1 << 2 0x11 << 3 0xF0 << 4 0x78 << 8 0x41 | (0x01 << 5) 0x81 & ~(0x01 << 5) À quelle opération arithmétique peut-on assimiler un décalage à gauche d’un bit ? Est-ce que le résultat de cette opération arithmétique est juste quelque soit la valeur à décaler ? Comment pourrait-on résumer l’action de l’opération octet & ~(1 << n) avec n compris entre 0 et 7 ? le DÉCALAGE À DROITE Cet opérateur permet de faire “glisser” vers la droite tous les bits d’une valeur binaire. Le 1er opérande spécifie la valeur sur laquelle le décalage doit être effectué et le 2ème opérande indique son amplitude. En langage C, il est noté avec un double chevrons orienté vers la droite : >> Le “vide” de gauche occasionné par le déplacement est comblé soit par des 0 soit par des 1 selon le signe (+/-) de la valeur de départ. Les bits de droite qui “sortent” de la valeur sont, quant à eux, perdus. Lorsque la valeur à décaler est signée — c’est-à-dire qu’elle peut représenter une valeur positive ou négative — et que cette valeur est positive (bit de poids fort à 0), le vide de gauche est comblé par des 0. Quand la valeur à décaler est signée et négative (bit de poids fort à 1), le vide de gauche est comblé par des 1. La valeur du signe est donc conservée. Quand la valeur à décaler n’est pas signée (spécificateur unsigned devant le type d’une variable en langage C/C++), le vide de gauche est toujours comblé par des 0. unsigned char octet1; signed char octet2; // ('signed' est implicite pour un 'char' // sauf mention contraire dans les options // du compilateur) /* Décalage à droite d'une valeur non signée */ octet1 = 0xA5; // 0b10100101 octet1 >>= 3; // rappel : équivalent à 'octet1 = octet1 >> 3' // => 'octet1' contient la valeur binaire 0b00010100 (0x14 en hexadécimal) // // 10100101 >> 3 provoque : // 000 -> [10100101] -> // 00 -> [01010010] -> 1 // 0 -> [00101001] -> 01 // -> [00010100] -> 101 // \ / +-+--> 3 bits perdus // v // 3 bits 0 insérés /* Décalage à droite d'une valeur signée positive (bit de poids fort à 0) */ octet2 = 0x5A; // 0b01011010 octet2 >>= 3; // => 'octet2' contient la valeur binaire 0b00001011 (0x0B en hexadécimal) // // 01011010 >> 3 provoque : // 000 -> [01011010] -> // 00 -> [00101101] -> 0 // 0 -> [00010110] -> 10 // -> [00001011] -> 010 // \ / +-+--> 3 bits perdus // v // 3 bits 0 insérés /* Décalage à droite d'une valeur signée négativee (bit de poids fort à 1) */ octet2 = 0xA5; // 0b10100101 // ici 'octet2' représente une valeur négative (-91 en décimal) // car le bit de poids fort est à 1. octet2 >>= 3; // => 'octet2' contient la valeur binaire 0b1111100 (0xF4 en hexadécimal) // // 10100101 >> 3 provoque : // 111 -> [10100101] -> // 11 -> [11010010] -> 1 // 1 -> [11101001] -> 01 // -> [11110100] -> 101 // \ / +-+--> 3 bits perdus // v // 3 bits 1 insérés 🖮 Exercice n° 7 Décalage à droite Déterminer le résultat des opérations suivantes en se basant sur le fait que les valeurs sont toutes codées sur un seul octet signé. Les résultats seront exprimés dans la base utilisée pour spécifier la valeur à décaler dans la question. 0x60 >> 2 0x78 >> 3 0xF0 >> 4 1 >> 0 1 >> 1 127 >> 2 -100 >> 3 À quelle opération arithmétique peut-on assimiler un décalage à droite d’un bit ? Est-ce que le résultat de cette opération arithmétique est juste quelque soit la valeur à décaler ? À quelle opération correspond octet >> 3 pour des valeurs de octet multiples de 8 et comprises entre -128 et +120 ? Que peut-on dire du résultat pour des valeurs d' octet positives et négatives non multiples de 8 (-128 < octet ≤ 127) ? 1. En fait, le langage C propose bien une fonctionnalité — nommée champ de bits (bitfield) qui permet d’accéder à des fragments d’un octet mais celle-ci dépasse le cadre de ce cours et doit être utilisée avec précaution car non portable d’une plateforme à l’autre 🞄 🞄 🞄 Aide-mémoire C++/UML Techniques de masquage en C/C++