Lenguaje ensamblador X86

format_list_bulleted Contenido keyboard_arrow_down
ImprimirCitar
Familia de lenguajes de montaje atrasados


El lenguaje ensamblador x86 es el nombre de la familia de lenguajes ensambladores que brinda cierto nivel de compatibilidad con versiones anteriores de CPU hasta el microprocesador Intel 8008, que se lanzó en abril de 1972. Se utiliza para producir código objeto para la clase de procesadores x86.

Considerado como un lenguaje de programación, el ensamblaje es específico de la máquina y de bajo nivel. Como todos los lenguajes ensambladores, el ensamblador x86 usa mnemónicos para representar instrucciones fundamentales de la CPU o código de máquina. Los lenguajes ensambladores se usan con mayor frecuencia para aplicaciones detalladas y de tiempo crítico, como pequeños sistemas integrados en tiempo real, kernels de sistemas operativos y controladores de dispositivos, pero también se pueden usar para otras aplicaciones. Un compilador a veces producirá código ensamblador como un paso intermedio al traducir un programa de alto nivel en código de máquina.

Palabra clave

Palabras clave reservadas del lenguaje ensamblador x86

  • Ids
  • les
  • lfs
  • Igs
  • Iss
  • pop
  • empujar
  • dentro
  • ins
  • Fuera.
  • fuera
  • Lahf
  • sahf
  • popf
  • pushf
  • cmc
  • clc
  • stc
  • cli
  • sti
  • sujetado
  • std
  • añadir
  • adc
  • sub
  • Sbb
  • cmp
  • inc
  • dec
  • prueba
  • sal
  • shl
  • sar
  • Shr
  • shld
  • Shrd
  • no
  • neg
  • límites
  • y
  • o
  • xor
  • imul
  • mul
  • div
  • idiv
  • cbtw
  • cwtl
  • cwtd
  • clt.
  • daa
  • das
  • aaaa
  • aas
  • aam
  • aad
  • Espera.
  • fwait
  • movs
  • cmps
  • stos
  • Lods
  • scas
  • xlat
  • rep
  • repnz
  • repz
  • Icall
  • llamada
  • Ret
  • Iret
  • Entra
  • licencia
  • jcxz
  • bucle
  • loopnz
  • loopz
  • jmp
  • ljmp
  • int
  • en
  • iret
  • Sldt
  • str
  • Soldado
  • ltr
  • verr
  • Verw
  • sgdt
  • Sidt
  • Igd
  • Lidt
  • Smsw
  • Lmw
  • lar
  • lsl
  • clts
  • arpl
  • bsf
  • bsr
  • bt
  • btc
  • btr
  • bts
  • cmpxchg
  • fsin
  • fcos
  • fsincos
  • fld
  • fldcw
  • fldenv
  • fprem
  • fucom
  • fucomp
  • fucompp
  • lea
  • mov
  • movw
  • movsx
  • movzb
  • popa
  • pusha
  • rcl
  • rcr
  • rol
  • Ror
  • setcc
  • bswap
  • xadd
  • xchg
  • wbinvd
  • invd
  • invlpg
  • Cerradura
  • Nop
  • hlt
  • fld
  • f
  • fstp
  • fxch
  • .
  • fist
  • fistp
  • fbld
  • fbstp
  • #
  • Faddp
  • fiadd
  • fsub
  • fsubp
  • fsubr
  • fsubrp
  • fisubrp
  • fisubr
  • fmul
  • fmulp
  • fimul
  • fdiv
  • fdivp
  • fdivr
  • fdivrp
  • fidedición
  • fidivora
  • fsqrt
  • fscale
  • fprem
  • frndint
  • fxtract
  • fabs
  • Fchs
  • fcom
  • fcomp
  • fcompp
  • ficom
  • ficompleta
  • ftst
  • fxam
  • fptan
  • fpatan
  • f2xm1
  • fyl2x
  • fyl2xp1
  • fldl2e
  • fldl2t
  • fldlg2
  • fldln2
  • fldpi
  • fldz
  • finit
  • fnint
  • fnop
  • fsave
  • fnsave
  • Fstew
  • Fnstew
  • fstenv
  • fnstenv
  • fstsw
  • Fnstsw
  • frstor
  • fwait
  • Espera.
  • fclex
  • fnclex
  • fdecstp
  • ffree
  • fincstp

Mnemónicos y códigos de operación

Cada instrucción de ensamblaje x86 está representada por un mnemotécnico que, a menudo combinado con uno o más operandos, se traduce en uno o más bytes llamados código de operación; la instrucción NOP se traduce en 0x90, por ejemplo, y la instrucción HLT se traduce en 0xF4. Hay posibles códigos de operación sin mnemotécnicos documentados que diferentes procesadores pueden interpretar de manera diferente, lo que hace que un programa que los usa se comporte de manera inconsistente o incluso genere una excepción en algunos procesadores. Estos códigos de operación a menudo aparecen en concursos de escritura de código como una forma de hacer que el código sea más pequeño, más rápido, más elegante o simplemente para mostrar la destreza del autor.

Sintaxis

El lenguaje ensamblador x86 tiene dos ramas de sintaxis principales: sintaxis de Intel y sintaxis de AT&T. La sintaxis de Intel es dominante en el mundo de DOS y Windows, y la sintaxis de AT&T es dominante en el mundo de Unix, ya que Unix fue creado en AT&T Bell Labs. Este es un resumen de las principales diferencias entre la sintaxis de Intel y la sintaxis de AT&T:

