Cada bit de un número 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 número entero de N bits proporciona 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.
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 primer operando. 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 pone a 1 en el operando destino |
btr |
copia el bit en CF y lo pone a 0 en el operando destino |
btc |
copia el bit en CF y lo complementa (lo invierte) en el operando destino |
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 distintos |
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 almacena el resultado en el operando destino.
La excepción es not, que solo recibe un operando destino.
Cuando interpretamos uno y cero como inclusión y exclusión, respectivamente, un número 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 es habitual usar una máscara de bits para incluir ciertos bits de un número entero y excluir otros.
Por ejemplo, sea A un número entero cuya representación binaria es:
| índice | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
|---|---|---|---|---|---|---|---|---|
| bits | 1 | 0 | 0 | 1 | 0 | 1 | 0 | 1 |
Sea también M un número 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 números 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 comentadas antes son útiles para manipular números enteros con máscaras. Por ejemplo:
A que no selecciona M, aplica el AND bit a bit: A AND M.A que selecciona M, aplica el OR bit a bit: A OR M.test
La instrucción test realiza 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 pone a 0 |
ZF |
A AND B == 0 |
SF |
el bit de signo de A AND B está a 1 |
OF |
siempre se pone a 0 |
Esta instrucción recibe dos operandos y actualiza las banderas, pero no modifica sus operandos.
Estas instrucciones mueven los bits del operando destino un número de posiciones especificado 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 |
Ten en cuenta que el recuento del segundo operando se enmascara a 5 bits, o a 6 bits con un operando destino de 64 bits.
Cualquier bit a partir de ahí se ignora de forma efectiva.
Esto significa que el desplazamiento máximo es 31, o 63 con un operando de 64 bits.
Tanto shl como sal realizan exactamente la misma operación; una es un alias de la otra.
Siempre 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 añaden al principio tantos bits nuevos puestos a cero como la longitud del desplazamiento.
Como cada bit de un número entero representa una potencia de 2, un desplazamiento a la izquierda de n posiciones tiene el efecto de multiplicar el número entero por 2ⁿ.
Hay dos instrucciones para mover bits a la derecha: shr y sar.
Siempre que se usa cualquiera de las dos instrucciones, los bits que están más cerca del principio de la secuencia que la longitud del desplazamiento se mueven primero a CF y luego se descartan.
Por otro lado, se añaden al final tantos bits nuevos como 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 a 1 y 0 en caso contrario.
Esto significa que sar conserva el signo en el desplazamiento de un número entero con signo.
Como cada bit de un número entero representa una potencia de 2, un desplazamiento a la derecha de n posiciones usando shr tiene el efecto de hacer una división sin signo por 2ⁿ.
Del mismo modo, un desplazamiento a la derecha de n posiciones usando sar tiene el efecto de hacer una división con signo por 2ⁿ.
Estas instrucciones mueven los bits del operando destino un número de posiciones especificado 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 añade ningún bit. Los bits que un desplazamiento descartaría se mueven, en su lugar, al extremo opuesto. Así, todos los bits permanecen; todos cambian de sitio.
| Nombre | Descripción |
|---|---|
rol |
Rota los bits a la izquierda |
ror |
Rota los bits a la derecha |
Ten en cuenta que el recuento del segundo operando se enmascara a 5 bits, o a 6 bits con un operando destino de 64 bits.
Cualquier bit a partir de ahí se ignora de forma efectiva.
Esto significa que la rotación máxima es 31, o 63 con un operando de 64 bits.
Hay otras instrucciones útiles de manipulación de bits:
| Nombre | Descripción |
|---|---|
popcnt |
Cuenta el número de bits puestos a 1 |
bsr |
Obtiene el índice del bit puesto a 1 más significativo. Si no hay ningún bit puesto a 1, el resultado no está definido |
bsf |
Obtiene el índice del bit puesto a 1 menos significativo. Si no hay ningún bit puesto a 1, el resultado no está definido |
Todas estas instrucciones funcionan con dos operandos de 16 bits, 32 bits o 64 bits.
No se pueden usar con operandos de 8 bits.
Tu amigo acaba de enviarte un mensaje con un secreto importante. Como no quería que los demás pudieran leerlo con facilidad, el mensaje se cifró realizando una serie de manipulaciones de bits. Tendrás que escribir los métodos que ayuden a descifrar el mensaje.
Estas son las instrucciones de bit único mencionadas en este concepto:
| 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 de destino |
| btr | copia el bit en CF y lo borra en el operando de destino |
| btc | copia el bit en CF y lo complementa (lo invierte) en el operando de destino |
Estas son las instrucciones bit a bit mencionadas en este concepto:
| 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 distintos |
| not | 1 si el bit era 0; 0 si el bit era 1 |
Estas son las instrucciones de desplazamiento mencionadas en este concepto:
| Nombre | Descripción |
|---|---|
| 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:
| Nombre | Descripción |
|---|---|
| rol | Rota los bits a la izquierda |
| ror | Rota los bits a la derecha |
Estas son las instrucciones misceláneas mencionadas en este concepto:
| 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 no está definido |
| bsf | Obtiene el índice del bit establecido menos significativo. Si no hay ningún bit establecido, el resultado no está definido |
El mensaje está codificado en un entero de 16 bits. Sin embargo, de esos, los 8 bits más altos no forman parte realmente del mensaje, sino que son una máscara que hay que usar 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
Tener la capacidad de extraer la máscara no es suficiente; 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
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 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 a 1 allí donde también sea 1 tanto en el mensaje como en la máscara.
Todos los demás bits deben borrarse.
extract_redundant_bits(0b1010010011000101);
// => 0b10000100
A continuación, hay algunos bits que deben establecerse a 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 a 1 los bits del mensaje.
Un bit del mensaje debe establecerse a 1 allí 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
Hay una pieza del rompecabezas que no aparece explícita en el mensaje: el número de 16 bits 0b1011001100111100.
Este número es tu clave privada compartida y debes usarlo para ayudar a descifrar el mensaje.
Para ello, primero tienes que rotar los bits de tu clave privada a la izquierda un cierto número de posiciones. El número de posiciones es igual al número 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
NASM (The Netwide Assembler, el ensamblador que usa este track) admite constantes en formato binario con el prefijo 0b.
También admite el uso de un guion bajo (_) como separador en una constante, para mayor legibilidad:
PRIVATE_KEY equ 0b1011_0011_0011_1100
Para poder usarse en el descifrado, tu clave privada debe formatearse para aislar los bits relevantes.
Para formatear por completo una clave privada, debes:
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
Una vez que tengas el mensaje con todos los bits relevantes establecidos y la clave privada formateada, es el momento de unirlos para obtener el mensaje resultante.
El mensaje resultante es un entero de 16 bits, del cual:
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
Regístrate en Exercism para aprender y dominar x86-64 Assembly con 22 conceptos130 ejercicios y mentoría humana real, todo gratis.