Secrets

Secrets

Exercice d'apprentissage

Introduction

Manipulation de bits

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.

Manipulation d'un seul bit

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

Opérations bit à bit

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.

Masques

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 :

  • Pour effacer les bits de A qui ne sont pas sélectionnés par M, on utilise le ET bit à bit : A AND M.
  • Pour définir les bits de A sélectionnés par M, on utilise le OU bit à bit : A OR M.

L'instruction 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.

Opérations de décalage

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 / Sal

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ⁿ.

Shr / Sar

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ⁿ.

Opérations de rotation

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.

Autres instructions de manipulation de 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.

Instructions

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.

Note

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

1. Extrais le masque

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

2. Extrais le message

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

3. Extrais les bits redondants

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

4. Mets à 1 tous les bits du message

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

5. Fais tourner la clé privée

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
Note

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

6. Formate la clé privée

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 :

  • La faire tourner.
  • Isoler les 8 bits les plus bas de la clé privée après rotation, ce qui constitue la valeur de base.
  • Isoler les 8 bits les plus élevés de la clé privée après rotation, qui forment un masque à appliquer à la valeur de base.
  • Inverser les bits de la valeur de base qui sont aussi à 1 dans le masque.
  • Inverser tous les bits du résultat.

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

7. Termine le déchiffrement

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 :

  • Les 8 bits les plus élevés sont remplis par la clé privée formatée.
  • Les 8 bits les plus bas sont remplis par le message, après mise à 1 de tous les bits pertinents.

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
Modifie via GitHub Le lien s'ouvre dans une nouvelle fenêtre ou un nouvel onglet
x86-64 Assembly Exercism

Prêt à commencer Secrets ?

Inscris-toi sur Exercism pour apprendre et maîtriser x86-64 Assembly avec 22 concepts130 exercices, et un vrai mentorat humain, le tout gratuitement.