AT Intel
Orden del parámetro
movl $5, %eax
Fuente antes del destino.
mov eax, 5
Destino antes de la fuente.
Tamaño del parámetro
addl $0x24, %espmovslq %ecx, %rax# %xmm1, %xmm2
Los mnemonics se sufigen con una carta que indica el tamaño de los operandos: q para qword (64 bits), l por largo (dword, 32 bits), w por palabra (16 bits), y b para byte (8 bits).
añadir esp, 24 horasmovsxd rax, ecx# xmm2, xmm1
Derivado del nombre del registro que se utiliza (por ejemplo, rax, eax, ax, al implicaciones q, l, w, b, respectivamente).

Los nombres basados en la anchura pueden aparecer en instrucciones cuando definen una operación diferente.

  • MOVXSD se refiere a la extensión de firma con entrada dword, a diferencia de MOVXS.
  • Los registros SIMD tienen instrucciones de nombre ancho que determinan cómo dividir el registro. ATT tiende a mantener los nombres sin cambios, por lo que PADDD no es renombrado a "paddl".
Sigils Valores inmediatos prefijados con un "$", registra prefijo con un "%". El ensamblador detecta automáticamente el tipo de símbolos; es decir, si son registros, constantes o algo más.
Direcciones efectivas
movl offset()%ebx,%ecx,4), %eax
Sintaxis general de DISP(BASE,INDEX,SCALE).
mov eax, [ebx + ecx*4 + offset]
Expresiones rítmicas entre corchetes; además, palabras clave de tamaño como byte, palabra, o dword debe ser utilizado si el tamaño no puede ser determinado de los operandos.

Muchos ensambladores x86 usan sintaxis de Intel, incluidos FASM, MASM, NASM, TASM y YASM. GAS, que originalmente usaba la sintaxis de AT&T, admite ambas sintaxis desde la versión 2.10 a través de la directiva .intel_syntax. Una peculiaridad en la sintaxis de AT&T para x86 es que los operandos x87 están invertidos, un error heredado del ensamblador original de AT&T.

La sintaxis de AT&T es casi universal para todas las demás arquitecturas (conservando el mismo mov); originalmente era una sintaxis para el ensamblado de PDP-11. La sintaxis de Intel es específica de la arquitectura x86 y es la que se usa en la documentación de la plataforma x86. El Intel 8080, que es anterior al x86, también utiliza el "destino primero" pedir mov.

Registros

Los procesadores x86 tienen una colección de registros disponibles para usar como almacenes de datos binarios. En conjunto, los registros de datos y direcciones se denominan registros generales. Cada registro tiene un propósito especial además de lo que todos pueden hacer:

  • AX multiplique/divide, carga de cadena > tienda
  • BX index register for MOVE
  • CX cuenta para operaciones de cadenas " cambios
  • Dirección de puerto DX para IN y OUT
  • Puntos SP a la parte superior de la pila
  • BP apunta a la base del marco de la pila
  • SI apunta a una fuente en operaciones de flujo
  • DI apunta a un destino en operaciones de flujo

Junto a los registros generales existen adicionalmente:

  • Punto de instrucción IP
  • FLAGS
  • registros de segmentos (CS, DS, ES, FS, GS, SS) que determinan dónde comienza un segmento de 64k (sin FS " GS en 80286 " antes)
  • Registros de extensión adicionales (MMX, 3DNow!, SSE, etc.) (Pentium " más tarde solamente).

El registro IP apunta al desplazamiento de memoria de la siguiente instrucción en el segmento de código (apunta al primer byte de la instrucción). El registro de IP no puede ser accedido directamente por el programador.

Los registros x86 se pueden usar usando las instrucciones MOV. 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 en modo 8086 real y virtual utiliza un proceso conocido como segmentación para abordar la memoria, no el modelo de memoria plana utilizado en muchos otros entornos. La segmentación implica componer una dirección de memoria a partir de dos partes, un segmento y un desplazamiento; el segmento apunta al comienzo de un grupo de direcciones de 64 KiB (64 × 210) y el desplazamiento determina qué tan lejos de esta dirección inicial está la dirección deseada. En el direccionamiento segmentado, se requieren dos registros para una dirección de memoria completa. Uno para mantener el segmento, el otro para mantener el desplazamiento. Para volver a traducir a una dirección plana, el valor del segmento se desplaza cuatro bits hacia la izquierda (equivalente a la multiplicación por 24 o 16) y luego se suma al desplazamiento para formar la dirección completa, lo que permite romper la Barrera de 64k a través de una elección inteligente de direcciones, aunque hace que la programación sea considerablemente más compleja.

En modo real/solo protegido, por ejemplo, si DS contiene el número hexadecimal 0xDEAD y DX contiene el número 0xCAFE, juntos apuntarían a la dirección de memoria 0xDEAD * 0x10 + 0xCAFE = 0xEB5CE. Por lo tanto, la CPU puede direccionar hasta 1 048 576 bytes (1 MB) en modo real. Combinando valores de segmento y compensación encontramos una dirección de 20 bits.

La IBM PC original restringía los programas a 640 KB, pero se usó una especificación de memoria expandida para implementar un esquema de cambio de banco que dejó de usarse cuando los sistemas operativos posteriores, como Windows, usaron los rangos de direcciones más grandes de los procesadores más nuevos e implementaron su propios esquemas de memoria virtual.

OS/2 utilizó el modo protegido, comenzando con Intel 80286. Varias deficiencias, como la imposibilidad de acceder al BIOS y la imposibilidad de volver al modo real sin reiniciar el procesador, impidieron un uso generalizado. El 80286 también estaba limitado a direccionar la memoria en segmentos de 16 bits, lo que significa que solo se podía acceder a 216 bytes (64 kilobytes) a la vez. Para acceder a la funcionalidad extendida del 80286, el sistema operativo configuraría el procesador en modo protegido, lo que habilitaría el direccionamiento de 24 bits y, por lo tanto, 224 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 Indicador de tabla que determina si la entrada está en GDT o LDT y un bit de 2 bits. bit Nivel de privilegio solicitado; ver segmentación de memoria x86.

Cuando se hace referencia a una dirección con un segmento y un desplazamiento, se utiliza la notación de 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 registro de segmento y desplazamiento; DS:DX.

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

  • CS:IP (CS es Código de Procedimiento Civil, IP es Instruction Pointer) apunta a la dirección donde el procesador buscará el siguiente byte de código.
  • SS:SP Stack Segment, SP es Stack Pointer) apunta a la dirección de la parte superior de la pila, es decir, el byte más recientemente empujado.
  • DS:SI (DS es Segmento de datos, SI es Índice de fuentes) se utiliza a menudo para apuntar a datos de cadena que está a punto de ser copiado a ES:DI.
  • ES:DI (ES es Segmento extra, DI es Índice) se utiliza normalmente para apuntar al destino para una copia de cadena, como se mencionó anteriormente.

