Secretos

Secretos

Ejercicio de aprendizaje

Introducción

Manipulación de bits

Cada bit de un entero se puede usar para almacenar un valor binario. Como muchas situaciones implican información binaria, como verdadero o falso, inclusión o exclusión, encendido o apagado, la representación binaria de un entero de N bits ofrece una forma compacta de codificar el estado binario de N elementos. Esto hace que la capacidad de manipular bits y bytes sea esencial en ensamblador. El conjunto de instrucciones x86-64 ofrece una amplia variedad de instrucciones de manipulación de bits.

Manipulación de un solo bit

Estas instrucciones operan sobre bits individuales de un operando.

Todas reciben dos operandos; el segundo indica el índice del bit sobre el que se opera en el primero. Todas copian el bit seleccionado en la bandera de acarreo (CF).

Nombre Descripción
bt copia el bit en CF sin modificar ningún operando
bts copia el bit en CF y lo establece en el operando destino
btr copia el bit en CF y lo borra en el operando destino
btc copia el bit en CF y lo complementa (lo invierte) en el operando destino

Operaciones bit a bit

Las operaciones bit a bit se realizan sobre todos los bits de un operando.

Todas tienen una instrucción con el mismo nombre que la operación bit a bit que realizan:

Nombre Descripción
and 1 si ambos bits son 1
or 1 si al menos uno de los bits es 1
xor 1 si los bits son diferentes
not 1 si el bit era 0; 0 si el bit era 1

La mayoría recibe dos operandos, realiza una operación bit a bit sobre ambos y guarda el resultado en el operando destino. La excepción es not, que recibe un solo operando destino.

Máscaras

Cuando interpretamos uno y cero como inclusión y exclusión, respectivamente, un entero se denomina máscara de bits (o simplemente máscara).

Una máscara de bits «enmascara» elementos porque un cero en el bit i-ésimo excluye el elemento i-ésimo, mientras que un uno lo incluye. También usamos comúnmente una máscara de bits para incluir ciertos bits de un entero y excluir otros.

Por ejemplo, sea A un entero cuya representación binaria es:

índice 7 6 5 4 3 2 1 0
bits 1 0 0 1 0 1 0 1

Además, sea M un entero cuya representación binaria es:

índice 7 6 5 4 3 2 1 0
bits 0 0 0 0 1 1 0 1

Ambos son enteros de 8 bits. En este caso, podemos decir que M selecciona los bits 0, 2 y 3 de A, y excluye el resto.

Las instrucciones bit a bit que vimos antes son útiles para manipular enteros con máscaras. Por ejemplo:

  • Para borrar los bits de A que M no selecciona, aplica el AND bit a bit: A AND M.
  • Para establecer los bits de A que M sí selecciona, aplica el OR bit a bit: A OR M.

Instrucción TEST

La instrucción test hace un AND bit a bit entre ambos operandos y establece las banderas según el resultado.

Si A es el primer operando y B el segundo:

bandera se establece cuando
CF siempre se borra
ZF A AND B == 0
SF el bit de signo de A AND B está establecido
OF siempre se borra

Esta instrucción recibe dos operandos y actualiza las banderas, pero no modifica sus operandos.

Operaciones de desplazamiento

Estas instrucciones mueven los bits del operando destino una cantidad de posiciones especificada por el segundo operando. El segundo operando debe ser un número constante (un immediate) o el registro cl (los 8 bits más bajos de rcx).

Nombre Descripción
shl/sal Desplaza los bits a la izquierda
shr/sar Desplaza los bits a la derecha

Fíjate que el conteo del segundo operando se enmascara a 5 bits, o a 6 bits con un operando destino de 64 bits. Cualquier bit después de eso se ignora en la práctica. Esto significa que el desplazamiento máximo es 31, o 63 con un operando de 64 bits.

Shl / Sal

Tanto shl como sal realizan exactamente la misma operación; una es un alias de la otra.

Cada vez que se hace un desplazamiento a la izquierda, los bits que están más cerca del final de la secuencia que la longitud del desplazamiento se mueven primero a CF y luego se descartan. Por otro lado, se agrega al principio una cantidad de bits nuevos en cero igual a la longitud del desplazamiento.

Como cada bit de un entero representa una potencia de 2, un desplazamiento a la izquierda de n posiciones tiene el efecto de multiplicar el entero por 2ⁿ.

