Articulo de referencia

lenguaje ensamblador x86

El lenguaje ensamblador x86 es una familia de lenguajes de programación de bajo nivel que se utilizan para generar código objeto para la clase de procesadores x86 . Las evolucio...

El lenguaje ensamblador x86 es una familia de lenguajes de programación de bajo nivel que se utilizan para generar código objeto para la clase de procesadores x86 . Las evoluciones anteriores de esta familia de lenguajes ofrecen compatibilidad con versiones anteriores de CPU que se remontan al microprocesador Intel 8008 , presentado en abril de 1972. [ 1 ] [ 2 ] Como lenguajes ensambladores , están estrechamente vinculados a las instrucciones de código máquina de la arquitectura, lo que permite un control preciso sobre el hardware.

En los lenguajes ensamblador x86, se utilizan mnemónicos para representar las instrucciones fundamentales de la CPU, lo que hace que el código sea más legible para los humanos en comparación con el código máquina sin procesar . Cada instrucción de código máquina es un código de operación que, en ensamblador, se reemplaza con un mnemónico. [ 3 ] Cada mnemónico corresponde a una operación básica realizada por el procesador, como cálculos aritméticos, movimiento de datos o decisiones de flujo de control. Los lenguajes ensamblador se utilizan con mayor frecuencia en aplicaciones donde el rendimiento y la eficiencia son críticos. Esto incluye sistemas embebidos en tiempo real , núcleos de sistemas operativos y controladores de dispositivos , todos los cuales pueden requerir la manipulación directa de los recursos de hardware.

Además, los compiladores de lenguajes de programación de alto nivel a veces generan código ensamblador como paso intermedio durante el proceso de compilación. Esto permite optimizar el código ensamblador antes de generar el código máquina final que ejecuta el procesador.

Mnemotecnias y códigos de operación

Cada instrucción en el lenguaje ensamblador x86 se representa mediante un mnemónico que, a menudo, se combina con uno o más operandos para traducirse en uno o más bytes conocidos como opcode . Por ejemplo, la instrucción NOP se traduce al opcode 0x90, y la instrucción HLT se traduce a 0xF4. [ 3 ] Existen posibles opcodes sin mnemónicos documentados, que distintos procesadores pueden interpretar de forma diferente. El uso de dichos opcodes puede provocar que un programa se comporte de forma inconsistente o incluso genere excepciones en algunos procesadores.

Sintaxis

El lenguaje ensamblador x86 tiene dos ramas de sintaxis principales : la sintaxis Intel y la sintaxis AT & T . [ 4 ] La sintaxis Intel predomina en los entornos DOS y Windows , mientras que la sintaxis AT & T predomina en sistemas tipo Unix , ya que Unix se desarrolló originalmente en los laboratorios Bell de AT&T . [ 5 ] A continuación se presenta un resumen de las principales diferencias entre la sintaxis Intel y la sintaxis AT & T :

Muchos ensambladores x86 utilizan la sintaxis de Intel , incluyendo FASM , MASM , NASM , TASM y YASM . El ensamblador GNU , que originalmente utilizaba la sintaxis de AT & T , ha admitido ambas sintaxis desde la versión 2.10 mediante la .intel_syntaxdirectiva. [ 4 ] [ 6 ] [ 7 ] Una peculiaridad de la sintaxis de AT & T para x86 es que los operandos de punto flotante de x87 están invertidos, un error heredado del ensamblador AT&T original. [ 8 ]

La sintaxis de AT&T es prácticamente universal en otras arquitecturas (manteniendo el mismo orden de operandos para la movinstrucción); fue diseñada originalmente para el lenguaje ensamblador PDP-11 y se heredó en sistemas tipo Unix . En contraste, la sintaxis de Intel es específica de la arquitectura x86 y es la que se utiliza en la documentación oficial de la plataforma x86. El Intel 8080 , anterior a la arquitectura x86, también utiliza el orden "destino primero" para movlas instrucciones. [ 9 ]

Palabras reservadas

En la mayoría de los lenguajes ensamblador x86, las palabras reservadas constan de dos partes: mnemónicos que se traducen a códigos de operación y directivas (o "pseudooperaciones") que acceden a características del programa ensamblador más allá de la simple traducción de códigos de operación. Para ver una lista de la primera parte, consulte los listados de instrucciones x86 . La segunda parte depende en gran medida del ensamblador, ya que no existe un estándar entre los ensambladores con sintaxis Intel. [ 10 ] Los ensambladores con sintaxis AT&T comparten una forma común de nombrar las directivas (todas las directivas comienzan con un punto, como .ascii), [ 11 ] y varias directivas básicas como .asciiy .stringson ampliamente compatibles. [ 12 ] [ 13 ]

Registros

Los procesadores x86 cuentan con un conjunto de registros que sirven como almacenamiento para datos binarios y direcciones durante la ejecución del programa. Estos registros se clasifican en registros de propósito general , registros de segmento, el puntero de instrucción, el registro FLAGS y varios registros de extensión introducidos en modelos de procesadores posteriores. Cada registro tiene funciones específicas además de sus capacidades generales: [ 3 ]

Registros de uso general

Estos registros tienen funciones convencionales, pero su uso no está estrictamente regulado. Por lo general, los programas pueden utilizarlos para otros fines.

  • AX (Registro acumulador): Se utiliza principalmente en operaciones aritméticas, lógicas y de transferencia de datos. Es el preferido por las instrucciones que realizan multiplicaciones y divisiones, así como por las operaciones de carga y almacenamiento de cadenas. Las operaciones inmediatas de la ALU y los intercambios con AX se pueden codificar de forma más compacta.
  • BX (Registro base): Puntero base para acceso a memoria. Puede contener la dirección base de las estructuras de datos y es útil en modos de direccionamiento indexado. Se utiliza con XLAT.
  • CX (Registro de conteo): Funciona como contador en instrucciones de bucle, cadena y desplazamiento/rotación. Las operaciones iterativas suelen usar CX para determinar cuántas veces debe ejecutarse un bucle u operación.
  • DX (Registro de datos): Se utiliza junto con AX para operaciones de multiplicación y división que producen resultados mayores de 16 bits. También almacena direcciones de puertos de E/S para INinstrucciones OUT.
  • SP (puntero de pila): apunta a la parte superior de la pila en la memoria. Se actualiza automáticamente durante PUSHlas POPoperaciones.
  • BP (Puntero Base): Apunta a la parte superior de la pila de llamadas . Se utiliza principalmente para acceder a los parámetros de las funciones y a las variables locales dentro de la pila de llamadas.
  • SI (Índice de origen): Se utiliza como puntero al origen en operaciones con cadenas y matrices de memoria. Instrucciones como MOVS(mover cadena) utilizan SI para leer datos de la memoria. Al igual que BX, se puede utilizar para la indexación. Se puede añadir a BP o BX para la doble indexación.
  • DI (Índice de Destino): Sirve como puntero al destino en operaciones con cadenas y matrices de memoria. Funciona junto con SI en instrucciones que copian o comparan datos, escribiendo los resultados en la memoria. Al igual que BX, se puede usar para indexación. Se puede agregar a BP o BX para indexación doble.

