Codigo de maquina

Compartir Imprimir Citar
Monitor de lenguaje de la máquina en un ordenador de solabor W65C816S, mostrando desmontaje de código, así como registro de procesadores y vertederos de memoria.

En la programación de computadoras, código de máquina es cualquier lenguaje de programación de bajo nivel, que consta de instrucciones en lenguaje de máquina, que se utilizan para controlar el procesamiento central de una computadora. unidad (CPU). Cada instrucción hace que la CPU realice una tarea muy específica, como una operación de carga, almacenamiento, salto o unidad lógica aritmética (ALU) en una o más unidades de datos en los registros o la memoria de la CPU.

Las primeras CPU tenían un código de máquina específico que podría romper la compatibilidad con versiones anteriores con cada nueva CPU lanzada. La noción de arquitectura de conjunto de instrucciones (ISA) define y especifica el comportamiento y la codificación en la memoria del conjunto de instrucciones del sistema, sin especificar su implementación exacta. Esto actúa como una capa de abstracción, lo que permite la compatibilidad dentro de la misma familia de CPU, de modo que el código de máquina escrito o generado de acuerdo con la ISA para la familia se ejecutará en todas las CPU de la familia, incluidas las CPU futuras.

En general, cada familia de arquitectura (por ejemplo, x86, ARM) tiene su propia ISA y, por lo tanto, su propio lenguaje de código de máquina específico. Hay excepciones, p. el IA-64 puede emular 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. Hay, en algunas CPU, una interfaz de nivel inferior en forma de microcódigo (modificable) que implementa el código de máquina. Sin embargo, el usuario final no debe cambiar el microcódigo en las CPU comerciales normales. El lenguaje ensamblador proporciona un mapeo directo entre el código de máquina numérico y una versión legible por humanos donde los códigos de operación numéricos y los operandos se reemplazan por cadenas legibles (por ejemplo, 0x90 es la instrucción NOP en x86). Si bien es posible escribir programas directamente en código de máquina, administrar bits individuales y calcular direcciones numéricas y constantes manualmente es tedioso y propenso a errores. Por esta razón, los programas rara vez se escriben directamente en código de máquina en contextos modernos, pero se pueden hacer para depuración de bajo nivel, parches de programas (especialmente cuando la fuente del ensamblador no está disponible) y desensamblaje de lenguaje ensamblador.

La mayoría de los programas prácticos actuales están escritos en lenguajes de alto nivel o lenguaje ensamblador. Luego, el código fuente se traduce a código de máquina ejecutable mediante utilidades como compiladores, ensambladores y enlazadores, con la importante excepción de los programas interpretados, que no se traducen a código de máquina. Sin embargo, el propio intérprete, que puede verse como un ejecutor o procesador que ejecuta las instrucciones del código fuente, generalmente consiste en un código de máquina directamente ejecutable (generado a partir de un código fuente ensamblador o de lenguaje de alto nivel).

El código de máquina es, por definición, el nivel más bajo de detalles de programación visible para el programador, pero internamente, muchos procesadores usan microcódigo u optimizan y transforman instrucciones de código de máquina en secuencias de microoperaciones. Esto generalmente no se considera un código de máquina.

Conjunto de instrucciones

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 usan (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 descontinuará o alterará el significado de algún código de instrucción (típicamente porque es necesario para nuevos propósitos), afectando 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 se basa en 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 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 dar el tipo de operando(s).), el(los) modo(s) de direccionamiento, el(los) desplazamiento(s) o índice de direccionamiento, o el propio valor del operando (estos operandos constantes contenidos en una instrucción se denominan inmediatos).

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 suelen carecer 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 tanto implícitos como explícitos, lo que puede permitir una propagación constante más frecuente, plegamiento constante de registros (un registro al que se asigna el resultado de una expresión constante liberada reemplazándolo por esa constante) y otras mejoras de código.

Programas

Un programa de computadora es una lista de instrucciones que puede ejecutar una unidad central de procesamiento (CPU). La ejecución de un programa se realiza para que la CPU que lo está ejecutando resuelva un problema y así lograr un resultado. Mientras que los procesadores simples pueden ejecutar instrucciones una tras otra, los procesadores superescalares pueden, bajo ciertas circunstancias (cuando la tubería está llena), ejecutar dos o más instrucciones simultáneamente. Como ejemplo, el Intel Pentium original de 1993 puede ejecutar como máximo dos instrucciones por ciclo de reloj cuando su tubería está llena.

El flujo del programa puede verse afectado por 'saltos' especiales; instrucciones que transfieren la ejecución a una dirección (y, por lo tanto, a una instrucción) distinta de la siguiente dirección numéricamente secuencial. Que estos saltos condicionales ocurran depende de una condición, como que un valor sea mayor, menor o igual que otro valor.

Idiomas ensambladores

Una interpretación del lenguaje de máquina mucho más amigable para los humanos, llamada lenguaje ensamblador, utiliza códigos mnemotécnicos para referirse a las instrucciones del código de máquina, en lugar de usar las instrucciones' valores numéricos directamente y utiliza nombres simbólicos para referirse a ubicaciones de almacenamiento y, a veces, a registros. Por ejemplo, en el procesador Zilog Z80, el código de máquina 00000101, que hace que la CPU disminuya el registro del procesador B, se representaría en lenguaje ensamblador como DEC B.

Ejemplo

La arquitectura MIPS proporciona un ejemplo específico para un código de máquina cuyas instrucciones siempre tienen una longitud de 32 bits. El tipo general de instrucción viene dado por el campo op (operación), los 6 bits más altos. Las instrucciones de tipo J (salto) y de tipo I (inmediato) están totalmente especificadas por op. Las instrucciones de tipo R (registro) incluyen un campo adicional función para determinar la operación exacta. Los campos utilizados en estos tipos son:

 6 5 5 5 5 6 bits
