En programación informática , el código máquina es un código informático que consiste en instrucciones en lenguaje máquina , que se utilizan para controlar la unidad central de procesamiento (CPU) de una computadora. Para las computadoras binarias convencionales , el código máquina es "la representación binaria de un programa informático que es realmente leído e interpretado por la computadora. Un programa en código máquina consiste en una secuencia de instrucciones de máquina (posiblemente intercaladas con datos)". [1]
Cada instrucción de código de máquina hace que la CPU realice una tarea muy específica. Algunos ejemplos de tareas son:
En general, cada familia de arquitectura (por ejemplo, x86 , ARM ) tiene su propia arquitectura de conjunto de instrucciones (ISA) y, por lo tanto, su propio lenguaje de código de máquina específico. Existen excepciones, como la arquitectura VAX , que incluye soporte opcional del conjunto de instrucciones PDP-11 ; la arquitectura IA-64 , que incluye soporte opcional del conjunto de instrucciones IA-32 ; y el microprocesador PowerPC 615 , que puede procesar de forma nativa conjuntos de instrucciones PowerPC y x86.
El código de máquina es un lenguaje estrictamente numérico y es la interfaz de nivel más bajo para la CPU destinada a un programador. El lenguaje ensamblador proporciona un mapa directo entre el código de máquina numérico y un mnemónico legible por humanos. En el lenguaje ensamblador, los códigos de operación y operandos numéricos se reemplazan con mnemónicos y etiquetas. Por ejemplo, la arquitectura x86 tiene disponible el código de operación 0x90; se representa como NOP en el código fuente del ensamblador . Si bien es posible escribir programas directamente en código de máquina, administrar bits individuales y calcular direcciones numéricas es tedioso y propenso a errores. Por lo tanto, los programas rara vez se escriben directamente en código de máquina. Sin embargo, un programa de código de máquina existente puede editarse si el código fuente del ensamblador no está disponible.
La mayoría de los programas actuales están escritos en un lenguaje de alto nivel . Un compilador puede traducir un programa de alto nivel a código de máquina .
Cada procesador o familia de procesadores tiene su propio conjunto de instrucciones . Las instrucciones son patrones de bits , dígitos o caracteres que corresponden a comandos de máquina. Por lo tanto, el conjunto de instrucciones es específico para una clase de procesadores que utilizan (en su mayoría) la misma arquitectura . Los diseños de procesadores sucesores o derivados a menudo incluyen instrucciones de un predecesor y pueden agregar nuevas instrucciones adicionales. Ocasionalmente, un diseño sucesor discontinuará o alterará el significado de algún código de instrucción (generalmente porque es necesario para nuevos propósitos), lo que afectará la compatibilidad del código hasta cierto punto; incluso los procesadores compatibles pueden mostrar un comportamiento ligeramente diferente para algunas instrucciones, pero esto rara vez es un problema. Los sistemas también pueden diferir en otros detalles, como la disposición de la memoria, los sistemas operativos o los dispositivos periféricos . Debido a que un programa normalmente depende de tales factores, los diferentes sistemas normalmente no ejecutarán el mismo código de máquina, incluso cuando se use el mismo tipo de procesador.
El conjunto de instrucciones de un procesador puede tener instrucciones de longitud fija o de longitud variable. La forma en que se organizan los patrones varía según la arquitectura particular y el tipo de instrucción. La mayoría de las instrucciones tienen uno o más campos de código de operación que especifican el tipo de instrucción básica (como aritmética, lógica, salto , etc.), la operación (como sumar o comparar) y otros campos que pueden indicar el tipo de operando (s), el modo de direccionamiento (s), el desplazamiento o índice de direccionamiento, o el valor del operando en sí (estos operandos constantes contenidos en una instrucción se denominan inmediatos ). [2]
No todas las máquinas o instrucciones individuales tienen operandos explícitos. En una máquina con un solo acumulador , el acumulador es implícitamente tanto el operando izquierdo como el resultado de la mayoría de las instrucciones aritméticas. Algunas otras arquitecturas, como la arquitectura x86 , tienen versiones de acumulador de instrucciones comunes, con el acumulador considerado como uno de los registros generales por instrucciones más largas. Una máquina de pila tiene la mayoría o todos sus operandos en una pila implícita. Las instrucciones de propósito especial también carecen a menudo de operandos explícitos; por ejemplo, CPUID en la arquitectura x86 escribe valores en cuatro registros de destino implícitos. Esta distinción entre operandos explícitos e implícitos es importante en los generadores de código, especialmente en las partes de asignación de registros y seguimiento de rango en vivo. Un buen optimizador de código puede rastrear operandos implícitos y explícitos que pueden permitir una propagación constante más frecuente , plegado constante de registros (un registro asignado al resultado de una expresión constante liberada al reemplazarlo por esa constante) y otras mejoras de código.
Una representación mucho más amigable para los humanos del lenguaje de máquina, llamada lenguaje ensamblador , utiliza códigos mnemotécnicos para referirse a las instrucciones del código de máquina, en lugar de usar los valores numéricos de las instrucciones directamente, y utiliza nombres simbólicos para referirse a las ubicaciones de almacenamiento y, a veces, a los registros . [3] Por ejemplo, en el procesador Zilog Z80 , el código de máquina 00000101
, que hace que la CPU disminuya el B
registro de propósito general , se representaría en lenguaje ensamblador como DEC B
. [4]
Los IBM 704, 709, 704x y 709x almacenan una instrucción en cada palabra de instrucción; IBM numera el bit desde la izquierda como S, 1, ..., 35. La mayoría de las instrucciones tienen uno de dos formatos:
En todos los modelos, excepto en los IBM 7094 y 7094 II, hay tres registros de índice designados A, B y C; la indexación con varios bits 1 en la etiqueta resta el o lógico de los registros de índice seleccionados y la carga con varios bits 1 en la etiqueta carga todos los registros de índice seleccionados. Los modelos 7094 y 7094 II tienen siete registros de índice, pero cuando se encienden están en modo de etiquetas múltiples , en el que utilizan solo tres de los registros de índice de una manera compatible con máquinas anteriores, y requieren una instrucción Leave Multiple Tag Mode ( LMTM ) para acceder a los otros cuatro registros de índice.
La dirección efectiva normalmente es YC(T), donde C(T) es 0 para una etiqueta de 0, la dirección lógica o de los registros de índice seleccionados en el modo de etiquetas múltiples o el registro de índice seleccionado si no está en el modo de etiquetas múltiples. Sin embargo, la dirección efectiva para las instrucciones de control de registro de índice es simplemente Y.
Una bandera con ambos bits 1 selecciona direccionamiento indirecto; la palabra de dirección indirecta tiene una etiqueta y un campo Y.
Además de las instrucciones de transferencia (ramificación), estas máquinas tienen instrucciones de omisión que omiten condicionalmente una o dos palabras, por ejemplo, Comparar acumulador con almacenamiento (CAS) hace una comparación de tres vías y omite condicionalmente NSI, NSI+1 o NSI+2, dependiendo del resultado.
La arquitectura MIPS proporciona un ejemplo específico para un código de máquina cuyas instrucciones tienen siempre 32 bits de longitud. [5] : 299 El tipo general de instrucción se proporciona mediante el campo op (operación), los 6 bits más altos. Las instrucciones de tipo J (salto) y de tipo I (inmediato) se especifican completamente mediante op . Las instrucciones de tipo R (registro) incluyen un campo adicional funct para determinar la operación exacta. Los campos utilizados en estos tipos son:
6 5 5 5 5 6 bits[ op | rs | rt | rd | shamt | funct] Tipo R[ op | rs | rt | dirección/inmediato] Tipo I[ op | dirección de destino ] Tipo J
rs , rt y rd indican operandos de registro; shamt proporciona una cantidad de desplazamiento; y los campos de dirección o inmediatos contienen un operando directamente. [5] : 299–301
Por ejemplo, sumar los registros 1 y 2 y colocar el resultado en el registro 6 se codifica: [5] : 554
[ op | rs | rt | rd | shamt | función] 0 1 2 6 0 32 decimal 000000 00001 00010 00110 00000 100000 binario
Cargar un valor en el registro 8, tomado de la celda de memoria 68 celdas después de la ubicación indicada en el registro 3: [5] : 552
[ op | rs | rt | dirección/inmediata] 35 3 8 68 decimal 100011 00011 01000 00000 00001 000100 binario
Saltando a la dirección 1024: [5] : 552
[ op | dirección de destino ] 2 1024 decimal 000010 00000 00000 00000 10000 000000 binario
En arquitecturas de procesadores con conjuntos de instrucciones de longitud variable [6] (como la familia de procesadores x86 de Intel ), dentro de los límites del fenómeno de resincronización del flujo de control conocido como el recuento de Kruskal , [7] [6 ] [8] [9] [10] a veces es posible, a través de la programación a nivel de código de operación, organizar deliberadamente el código resultante de modo que dos rutas de código compartan un fragmento común de secuencias de código de operación. [nb 1] Estas se denominan instrucciones superpuestas , códigos de operación superpuestos , código superpuesto , código superpuesto , escisión de instrucción o salto al medio de una instrucción . [11] [12] [13]
En los años 1970 y 1980, a veces se utilizaban instrucciones superpuestas para preservar el espacio de memoria. Un ejemplo fue la implementación de tablas de errores en Altair BASIC de Microsoft , donde las instrucciones intercaladas compartían mutuamente sus bytes de instrucción. [14] [6] [11] La técnica rara vez se utiliza hoy en día, pero aún puede ser necesario recurrir a ella en áreas donde es necesaria una optimización extrema del tamaño a nivel de bytes, como en la implementación de cargadores de arranque que tienen que caber en sectores de arranque . [nb 2]
También se utiliza a veces como técnica de ofuscación de código como medida contra el desmontaje y la manipulación. [6] [9]
El principio también se utiliza en secuencias de código compartido de binarios fat que deben ejecutarse en múltiples plataformas de procesadores incompatibles con el conjunto de instrucciones. [nb 1]
Esta propiedad también se utiliza para encontrar instrucciones no deseadas llamadas gadgets en repositorios de código existentes y se utiliza en programación orientada al retorno como alternativa a la inyección de código para exploits como ataques de retorno a libc . [15] [6]
En algunas computadoras, el código de máquina de la arquitectura se implementa mediante una capa subyacente aún más fundamental llamada microcódigo , que proporciona una interfaz de lenguaje de máquina común a lo largo de una línea o familia de diferentes modelos de computadora con flujos de datos subyacentes muy diferentes . Esto se hace para facilitar la transferencia de programas de lenguaje de máquina entre diferentes modelos. Un ejemplo de este uso es la familia de computadoras IBM System/360 y sus sucesores.
El código de máquina es generalmente diferente del código de bytes (también conocido como código p), que es ejecutado por un intérprete o compilado en código de máquina para una ejecución más rápida (directa). Una excepción es cuando un procesador está diseñado para usar un código de bytes particular directamente como su código de máquina, como es el caso de los procesadores Java .
El código de máquina y el código ensamblador a veces se denominan código nativo cuando se hace referencia a partes de características o bibliotecas del lenguaje que dependen de la plataforma. [16]
Desde el punto de vista de la CPU, el código de máquina se almacena en la memoria RAM, pero normalmente también se guarda en un conjunto de cachés por razones de rendimiento. Puede haber diferentes cachés para instrucciones y datos, según la arquitectura.
La CPU sabe qué código de máquina ejecutar, basándose en su contador de programa interno. El contador de programa apunta a una dirección de memoria y se modifica en función de instrucciones especiales que pueden provocar bifurcaciones programáticas. El contador de programa normalmente se establece en un valor codificado de forma fija cuando se enciende la CPU por primera vez y, por lo tanto, ejecutará cualquier código de máquina que se encuentre en esta dirección.
De manera similar, el contador del programa se puede configurar para ejecutar cualquier código de máquina que se encuentre en una dirección arbitraria, incluso si no se trata de un código de máquina válido. Esto normalmente activará una falla de protección específica de la arquitectura.
En un sistema basado en paginación, los permisos de página suelen indicar a la CPU si la página actual contiene realmente código de máquina mediante un bit de ejecución; las páginas tienen varios bits de permiso de este tipo (de lectura, escritura, etc.) para diversas funciones de mantenimiento. Por ejemplo, en sistemas tipo Unix, las páginas de memoria se pueden cambiar para que sean ejecutables con la mprotect()
llamada del sistema y, en Windows, VirtualProtect()
se pueden utilizar para lograr un resultado similar. Si se intenta ejecutar código de máquina en una página no ejecutable, normalmente se producirá un fallo específico de la arquitectura. Tratar los datos como código de máquina o encontrar nuevas formas de utilizar el código de máquina existente mediante diversas técnicas es la base de algunas vulnerabilidades de seguridad.
De manera similar, en un sistema basado en segmentos, los descriptores de segmento pueden indicar si un segmento puede contener código ejecutable y en qué anillos puede ejecutarse ese código.
Desde el punto de vista de un proceso , el espacio de código es la parte de su espacio de direcciones donde se almacena el código en ejecución. En sistemas multitarea , esto comprende el segmento de código del programa y, por lo general, las bibliotecas compartidas . En un entorno multihilo , los diferentes hilos de un proceso comparten el espacio de código junto con el espacio de datos, lo que reduce considerablemente la sobrecarga del cambio de contexto en comparación con el cambio de proceso.
Existen varias herramientas y métodos para decodificar el código de máquina a su código fuente correspondiente .
El código de máquina se puede decodificar fácilmente a su código fuente en lenguaje ensamblador correspondiente porque el lenguaje ensamblador forma una correspondencia uno a uno con el código de máquina. [17] El método de decodificación del lenguaje ensamblador se denomina desensamblaje .
El código de máquina puede decodificarse nuevamente a su lenguaje de alto nivel correspondiente bajo dos condiciones:
La primera condición es aceptar una lectura ofuscada del código fuente. Se muestra una versión ofuscada del código fuente si el código de máquina se envía a un descompilador del lenguaje fuente.
La segunda condición requiere que el código de máquina tenga información sobre el código fuente codificado en su interior. La información incluye una tabla de símbolos que contiene símbolos de depuración . La tabla de símbolos puede estar almacenada dentro del ejecutable o puede existir en archivos separados. Un depurador puede entonces leer la tabla de símbolos para ayudar al programador a depurar de forma interactiva el código de máquina en ejecución .
.pdb
). [22].dSYM
archivo independiente.