Junto con los registros generales, también existen los siguientes:

  • Puntero de instrucción (IP): Almacena la dirección de desplazamiento de la siguiente instrucción que se ejecutará dentro del segmento de código (CS). Apunta al primer byte de la siguiente instrucción. Si bien los programadores no pueden leer directamente el registro IP, su valor cambia mediante instrucciones de control de flujo, como saltos, llamadas e interrupciones, que modifican el flujo de ejecución.
  • Registro FLAGS : Contiene un conjunto de indicadores de estado, control y sistema que reflejan el resultado de las operaciones y controlan las operaciones del procesador.
  • Registros de segmento (CS, DS, ES, SS): Determinan dónde comienza un segmento de 64k (FS y GS se agregaron al 80386 y posteriores).
  • Registros de extensión adicionales ( MMX , 3DNow!, SSE , etc.) (solo para Pentium y posteriores).

La mayoría de las instrucciones pueden usar los registros x86. Por ejemplo, en la sintaxis de Intel:

mov ax , 1234h ; copia el valor 1234hex (4660d) en el registro AX.
mov bx , ax ; copia el valor del registro AX en el registro BX.

Direccionamiento segmentado

La arquitectura x86, tanto en modo 8086 real como virtual, utiliza un proceso conocido como segmentación para direccionar la memoria, en lugar del modelo de memoria plana empleado en muchos otros entornos. La segmentación consiste en componer una dirección de memoria a partir de dos partes: un segmento y un desplazamiento . El segmento apunta al inicio de un grupo de direcciones de 64 KiB (64 × 2¹⁰ ) y el desplazamiento determina la distancia a la que se encuentra la dirección deseada con respecto a esta dirección inicial. En el direccionamiento segmentado, se requieren dos registros para una dirección de memoria completa: uno para almacenar el segmento y otro para almacenar el desplazamiento. Para convertirla de nuevo en una dirección plana, el valor del segmento se desplaza cuatro bits a la izquierda (equivalente a multiplicarlo por 2¹⁶ o 16) y se suma al desplazamiento para formar la dirección completa. Esto permite superar la barrera de los 64k mediante una selección inteligente de direcciones, aunque complica considerablemente la programación.

En modo real /protegido únicamente, por ejemplo, si DS contiene el número hexadecimal 0xDEAD y DX contiene el número 0xCAFE, ambos apuntarían a la dirección de memoria . Por lo tanto, la CPU puede direccionar hasta 1.048.576 bytes (1 MiB) en modo real. Al combinar los valores de segmento y desplazamiento , obtenemos una dirección de 20 bits.0xDEAD * 0x10 + 0xCAFE == 0xEB5CE

El IBM PC original limitaba los programas a 640 KB, pero se utilizó una especificación de memoria ampliada para implementar un esquema de conmutación de bancos que cayó en desuso cuando los sistemas operativos posteriores, como Windows, utilizaron los rangos de direcciones más amplios de los procesadores más nuevos e implementaron sus propios esquemas de memoria virtual.

El modo protegido, a partir del Intel 80286, fue utilizado por OS/2 . Varias deficiencias, como la imposibilidad de acceder a la BIOS y la imposibilidad de volver al modo real sin reiniciar el procesador, impidieron su uso generalizado. [ 14 ] El 80286 también seguía limitado a direccionar la memoria en segmentos de 16 bits, lo que significaba que solo se podía acceder a 2 16 bytes (64 kilobytes ) a la vez. Para acceder a la funcionalidad extendida del 80286, el sistema operativo ponía el procesador en modo protegido, lo que permitía el direccionamiento de 24 bits y, por lo tanto, 2 24 bytes de memoria (16 megabytes ).

En modo protegido , el selector de segmento se puede dividir en tres partes: un índice de 13 bits, un bit de indicador de tabla que determina si la entrada está en la GDT o la LDT y un nivel de privilegio solicitado de 2 bits ; consulte la segmentación de memoria x86 .

Cuando se hace referencia a una dirección con un segmento y un desplazamiento, se utiliza la notación segmento:desplazamiento , por lo que en el ejemplo anterior la dirección plana 0xEB5CE se puede escribir como 0xDEAD:0xCAFE o como un par de registros de segmento y desplazamiento; DS:DX.

Existen algunas combinaciones especiales de registros de segmento y registros generales que apuntan a direcciones importantes:

  • CS:IP (CS es segmento de código , IP es puntero de instrucción ) apunta a la dirección donde el procesador obtendrá el siguiente byte de código.
  • SS:SP (SS es Segmento de pila , SP es Puntero de pila ) apunta a la dirección de la parte superior de la pila, es decir, el byte insertado más recientemente.
  • SS:BP (SS es Segmento de pila , BP es Puntero de marco de pila ) apunta a la dirección de la parte superior del marco de pila, es decir, la base del área de datos en la pila de llamadas para el subprograma actualmente activo.
  • DS:SI (DS significa Segmento de datos , SI significa Índice de origen ) se usa a menudo para señalar datos de cadena que están a punto de copiarse a ES:DI.
  • ES:DI (ES significa Segmento Extra , DI significa Índice de Destino ) se utiliza normalmente para indicar el destino de una copia de cadena, como se mencionó anteriormente.