[O ANTE TERRITOR ANTE TEN ANTE TER ANTE TENShamt Tipo R
[Op.... Tipo I
[Optima dirección de destino] Tipo J

rs, rt y rd indican operandos de registro; shamt da una cantidad de cambio; y los campos dirección o inmediato contienen directamente un operando.

Por ejemplo, sumar los registros 1 y 2 y colocar el resultado en el registro 6 está codificado:

[O ANTE TERRITOR ANTE TEN ANTE TER ANTE TENShamt
0 1 2 6 0 32 decimal
000 000 00001 00010 00110 00000 100000 binario

Cargue 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:

[Op....
35 3 8 68 decimal
100011 00011 01000 00000 00001 000100 binario

Saltando a la dirección 1024:

[Optima dirección de destino]
2 1024 decimal
000010 00000 00000 00000 10000 000 000 binario

Instrucciones superpuestas

En las arquitecturas de procesadores con conjuntos de instrucciones de longitud variable (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 Kruskal Count, a veces es posible a través de la programación a nivel de código de operación. para 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. Estos se denominan instrucciones superpuestas, códigos de operación superpuestos, código superpuesto, código superpuesto, escisión de instrucciones, o saltar a la mitad de una instrucción, y representan una forma de superposición.

En las décadas de 1970 y 1980, a veces se usaban instrucciones superpuestas para conservar espacio en la 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. La técnica rara vez se usa en la actualidad, pero aún podría ser necesario recurrir a ella en áreas donde es necesaria una optimización extrema del tamaño a nivel de byte, como en la implementación de cargadores de arranque que tienen que encajar en los sectores de arranque.

A veces también se usa como una técnica de ofuscación de código como medida contra el desmontaje y la manipulación.

El principio también se utiliza en secuencias de código compartidas de archivos binarios pesados que deben ejecutarse en varias plataformas de procesador incompatibles con el conjunto de instrucciones.

Esta propiedad también se usa para encontrar instrucciones no deseadas denominadas gadgets en repositorios de código existentes y se utiliza en la programación orientada al retorno como alternativa a la inyección de código para vulnerabilidades como los ataques de retorno a libc.

Relación con el microcódigo

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 en 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 en lenguaje de máquina entre diferentes modelos. Un ejemplo de este uso es la familia de computadoras IBM System/360 y sus sucesores. Con anchos de ruta de flujo de datos de 8 bits a 64 bits y más, sin embargo, presentan una arquitectura común a nivel de lenguaje de máquina en toda la línea.

Usar microcódigo para implementar un emulador permite que la computadora presente la arquitectura de una computadora completamente diferente. La línea System/360 usó esto para permitir la transferencia de programas de máquinas anteriores de IBM a la nueva familia de computadoras, p. un emulador IBM 1401/1440/1460 en IBM S/360 modelo 40.

Relación con el código de bytes

El código de máquina generalmente es 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 en 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 refieren a partes de funciones o bibliotecas de lenguaje que dependen de la plataforma.

Almacenamiento en memoria

Desde el punto de vista de la CPU, el código de máquina se almacena en la RAM, pero normalmente también se guarda en un conjunto de cachés por motivos 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, según su contador de programa interno. El contador del programa apunta a una dirección de memoria y se cambia según instrucciones especiales que pueden causar bifurcaciones programáticas. El contador del programa generalmente se establece en un valor codificado cuando la CPU se enciende por primera vez y, por lo tanto, ejecutará cualquier código de máquina que se encuentre en esta dirección.

Del mismo modo, el contador del programa se puede configurar para ejecutar cualquier código de máquina que se encuentre en alguna dirección arbitraria, incluso si no es un código de máquina válido. Por lo general, esto activará una falla de protección específica de la arquitectura.

A menudo, los permisos de página en un sistema basado en paginación informan a la CPU si la página actual realmente contiene código de máquina mediante un bit de ejecución; las páginas tienen múltiples bits de permiso (de lectura, escritura, etc.) para varias funciones de mantenimiento. P.ej. en sistemas similares a Unix, las páginas de memoria se pueden alternar para que sean ejecutables con llamada al sistema mprotect(), y en Windows, VirtualProtect () se puede 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 error específico de la arquitectura. Tratar los datos como código de máquina o encontrar nuevas formas de usar el código de máquina existente mediante diversas técnicas es la base de algunas vulnerabilidades de seguridad.

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 los sistemas multitarea, esto comprende el segmento de código del programa y, por lo general, las bibliotecas compartidas. En un entorno de subprocesos múltiples, diferentes subprocesos de un proceso comparten 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.

Legible por humanos

Pamela Samuelson escribió que el código de máquina es tan ilegible que la Oficina de derechos de autor de los Estados Unidos no puede identificar si un programa codificado en particular es un trabajo original de autoría; sin embargo, la Oficina de derechos de autor de EE. UU. permite el registro de derechos de autor de programas informáticos y, a veces, el código de máquina de un programa se puede descompilar para que su funcionamiento sea más comprensible para los humanos. Sin embargo, a la salida de un descompilador o desensamblador le faltarán los comentarios y las referencias simbólicas, por lo que, si bien la salida puede ser más fácil de leer que el código objeto, seguirá siendo más difícil que el código fuente original. Este problema no existe para formatos de código objeto como SQUOZE, donde el código fuente está incluido en el archivo.

El profesor de ciencias cognitivas Douglas Hofstadter comparó el código de máquina con el código genético y dijo que "mirar un programa escrito en lenguaje de máquina es vagamente comparable a mirar una molécula de ADN átomo por átomo".