El Intel 80386 presentaba tres modos operativos: modo real, modo protegido y modo virtual. El modo protegido que debutó en el 80286 se amplió para permitir que el 80386 gestione hasta 4 GB de memoria, el nuevo modo virtual 8086 (VM86) hizo posible ejecutar 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 (generalmente como resultado de trucos de direccionamiento de memoria o el 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ó x86-64 en 2003, ya que ayudó a impulsar la adopción a gran escala de Windows. 3.1 (que se basaba en el modo protegido) ya que Windows ahora podía ejecutar muchas aplicaciones a la vez, incluidas las aplicaciones de DOS, mediante el uso de memoria virtual y multitarea simple.

Modos de ejecución

Los procesadores x86 admiten cinco modos de funcionamiento para el código x86, Modo real, Modo protegido, Modo largo, Virtual 86 Modo y Modo de Gestión del Sistema, en los que unas 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 8086, 8088, 80186, 80188 y 80286. Estas instrucciones están disponibles en modo real en todos los procesadores x86 y en modo protegido de 16 bits. (80286 en adelante), hay 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 funciones. SMM, con algunas de sus propias instrucciones especiales, está disponible en algunas CPU Intel i386SL, i486 y posteriores. Finalmente, en modo largo (AMD Opteron en adelante), también están disponibles instrucciones de 64 bits y más registros. El conjunto de instrucciones es similar en cada modo, 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-bit)
    • Espacio de dirección de memoria segmentada de 20 bits (que significa que sólo 1 MB de memoria se puede abordar – en realidad, ligeramente más), acceso directo al hardware periférico, y ningún concepto de protección de memoria o multitarea a nivel de hardware. Las computadoras que usan BIOS comienzan en este modo.
  • Modo protegido (16-bit y 32-bit)
    • Amplia 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 evita que los programas se corrompan. El modo protegido de 16 bits (utilizado durante el final de la era de DOS) utilizó un modelo de memoria complejo y multi-segmentado. El modo protegido de 32 bits utiliza un modelo de memoria simple y plano.
  • Modo largo (64-bit)
    • Sobre todo 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 dejaron en el modo 64 bits. Pioneered by AMD.
  • Modo 8086 virtual (16 bits)
    • Un modo de funcionamiento híbrido especial que permite que los programas de modo real y los sistemas operativos funcionen bajo el control de un sistema operativo de control de modo protegido
  • Modo de gestión del sistema (16 bits)
    • Maneja funciones a nivel de todo el sistema como la gestión de energía, el control de hardware del sistema y el código diseñado de OEM patentado. Está destinado a ser utilizado sólo por el firmware del sistema. Toda ejecución normal, incluido el sistema operativo, está suspendida. Un sistema de software alternativo (que generalmente reside en el firmware del ordenador, o un depurador asistido por hardware) se ejecuta con altos privilegios.

Modos de cambio

El procesador se ejecuta en modo real inmediatamente después de encenderse, por lo que el núcleo de un sistema operativo u otro programa debe cambiar explícitamente a otro modo si desea ejecutar cualquier cosa que no sea el modo real. El cambio de modos se logra modificando ciertos bits de los registros de control del procesador después de cierta preparación, y es posible que se requiera alguna configuración adicional después del cambio.

Ejemplos

Con una computadora que ejecuta un BIOS heredado, el BIOS y el cargador de arranque se ejecutan en modo Real, luego el kernel del sistema operativo de 64 bits verifica y cambia la CPU al modo Long y luego inicia nuevos subprocesos en modo kernel que ejecutan 64 bits código.

Con una computadora que ejecuta UEFI, el firmware UEFI (excepto CSM y la ROM de opción heredada), el cargador de arranque UEFI y el kernel del sistema operativo UEFI se ejecutan en modo largo.