El Intel 80386 contaba con tres modos de funcionamiento: modo real, modo protegido y modo virtual. El modo protegido , que debutó en el 80286, se amplió para permitir que el 80386 direccionara hasta 4 GB de memoria. El nuevo modo virtual 8086 ( VM86 ) posibilitó la ejecución de uno o más programas en modo real en un entorno protegido que emulaba en gran medida el modo real, aunque algunos programas no eran compatibles (normalmente debido a trucos de direccionamiento de memoria o al uso de códigos de operación no especificados).

El modelo de memoria plana de 32 bits del modo protegido extendido del 80386 puede ser el cambio de características más importante para la familia de procesadores x86 hasta que AMD lanzó el x86-64 en 2003, ya que ayudó a impulsar la adopción a gran escala de Windows 3.1 (que dependía del modo protegido), puesto que Windows ahora podía ejecutar muchas aplicaciones a la vez, incluidas aplicaciones de DOS, mediante el uso de memoria virtual y multitarea simple.

Modos de ejecución

Los procesadores x86 admiten cinco modos de operación para el código x86: Modo Real , Modo Protegido , Modo Largo , Modo Virtual 86 y Modo de Gestión del Sistema , en los que algunas instrucciones están disponibles y otras no. Un subconjunto de instrucciones de 16 bits está disponible en los procesadores x86 de 16 bits, que son el 8086, 8088, 80186, 80188 y 80286. Estas instrucciones están disponibles en el modo real en todos los procesadores x86, y en el modo protegido de 16 bits ( a partir del 80286 ), están disponibles instrucciones adicionales relacionadas con el modo protegido. En el 80386 y posteriores, las instrucciones de 32 bits (incluidas las extensiones posteriores) también están disponibles en todos los modos, incluido el modo real; en estas CPU, se agregan el modo V86 y el modo protegido de 32 bits, con instrucciones adicionales proporcionadas en estos modos para administrar sus características. SMM, con algunas instrucciones especiales propias, está disponible en algunos procesadores Intel i386SL, i486 y posteriores. Finalmente, en modo largo (AMD Opteron en adelante), también se dispone de instrucciones de 64 bits y más registros. El conjunto de instrucciones es similar en ambos modos, pero el direccionamiento de memoria y el tamaño de palabra varían, lo que requiere diferentes estrategias de programación.

Los modos en los que se puede ejecutar el código x86 son:

  • Modo real (16 bits)
  • Modo protegido (16 bits y 32 bits)
    • Amplía la memoria física direccionable a 16 MB y la memoria virtual direccionable a 1 GB . Proporciona niveles de privilegio y memoria protegida , lo que impide que los programas se corrompan entre sí. El modo protegido de 16 bits (utilizado al final de la era DOS ) empleaba un modelo de memoria complejo y multisegmentado. El modo protegido de 32 bits utiliza un modelo de memoria simple y plano.
  • Modo largo (64 bits)
    • En su mayoría una extensión del conjunto de instrucciones de 32 bits (modo protegido), pero a diferencia de la transición de 16 a 32 bits, muchas instrucciones se eliminaron en el modo de 64 bits. Desarrollado por AMD .
  • Modo virtual 8086 (16 bits)
    • Un modo de funcionamiento híbrido especial que permite que los programas y sistemas operativos en modo real se ejecuten bajo el control de un sistema operativo supervisor en modo protegido.
  • Modo de administración del sistema (16 bits)
    • Gestiona funciones de todo el sistema, como la administración de energía, el control del hardware y el código propietario diseñado por el fabricante. Su uso está destinado únicamente al firmware del sistema. Toda la ejecución normal, incluido el sistema operativo , se suspende. A continuación, se ejecuta un sistema de software alternativo (que suele residir en el firmware del ordenador o en un depurador asistido por hardware ) con altos privilegios.

Cambio de modos

El procesador funciona en modo real inmediatamente después de encenderse, por lo que el núcleo del sistema operativo u otro programa debe cambiar explícitamente a otro modo si desea ejecutarse en un modo distinto al real. El cambio de modo se realiza modificando ciertos bits de los registros de control del procesador tras una preparación previa, y puede ser necesaria una configuración adicional después del cambio.

Ejemplos

En un ordenador con BIOS heredada , la BIOS y el gestor de arranque funcionan en modo real . El núcleo del sistema operativo de 64 bits comprueba y cambia la CPU al modo largo y, a continuación, inicia nuevos subprocesos en modo núcleo que ejecutan código de 64 bits.

En un ordenador que ejecuta UEFI , el firmware UEFI (excepto CSM y la ROM de opciones heredada), el cargador de arranque UEFI y el núcleo del sistema operativo UEFI se ejecutan en modo largo.

Tipos de instrucciones

En general, las características del conjunto de instrucciones x86 moderno son:

  • Una codificación compacta
    • Longitud variable e independiente de la alineación (codificada como little endian , como todos los datos en la arquitectura x86).
    • Principalmente instrucciones de una o dos direcciones, es decir, el primer operando es también el destino.
    • Se admiten operandos de memoria tanto como origen como destino (se utilizan frecuentemente para leer/escribir elementos de la pila a los que se accede mediante pequeños desplazamientos inmediatos).
    • Uso de registros tanto general como implícito ; aunque los siete (contando ebp) registros generales en modo de 32 bits y los quince (contando rbp) registros generales en modo de 64 bits pueden usarse libremente como acumuladores o para direccionamiento, la mayoría de ellos también son utilizados implícitamente por ciertas instrucciones (más o menos) especiales; por lo tanto, los registros afectados deben conservarse temporalmente (normalmente apilados) si están activos durante dichas secuencias de instrucciones.
  • Genera indicadores condicionales de forma implícita a través de la mayoría de las instrucciones ALU de enteros .
  • Admite varios modos de direccionamiento, incluidos el inmediato, el de desplazamiento y el de índice escalado, pero no el relativo al PC, excepto los saltos (introducidos como una mejora en la arquitectura x86-64 ).
  • Incluye punto flotante a una pila de registros.
  • Contiene soporte especial para instrucciones atómicas de lectura-modificación-escrituraxchg ( , cmpxchg/ cmpxchg8b, xadd, e instrucciones enteras que se combinan con el lockprefijo)
  • Instrucciones SIMD (instrucciones que realizan instrucciones individuales simultáneas en paralelo sobre muchos operandos codificados en celdas adyacentes de registros más amplios).

Instrucciones de pila