Shr / Sar

Hay dos instrucciones para mover bits a la derecha: shr y sar.

Cada vez que se usa cualquiera de las dos instrucciones, los bits que están más cerca del inicio de la secuencia que la longitud del desplazamiento se mueven primero a CF y luego se descartan. Por otro lado, se agrega al final una cantidad de bits nuevos igual a la longitud del desplazamiento.

La diferencia entre ellas es que shr mueve bits 0 al extremo izquierdo, mientras que sar mueve 1 si el más significativo estaba establecido y 0 en caso contrario. Esto significa que sar conserva el signo en el desplazamiento de un entero con signo.

Como cada bit de un entero representa una potencia de 2, un desplazamiento a la derecha de n posiciones con shr tiene el efecto de hacer una división sin signo entre 2ⁿ.

De forma similar, un desplazamiento a la derecha de n posiciones con sar tiene el efecto de hacer una división con signo entre 2ⁿ.

Operaciones de rotación

Estas instrucciones mueven los bits del operando destino una cantidad de posiciones especificada por el segundo operando. El segundo operando debe ser un número constante (un immediate) o el registro cl (los 8 bits más bajos de rcx).

La diferencia entre una rotación y un desplazamiento es que una rotación no descarta ni agrega bits. Los bits que un desplazamiento descartaría se mueven, en cambio, al extremo opuesto. Así, todos los bits permanecen; todos cambian de lugar.

Nombre Descripción
rol Rota los bits a la izquierda
ror Rota los bits a la derecha

Fíjate que el conteo del segundo operando se enmascara a 5 bits, o a 6 bits con un operando destino de 64 bits. Cualquier bit después de eso se ignora en la práctica. Esto significa que la rotación máxima es 31, o 63 con un operando de 64 bits.

Otras instrucciones de manipulación de bits

Hay otras instrucciones útiles de manipulación de bits:

Nombre Descripción
popcnt Cuenta el número de bits establecidos
bsr Obtiene el índice del bit establecido más significativo. Si no hay ningún bit establecido, el resultado es indefinido
bsf Obtiene el índice del bit establecido menos significativo. Si no hay ningún bit establecido, el resultado es indefinido

Estas instrucciones trabajan con dos operandos de 16, 32 o 64 bits. No se pueden usar con operandos de 8 bits.

Instrucciones

Tu amigo te acaba de enviar un mensaje con un secreto importante. Como no quería que a otras personas les resultara fácil leerlo, el mensaje se cifró realizando una serie de manipulaciones de bits. Tendrás que escribir los métodos que ayuden a descifrar el mensaje.

Note

Estas son las instrucciones de bit único mencionadas en este concepto:

Name Description
bt copia el bit en CF sin modificar ningún operando
bts copia el bit en CF y lo establece en el operando destino
btr copia el bit en CF y lo borra en el operando destino
btc copia el bit en CF y lo complementa (lo invierte) en el operando destino

Estas son las instrucciones bit a bit mencionadas en este concepto:

Name Description
and 1 si ambos bits son 1
or 1 si al menos uno de los bits es 1
xor 1 si los bits son diferentes
not 1 si el bit era 0; 0 si el bit era 1

Estas son las instrucciones de desplazamiento mencionadas en este concepto:

Name Description
shl/sal Desplaza los bits a la izquierda
shr/sar Desplaza los bits a la derecha

Estas son las instrucciones de rotación mencionadas en este concepto:

Name Description
rol Rota los bits a la izquierda
ror Rota los bits a la derecha

Estas son las instrucciones misceláneas mencionadas en este concepto:

Name Description
popcnt Cuenta la cantidad de bits establecidos
bsr Obtiene el índice del bit establecido más significativo. Si no hay ningún bit establecido, el resultado no está definido
bsf Obtiene el índice del bit establecido menos significativo. Si no hay ningún bit establecido, el resultado no está definido

1. Extrae la máscara

El mensaje está codificado en un entero de 16 bits. Sin embargo, de esos, los 8 bits más altos en realidad no forman parte del mensaje, sino que son una máscara que debe usarse en el descifrado.

Implementa la función extract_higher_bits, que recibe un entero de 16 bits y devuelve sus 8 bits más altos.

extract_higher_bits(0b1010010011000101)
// => 0b10100100

2. Extrae el mensaje

No basta con poder extraer la máscara; también debes aislar el mensaje.