Tipos de instrucciones

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

  • Una codificación compacta
    • Longitud variable y alineación independiente (codificada como poco endian, al igual que todos los datos en la arquitectura x86)
    • Principalmente una dirección y dos direcciones instrucciones, es decir, el primer operado es también el destino.
    • Los operandos de memoria como fuente y destino son compatibles (frecuentemente utilizados para leer/escribir elementos de pila dirigidos utilizando pequeños offsets inmediatos).
    • Uso general e implícito del registro; aunque los siete (contando ebp) registro general en modo 32-bit, y los quince (contando rbp) registros generales en modo de 64 bits, se pueden utilizar libremente como acumuladores o para abordar, la mayoría de ellos también implícitamente utilizado por ciertas (más o menos) instrucciones especiales; los registros afectados deben ser preservados temporalmente (normalmente apilados), si están activos durante tales secuencias de instrucciones.
  • Produce banderas condicionales implícitamente a través de las instrucciones de ALU más integer.
  • Soporta varios modos de abordaje, incluyendo índice inmediato, offset y escalado pero no relacionado con PC, excepto saltos (introducidos como una mejora en la arquitectura x86-64).
  • Incluye punto flotante a una pila de registros.
  • Contiene apoyo especial para instrucciones de lectura-modificación atómica ()xchg, cmpxchg/cmpxchg8b, xadd, e instrucciones de entero que se combinan con lock prefijo)
  • Instrucciones SIMD (instrucción que realizan instrucciones individuales simultáneas paralelas en muchos operandos codificados en células adyacentes de registros más amplios).

Instrucciones de pila

La arquitectura x86 tiene soporte de hardware para un mecanismo de pila de ejecución. Instrucciones como push, pop, call y ret se utilizan con la pila correctamente configurada para pasar parámetros, para asignar espacio para datos locales y para guardar y restaurar puntos de devolución de llamadas. La instrucción ret size es muy útil para implementar convenciones de llamada eficientes (y rápidas) en el espacio donde el receptor de la llamada es responsable de reclamar el espacio de pila ocupado por los parámetros.

Al configurar un marco de pila para contener datos locales de un procedimiento recursivo, hay varias opciones; la instrucción enter de alto nivel (introducida con el 80186) toma un argumento procedure-nesting- depth así como un argumento local size, y puede ser más rápido que una manipulación más explícita de los registros (como push bp; mov bp, sp; sub sp, tamaño). El hecho de que sea más rápido o más lento depende de la implementación particular del procesador x86, así como de la convención de llamada utilizada por el compilador, programador o código de programa 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 diversas, así como también opciones de diseño a nivel de puerta y transistor.

La gama completa de modos de direccionamiento (incluidos inmediato y base+offset) incluso para instrucciones como push y pop, simplifica el uso directo de la pila para datos enteros, de coma flotante y de direcciones, además de mantener las especificaciones y los mecanismos de ABI relativamente simples en comparación con algunas arquitecturas RISC (requiere detalles de pila de llamadas más explícitos).

Instrucciones ALU enteras

El ensamblado

x86 tiene las operaciones matemáticas estándar, add, sub, mul, con idiv; los operadores lógicos and, or, xor, neg; bitshift aritmético y lógico, sal/sar, shl/shr; rotar con y sin acarreo, rcl/rcr, rol/ror, un complemento de las instrucciones aritméticas BCD, aaa, aad, daa y otros.

Instrucciones de coma flotante

El lenguaje ensamblador x86 incluye instrucciones para una unidad de punto flotante (FPU) basada en pila. La FPU era un coprocesador separado opcional para el 8086 hasta el 80386, era una opción en el chip para la serie 80486 y es una característica estándar en cada CPU Intel x86 desde el 80486, comenzando con el Pentium. Las instrucciones de FPU incluyen suma, resta, negación, multiplicación, división, resto, raíces cuadradas, truncamiento de enteros, truncamiento de fracciones y escala 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, flotante de 64 bits. o punto flotante de 80 bits (al cargar, el valor se convierte al modo de punto flotante actualmente utilizado). x86 también incluye una serie de funciones trascendentales, que incluyen seno, coseno, tangente, arcotangente, exponenciación en base 2 y logaritmos en base 2, 10 o e.

El formato de registro de pila a registro de pila de las instrucciones suele ser fop st, st(n) o fop st(n), st, donde st es equivalente a st(0), y st(n) es uno de los 8 registros de pila (st(0), st(1),..., st(7)). Al igual que los números enteros, el primer operando es tanto el primer operando de origen como el de destino. fsubr y fdivr deben destacarse como el primer intercambio de los operandos de origen 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 aparecen en la parte superior de la pila una vez que se completa la operación. Entonces, por ejemplo, faddp st(1), st realiza el cálculo st(1) = st(1) + st(0), luego elimina st (0) desde la parte superior de la pila, lo que hace que el resultado en st(1) sea la parte superior de la pila en st(0).

Instrucciones SIMD