La arquitectura x86 cuenta con soporte de hardware para un mecanismo de pila de ejecución . Instrucciones como push, pop, cally retse utilizan con la pila configurada correctamente para pasar parámetros, asignar espacio para datos locales y guardar y restaurar puntos de retorno de llamada. La instrucción retsize es muy útil para implementar convenciones de llamada eficientes en espacio (y rápidas), donde la función llamada es responsable de recuperar el espacio de la pila ocupado por los parámetros.

Al configurar un marco de pila para almacenar datos locales de un procedimiento recursivo , existen varias opciones; la enterinstrucción de alto nivel (introducida con el 80186) toma un argumento de profundidad de anidamiento del procedimiento , así como un argumento de tamaño local , y puede ser más rápida que la manipulación más explícita de los registros (como push bp ; mov bp, sp ; ). Si es más rápida o más lenta depende de la implementación particular del procesador x86, así como de la convención de llamada utilizada por el compilador, el programador o el código del programa en particular; la mayoría del código x86 está diseñado para ejecutarse en procesadores x86 de varios fabricantes y en diferentes generaciones tecnológicas de procesadores, lo que implica microarquitecturas y soluciones de microcódigo muy variables , así como diferentes opciones de diseño a nivel de puerta y transistor .sub sp, size

La gama completa de modos de direccionamiento (incluidos inmediato y base+desplazamiento ), incluso para instrucciones como pushy pop, hace que el uso directo de la pila para datos enteros , de punto flotante y de direcciones sea sencillo, además de mantener las especificaciones y mecanismos ABI relativamente simples en comparación con algunas arquitecturas RISC (que requieren detalles de pila de llamadas más explícitos).

Instrucciones de la ALU de enteros

El lenguaje ensamblador x86 tiene las operaciones matemáticas estándar, add, sub, neg, imuly idiv(para enteros con signo), con muly div(para enteros sin signo); los operadores lógicosand , or, xor, not; aritmética y lógica de desplazamiento de bitssal , / sar(para enteros con signo), shl/ shr(para enteros sin signo); rotación con y sin acarreo, rcl/ rcr, rol/ ror, y un complemento de instrucciones aritméticas BCDaaa , , aad, daa. Instrucciones como cmpy testestablecen los indicadores sin alterar los operandos.

Instrucciones de punto flotante

El lenguaje ensamblador x86 incluye instrucciones para una unidad de punto flotante (FPU) basada en pila . La FPU era un coprocesador independiente opcional para los procesadores 8086 a 80386, una opción integrada en el chip para la serie 80486 y una característica estándar en todas las CPU Intel x86 desde el 80486, comenzando con el Pentium. Las instrucciones de la FPU incluyen suma, resta, negación, multiplicación, división, resto, raíces cuadradas, truncamiento de enteros, truncamiento de fracciones y escalado por potencia de dos. Las operaciones también incluyen instrucciones de conversión, que pueden cargar o almacenar un valor de la memoria en cualquiera de los siguientes formatos: decimal codificado en binario, entero de 32 bits, entero de 64 bits, punto flotante de 32 bits, punto flotante de 64 bits o punto flotante de 80 bits (al cargarlo, el valor se convierte al modo de punto flotante que se esté utilizando). x86 también incluye una serie de funciones trascendentales , entre ellas seno, coseno, tangente, arcotangente, exponenciación con base 2 y logaritmos en bases 2, 10 o e .

El formato de registro de pila a registro de pila de las instrucciones suele ser o , donde es equivalente a , y es uno de los 8 registros de pila ( , , ..., ). Al igual que los enteros, el primer operando es tanto el primer operando fuente como el operando destino. y deben destacarse como los primeros en intercambiar los operandos fuente antes de realizar la resta o la división. Las instrucciones de suma, resta, multiplicación, división, almacenamiento y comparación incluyen modos de instrucción que extraen la parte superior de la pila una vez completada su operación. Así, por ejemplo, realiza el cálculo , luego elimina de la parte superior de la pila, haciendo que lo que era el resultado en la parte superior de la pila en .fop st, st(n)fop st(n), ststst(0)st(n)st(0)st(1)st(7)fsubrfdivrfaddp st(1), stst(1) = st(1) + st(0)st(0)st(1)st(0)

Instrucciones SIMD

Las CPU x86 modernas contienen instrucciones SIMD , que en gran medida realizan la misma operación en paralelo sobre muchos valores codificados en un amplio registro SIMD. Varias tecnologías de instrucciones admiten diferentes operaciones en diferentes conjuntos de registros, pero tomadas en su conjunto (desde MMX hasta SSE4.2 ) incluyen cálculos generales en aritmética de enteros o de punto flotante (suma, resta, multiplicación, desplazamiento, minimización, maximización, comparación, división o raíz cuadrada). Así, por ejemplo, paddw mm0, mm1realiza 4 sumas paralelas de enteros de 16 bits (indicadas por w) (indicadas por padd) de mm0valores a mm1y almacena el resultado en mm0. Streaming SIMD Extensions o SSE también incluye un modo de punto flotante en el que solo se modifica el primer valor de los registros (ampliado en SSE2 ). Se han añadido otras instrucciones inusuales, incluida una suma de diferencias absolutas (utilizada para la estimación de movimiento en compresión de vídeo , como se hace en MPEG ) y una instrucción de acumulación de multiplicación de 16 bits (útil para la mezcla alfa basada en software y el filtrado digital ). Las extensiones SSE (desde SSE3 ) y 3DNow! incluyen instrucciones de suma y resta para tratar pares de valores de punto flotante como números complejos.

Estos conjuntos de instrucciones también incluyen numerosas instrucciones de subpalabra fija para reorganizar, insertar y extraer valores dentro de los registros. Además, existen instrucciones para transferir datos entre los registros de enteros y los registros XMM (utilizados en SSE)/FPU (utilizados en MMX).

Instrucciones de memoria

El procesador x86 también incluye modos de direccionamiento complejos para acceder a la memoria mediante un desplazamiento inmediato, un registro, un registro con desplazamiento, un registro escalado con o sin desplazamiento, y un registro con desplazamiento opcional y otro registro escalado. Por ejemplo, se puede codificar mov eax, [Table + ebx + esi*4]una instrucción que carga 32 bits de datos desde la dirección calculada como (Table + ebx + esi * 4)desplazamiento desde el dsselector y los almacena en el eaxregistro. En general, los procesadores x86 pueden cargar y usar memoria del tamaño de cualquier registro con el que estén operando. (Las instrucciones SIMD también incluyen instrucciones de carga parcial).

