Las instrucciones indocumentadas, o los op_codes, son un tipo de instrucciones presentes en muchos procesadores antiguos y actuales, pero son también unas completas desconocidas para muchos, resultando incluso hasta misteriosas o sospechosas para los más paranoicos con los temas de la seguridad. En este artículo vamos a ver qué son estas instrucciones y por qué están ahí…
Índice de contenidos
¿Qué es una instrucción?
Una instrucción, código de operación u op_code, como tantas veces he explicado, es un código binario que cuando llega a la CPU, la unidad de control mediante el decodificador la decodificará para determinar qué operación tiene implícita para operar con los datos que la acompañan. Por ejemplo, una instrucción puede ser ADD (suma), SUB (resta), MUL (multiplicación), DIV (división), AND (Y lógico), OR (O lógico), etc., es decir, operaciones aritmeticológicas aplicadas sobre datos enteros o de coma flotante, que son los operandos.
Como debes saber, un programa no es más que una consecución de instrucciones de este tipo. Por ejemplo, veamos un código fuente en C para un programa simple que muestra en pantalla un Hola mundo:
#include <stdio.h>
int main() {
printf("¡Hola mundo!");
return 0;
}
Cuando este código se pasa a lenguaje ensamblador, por ejemplo el de la ISA x86-64, obtenemos estas instrucciones que la CPU deberá procesar para que este programa se pueda ejecutar:
global _start
section .text
_start:
mov rax, 1 ; escribe
mov rdi, 1 ; en la salida estándar (pantalla)
mov rsi, msg ; el mensaje Hola mundo.
mov rdx, msglen ; con una determinada longitud
syscall ; y esta es la llamada al systema o sycall para esta escritura
mov rax, 60 ; finalmente
mov rdi, 0 ; se carga el valor
syscall ; para la syscall que finaliza el programa
section .rodata
msg: db "¡Hola mundo!", 12
msglen: equ $ - msg
Como ves, lo que tendría que ejecutar la CPU para que este programa funcione es una serie de instrucciones MOV, que son movimientos de datos a registros, y la instrucción syscall que en x86-64 corresponde a la llamada al sistema, aunque en x86 más antiguos puede usarse la instrucción int. Por otro lado están los datos, que en este programa son una serie de constantes y el mensaje Hola mundo…
Estos op_codes o instrucciones son los que están definidos en la ISA de la CPU, es decir, en la arquitectura. Por tanto, si la ISA cambia, también deben hacerlo el código ensamblador para que sea comprensible por la CPU. El código ensamblador no es más que una serie de mnemónicos para representar las instrucciones de la ISA.
Una vez que este código se pasa a binario, es decir, se convierte en código máquina o unos y ceros, ya es comprensible por la CPU y la electrónica.
El sistema operativo puede cargarlo en la memoria RAM como un proceso cuando se ejecuta el binario ejecutable y desde allí la CPU comenzará a acceder a las instrucciones y datos que componente este programa. Lo hará de forma secuencial, es decir, en orden, instrucción por instrucción.
Cuando, por ejemplo, llegue la primera instrucción MOV a la CPU, primero pasará a la unidad de control, concretamente al decodificador. Éste, gracias al microcódigo, sabrá decodificar la instrucción, que representa un movimiento de datos al registro en este caso.
Una vez decodificada, la unidad de control sabrá que es una instrucción de movimiento, por lo que generará unas señales de control que indicarán a las unidades de ejecución lo que deben hacer. En este caso el movimiento. Pero igual si fuese una suma, una multiplicación, etc. Así es como funciona la base de la informática.
Es decir, la instrucción MOV, es un op_code (tipo: b8 01 00 00 00) que corresponde en x86 a una serie de códigos binarios, y MOV RAX, 1, lo que hará es grabar una constante, en este caso 1, al registro RAX de la CPU. Cuando llegue este código, el decodificador buscará en la ROM del microcódigo para obtener la interpretación.
Y ¿por qué te cuento esto? Muy sencillo, quería que entendieses esto para saber qué es un op_code o instrucción, y cómo funciona.
Tipos de instrucciones
Como puedes deducir, existen op_codes o instrucciones de varios tipos, para que la CPU pueda ejecutar cualquier programa. En el ejemplo anterior hemos usado solamente un hola mucho, para lo cual con instrucciones MOV y syscall es suficiente. Por ejemplo, veamos este otro código en C para sumar dos números enteros:
#include <stdio.h>
int main() {
int num1, num2, suma;
printf("Introduce dos números enteros: ");
scanf("%d %d", &num1, &num2);
// calcula la suma
suma = num1 + num2;
//Muestra el resultado
printf("%d + %d = %d", num1, num2, suma);
return 0;
}
El ensamblador para este sería este otro código:
.LC0: .string"Introduce dos numeros enteros: " .LC1: .string"%d %d" .LC2: .string"%d + %d = %d" main: push rbp mov rbp, rsp sub rsp, 16 mov edi, OFFSETFLAT:.LC0 mov eax, 0 call printf lea rdx, [rbp-12] lea rax, [rbp-8] mov rsi, rax mov edi, OFFSETFLAT:.LC1 mov eax, 0 call__isoc99_scanf mov edx, DWORDPTR [rbp-8] mov eax, DWORDPTR [rbp-12] add eax, edx mov DWORDPTR [rbp-4], eax mov edx, DWORDPTR [rbp-12] mov eax, DWORDPTR [rbp-8] mov ecx, DWORDPTR [rbp-4] mov esi, eax mov edi, OFFSETFLAT:.LC2 mov eax, 0 call printf mov eax, 0 leave ret
Como ves, en este otro programa también se usan otras instrucciones, como es el caso de ADD EAX, EDX, es decir, los valores de num1 y num2 que se han cargado previamente en estos registros.
Lo que pretendo decirte con esto es que las instrucciones no solo son de movimiento de datos, también podemos encontrar tipos como:
-
- De transferencia de datos: permiten mover datos de un registro a otro, de una dirección a otra, cargar una constante o variable en un registro, etc. Es el caso de la MOV que hemos visto anteriormente.
- Aritméticas: son instrucciones u op_codes que, una vez decodificadas, se traducen en señales que le dirá a la ALU que tiene que realizar una operación aritmética con los operandos. Por ejemplo, de este tipo serían las ADD, SUB, MUL, DIV, etc. ADD EAX, EDX en el ejemplo anterior, sumará el contenido del registro EAX con el de EDX y guardará el resultado en EAX nuevamente.
- Lógicas: por supuesto, también existen instrucciones para realizar operaciones lógicas sobre bits, como puede ser AND, OR, NOT, XOR, etc.
- Control: otro tipo de instrucciones importantes son las que pueden alterar el registro PC, es decir, el registro Program Counter de la CPU. Antes he dicho que la CPU procesará las instrucciones el programa en orden, se forma secuencial. Para ello, tiene un registro PC en el que se va sumando 1 a la dirección de la instrucción actual para que apunte a la siguiente. Alterar este registro puede ser necesario cuando el programa necesita que no se siga ese orden secuencial, como cuando se producen saltos condicionales, bucles, llamadas al sistema, etc. Por ejemplo, algunos casos de este tipo de instrucciones serían SYSCALL, JMP, RET, etc.
- De E/S: para finalizar, también tenemos las instrucciones de operaciones de entrada y salida, que actuarán como si de direcciones de memoria se tratase para leer o escribir en ellas, pero que irán destinadas a los periféricos mapeados. Por ejemplo, IN, OUT,… seguidas del número de puerto.
También te interesará conocer cuáles son las mejores CPUs del mercado. ¿Tendrán este tipo de instrucciones? ¿Te atreves a averiguarlo?
Instrucciones indocumentadas (illegal op_code)
Un illegal op_code, o instrucción no documentada, o instrucción no deseada, es aquella instrucción que el fabricante de la CPU no ha documentado para que sea usada por los programadores. Y el motivo para que esto ocurra pueden ser varios:
- Que el diseñador de la CPU desconozca también su existencia y que simplemente haya sido un código implícito en la ISA por error. En estos casos podría generar un funcionamiento errático o no deseado si se usase. Si la CPU ha sido bien diseñada, debería generar una excepción o condición de falla si se trata de ejecutar una de estas instrucciones no documentadas.
- En algunos otros casos puede ser que el diseñador las haya incluido de forma consciente para ciertas tareas específicas que no pueden ser explotadas directamente por los programadores, es decir, son instrucciones opacas para los desarrolladores, y que pueden servir, por ejemplo, para acelerar ciertas tareas.
- Otro caso podría ser que sean instrucciones que no se usarán durante el uso habitual de la CPU, pero que se integran para hacer ciertas pruebas durante las etapas de verificación de los sample engineering, etc.
- En algunos casos podría ser para evitar o dificultar la ingeniería inversa.
- E incluso podría ser por motivos mucho más oscuros, como poderlas ejecutar con ciertos códigos para poder realizar tareas maliciosas.
En las actuales CPUs, si hubiese algún fallo, se podría corregir mediante un parche del microcódigo o firmware. Antes era más difícil con las ROMs o las unidades de control cableadas, donde simplemente no se podía hacer nada…
Sea como sea, estas instrucciones ilegales son más populares de lo que muchos creen.
Historia
Por ejemplo, uno de los primeros casos de illegal op_codes detectados fue el Intel 8086, el Zilog Z80, el Texas Instruments TM9900, o el MOS Technology 6502 de la década de los 1970. Pero no solo estaban presentes en estas CPUs antiguas, también en otras actuales.
Muchos usuarios han descubierto este tipo de códigos realizando técnicas de fuzzing, viendo que existen algunas instrucciones no documentadas en multitud de modelos de CPU. Un caso muy actual que os comentamos es el de las instrucciones Apple AMX.