Las CPU x86 modernas contienen instrucciones SIMD, que en gran medida realizan la misma operación en paralelo en muchos valores codificados en un amplio registro SIMD. Varias tecnologías de instrucción admiten diferentes operaciones en diferentes conjuntos de registros, pero tomados como un todo completo (desde MMX hasta SSE4.2) incluyen cálculos generales sobre aritmética de números enteros o de coma flotante (suma, resta, multiplicación, desplazamiento, minimización, maximización, comparación, división o raíz cuadrada). Entonces, por ejemplo, paddw mm0, mm1 realiza 4 sumas enteras paralelas de 16 bits (indicadas por w) (indicadas por padd) de mm0 valores a mm1 y 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 (expandido en SSE2). Se han agregado algunas otras instrucciones inusuales, incluida una suma de diferencias absolutas (utilizada para la estimación de movimiento en la compresión de video, como se hace en MPEG) y una instrucción de acumulación de multiplicación de 16 bits (útil para filtrado digital y mezcla alfa basada en software). SSE (desde SSE3) y 3DNow! las extensiones incluyen instrucciones de suma y resta para tratar valores de punto flotante emparejados como números complejos.

Estos conjuntos de instrucciones también incluyen numerosas instrucciones de subpalabras fijas para barajar, insertar y extraer los valores dentro de los registros. Además, hay instrucciones para mover datos entre los registros de enteros y los registros XMM (usado en SSE)/FPU (usado en MMX).

Instrucciones de memoria

El procesador x86 también incluye modos de direccionamiento complejos para direccionar la memoria con 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. Entonces, por ejemplo, uno puede codificar mov eax, [Table + ebx + esi*4] como una sola instrucción que carga 32 bits de datos desde la dirección calculada como (Table + ebx + esi * 4) desplazado del selector ds, y lo almacena en el registro eax. En general, los procesadores x86 pueden cargar y usar memoria que coincida con el tamaño de cualquier registro en el que esté operando. (Las instrucciones SIMD también incluyen instrucciones de media carga).

La mayoría de las instrucciones x86 de 2 operandos, incluidas las instrucciones ALU enteras, use un "byte de modo de direccionamiento" a menudo llamado byte MOD-REG-R/M. Muchas instrucciones x86 de 32 bits también tienen un byte de modo de direccionamiento SIB que sigue al byte MOD-REG-R/M.

En principio, debido a que el código de operación de la instrucción está separado del byte del modo de direccionamiento, esas instrucciones son ortogonales porque cualquiera de esos códigos de operación se puede mezclar y combinar con cualquier modo de direccionamiento. Sin embargo, el conjunto de instrucciones x86 generalmente se considera no ortogonal porque muchos otros códigos de operación tienen algún modo de direccionamiento fijo (no tienen un byte de modo de direccionamiento) y cada registro es especial.

El conjunto de instrucciones x86 incluye instrucciones para cargar, almacenar, mover, escanear y comparar cadenas (lods, stos, movs, scas y cmps) que realizan cada operación en un tamaño específico (b para bytes de 8 bits, w para bytes de 16 bits palabra, d para palabra doble de 32 bits) luego incrementa/decrementa (dependiendo de DF, bandera de dirección) el registro de dirección implícito (si para lods, di para stos y scas, y ambos para movs y cmps). Para las operaciones de carga, almacenamiento y escaneo, el registro implícito de destino/fuente/comparación está en el registro al, ax o eax (dependiendo de tamaño). Los registros de segmentos implícitos utilizados son ds para si y es para di. El registro cx o ecx se utiliza como contador decreciente y la operación se detiene cuando el contador llega a cero o (para exploraciones y comparaciones) cuando se detecta desigualdad. Desafortunadamente, a lo largo de los años, el rendimiento de algunas de estas instrucciones se descuidó y, en ciertos casos, ahora es posible obtener resultados más rápidos escribiendo los algoritmos usted mismo. Sin embargo, Intel y AMD han actualizado algunas de las instrucciones, y algunas ahora tienen un rendimiento muy respetable, por lo que se recomienda que el programador lea artículos de referencia respetados recientes antes de elegir usar una instrucción particular de este grupo.

La pila es una región de la memoria y un "puntero de pila" asociado, que apunta al final de la pila. El puntero de pila se reduce cuando se agregan elementos ('push') y se incrementa después de que se eliminan elementos ('pop'). En el modo de 16 bits, este puntero de pila implícito se direcciona como SS:[SP], en el modo de 32 bits es SS:[ESP] y en el modo de 64 bits es [RSP]. El puntero de la pila en realidad apunta al último valor que se almacenó, suponiendo que su tamaño coincidirá con el modo operativo del procesador (es decir, 16, 32 o 64 bits) para coincidir con el ancho predeterminado del push/pop/call/ret instrucciones. También se incluyen las instrucciones enter y leave que reservan y eliminan datos de la parte superior de la pila al configurar 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 del registro sp/esp/rsp, por lo que el enter/leave generalmente son innecesarias.

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:

 empujar rbp ; Guardar el puntero de marco de la función de llamada (registro de comandos) mov rbp, rsp ; Hacer un nuevo marco de pila debajo de la pila de nuestro callador sub rsp, 32 ; Reserva 32 bytes de espacio de pila para las variables locales de esta función. ; Las variables locales estarán por debajo de rbp y pueden ser referenciadas en relación con rbp, ; de nuevo lo mejor para la facilidad de depuración, pero para el mejor rendimiento rbp no ; ser utilizado en absoluto, y las variables locales se referenciarían en relación con rsp ; porque, aparte del ahorro de código, rbp entonces es libre para otros usos. ... ... ; Sin embargo, si el rbp se altera aquí, su valor debe ser preservado para el llamante. mov [rbp-8] rdx ; Ejemplo de acceso a una variable local, desde la ubicación de memoria en rdx registro