La mayoría de las instrucciones x86 de 2 operandos, incluidas las instrucciones ALU de enteros, utilizan un " byte de modo de direccionamiento " estándar [ 15 ] a menudo llamado byte MOD-REG-R/M . [ 16 ] [ 17 ] [ 18 ] Muchas instrucciones x86 de 32 bits también tienen un byte de modo de direccionamiento SIB que sigue al byte MOD-REG-R/M. [ 19 ] [ 20 ] [ 21 ] [ 22 ] [ 23 ]

En principio, dado que el código de operación de la instrucción está separado del byte de modo de direccionamiento, dichas instrucciones son ortogonales , ya que cualquiera de esos códigos de operación puede combinarse con cualquier modo de direccionamiento. Sin embargo, el conjunto de instrucciones x86 generalmente se considera no ortogonal porque la mayoría de las operaciones diádicas no pueden operar de memoria a memoria, otros códigos de operación tienen un modo de direccionamiento fijo (no tienen byte de modo de direccionamiento) y cada registro tiene un uso preferido. [ 23 ] [ 24 ]

El conjunto de instrucciones x86 incluye instrucciones de carga, almacenamiento, movimiento, escaneo y comparación de cadenas ( lods, stos, movs, scasy cmps) que realizan cada operación a un tamaño especificado ( bpara byte de 8 bits, wpara palabra de 16 bits, dpara palabra doble de 32 bits) y luego incrementan/decrementan (dependiendo de DF, indicador de dirección) el registro de dirección implícito ( sipara lods, dipara stosy scas, y ambos para movsy cmps). Para las operaciones de carga, almacenamiento y escaneo, el registro implícito de destino/origen/comparación está en el registro al, axo eax(dependiendo del tamaño). Los registros de segmento implícitos utilizados son dspara siy espara di. El registro cxo ecxse utiliza como un contador decremental, y la operación se detiene cuando el contador llega a cero o, para escaneos y comparaciones, cuando se detecta igualdad o desigualdad. Desafortunadamente, con los años el rendimiento de algunas de estas instrucciones se ha descuidado y en ciertos casos es posible obtener resultados más rápidos codificando con instrucciones más elementales. Sin embargo, Intel y AMD han actualizado algunas de las instrucciones, y a partir de 2025Algunos tienen un desempeño muy respetable.

La pila es una región de memoria y un puntero de pila asociado , que apunta al último elemento insertado en la pila. El puntero de pila se decrementa antes de agregar elementos, pushy se incrementa después de eliminarlos pop. En modo de 16 bits, este puntero de pila implícito se direcciona como SS:[SP], en modo de 32 bits es SS:[ESP], y en modo de 64 bits es [RSP]. El puntero de pila apunta al último valor que se almacenó, bajo el supuesto de que su tamaño coincidirá con el modo de operación del procesador (es decir, 16, 32 o 64 bits) para coincidir con el ancho predeterminado de las instrucciones push/ pop/ call/ ret. También se incluyen las instrucciones entery leaveque reservan y eliminan datos de la parte superior de la pila mientras configuran un puntero de marco de pila en bp/ ebp/ rbp. Sin embargo, también se admite la configuración directa, o la suma y resta al registro sp/ esp/ , por lo que las instrucciones / son generalmente innecesarias.rspenterleave

Este código es el comienzo de una función típica de un lenguaje de alto nivel cuando la optimización del compilador está desactivada para facilitar la depuración:

push rbp ; Guarda el puntero del marco de pila de la función que llama (registro rbp) mov rbp , rsp ; Crea un nuevo marco de pila debajo de la pila de nuestra función que llama sub rsp , 32 ; Reserva 32 bytes de espacio de pila para las variables locales de esta función. ; Las variables locales estarán debajo de rbp y se pueden referenciar en relación con rbp, ; nuevamente, lo mejor para facilitar la depuración, pero para un mejor rendimiento, rbp no ; se utilizará en absoluto, y las variables locales se referenciarían en relación con rsp ; porque, aparte del ahorro de código, rbp queda libre para otros usos. ; Sin embargo, si rbp se modifica aquí, su valor debe conservarse para la función que llama. mov [ rbp - 8 ], rdx ; Ejemplo de escritura en una variable local (por su ubicación de memoria) desde el registro rdx

...es funcionalmente equivalente a simplemente:

ingrese 32 , 0

Otras instrucciones para manipular la pila incluyen pushfd(32 bits) / pushfq(64 bits) y popfd/popfqpara almacenar y recuperar el registro EFLAGS (32 bits) / RFLAGS (64 bits).

Se asume que los valores para una carga o almacenamiento SIMD están empaquetados en posiciones adyacentes para el registro SIMD y se alinean en orden little-endian secuencial. Algunas instrucciones de carga y almacenamiento SSE requieren una alineación de 16 bytes para funcionar correctamente. Los conjuntos de instrucciones SIMD también incluyen instrucciones de "precarga" que realizan la carga pero no se dirigen a ningún registro, utilizado para la carga de la caché. Los conjuntos de instrucciones SSE también incluyen instrucciones de almacenamiento no temporal que almacenan directamente en memoria sin realizar una asignación de caché si el destino no está ya en caché (de lo contrario, se comporta como un almacenamiento normal).

La mayoría de las instrucciones genéricas de enteros y de punto flotante (pero no las SIMD) pueden usar una dirección compleja como segundo parámetro de origen. Las instrucciones de enteros también pueden aceptar un parámetro de memoria como operando de destino.

Flujo del programa

El ensamblador x86 tiene una operación de salto incondicional, jmpque puede tomar una dirección inmediata, un registro o una dirección indirecta como parámetro (tenga en cuenta que la mayoría de los procesadores RISC solo admiten un registro de enlace o un desplazamiento inmediato corto para el salto).