Implementa la función extract_lower_bits, que recibe un entero de 16 bits y devuelve sus 8 bits más bajos.

extract_lower_bits(0b1010010011000101);
// => 0b11000101

3. Extrae los bits redundantes

Algunos bits están establecidos tanto en el mensaje como en la máscara. Esta es una información muy importante que se usará más adelante.

Implementa la función extract_redundant_bits, que recibe un entero de 16 bits que codifica tanto el mensaje como una máscara, y devuelve un entero de 8 bits con solo los bits redundantes establecidos. Un bit del número devuelto debe establecerse en 1 donde también sea 1 tanto en el mensaje como en la máscara. Todos los demás bits deben quedar borrados.

extract_redundant_bits(0b1010010011000101);
// => 0b10000100

4. Establece todos los bits del mensaje

A continuación, hay algunos bits que deben establecerse en 1 en el mensaje, según la máscara.

Implementa la función set_message_bits, que recibe un entero de 16 bits que codifica tanto el mensaje como una máscara, y devuelve el resultado de establecer en 1 los bits del mensaje. Un bit del mensaje debe establecerse en 1 donde el bit de la máscara sea 1. Todos los demás bits deben mantenerse sin cambios, de modo que sigan establecidos si ya lo estaban, y borrados si ya lo estaban.

set_message_bits(0b1010010011000101);
// => 0b11100101

5. Rota la clave privada

Hay una pieza del rompecabezas que no está explícita en el mensaje: el número de 16 bits 0b1011001100111100. Este número es tu clave privada compartida y debes usarla para ayudar a descifrar el mensaje.

Para hacerlo, primero debes rotar los bits de tu clave privada a la izquierda cierta cantidad de posiciones. La cantidad de posiciones es igual a la cantidad de bits redundantes establecidos tanto en el mensaje como en la máscara.

Implementa la función rotate_private_key, que recibe un entero de 16 bits que codifica tanto el mensaje como una máscara, y devuelve el resultado de rotar tu clave privada. Este resultado es un entero de 16 bits.

rotate_private_key(0b1010010011000101);
// => 0b1100110011110010
Note

NASM (el Netwide Assembler, el ensamblador que usa este track) tiene soporte para constantes en formato binario con el prefijo 0b. También admite usar un guion bajo (_) como separador dentro de una constante, para que sea más legible:

PRIVATE_KEY equ 0b1011_0011_0011_1100

6. Formatea la clave privada

Para poder usarla en el descifrado, tu clave privada debe formatearse para aislar los bits relevantes.

Para formatear por completo una clave privada, debes:

  • Rotarla.
  • Aislar la porción de 8 bits más baja de la clave privada rotada, que es el valor base.
  • Aislar la porción de 8 bits más alta de la clave privada rotada, que es una máscara que se aplicará al valor base.
  • Invertir los bits del valor base que también estén establecidos en la máscara.
  • Invertir todos los bits del resultado.

Un bit invertido es 1 si era 0 y 0 si era 1.

Implementa la función format_private_key, que recibe un entero de 16 bits que codifica tanto el mensaje como una máscara, y devuelve una clave privada de 8 bits totalmente formateada.

format_private_key(0b1010010011000101);
// => 0b11000001

7. Termina el descifrado

Una vez que tengas el mensaje con todos los bits relevantes establecidos y la clave privada formateada, es momento de unirlos para obtener el mensaje resultante.

El mensaje resultante es un entero de 16 bits, en el que:

  • Los 8 bits más altos se llenan con la clave privada formateada.
  • Los 8 bits más bajos se llenan con el mensaje, después de establecer todos los bits relevantes.

Implementa la función decrypt_message, que recibe un entero de 16 bits que codifica tanto el mensaje como una máscara, y devuelve un entero de 16 bits con el mensaje totalmente descifrado.

Esta función debe hacer uso de la clave privada formateada que generes con format_private_key y también del mensaje con todos los bits relevantes establecidos con set_message_bits.

decrypt_message(0b1010010011000101);
// => 0b1100000111100101
Editar en GitHub El enlace se abre en una ventana o una pestaña nuevas
x86-64 Assembly Exercism

¿Todo listo para empezar Secretos?

Regístrate en Exercism para aprender y dominar x86-64 Assembly con 22 conceptos130 ejercicios y mentoría humana real, todo gratis.