... es funcionalmente equivalente a simplemente:

 Entra 32, 0

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

Se supone que los valores para una carga o almacenamiento SIMD se empaquetan en posiciones adyacentes para el registro SIMD y se alinearán en orden secuencial little-endian. Algunas instrucciones de carga y almacenamiento de SSE requieren una alineación de 16 bytes para funcionar correctamente. Los conjuntos de instrucciones SIMD también incluyen "prefetch" instrucciones que realizan la carga pero no apuntan a ningún registro, utilizadas para la carga de caché. Los conjuntos de instrucciones SSE también incluyen instrucciones de almacenamiento no temporales que realizarán almacenamientos directamente en la memoria sin realizar una asignación de caché si el destino aún no está en caché (de lo contrario, se comportará como un almacenamiento normal).

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

Flujo del programa

El ensamblaje x86 tiene una operación de salto incondicional, jmp, que 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 saltar).

También se admiten varios saltos condicionales, incluidos jz (salto en cero), jnz (salto en distinto de cero), jg (saltar sobre mayor que, firmado), jl (saltar sobre menor que, firmado), ja (saltar sobre arriba/mayor que, sin firmar), jb (saltar debajo/menor que, sin firmar). Estas operaciones condicionales se basan en el estado de bits específicos en el registro (E)FLAGS. Muchas operaciones aritméticas y lógicas activan, borran o complementan estas banderas dependiendo de su resultado. Las instrucciones de comparación cmp (comparar) y de prueba establecen las banderas 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 bandera de acarreo) y cmc (complementar bandera de acarreo) que funcionan directamente en las banderas. Las comparaciones de punto flotante se realizan a través de instrucciones fcom o ficom que eventualmente deben convertirse en banderas de enteros.

Cada operación de salto tiene tres formas diferentes, dependiendo del tamaño del operando. Un salto corto utiliza un operando con signo de 8 bits, que es un desplazamiento relativo de 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 lejos es aquel que utiliza el valor base:offset del segmento completo como una dirección absoluta. También hay formas indirectas e indexadas de cada uno de estos.

Además de las operaciones de salto simples, existen las instrucciones call (llamar a una subrutina) y ret (regresar de la subrutina). Antes de transferir el control a la subrutina, call empuja la dirección de desplazamiento del segmento de la instrucción que sigue a call a la pila; ret extrae este valor de la pila y salta a él, devolviendo efectivamente el flujo de control a esa parte del programa. En el caso de una llamada lejana, la base del segmento se empuja siguiendo el desplazamiento; far ret muestra 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)FLAGS en la pila, luego realiza una llamada lejana, excepto que en lugar de una dirección, utiliza un vector de interrupción, un índice en una tabla de direcciones de controladores de interrupción. Por lo general, el controlador de interrupciones 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 el software llamado interrupciones). El retorno coincidente de la instrucción de interrupción es iret, que restaura las banderas después del retorno. Algunos sistemas operativos utilizan interrupciones leves del tipo descrito anteriormente para llamadas al sistema y también se pueden usar para depurar controladores de interrupciones duras. Las interrupciones fuertes se desencadenan por eventos de hardware externo y deben conservar 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 usan el llamado estilo de sintaxis de Intelcomo lo usan los ensambladores Microsoft MASM, NASM y muchos otros. (Nota: también existe un tipo de sintaxis AT&T alternativo en el que se intercambian el orden de los operandos de origen y destino, entre muchas otras diferencias).

"¡Hola mundo!" programa para MS-DOS en ensamblador estilo MASM

Uso de la instrucción de interrupción de software 21h para llamar al sistema operativo MS-DOS para la salida a la pantalla; otras muestras usan la rutina C printf() de libc para escribir en la salida estándar. Tenga en cuenta que el primer ejemplo es un ejemplo de hace 30 años que usa el modo de 16 bits como en un Intel 8086. El segundo ejemplo es el código Intel 386 en el modo de 32 bits. El código moderno estará en modo de 64 bits.

.model pequeño.stack 100h.datamsgdb¡Hola mundo! ', 0.codeempezar:movah, 09h ; Sets 8-bit registro ‘ah’, el alto byte del hacha registrado, a 9, a ; seleccione un número de subfunción de una rutina MS-DOS llamada abajo ; vía el software interrumpe int 21h para mostrar un mensajeleadx, msg ; Toma la dirección de msg, almacena la dirección en dx registro de 16 bitsint21h ; Diversas rutinas MS-DOS son llamadas por la interrupción del software 21h ; Nuestra subfunción requerida se estableció en el registro ah arribamovax, 4C00h ; Sets register ax to the sub-function number for MS-DOS’s software ; interrumpir int 21h para el servicio 'terminate programa'.int21h ; Llamar a este servicio MS-DOS nunca regresa, ya que termina el programa.final Empieza

"¡Hola mundo!" programa para Windows en ensamblador estilo MASM

; requiere /coff conmutar en 6.15 y versiones anteriores.386.model pequeño,c.stack 1000h.datamsg db "¡Hola mundo!",0.codeincluido el colibrí libcmt.libincluido el colibrí libvcruntime.libincluido el colibrí libucrt.libincluido el colibrí legacy_stdio_definitions.libextrn printf:cercaextrn Salida:cercapúblico principalprincipal proc empujar offset msg llamada printf empujar 0 llamada Salidaprincipal endpfinal