También se admiten varios saltos condicionales, incluyendo jz(salto en cero), jnz(salto en distinto de cero), jg(salto en mayor que, con signo), jl(salto en menor que, con signo), ja(salto en superior/mayor que, sin signo), jb(salto en inferior/menor que, sin signo). Estas operaciones condicionales se basan en el estado de bits específicos en el registro (E)FLAGS . Muchas operaciones aritméticas y lógicas establecen, borran o complementan estos indicadores según su resultado. Las instrucciones de comparación cmp(compare) y testestablecen los indicadores como si hubieran realizado una resta o una operación AND bit a bit, respectivamente, sin alterar los valores de los operandos. También hay instrucciones como clc(borrar indicador de acarreo) y cmc(complementar indicador de acarreo) que trabajan directamente sobre los indicadores. Las comparaciones de punto flotante se realizan mediante instrucciones fcomor ficomque eventualmente deben convertirse a indicadores enteros.

Cada operación de salto tiene tres formas distintas, según el tamaño del operando. Un salto corto utiliza un operando con signo de 8 bits, que es un desplazamiento relativo desde la instrucción actual. Un salto cercano es similar a un salto corto, pero utiliza un operando con signo de 16 bits (en modo real o protegido) o un operando con signo de 32 bits (solo en modo protegido de 32 bits). Un salto lejano utiliza el valor completo de la base del segmento y el desplazamiento como dirección absoluta. También existen formas indirectas e indexadas de cada una de estas operaciones.

Además de las operaciones de salto simples, existen las instrucciones call(llamar a una subrutina) y ret(regresar de una subrutina). Antes de transferir el control a la subrutina, callse coloca en la pila la dirección de desplazamiento de la instrucción siguiente call; retse extrae este valor de la pila y se salta a él, devolviendo efectivamente el flujo de control a esa parte del programa. En el caso de una far call, se coloca la base del segmento seguida del desplazamiento; far retse extrae el desplazamiento y luego la base del segmento para regresar.

También hay dos instrucciones similares, int( interrupción ), que guarda el valor actual del registro (E)FLAGSfar call en la pila, luego realiza una , excepto que en lugar de una dirección, usa un vector de interrupción , un índice en una tabla de direcciones de manejadores de interrupción. Normalmente, el manejador de interrupción guarda todos los demás registros de la CPU que usa, a menos que se usen para devolver el resultado de una operación al programa que llama (en software llamado interrupciones). La instrucción de retorno de interrupción correspondiente es iret, que restaura las banderas después de regresar. Las interrupciones suaves del tipo descrito anteriormente son usadas por algunos sistemas operativos para llamadas al sistema , y ​​también se pueden usar en la depuración de manejadores de interrupción duros. Las interrupciones duras se activan por eventos de hardware externos y deben preservar todos los valores de registro ya que se desconoce el estado del programa que se está ejecutando actualmente. En el modo protegido, el sistema operativo puede configurar interrupciones para activar un cambio de tarea, que guardará automáticamente todos los registros de la tarea activa.

Ejemplos

Los siguientes ejemplos utilizan la denominada sintaxis Intel , empleada por los ensambladores Microsoft MASM, NASM y muchos otros. (Nota: También existe una sintaxis alternativa AT&T en la que se intercambia el orden de los operandos de origen y destino, entre otras muchas diferencias). [ 25 ]

Programa "¡Hola, mundo!" sin sistema operativo.

El mensaje "¡Hola, mundo!" puede generarse con poca ayuda del sistema operativo. La función "call outchr" invoca un mecanismo que imprime un carácter en AL en la consola. Una cadena de caracteres distinta de cero debe terminar con un byte cero. Tenga en cuenta que este ejemplo es para un procesador Intel 8086 de 16 bits.

hola: mov si , msg ; dirección de la cadena en SI cld ; dirección clara para incrementar SI lodsb ; cargar el primer carácter en AL, inc SI chrlp: call outchr ; imprimir carácter en AL lodsb ; cargar el siguiente carácter en AL, inc SI or al , al ; ¿es un terminador cero? jnz chrlp ; si no, continuar ret ; regresar a la función que llamómsg: db '¡Hola, mundo!' , 0xa , 0x0 ; cadena que se imprimirá

Programa "¡Hola mundo!" para MS-DOS en lenguaje ensamblador estilo MASM.

Se utiliza la instrucción de interrupción de software 21h para llamar al sistema operativo MS-DOS y mostrar la salida en la pantalla  ; otros ejemplos utilizan la rutina printf() de libc para escribir en la salida estándar . Tenga en cuenta que este ejemplo utiliza el modo de 16 bits, como en un Intel 8086. El siguiente ejemplo es código para Intel 386 en modo de 32 bits. El código moderno estará en modo de 64 bits. [ 26 ]

.modelo pequeño .pila 100h.data msg db '¡Hola mundo!$'.code start: mov ah , 09h ; Establece el registro de 8 bits 'ah', el byte alto del registro ax, a 9, para ; seleccionar un número de subfunción de una rutina de MS-DOS llamada a continuación ; a través de la interrupción de software int 21h para mostrar un mensaje lea dx , msg ; Toma la dirección de msg, almacena la dirección en el registro de 16 bits dx int 21h ; Varias rutinas de MS-DOS se pueden llamar mediante la interrupción de software 21h ; Nuestra subfunción requerida se estableció en el registro ah anteriormov ax , 4C00h ; Establece el registro ax al número de subfunción para la interrupción de software de MS-DOS ; int 21h para el servicio 'terminar programa'. int 21h ; Llamar a este servicio de MS-DOS nunca regresa, ya que finaliza el programa.fin inicio

Programa "¡Hola mundo!" para Windows escrito en lenguaje ensamblador estilo MASM y NASM.

Programa "¡Hola mundo!" para Linux en lenguaje ensamblador de AT&T y NASM.

Nota para NASM:

; Este programa se ejecuta en modo protegido de 32 bits. ; compilación: nasm -f elf -F stabs name.asm ; enlace: ld -o name name.o ; ; En modo largo de 64 bits puede usar registros de 64 bits (por ejemplo, rax en lugar de eax, rbx en lugar de ebx, etc.) ; También cambie "-f elf " por "-f elf64" en el comando de compilación. ; Para el modo largo de 64 bits, "lea rcx, str" sería la dirección del mensaje, tenga en cuenta el registro de 64 bits rcx.

Programa "¡Hola mundo!" para Linux en lenguaje ensamblador estilo NASM utilizando la biblioteca estándar de C.

