Un opcode identifica qué operación informática básica del conjunto de instrucciones debe realizarse. Se utiliza cuando se escribe código máquina. Le dice al ordenador que haga algo. Cada instrucción en lenguaje máquina suele constar de un opcode y de unos operandos. El opcode funciona como el verbo de una frase y los operandos actúan como el sujeto u objetos: los operandos son típicamente direcciones de memoria, registros o valores inmediatos que la operación utilizará.
Los opcodes sirven para muchas funciones: operaciones aritméticas y lógicas (suma, resta, AND, OR), movimientos entre memoria y registros, operaciones de punto flotante, cambios de control (saltos, llamadas y retornos), operaciones de entrada/salida, y órdenes privilegiadas como detener la CPU. Por ejemplo, hay opcodes para sumar dos registros, desplazar bits, almacenar datos en memoria o detener un programa. En los ordenadores modernos existen literalmente cientos de opcodes y combinaciones posibles, según la complejidad del conjunto de instrucciones.
Representación: binario y hexadecimal
Por la naturaleza del hardware, los opcodes son números binarios. Para facilitar la lectura y edición humana se representan a menudo en hexadecimal (por ejemplo, 10100101 = A5). En la memoria se almacenan como bits; en documentación y herramientas de depuración se muestran en hexadecimal o en forma de bytes. Muchos opcodes modernos ocupan al menos 1 byte (dos dígitos hexadecimales), aunque la instrucción completa puede incluir más campos además del opcode.
Codificación de instrucciones y campos adicionales
Una instrucción en código máquina no siempre es sólo "opcode": suele estar formada por varios campos que pueden incluir:
- Opcode: identifica la operación básica.
- Modificadores/μ-opcodes: bits adicionales que especifican variantes de la operación.
- ModR/M, SIB, desplazamientos: formatos comunes en familias como x86 para indicar registros y modos de direccionamiento.
- Operandos inmediatos: valores literales incluidos en la instrucción.
- Desplazamientos/direcciones: direcciones de memoria usadas por la instrucción.
En arquitecturas RISC es habitual que las instrucciones sean de longitud fija (por ejemplo, 32 bits) y que el campo opcode ocupe una porción fija de esos bits. En arquitecturas CISC (como x86) las instrucciones suelen ser de longitud variable y pueden combinar prefijos, opcode, bytes de modulación y datos, con longitudes que van desde 1 hasta varios bytes.
Variación entre arquitecturas: RISC vs CISC
Los opcodes dependen del hardware y, por tanto, varían entre arquitecturas. Por ejemplo, el opcode que representa una operación STORE podría ser 0xFA en una máquina y 0x02 en otra. Algunas instrucciones estarán disponibles en unas arquitecturas y no en otras.
En general existen dos enfoques básicos para diseñar conjuntos de instrucciones:
- RISC (Reduced Instruction Set Computer): conjunto reducido de instrucciones, generalmente con formato fijo y centrado en operaciones simples y rápidas; favorece un decodificador sencillo y ejecución eficiente por medio de pipelines y ejecución por etapas.
- CISC (Complex Instruction Set Computer): conjunto amplio y rico en instrucciones complejas y modos de direccionamiento; las instrucciones pueden ser más poderosas pero más costosas de decodificar. En muchos procesadores CISC las instrucciones complejas se descomponen en micro-ops internas que el procesador ejecuta internamente.
Programación: ensamblador, compiladores y compatibilidad
Los programadores raramente escriben opcodes en binario. En lenguaje ensamblador se utilizan mnemónicos legibles (por ejemplo, MOV, ADD, JMP) que un ensamblador traduce a sus correspondientes opcodes y formatos binarios. Así, el programador recuerda un mnemónico en lugar de una secuencia de bits.
Cuando se programan directamente en memoria (código máquina), ese código funcionará solamente en la arquitectura concreta para la que fue diseñado. En cambio, los lenguajes de alto nivel (C, Java, etc.) se compilan o traducen a código de niveles inferiores hasta llegar al código máquina, proceso que permite portar programas entre distintos ordenadores mediante recompilación o traducción en tiempo de ejecución.
Decodificación y ejecución
En la CPU, la ejecución de instrucciones sigue etapas como lectura (fetch), decodificación (decode) y ejecución (execute). El decodificador interpreta el opcode y los campos asociados, determina qué unidad ejecuta la operación y prepara los operandos. En CPUs modernas esto se realiza en pipelines y a menudo con etapas de microarquitectura que transforman instrucciones complejas en microinstrucciones más simples.
Ejemplos prácticos
A modo de ilustración, estos son ejemplos típicos que se pueden encontrar en diferentes arquitecturas (representados en hexadecimal):
- Instrucciones simples en x86:
- NOP (no operación): 0x90
- HLT (detener la CPU): 0xF4
- RET (retorno de subrutina): 0xC3
- MOV EAX, imm32 — codificado como B8 seguido del valor inmediato de 32 bits (por ejemplo, B8 01 00 00 00 para mover 1 a EAX).
- En ARM (modo ARM de 32 bits) las instrucciones suelen ser de 4 bytes y el opcode está repartido en varios bits del campo de instrucción; por ejemplo, una instrucción que carga un valor inmediato pequeño en un registro puede aparecer como 0xE3A00001 (MOV R0, #1) en muchas ediciones de ARMv7.
Estos ejemplos son ilustrativos: la codificación exacta depende de la versión y modo de la arquitectura (por ejemplo, x86 real-mode, protected-mode, x86-64, ARM32, ARM64) y de los prefijos o extensiones usados. En x86, además, existen prefijos y tablas de opcode que permiten combinaciones y extensiones; la longitud máxima de instrucción puede llegar a 15 bytes en x86 moderno.
Otras consideraciones
- Direccionamiento: los operandos pueden referirse a registros, memoria (con distintos modos de direccionamiento), o ser valores inmediatos.
- Microcódigo: algunas instrucciones complejas se implementan mediante microcódigo interno que descompone la instrucción en micro-ops.
- Emulación y virtualización: intérpretes y emuladores traducen opcodes de una arquitectura a otra; esto hace posible ejecutar software legacy en hardware distinto.
- Seguridad y análisis forense: el análisis de opcodes y su secuencia (disassembly) es básico en ingeniería inversa, detección de malware y creación de shellcode.
En resumen, un opcode es la pieza fundamental que define qué operación realiza la CPU. Su forma y significado dependen del conjunto de instrucciones y del hardware concreto, y su estudio resulta esencial para comprender cómo se ejecuta el software a nivel más bajo.