Chaque bit d'un entier peut servir à stocker une valeur binaire. Comme de nombreuses situations impliquent de l'information binaire, comme vrai ou faux, inclusion ou exclusion, activé ou désactivé, la représentation binaire d'un entier de N bits offre un moyen compact d'encoder l'état binaire de N éléments. Cela rend la capacité de manipuler des bits et des octets essentielle en assembleur. Le jeu d'instructions x86-64 offre une grande variété d'instructions de manipulation de bits.
Ces instructions agissent sur des bits individuels d'un opérande.
Elles prennent toutes deux opérandes, la seconde indique l'indice du bit manipulé dans le premier opérande. Toutes copient le bit sélectionné dans l'indicateur de retenue (CF).
| Name | Description |
|---|---|
bt |
copie le bit dans CF sans modifier d'opérande |
bts |
copie le bit dans CF et le définit dans l'opérande de destination |
btr |
copie le bit dans CF et l'efface dans l'opérande de destination |
btc |
copie le bit dans CF et le complémente (l'inverse) dans l'opérande de destination |
Les opérations bit à bit sont effectuées sur tous les bits d'un opérande.
Elles ont toutes une instruction portant le même nom que l'opération bit à bit effectuée :
| Name | Description |
|---|---|
and |
1 si les deux bits valent 1 |
or |
1 si au moins un des bits vaut 1 |
xor |
1 si les bits diffèrent |
not |
1 si le bit valait 0 ; 0 si le bit valait 1 |
La plupart prennent deux opérandes, effectuent une opération bit à bit sur les deux et stockent le résultat dans l'opérande de destination.
L'exception est not, qui ne prend qu'un seul opérande de destination.
Quand on interprète un et zéro comme inclusion et exclusion, respectivement, un entier est appelé un bitmask (ou simplement un mask).
Un masque binaire « masque » des éléments parce qu'un zéro au i-ième bit exclut le i-ième élément, tandis qu'un un l'inclut.
On utilise aussi couramment un masque binaire pour inclure certains bits d'un entier tout en excluant les autres.
Par exemple, soit A un entier dont la représentation binaire est :
| index | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|
| bits | 1 | 0 | 0 | 1 | 0 | 1 | 0 | 1 |
De même, soit M un entier dont la représentation binaire est :
| index | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|
| bits | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 1 |
Les deux sont des entiers de 8 bits.
Dans ce cas, on peut dire que M sélectionne les bits 0, 2 et 3 de A, et exclut le reste.
Les instructions bit à bit vues plus haut sont utiles pour manipuler des entiers à l'aide de masques. Par exemple :
A qui ne sont pas sélectionnés par M, on utilise le ET bit à bit : A AND M.A sélectionnés par M, on utilise le OU bit à bit : A OR M.test
L'instruction test effectue un ET bit à bit entre les deux opérandes et positionne les indicateurs en fonction du résultat.
Si A est le premier opérande et B, le second :
| flag | set when |
|---|---|
CF |
toujours effacé |
ZF |
A AND B == 0 |
SF |
le bit de signe de A AND B vaut 1 |
OF |
toujours effacé |
Cette instruction prend deux opérandes et met à jour les indicateurs, mais ne modifie pas ses opérandes.
Ces instructions décalent les bits de l'opérande de destination d'un nombre de positions spécifié par le second opérande.
Le second opérande doit être un nombre constant (un immediate) ou le registre cl (les 8 bits de poids faible de rcx).
| Name | Description |
|---|---|
shl/sal
|
Décale les bits vers la gauche |
shr/sar
|
Décale les bits vers la droite |
À noter que le compteur du second opérande est masqué sur 5 bits, ou 6 bits avec un opérande de destination de 64 bits.
Tout bit au-delà est de fait ignoré.
Cela signifie que le décalage maximal est de 31, ou de 63 avec un opérande de 64 bits.
shl et sal effectuent exactement la même opération ; l'une est un alias de l'autre.
À chaque décalage vers la gauche, les bits plus proches de la fin de la séquence que la longueur du décalage sont d'abord transférés dans CF, puis perdus.
En revanche, un nombre de nouveaux bits à zéro égal à la longueur du décalage est ajouté au début.
Comme chaque bit d'un entier représente une puissance de 2, un décalage vers la gauche de n positions a pour effet de multiplier l'entier par 2ⁿ.
Il existe deux instructions pour décaler les bits vers la droite : shr et sar.
Quand l'une de ces deux instructions est utilisée, les bits plus proches du début de la séquence que la longueur du décalage sont d'abord transférés dans CF, puis perdus.
En revanche, un nombre de nouveaux bits égal à la longueur du décalage est ajouté à la fin.
La différence entre les deux est que shr ajoute des bits 0 à l'extrémité gauche, tandis que sar ajoute 1 si le bit de poids fort était défini et 0 sinon.
Cela signifie que sar préserve le signe lors du décalage d'un entier signé.
Comme chaque bit d'un entier représente une puissance de 2, un décalage vers la droite de n positions avec shr a pour effet d'effectuer une division non signée par 2ⁿ.
De même, un décalage vers la droite de n positions avec sar a pour effet d'effectuer une division signée par 2ⁿ.
Ces instructions déplacent les bits de l'opérande de destination d'un nombre de positions spécifié par le second opérande.
Le second opérande doit être un nombre constant (un immediate) ou le registre cl (les 8 bits de poids faible de rcx).
La différence entre une rotation et un décalage est qu'une rotation n'écarte ni n'ajoute aucun bit. Les bits qui seraient perdus lors d'un décalage sont déplacés à l'extrémité opposée. Ainsi, tous les bits restent, ils changent tous de place.
| Name | Description |
|---|---|
rol |
Fait tourner les bits vers la gauche |
ror |
Fait tourner les bits vers la droite |
À noter que le compteur du second opérande est masqué sur 5 bits, ou 6 bits avec un opérande de destination de 64 bits.
Tout bit au-delà est de fait ignoré.
Cela signifie que la rotation maximale est de 31, ou de 63 avec un opérande de 64 bits.
Il existe d'autres instructions utiles de manipulation de bits :
| Name | Description |
|---|---|
popcnt |
Compte le nombre de bits définis |
bsr |
Récupère l'indice du bit défini le plus significatif. Si aucun bit n'est défini, le résultat n'est pas défini |
bsf |
Récupère l'indice du bit défini le moins significatif. Si aucun bit n'est défini, le résultat n'est pas défini |
Ces instructions fonctionnent toutes avec deux opérandes de 16 bits, 32 bits ou 64 bits.
Elles ne peuvent pas être utilisées avec des opérandes de 8 bits.
Un de tes amis vient de t'envoyer un message contenant un secret important. Pour ne pas en faciliter la lecture par d'autres, le message a été chiffré au moyen d'une série de manipulations de bits. Tu vas devoir écrire les méthodes qui permettront de déchiffrer le message.
Voici les instructions sur un seul bit mentionnées dans ce concept :
| Nom | Description |
|---|---|
| bt | copie le bit dans CF sans modifier d'opérande |
| bts | copie le bit dans CF et le met à 1 dans l'opérande de destination |
| btr | copie le bit dans CF et le met à 0 dans l'opérande de destination |
| btc | copie le bit dans CF et le complémente (l'inverse) dans l'opérande de destination |
Voici les instructions bit à bit mentionnées dans ce concept :
| Nom | Description |
|---|---|
| and | 1 si les deux bits valent 1 |
| or | 1 si au moins un des bits vaut 1 |
| xor | 1 si les bits diffèrent |
| not | 1 si le bit valait 0 ; 0 si le bit valait 1 |
Voici les instructions de décalage mentionnées dans ce concept :
| Nom | Description |
|---|---|
| shl/sal | Décale les bits vers la gauche |
| shr/sar | Décale les bits vers la droite |
Voici les instructions de rotation mentionnées dans ce concept :
| Nom | Description |
|---|---|
| rol | Fait tourner les bits vers la gauche |
| ror | Fait tourner les bits vers la droite |
Voici les instructions diverses mentionnées dans ce concept :
| Nom | Description |
|---|---|
| popcnt | Compte le nombre de bits à 1 |
| bsr | Donne l'indice du bit à 1 le plus significatif. Si aucun bit n'est à 1, le résultat n'est pas défini |
| bsf | Donne l'indice du bit à 1 le moins significatif. Si aucun bit n'est à 1, le résultat n'est pas défini |
Le message est encodé dans un entier de 16 bits. Cependant, parmi ceux-ci, les 8 bits les plus élevés ne font pas réellement partie du message : il s'agit d'un masque qu'il faut utiliser lors du déchiffrement.
Implémente la fonction extract_higher_bits qui prend un entier de 16 bits et renvoie ses 8 bits les plus élevés.
extract_higher_bits(0b1010010011000101)
// => 0b10100100
Savoir extraire le masque ne suffit pas : tu dois aussi isoler le message.
Implémente la fonction extract_lower_bits qui prend un entier de 16 bits et renvoie ses 8 bits les plus bas.
extract_lower_bits(0b1010010011000101);
// => 0b11000101
Certains bits sont à 1 à la fois dans le message et dans le masque. C'est une information très importante qui servira plus tard.
Implémente la fonction extract_redundant_bits qui prend un entier de 16 bits, encodant à la fois le message et un masque, et renvoie un entier de 8 bits dont seuls les bits redondants sont à 1.
Un bit du nombre renvoyé doit valoir 1 lorsqu'il vaut aussi 1 à la fois dans le message et dans le masque.
Tous les autres bits doivent être mis à zéro.
extract_redundant_bits(0b1010010011000101);
// => 0b10000100
Ensuite, certains bits doivent être mis à 1 dans le message, d'après le masque.
Implémente la fonction set_message_bits qui prend un entier de 16 bits, encodant à la fois le message et un masque, et renvoie le résultat de la mise à 1 des bits du message.
Un bit du message doit être mis à 1 lorsque le bit correspondant du masque vaut 1.
Tous les autres bits doivent rester inchangés : ils demeurent à 1 s'ils l'étaient déjà, et à 0 s'ils étaient déjà à 0.
set_message_bits(0b1010010011000101);
// => 0b11100101
Il reste une pièce du puzzle qui n'est pas explicite dans le message : le nombre de 16 bits 0b1011001100111100.
Ce nombre est ta clé privée partagée, et tu dois t'en servir pour aider à déchiffrer le message.
Pour ce faire, tu dois d'abord faire tourner les bits de ta clé privée vers la gauche d'un certain nombre de positions. Ce nombre de positions est égal au nombre de bits redondants à 1 à la fois dans le message et dans le masque.
Implémente la fonction rotate_private_key qui prend un entier de 16 bits, encodant à la fois le message et un masque, et renvoie le résultat de la rotation de ta clé privée.
Ce résultat est un entier de 16 bits.
rotate_private_key(0b1010010011000101);
// => 0b1100110011110010
NASM (The Netwide Assembler, l'assembleur utilisé par ce parcours) prend en charge les constantes au format binaire préfixées par 0b.
Il accepte aussi d'utiliser un tiret bas (_) comme séparateur dans une constante, pour plus de lisibilité :
PRIVATE_KEY equ 0b1011_0011_0011_1100
Pour pouvoir être utilisée lors du déchiffrement, ta clé privée doit être formatée afin d'isoler les bits pertinents.
Pour formater complètement une clé privée, tu dois :
Un bit inversé vaut 1 s'il valait 0, et 0 s'il valait 1.
Implémente la fonction format_private_key qui prend un entier de 16 bits, encodant à la fois le message et un masque, et renvoie une clé privée de 8 bits entièrement formatée.
format_private_key(0b1010010011000101);
// => 0b11000001
Une fois que tu disposes du message dont tous les bits pertinents sont à 1 et de la clé privée formatée, il est temps de les assembler pour obtenir le message final.
Le message obtenu est un entier de 16 bits, dont :
Implémente la fonction decrypt_message qui prend un entier de 16 bits encodant à la fois le message et un masque, et renvoie un entier de 16 bits avec le message entièrement déchiffré.
Cette fonction doit utiliser la clé privée formatée que tu génères avec format_private_key, ainsi que le message dont tous les bits pertinents ont été mis à 1 avec set_message_bits.
decrypt_message(0b1010010011000101);
// => 0b1100000111100101
Inscris-toi sur Exercism pour apprendre et maîtriser x86-64 Assembly avec 22 concepts130 exercices, et un vrai mentorat humain, le tout gratuitement.