; ; Este programa se ejecuta en modo protegido de 32 bits. ; gcc enlaza la biblioteca estándar de C por defecto; compilación: nasm -f elf -F stabs name.asm ; enlace: gcc -o name name.o ; ; En modo largo de 64 bits puede usar registros de 64 bits (por ejemplo, rax en lugar de eax, rbx en lugar de ebx, etc.) ; También cambie "-f elf " por "-f elf64" en el comando de compilación. ; global main ; 'main' debe estar definido, ya que se compila ; contra la biblioteca estándar de C extern printf ; declara el uso del símbolo externo, ya que printf ; printf está declarado en un módulo objeto diferente. ; El enlazador resuelve este símbolo más tarde.segmento .data ; sección para datos inicializados cadena db '¡Hola mundo!' , 0Ah , 0 ; cadena de mensaje que termina con un carácter de nueva línea (10 ; decimal) y el terminador de byte cero 'NUL' ; 'cadena' ahora se refiere a la dirección de inicio ; en la que se almacena 'Hola, mundo'.segmento .text main: push string ; Inserta la dirección de 'string' en la pila. ; Esto reduce esp en 4 bytes antes de almacenar ; la dirección de 4 bytes 'string' en la memoria en ; el nuevo esp, el nuevo fondo de la pila. ; Este será un argumento para printf()call printf ; llama a la función printf() de C. add esp , 4 ; Incrementa el puntero de pila en 4 para volver ; a donde estaba antes del 'push', que ; lo redujo en 4 bytes. ret ; Regresa a quien nos llamó.

Dado que se utiliza el entorno de ejecución de C, definimos una función main() tal como lo espera dicho entorno. En lugar de llamar a exit, simplemente regresamos de la función main para que el entorno de ejecución realice la limpieza.

Programa "¡Hola mundo!" para Linux de 64 bits en lenguaje ensamblador estilo NASM.

Este ejemplo está en modo moderno de 64 bits.

; compilación: nasm -f elf64 -F dwarf hello.asm ; enlace: ld -o hello hello.oDEFAULT REL ; utiliza modos de direccionamiento relativos a RIP por defecto, por lo que [foo] = [rel foo]SECCIÓN .rodata ; los datos de solo lectura deben ir en la sección .rodata en GNU/Linux, como .rdata en Windows Hello: db "¡Hola mundo!" , 10 ; Terminar con un byte 10 = salto de línea (ASCII LF) len_Hello: equ $ - Hello ; Hacer que NASM calcule la longitud como una constante de tiempo de ensamblaje ; el símbolo '$' significa 'aquí'. write() toma una longitud de modo que ; no se necesita una cadena de estilo C terminada en cero. ; Sería para C puts()SECCIÓN .textoglobal _start _start: mov eax , 1 ; __NR_write número de llamada al sistema de Linux asm/unistd_64.h (x86_64) mov edi , 1 ; int fd = STDOUT_FILENO lea rsi , [ rel Hello ] ; x86-64 usa LEA relativa a RIP para colocar direcciones estáticas en registros mov rdx , len_Hello ; size_t count = len_Hello syscall ; write(1, Hello, len_Hello); llamada al kernel para realizar realmente la llamada al sistema ;; valor de retorno en RAX. RCX y R11 también son sobrescritos por la llamada al sistemamov eax , 60 ; El número de llamada __NR_exit (x86_64) se almacena en el registro eax. xor edi , edi ; Esto pone a cero edi y también rdi.; Este truco xor-self es el modismo común preferido para poner a cero ; un registro, y siempre es, con mucho, el método más rápido.; Cuando se almacena un valor de 32 bits en, por ejemplo, edx, los bits altos 63:32 también se ponen automáticamente a cero en todos los casos. Esto le ahorra tener que establecer ; los bits con una instrucción adicional, ya que este es un caso muy común ; necesario, para que un registro completo de 64 bits se llene con un valor de 32 bits.; Esto establece el estado de salida de nuestra rutina = 0 (salir normalmente) syscall ; _exit(0)

Al ejecutarlo, se straceverifica que no se realicen llamadas al sistema adicionales durante el proceso. La versión con printf realizaría muchas más llamadas al sistema para inicializar libc y realizar el enlace dinámico . Pero este es un ejecutable estático porque lo enlazamos usando ld sin -pie ni bibliotecas compartidas; las únicas instrucciones que se ejecutan en el espacio de usuario son las que usted proporciona.

$ strace ./hello > /dev/null # Sin una redirección, la salida estándar de tu programa se mezcla con el registro de strace en stderr. Lo cual normalmente está bien execve("./hello", ["./hello"], 0x7ffc8b0b3570 /* 51 vars */) = 0 write(1, "Hello world!\n", 13) = 13 exit(0) = ? +++ salió con 0 +++

Utilizando el registro de banderas

Las banderas se utilizan ampliamente para comparaciones en la arquitectura x86. Cuando se realiza una comparación entre dos datos, la CPU establece la bandera o banderas correspondientes. Posteriormente, se pueden usar instrucciones de salto condicional para verificar las banderas y bifurcar al código que debe ejecutarse, por ejemplo:

cmp eax , ebx jne hacer_algo ; ... hacer_algo: ; hacer algo aquí

Además de las instrucciones de comparación, existen numerosas instrucciones aritméticas y de otro tipo que modifican bits en el registro de indicadores. Otros ejemplos son las instrucciones sub, test y add, entre muchas otras. Las combinaciones comunes, como cmp + salto condicional, se fusionan internamente (mediante una macrofusión ) en una única microinstrucción (μ-op) y son rápidas siempre que el procesador pueda predecir la dirección del salto condicional: salto o continuación.

El registro de indicadores también se utiliza en la arquitectura x86 para activar y desactivar ciertas funciones o modos de ejecución. Por ejemplo, para deshabilitar todas las interrupciones enmascarables, puede utilizar la instrucción:

cli

También se puede acceder directamente al registro de indicadores. Los 8 bits inferiores del registro de indicadores se pueden cargar ahmediante la lahfinstrucción. El registro completo de indicadores también se puede mover dentro y fuera de la pila mediante las instrucciones pushfd/pushfq, popfd/popfq, int(incluyendo into) y iret.