"¡Hola mundo!" programa para Windows en ensamblador estilo NASM

; Base de imagen = 0x00400000%define RVA(x) (x-0x00400000)Sección .textempujar dword Hola.llamada dword [printf]empujar byte +0llamada dword [Salida]RetSección .dataHola. db "¡Hola mundo!"Sección .idatadd RVA()msvcrt_LookupTable)dd -1dd 0dd RVA()msvcrt_string)dd RVA()msvcrt_imports)veces 5 dd 0 ; termina la tabla descriptormsvcrt_string dd "msvcrt.dll", 0msvcrt_LookupTabla:dd RVA()msvcrt_printf)dd RVA()msvcrt_exit)dd 0msvcrt_imports:printf dd RVA()msvcrt_printf)Salida dd RVA()msvcrt_exit)dd 0msvcrt_printf: 1 "printf", 0msvcrt_exit: 2 "exit", 0dd 0

156549646549874

"¡Hola mundo!" programa para Linux en ensamblador estilo NASM

;; Este programa funciona en modo protegido de 32 bits.; construir: nasm -f elf -F apuñala nombre. asm; enlace: ld -o nombre. o;; En modo largo de 64 bits puede utilizar registros de 64 bits (por ejemplo, rax en lugar de eax, rbx en lugar de ebx, etc.); También cambiar "-f elf " para "-f elf64" en el comando de construcción.;Sección .data ; sección para datos inicializadosstr: db ¡Hola mundo! ', 0Ah ; cadena de mensajes con carbón de nueva línea al final (10 decimal)str_len: equo $ - str ; longitud de cordones (bytes) restando la dirección de inicio del str ; desde ‘aquí, esta dirección’ (‘$’ símbolo que significa ‘aquí’)Sección .text ; esta es la sección de código (texto de programa) en memoria mundial ¡No! ; _start es el punto de entrada y necesita un alcance global para ser visto por el ; linker --equivallent to main() in C/C++_start: ; definición del procedimiento _start comienza aquímoveax, 4 ; especificar el código de función sys_write (de la tabla vectorial OS)movebx, 1 ; especificar archivo descriptor stdout --in gnu/linux, todo se trata como un archivo, ; incluso dispositivos de hardwaremovecx, str ; inicio de movimiento _address_ de mensaje de cadena a ecx registromovedx, str_len ; mover la longitud del mensaje (en bytes)int80h ; Interrumpir el kernel para realizar la llamada del sistema que acabamos de configurar - ; en los servicios gnu/linux se solicitan a través del núcleomoveax, 1 ; especificar el código de función sys_exit (de la tabla vectorial OS)movebx, 0 ; especifique el código de retorno para OS (cero le dice a OS todo salió bien)int80h ; Interrumpir el kernel para realizar llamada del sistema (a la salida)

Para el modo largo de 64 bits, "lea rcx, str" sería la dirección del mensaje, observe el registro rcx de 64 bits.

"¡Hola mundo!" programa para Linux en ensamblaje de estilo NASM utilizando la biblioteca estándar C

;; Este programa funciona en modo protegido de 32 bits.; gcc vincula la biblioteca estándar-C por defecto; construir: nasm -f elf -F apuñala nombre. asm; enlace: gcc -o nombre. o;; En modo largo de 64 bits puede utilizar registros de 64 bits (por ejemplo, rax en lugar de eax, rbx en lugar de ebx, etc.).; También cambiar "-f elf " para "-f elf64" en el comando de construcción.; mundial principal ; ‘mano’ debe definirse, ya que se está compilando ; contra la Biblioteca Estándar C externa printf ; declara el uso del símbolo externo, como printf ; printf se declara en un tipo objeto diferente. ; El enlace resuelve este símbolo más tarde.Serie de sesiones .data ; sección para datos inicializadoscuerda db ¡Hola mundo! ', 0Ah, 0 ; cadena de mensajes terminando con una nueva línea de carbón (10 ; decimal) y el cero byte ‘NUL’ terminator ; ‘estring’ ahora se refiere a la dirección de inicio ; en el cual 'Hola, Mundo' se almacena.Serie de sesiones .textprincipal: empujar cuerda ; Empuje la dirección de ‘estring’ en la pila. ; Esto reduce el esp por 4 bytes antes de almacenar ; la dirección de 4 bytes ‘estring’ en memoria ; el nuevo esp, el nuevo fondo de la pila. ; Este será un argumento para printf() llamada printf ; llama la función C printf(). añadir esp, 4 ; Aumenta el apilador por 4 para ponerlo de nuevo ; a donde estaba antes de la 'poca', que ; reducido en 4 bytes. Ret ; Volver a nuestro llamador.

"¡Hola mundo!" programa para Linux en modo de 64 bits en ensamblaje de estilo NASM

Este ejemplo está en el modo moderno de 64 bits.