El subsistema matemático de punto flotante x87 también tiene su propio registro independiente de tipo "banderas", la palabra de estado fp. En la década de 1990, acceder a los bits de bandera en este registro era un procedimiento engorroso y lento, pero en los procesadores modernos existen instrucciones para "comparar dos valores de punto flotante" que se pueden usar directamente con las instrucciones de salto/bifurcación condicionales normales sin pasos intermedios.

Utilizando el registro del puntero de instrucciones

El puntero de instrucción se llama ipen modo de 16 bits, eipen modo de 32 bits y ripen modo de 64 bits. El registro del puntero de instrucción apunta a la dirección de la siguiente instrucción que el procesador intentará ejecutar. No se puede acceder directamente a él en modo de 16 bits o de 32 bits, pero se puede escribir una secuencia como la siguiente para colocar la dirección next_lineen eax(código de 32 bits):

llamar a next_line next_line: pop eax

Escribir en el puntero de instrucción es sencillo  : una jmpinstrucción almacena la dirección de destino dada en el puntero de instrucción, por lo que, por ejemplo, una secuencia como la siguiente colocará el contenido de raxen rip(código de 64 bits):

salto rax

En el modo de 64 bits, las instrucciones pueden hacer referencia a datos en relación con el puntero de instrucción, por lo que hay menos necesidad de copiar el valor del puntero de instrucción a otro registro.

Véase también

Referencias

  1. "Familia de microprocesadores Intel 8008 (i8008)" . www.cpu-world.com . Archivado del original el 17 de abril de 2021. Consultado el 25 de marzo de 2021 .
  2. "Intel 8008" . MUSEO DE CPU - MUSEO DE MICROPROCESADORES Y FOTOGRAFÍA DE DIE . Archivado del original el 1 de abril de 2021. Consultado el 25 de marzo de 2021 .
  3. 1 2 3 "Mapa de códigos de operación 8086" . Inicio del mililitro . Archivado del original el 23/01/2026 . Recuperado el 22/12/2025 .
  4. 1 2 3 4 5 Narayam, Ram (17 de octubre de 2007). "Ensambladores de Linux: una comparación de GAS y NASM" . IBM . Archivado del original el 3 de octubre de 2013. Recuperado el 2 de julio de 2008 .
  5. "La creación de Unix" . Archivado del original el 2 de abril de 2014.
  6. Hyde, Randall. "¿Cuál ensamblador es el mejor?" . Archivado del original el 18 de octubre de 2007. Consultado el 18 de mayo de 2008 .
  7. "Noticias del ensamblador GNU, la versión 2.1 admite la sintaxis de Intel" . 4 de abril de 2008. Consultado el 2 de julio de 2008 .
  8. "i386-Bugs (Using as)" . Documentación de Binutils . Archivado del original el 15 de enero de 2020 . Consultado el 15 de enero de 2020 .
  9. "Manual de programación en lenguaje ensamblador de Intel 8080" (PDF) . Archivado del original (PDF) el 12 de mayo de 2023. Consultado el 12 de mayo de 2023 .
  10. " NASM - El ensamblador de red" . www.nasm.us. Archivado del original el 14 de agosto de 2025. Consultado el 9 de mayo de 2025 .
  11. "Declaraciones (Usando como)" . sourceware.org . Archivado del original el 10 de mayo de 2025. Consultado el 9 de mayo de 2025 .
  12. "Pseudo Ops (Using as) :: Directivas del ensamblador" . sourceware.org . Archivado del original el 17 de mayo de 2025. Consultado el 9 de mayo de 2025 . 
  13. "Directivas del ensamblador - Manual de referencia del lenguaje ensamblador x86" . docs.oracle.com . Archivado del original el 14 de agosto de 2025. Consultado el 9 de mayo de 2025 .
  14. Mueller, Scott (24 de marzo de 2006). "Procesadores P2 (286) de segunda generación" . Actualización y reparación de PC, 17.ª edición (libro) (17.ª ed.). Que. ISBN  0-7897-3404-4. Consultado el 06-12-2017 .
  15. Curtis Meadow. "Codificación de instrucciones 8086" Archivado el 6 de julio de 2022 en Wayback Machine .
  16. Igor Kholodov. "6. Codificación de operandos de instrucciones x86, byte MOD-REG-R/M" Archivado el 31-03-2022 en Wayback Machine .
  17. "Instrucciones de codificación x86" Archivado el 31/03/2022 en Wayback Machine .
  18. Michael Abrash. "Zen del lenguaje ensamblador: Volumen I, Conocimiento". "Capítulo 7: Direccionamiento de memoria". Sección "Direccionamiento mod-reg-rm" Archivado el 4 de marzo de 2022 en Wayback Machine .
  19. Manual de referencia del programador Intel 80386. "17.2.1 ModR/M y bytes SIB". Archivado el 20 de diciembre de 2021 en Wayback Machine.
  20. ""Codificación de instrucciones X86-64: ModR/M y bytes SIB"" . Archivado del original el 04-03-2022 . Recuperado el 04-03-2022 .
  21. "Figura 2-1. Formato de instrucción de las arquitecturas Intel 64 e IA-32" Archivado el 4 de marzo de 2022 en Wayback Machine .
  22. "Direccionamiento x86 bajo el capó" Archivado el 4 de marzo de 2022 en Wayback Machine .
  23. 1 2 Stephen McCamant. "Ingeniería inversa binaria manual y automatizada" Archivado el 4 de marzo de 2022 en Wayback Machine .
  24. "Lista de deseos de instrucciones X86" Archivada el 4 de marzo de 2022 en Wayback Machine .
  25. Peter Cordes (18 de diciembre de 2011). "NASM (Intel) versus AT&T Syntax: ¿cuáles son las ventajas?" . Stack Overflow . Archivado del original el 13 de febrero de 2023. Recuperado el 13 de febrero de 2023 .
  26. "Acabo de empezar Assembly" . daniweb.com . 2008. Archivado del original el 29 de enero de 2019. Consultado el 29 de enero de 2019 .

Lecturas adicionales

Manuales

  • Manuales de desarrollo de software para Intel 64 e IA-32
  • Manual del programador de la arquitectura AMD64 (Volumen 1-5)

Libros

  • Ed, Jorgensen (mayo de 2018). Programación en lenguaje ensamblador x86-64 con Ubuntu (PDF)  ( ed. 1.0.97 ). pág.  367.