; construir: nasm -f elf64 -F enano hola.asm; enlace: ld -o hola hola. oDEFAULT RELATIVAS ; use RIP-relative addressing modes by default, so [foo] = [rel foo]SECCIÓN .rodata; solo los datos de lectura deben ir en la sección de.rodata sobre GNU/Linux, como. rdata en WindowsHola:db "¡Hola mundo!", 10 ; Ending with a byte 10 = newline (ASCII LF)Hola.equo $-Hola. ; Obtener NASM para calcular la longitud como una constante de tiempo de montaje ; el símbolo ‘$’ significa ‘aquí’. escrito() toma una longitud para que ; una cadena de estilo C no se necesita. ; Sería para C puts()SECCIÓN .rodata; solo los datos de lectura pueden ir en la sección de.rodata sobre GNU/Linux, como. rdata en WindowsHola:db "¡Hola mundo!",10 ; 10 = ``.Hola.equo $-Hola. ; obtener NASM para calcular la longitud como una constante de tiempo de montaje;; escritura() toma una longitud para que una cadena de estilo C de 0-terminado no sea necesaria. Sería para los puñosSECCIÓN .textmundial ¡No!_start:mov eax, 1; __NR_write syscall number from Linux asm/unistd_64.h (x86_64)mov edi, 1; int fd = STDOUT_FILENOlea rsi, [rel Hola.]; x86-64 utiliza LEA relativa RIP para poner direcciones estáticas en registrosmov rdx, Len_Hello; Size_t count = len_ Hola.syscall; escriba(1, Hola, len_Hello); llame al núcleo para hacer realmente la llamada del sistema ;; valor de retorno en RAX. RCX y R11 también son sobrescritos por syscallmov eax, 60; __NR_exit call number (x86_64) is stored in register eax.xor edi, edi ; Este ceros edi y también rdi. ; Este truco de xor-self es el lenguaje común preferido para cero ; un registro, y es siempre por lejos el método más rápido. ; Cuando un valor de 32 bits se almacena en eg edx, las partes altas 63:32 son ; automáticamente se fijó también en cada caso. Esto te ahorra tener que establecer ; los bits con una instrucción extra, 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 (salvo normalmente)syscall; _exit(0)

Ejecutarlo bajo strace verifica que no se realicen llamadas adicionales al sistema en el proceso. La versión printf haría muchas más llamadas al sistema para inicializar libc y hacer enlaces dinámicos. Pero este es un ejecutable estático porque vinculamos usando ld sin -pie o cualquier biblioteca compartida; las únicas instrucciones que se ejecutan en el espacio de usuario son las que usted proporciona.

$ Strace ./hello  /dev/null # sin una redireccion, el stdout de tu programa está mezclado con la tala de estratos en stderr. Que normalmente está bien[./hello], 0x7ffc8b0b3570 /* 51 vars */) = 0escribe(1, "Hola mundo!n", 13) = 13(0) = ?+++ salida con 0 ++

Uso del registro de banderas

Las banderas se utilizan mucho para las comparaciones en la arquitectura x86. Cuando se realiza una comparación entre dos datos, la CPU establece la bandera o banderas relevantes. Después de esto, las instrucciones de salto condicional se pueden usar para verificar las banderas y la bifurcación al código que debe ejecutarse, por ejemplo:

cmpeax, ebxjnealgo;algo:; hacer algo aquí

Además de las instrucciones de comparación, hay muchas instrucciones aritméticas y de otro tipo que establecen bits en el registro de banderas. Otros ejemplos son las instrucciones sub, test y add y hay muchas más. Las combinaciones comunes como cmp + salto condicional se 'fusionan' internamente ('macro fusión') en una sola microinstrucción (μ-op) y son rápidas siempre que el procesador pueda adivinar en qué dirección irá el salto condicional, saltar o continuar.

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

cli

También se puede acceder directamente al registro de banderas. Los 8 bits inferiores del registro de bandera se pueden cargar en ah usando la instrucción lahf. El registro de banderas completo también se puede mover dentro y fuera de la pila usando las instrucciones pushfd/pushfq, popfd/popfq, int (incluyendo into) y iret.

El subsistema matemático de punto flotante x87 también tiene su propio tipo de "banderas" independientes que registran la palabra de estado fp. En la década de 1990, acceder a los bits de marca en este registro era un procedimiento incómodo y lento, pero en los procesadores modernos hay instrucciones de 'comparar dos valores de coma flotante' que se pueden usar con las instrucciones de salto/bifurcación condicionales normales directamente sin ningún paso intermedio..

Uso del registro de puntero de instrucción

El puntero de instrucción se llama ip en modo de 16 bits, eip en modo de 32 bits y rip en modo de 64 bits. El registro puntero de instrucción apunta a la dirección de la siguiente instrucción que el procesador intentará ejecutar. No se puede acceder directamente en modo de 16 bits o 32 bits, pero se puede escribir una secuencia como la siguiente para poner la dirección de next_line en eax (32 bits código):

llamadanext_linenext_line:popeax

Escribir en el puntero de instrucción es simple: una instrucción jmp 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 rax en rip (código de 64 bits):

jmprax

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

Contenido relacionado

Realidad virtual

La realidad virtual es una experiencia simulada que emplea el seguimiento de poses y visualizaciones 3D cercanas al ojo para brindar al usuario una sensación...

CPC de Amstrad

El Amstrad CPC es una serie de ordenadores domésticos de 8 bits producidos por Amstrad entre 1984 y 1990. Fue diseñado para competir en el mercado de...

Máquina hidráulica

Las máquinas hidráulicas utilizan energía de fluido líquido para realizar el trabajo. Los vehículos pesados ​​de construcción son un ejemplo común....
Más resultados...
Tamaño del texto:
  • Copiar
  • Editar
  • Resumir
undoredo
format_boldformat_italicformat_underlinedstrikethrough_ssuperscriptsubscriptlink
save