stringtranslate.com

lenguaje ensamblador x86


Lenguaje ensamblador x86 es el nombre de la familia de lenguajes ensambladores que proporcionan cierto nivel de compatibilidad con las CPU hasta el microprocesador Intel 8008 , que se lanzó en abril de 1972. [1] [2] Se utiliza para producir código objeto para el Clase de procesadores x86 .

Considerado como un lenguaje de programación , el ensamblador es específico de la máquina y de bajo nivel . Como todos los lenguajes ensambladores, el ensamblador x86 utiliza mnemónicos para representar instrucciones fundamentales de la CPU o código de máquina . [3] Los lenguajes ensambladores se utilizan con mayor frecuencia para aplicaciones detalladas y en las que el tiempo es crítico, como pequeños sistemas integrados en tiempo real , núcleos de sistemas operativos y controladores de dispositivos , pero también se pueden utilizar para otras aplicaciones. En ocasiones, un compilador producirá código ensamblador como paso intermedio al traducir un programa de alto nivel a código de máquina.

Palabra clave

Palabras clave reservadas del lenguaje ensamblador x86 [4] [5]

  • sud
  • les
  • lfs
  • LG
  • menos
  • estallido
  • empujar
  • en
  • En s
  • afuera
  • salidas
  • lahf
  • sahf
  • popf
  • empujar
  • cmc
  • clc
  • stc
  • cli
  • sti
  • cld
  • enfermedad de transmisión sexual
  • agregar
  • adc
  • sub
  • sbb
  • cmp
  • dic
  • prueba
  • sal
  • shl
  • sar
  • shr
  • shld
  • triturar
  • no
  • negativo
  • atado
  • y
  • o
  • xor
  • imul
  • mul
  • div
  • idiv
  • cbtw
  • cwtl
  • cwtd
  • cltd
  • daa
  • da
  • aaa
  • aas
  • soy
  • anuncio
  • esperar
  • espera
  • mov
  • cmps
  • esto
  • mucho
  • escas
  • xlat
  • reps
  • repnz
  • repz
  • llamar
  • llamar
  • retirado
  • lret
  • ingresar
  • dejar
  • jcxz
  • bucle
  • buclenz
  • loopz
  • jmp
  • ljmp
  • En t
  • en
  • irete
  • sldt
  • cadena
  • lldt
  • litros
  • verr
  • ver
  • sgdt
  • sidt
  • lgdt
  • tapado
  • SMS
  • lmsw
  • lar
  • lsl
  • clts
  • arpl
  • bsf
  • bsr
  • por cierto
  • btc
  • btr
  • bts
  • cmpxchg
  • fsin
  • fcos
  • fsincos
  • fld
  • fldcw
  • fldenv
  • prem
  • fucom
  • fucomp
  • fucompp
  • pasto
  • mover
  • movw
  • movx
  • movzb
  • popa
  • pusha
  • rcl
  • rcr
  • rol
  • ror
  • setcc
  • intercambio
  • xañadir
  • xchg
  • wbinvd
  • invd
  • invlpg
  • cerrar
  • nop
  • hlt
  • fld
  • primero
  • fstp
  • fxch
  • campo
  • puño
  • puño
  • fbld
  • fbstp
  • moda pasajera
  • faddp
  • fiadd
  • fsub
  • fsubp
  • fsubr
  • fsubrp
  • fisubrp
  • fisubr
  • fmul
  • fmulp
  • fimul
  • fdiv
  • fdivp
  • fdivr
  • fdivrp
  • fidiv
  • fidivr
  • fsqrt
  • escalaf
  • prem
  • amigo
  • extracto
  • fabulosos
  • fchs
  • fcom
  • fcomp
  • fcompp
  • ficom
  • ficomp
  • ftst
  • fxam
  • fptan
  • fpatan
  • f2xm1
  • fyl2x
  • fyl2xp1
  • fldl2e
  • fldl2t
  • fldlg2
  • fldln2
  • fldpi
  • fldz
  • finito
  • finta
  • fnop
  • guardar
  • guardar
  • estofado
  • estofado
  • fstenv
  • fnstenv
  • fstsw
  • fnstw
  • frstor
  • fclex
  • fnclex
  • fdecstp
  • libre
  • fincstp

Mnemónicos y códigos de operación

Cada instrucción ensambladora 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 llamado código de operación ; la instrucción NOP se traduce a 0x90, por ejemplo, y la instrucción HLT se traduce a 0xF4. [3] Existen posibles códigos de operación sin mnemónicos documentados que diferentes procesadores pueden interpretar de manera diferente, haciendo que un programa que los utilice 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ódigos 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 Intel y sintaxis AT&T . [6] 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 . [7] A continuación se muestra un resumen de las principales diferencias entre la sintaxis de Intel y la sintaxis de AT&T :

Muchos ensambladores x86 utilizan la sintaxis Intel , incluidos FASM , MASM , NASM , TASM y YASM. GAS , que originalmente usaba la sintaxis de AT&T , ha admitido ambas sintaxis desde la versión 2.10 a través de la .intel_syntaxdirectiva. [6] [8] [9] 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. [10]

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

Registros

Los procesadores x86 tienen una colección de registros disponibles para usarse 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: [3]

Junto a los registros generales existen adicionalmente los:

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 programador no puede acceder directamente al registro IP.

Los registros x86 se pueden utilizar mediante las instrucciones MOV . Por ejemplo, en la sintaxis de Intel:

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

Direccionamiento segmentado

La arquitectura x86 en modo real y virtual 8086 utiliza un proceso conocido como segmentación para direccionar 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 × 2 10 ) 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 y el otro para mantener el desplazamiento. Para volver a traducirlo a una dirección plana, el valor del segmento se desplaza cuatro bits hacia la izquierda (equivalente a una multiplicación por 2, 4 o 16) y luego se agrega al desplazamiento para formar la dirección completa, lo que permite romper la barrera de los 64k mediante una elección inteligente de direcciones. , aunque hace que la programación sea considerablemente más compleja.

En modo real /protegido únicamente, 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 . Por tanto, la CPU puede direccionar hasta 1.048.576 bytes (1 MB) en modo real. Combinando valores de segmento y desplazamiento encontramos una dirección de 20 bits.0xDEAD * 0x10 + 0xCAFE == 0xEB5CE

La IBM PC original restringía los programas a 640 KB, pero se utilizó una especificación de memoria ampliada para implementar un esquema de conmutación de bancos que quedó en desuso cuando los sistemas operativos posteriores, como Windows, utilizaron los rangos de direcciones más grandes de los procesadores más nuevos e implementaron su propia memoria virtual. esquemas.

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 su uso generalizado. [12] El 80286 también estaba todavía limitado a direccionar la memoria en segmentos de 16 bits, lo que significa que sólo se podía acceder a 2 16 bytes (64 kilobytes ) a la vez. Para acceder a la funcionalidad extendida del 80286, el sistema operativo configuraría el procesador en modo protegido, habilitando 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 nivel de privilegio solicitado de 2 bits ; consulte 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.

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

El Intel 80386 presentaba 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 ) hizo posible ejecutar uno o más programas en modo real en un entorno protegido que emulaba en gran medida modo real, aunque algunos programas no eran compatibles (normalmente 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 dependía de modo protegido) ya que Windows ahora podía ejecutar muchas aplicaciones a la vez, incluidas aplicaciones de DOS, mediante el uso de memoria virtual y una simple multitarea.

Modos de ejecución

Los procesadores x86 admiten cinco modos de funcionamiento para el código x86: modo real , modo protegido , modo largo , modo virtual 86 y modo de administració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 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), se encuentran 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 la memoria y el tamaño de las palabras varían, lo que requiere diferentes estrategias de programación.

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

Modos de conmutación

El procesador se ejecuta en modo real inmediatamente después del encendido, por lo que el kernel del sistema operativo , u otro programa, debe cambiar explícitamente a otro modo si desea ejecutarse en cualquier modo que no sea 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 BIOS heredado , el BIOS y el cargador de arranque se ejecutan en modo Real . El kernel del sistema operativo de 64 bits verifica y cambia la CPU al modo largo y luego inicia nuevos subprocesos en modo kernel que ejecutan código de 64 bits.

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

Tipos de instrucción

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

Instrucciones de pila

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

Al configurar un marco de pila para contener 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 de procedimiento así como un argumento de tamaño local , y puede ser más rápida que una manipulación más explícita de los registros (como push bp ; mov bp, sp ; ). Si es 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 parte 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+offset ), incluso para instrucciones como pushy pop, simplifica el uso directo de la pila para datos enteros , de punto flotante y de dirección , además de mantener las especificaciones y mecanismos de ABI relativamente simples en comparación con algunas arquitecturas RISC (requieren detalles más explícitos de la pila de llamadas).

Instrucciones ALU enteras

El ensamblado x86 tiene las operaciones matemáticas estándar, add, sub, y (para enteros con signo), con y ( negpara enteros sin signo); los operadores lógicos , , , ; aritmética y lógica de desplazamiento de bits , / (para enteros con signo), / (para enteros sin signo); rotar con y sin acarreo, / , / , un complemento de instrucciones aritméticas BCD , , , y otras.imulidivmuldiv andorxornotsalsarshlshrrclrcrrolroraaaaaddaa

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 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 todas las 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 desde 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. punto o punto flotante de 80 bits (al cargar, el valor se convierte al modo de punto flotante utilizado actualmente). x86 también incluye una serie de funciones trascendentales , incluidas seno, coseno, tangente, arcotangente, exponenciación con base 2 y logaritmos con bases 2, 10 o e .

El formato de registro de pila a registro de pila de las instrucciones suele ser o , donde equivale a , y es uno de los 8 registros de pila ( , , ..., ). Al igual que los números enteros, el primer operando es tanto el primer operando de origen como el operando de destino. y debe destacarse por intercambiar primero los operandos de origen antes de realizar la resta o 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 completada la operación. Entonces, por ejemplo, realiza el cálculo , luego lo elimina de la parte superior de la pila, obteniendo así el resultado que estaba en la parte superior de la pila .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 en muchos valores codificados en un registro SIMD amplio. Varias tecnologías de instrucción admiten diferentes operaciones en diferentes conjuntos de registros, pero tomadas como un todo completo (desde MMX hasta SSE4.2 ) incluyen cálculos generales en aritmética de números enteros o de punto flotante (suma, resta, multiplicación, desplazamiento, minimización, maximización, comparación, división o raíz cuadrada). Entonces, por ejemplo, realiza 4 sumas enteras paddw mm0, mm1paralelas de 16 bits (indicadas por ) (indicadas por ) de valores y almacena el resultado en . 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 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 multiplicada de 16 bits (útil para la combinación alfa basada en software y el filtrado digital ). . SSE (desde SSE3 ) y 3DNow! las extensiones incluyen instrucciones de suma y resta para tratar valores de punto flotante emparejados como números complejos.wpaddmm0mm1mm0

Estos conjuntos de instrucciones también incluyen numerosas instrucciones de subpalabras fijas para mezclar, insertar y extraer los valores dentro de los registros. Además, hay instrucciones para mover datos entre los registros de números enteros y los registros XMM (usados ​​en SSE)/FPU (usados ​​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 un desplazamiento, un registro escalado con o sin desplazamiento y un registro con un desplazamiento opcional y otro registro escalado. Entonces, por ejemplo, se puede codificar mov eax, [Table + ebx + esi*4]como una sola instrucción que carga 32 bits de datos de la dirección calculada como (Table + ebx + esi * 4)desplazamiento del dsselector y los almacena en el eaxregistro. En general, los procesadores x86 pueden cargar y utilizar memoria adaptada al 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 de números enteros, utilizan un " byte de modo de direccionamiento " estándar [13], a menudo llamado byte MOD-REG-R/M . [14] [15] [16] Muchas instrucciones x86 de 32 bits también tienen un byte de modo de direccionamiento SIB que sigue al byte MOD-REG-R/M. [17] [18] [19] [20] [21]

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 bytes de modo de direccionamiento) y cada registro es especial. [21] [22]

El conjunto de instrucciones x86 incluye instrucciones de carga, almacenamiento, movimiento, escaneo y comparación de cadenas ( lods,, y ) que realizan cada operación en un tamaño específico ( para bytes de 8 bits, para stospalabras de 16 bits, para palabras dobles de 32 bits). luego incrementa/disminuye (dependiendo del DF, indicador de dirección) el registro de dirección implícito ( para , para y , y ambos para y ). Para las operaciones de carga, almacenamiento y escaneo, el registro de destino/fuente/comparación implícito está en el registro o ( según el tamaño). Los registros de segmento implícitos utilizados son for y for . El registro o se utiliza como contador decreciente y la operación se detiene cuando el contador llega a cero o (para escaneos y comparaciones) cuando se detecta desigualdad. Desafortunadamente, con el paso de los años se descuidó el rendimiento de algunas de estas instrucciones y, en ciertos casos, ahora es posible obtener resultados más rápidos escribiendo los algoritmos usted mismo. 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 recientes y respetados antes de optar por utilizar una instrucción particular de este grupo.movsscascmpsbwdsilodsdistosscasmovscmpsalaxeaxdssiesdicxecx

La pila es una región de la memoria y un 'puntero de pila' asociado, que apunta a la parte inferior de la pila. El puntero de la pila disminuye cuando se agregan elementos ("push") y aumenta después de que se eliminan ("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 la pila en realidad apunta al último valor que se almacenó, bajo el supuesto de que su tamaño coincidirá con el modo operativo del procesador (es decir, 16, 32 o 64 bits) para coincidir con el ancho predeterminado de las instrucciones push/// . También se incluyen las instrucciones que reservan y eliminan datos de la parte superior de la pila mientras se configura un puntero de marco de pila en // . Sin embargo, también se admite la configuración directa o la suma y resta del registro // , por lo que las instrucciones / generalmente son innecesarias.popcallretenterleavebpebprbpspesprspenterleave

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 ; Guarde el puntero del marco de pila de la función de llamada (registro rbp) mov rbp , rsp ; Haga un nuevo marco de pila debajo de la pila de nuestra persona que llama sub rsp , 32 ; Reserve 32 bytes de espacio de pila para las variables locales de esta función. ; Las variables locales estarán por debajo de rbp y se puede hacer referencia a ellas en relación con rbp ; nuevamente, es mejor para facilitar la depuración, pero para obtener el mejor rendimiento, rbp no lo hará ; no se utilizará en absoluto y se hará referencia a las variables locales en relación con rsp ; porque, además de guardar el código, rbp es gratuito para otros usos. …… ; _ Sin embargo, si se modifica rbp aquí, su valor debe conservarse para la persona que llama. mov [ rbp - 8 ], rdx ; Ejemplo de acceso a una variable local, desde la ubicación de la memoria al 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 supone que los valores para una carga o almacenamiento SIMD están empaquetados en posiciones adyacentes para el registro SIMD y los 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 instrucciones de "búsqueda previa" que realizan la carga pero no apuntan a ningún registro y se utilizan 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á almacenado en caché (de lo contrario, se comportará como un almacén normal).

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

Flujo de programa

El ensamblado x86 tiene una operación de salto incondicional, jmpque puede tomar como parámetro una dirección inmediata, un registro o una dirección indirecta (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, incluidos jz(saltar a cero), jnz(saltar a distinto de cero), jg(saltar a mayor que, con signo), jl(saltar a menos que, con signo), ja(saltar a arriba/mayor que, sin signo) , jb(saltar debajo/menos 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. La comparación cmp(comparar) y testlas instrucciones configuran 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 transporte) y cmc(complementar bandera de transporte) que funcionan directamente en las banderas. Las comparaciones de coma flotante se realizan mediante instrucciones fcomo ficomque eventualmente deben convertirse en indicadores de números enteros.

Cada operación de salto tiene tres formas diferentes, según el tamaño del operando. Un salto corto utiliza un operando de 8 bits con signo, 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 lejano es aquel que utiliza la base del segmento completo: valor de compensación como dirección absoluta. También existen 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 una subrutina). Antes de transferir el control a la subrutina, callempuja la dirección de desplazamiento del segmento de la instrucción que sigue a callla pila; retsaca este valor de la pila y salta a él, devolviendo efectivamente el flujo de control a esa parte del programa. En el caso de a far call, la base del segmento se empuja siguiendo el desplazamiento; far retmuestra 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 far call, excepto que en lugar de una dirección, utiliza un vector de interrupción , un índice en una tabla de manejador de interrupciones. direcciones. Normalmente, el controlador de interrupciones guarda todos los demás registros de la CPU que utiliza, a menos que se utilicen para devolver el resultado de una operación al programa que realiza la llamada (en el software llamado interrupciones). El retorno coincidente de la instrucción de interrupción es iret, que restaura las banderas después del regreso. Algunos sistemas operativos utilizan interrupciones suaves del tipo descrito anteriormente para llamadas al sistema y también se pueden usar para depurar controladores de interrupciones duras. 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 el llamado tipo de sintaxis Intel tal como lo utilizan los ensambladores Microsoft MASM, NASM y muchos otros. (Nota: también existe una versión alternativa de sintaxis de AT&T en la que se intercambia 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 y enviarlo a la pantalla; otros ejemplos usan la rutina C printf() de libc para escribir en stdout . Tenga en cuenta que el primer ejemplo es un ejemplo de hace 30 años que utiliza el modo de 16 bits como en un Intel 8086. El segundo ejemplo es el código Intel 386 en modo de 32 bits. El código moderno estará en modo de 64 bits. [24]

.modelo pequeño .stack 100h  .data msg db '¡Hola mundo!$'.código de inicio: mov ax , @DATA ; Inicializa el segmento de datos mov ds , ax mov ah , 09h ; Establece el registro de 8 bits 'ah', el byte superior del registro ax, en 9, en ; seleccione un número de subfunción de una rutina de MS-DOS llamada a continuación ; a través de la interrupción del software int 21h para mostrar un mensaje lea dx , msg ; Toma la dirección del mensaje, almacena la dirección en un registro de 16 bits dx int 21h ; Se pueden llamar varias rutinas de MS-DOS mediante la interrupción del software 21h ; Nuestra subfunción requerida se configuró en el registro ah arriba               mov hacha , 4C00h ; Establece el registro ax en el número de subfunción para el software de MS-DOS ; interrupción int 21h para el servicio 'terminar programa'. en 21h ; La llamada a este servicio MS-DOS nunca vuelve, ya que finaliza el programa.    fin comienzo 

"¡Hola Mundo!" programa para Windows en ensamblaje estilo MASM

; requiere el interruptor /coff en 6.15 y versiones anteriores .386 .model pequeño , c .stack 1000h  .data msg db "¡Hola mundo!" , 0  .code includelib libcmt.lib includelib libvcruntime.lib includelib libucrt.lib includelib Legacy_stdio_definitions.lib    extrn printf : cerca salida extrn : cerca  public main main proc push offset msg call printf push 0 llamada salir main endp            fin

"¡Hola Mundo!" programa para Windows en ensamblaje estilo NASM

; Base de imagen = 0x00400000 %definir RVA(x) (x-0x00400000) sección .text push dword hola call dword [ printf ] push byte + 0 call dword [ exit ] ret         sección .data hola db "¡Hola mundo!"   sección .idata dd RVA ( msvcrt_LookupTable ) dd - 1 dd 0 dd RVA ( msvcrt_string ) dd RVA ( msvcrt_imports ) multiplicado por 5 dd 0 ; finaliza la tabla de descriptores          msvcrt_string dd "msvcrt.dll" , 0 msvcrt_LookupTable: dd RVA ( msvcrt_printf ) dd RVA ( msvcrt_exit ) dd 0      msvcrt_imports: printf dd RVA ( msvcrt_printf ) salir dd RVA ( msvcrt_exit ) dd 0     msvcrt_printf: dw 1 dw "printf" , 0 msvcrt_exit: dw 2 dw "salir" , 0 dd 0       

.datos ; sección para datos inicializados str: .ascii "¡Hola, mundo!\n" ; defina una cadena de texto que contenga "¡Hola, mundo!" y luego una nueva línea. str_len = . -cadena ; _ obtener la longitud de str restando su dirección         .texto ; sección para funciones del programa .globl _start ; exportar la función _start para que pueda ejecutarse _start: ; comience la función _start movl $4 , %eax ; especifique la instrucción para 'sys_write' movl $1 , %ebx ; especifique la salida a la salida estándar, 'stdout' movl $str , %ecx ; especifique el texto generado en nuestra cadena definida movl $str_len , %edx ; especifique la cantidad de caracteres a escribir como la longitud de nuestra cadena definida. int $0x80 ; llame a una interrupción del sistema para iniciar la llamada al sistema que hemos creado.                        movl $1 , %eax ; especifique la instrucción para 'sys_exit' movl $0 , %ebx ; especifique el código de salida en 0, lo que significa éxito int $0x80 ; llamar a otra interrupción del sistema para finalizar el programa          

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

; ; Este programa se ejecuta en modo protegido de 32 bits. ; construir: nasm -f elf -F apuñala nombre.asm ; enlace: ld -o nombre nombre.o ; ; En el 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 cambie "-f elf" por "-f elf64" en el comando de compilación. ; sección .datos ; sección para datos inicializados str: db '¡Hola mundo!' , 0Ah ; cadena de mensaje con carácter de nueva línea al final (10 decimal) str_len: equ $ - str ; calcula la longitud de la cadena (bytes) restando la dirección inicial de la cadena ; de 'aquí, esta dirección' (símbolo '$' que significa 'aquí')            sección .texto ; esta es la sección de código (texto del programa) en la memoria global _start ; _start es el punto de entrada y necesita un alcance global para ser "visto" por el ; enlazador --equivalente a main() en C/C++ _start: ; La definición del procedimiento _start comienza aquí mov eax , 4 ; especifique el código de función sys_write (de la tabla de vectores del sistema operativo) mov ebx , 1 ; especifique el descriptor de archivo stdout --en gnu/linux, todo se trata como un archivo ; incluso los dispositivos de hardware mov ecx , str ; mover la _dirección_ inicial del mensaje de cadena al registro ecx mov edx , str_len ; mover longitud del mensaje (en bytes) int 80h ; interrumpir el kernel para realizar la llamada al sistema que acabamos de configurar - ; en gnu/linux los servicios se solicitan a través del kernel mov eax , 1 ; especifique el código de función sys_exit (de la tabla de vectores del sistema operativo) mov ebx , 0 ; especifique el código de retorno para el sistema operativo (cero le dice al sistema operativo que todo salió bien) int 80h ; interrumpir el kernel para realizar una llamada al sistema (para salir)                      

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

"¡Hola Mundo!" programa para Linux en ensamblaje estilo NASM usando la biblioteca estándar C

; ; Este programa se ejecuta en modo protegido de 32 bits. ; gcc vincula la biblioteca estándar C de forma predeterminada; construir: nasm -f elf -F apuñala nombre.asm ; enlace: gcc -o nombre nombre.o ; ; En el 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 cambie "-f elf" por "-f elf64" en el comando de compilación. ; principal global ; 'principal' debe estar definido, tal como se está compilando ; contra la biblioteca estándar C extern printf ; declara el uso de un símbolo externo, como printf ; printf se declara en un módulo de objeto diferente. ; El vinculador resuelve este símbolo más adelante.         segmento .datos ; sección para la cadena de datos inicializada db '¡Hola mundo!' , 0Ah , 0 ; cadena de mensaje que termina con un carácter de nueva línea (10 ; decimal) y el terminador 'NUL' de cero bytes ; 'cadena' ahora se refiere a la dirección inicial ; en el que se almacena 'Hola, mundo'.          segmento .text principal: empujar cadena ; Empuje la dirección de 'cadena' en la pila. ; Esto reduce esp en 4 bytes antes de almacenarlo ; la 'cadena' de dirección de 4 bytes en la memoria en ; el nuevo esp, el nuevo fondo de la pila.        ; Este será un argumento para printf() llamar a printf ; llama a la función C printf(). añadir esp , 4 ; Aumenta el puntero de pila en 4 para volver a colocarlo ; a donde estaba antes del 'empujón', que ; lo redujo en 4 bytes. retirarse ; Regrese a nuestra persona que llama.           

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

Este ejemplo está en modo moderno de 64 bits.

; construir: nasm -f elf64 -F enano hola.asm ; enlace: ld -o hola hola.oRELACIÓN PREDETERMINADA ; use modos de direccionamiento relativos a RIP de forma predeterminada, por lo que [foo] = [rel foo]  SECCIÓN .rodata ; los datos de sólo lectura deben ir en la sección .rodata en GNU/Linux, como .rdata en Windows Hello: db "¡Hola mundo!" , 10 ; Terminando con un byte 10 = nueva línea (ASCII LF) len_Hello: equ $ - Hola ; Obtenga NASM para calcular la longitud como una constante de tiempo de ensamblaje ; el símbolo '$' significa 'aquí'. write() toma una longitud tal que ; No se necesita una cadena estilo C terminada en cero. ; Sería para C puts()         SECCIÓN .rodata ; los datos de solo lectura pueden ir en la sección .rodata en GNU/Linux, como .rdata en Windows Hello: db "¡Hola mundo!" , 10 ; 10 = `\n`. len_Hola: equ $ - Hola ; obtenga NASM para calcular la longitud como una constante de tiempo de ensamblaje ;; write() toma una longitud por lo que no se necesita una cadena estilo C terminada en 0. seria para puestas     SECCIÓN .texto global _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 Hola ] ; x86-64 usa LEA relativa a RIP para colocar direcciones estáticas en regs mov rdx , len_Hello ; size_t count = len_Hello syscall ; escribir(1, Hola, len_Hola); llamar al kernel para realizar la llamada al sistema ;; valor de retorno en RAX. RCX y R11 también se sobrescriben con syscall           movimiento 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 es siempre, con diferencia, el método más rápido. ; Cuando se almacena un valor de 32 bits, por ejemplo, en edx, los bits altos 63:32 son ; también se pone a cero automáticamente en todos los casos. Esto le ahorra tener que configurar ; 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 (salir normalmente) syscall ; _salir(0)            

Ejecutarlo straceverifica 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 realizar enlaces dinámicos . Pero este es un ejecutable estático porque lo vinculamos usando ld sin -pie ni ninguna biblioteca compartida; las únicas instrucciones que se ejecutan en el espacio del usuario son las que usted proporciona.

$ strace  ./hello  >  /dev/null # sin redirección, la salida estándar de su programa se mezcla con el inicio de sesión de strace en stderr. Lo cual normalmente está bien execve("./hello", ["./hello"], 0x7ffc8b0b3570 /* 51 vars */) = 0 write(1, "¡Hola mundo!\n", 13) = 13 exit(0) = ? +++ salió con 0 +++ 

Usando el registro de banderas

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

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

Aparte 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 están 'fusionadas' 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 utiliza en la arquitectura x86 para activar y desactivar ciertas funciones o modos de ejecución. Por ejemplo, para desactivar todas las interrupciones enmascarables, puede utilizar 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 ahusando la lahfinstrucción. Todo el registro de banderas 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 registro independiente de tipo 'banderas' para la palabra de estado fp. En la década de 1990, acceder a los bits de bandera en este registro era un procedimiento incómodo y lento, pero en los procesadores modernos existen instrucciones para 'comparar dos valores de punto flotante' que se pueden usar con las instrucciones normales de salto/bifurcación condicionales directamente sin ningún paso intermedio. .

Usando el registro de puntero de instrucción

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 de 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 o 32 bits, pero se puede escribir una secuencia como la siguiente para colocar la dirección en next_line( eaxcódigo de 32 bits):

llamar a next_line next_line: pop eax

Escribir en el puntero de instrucción es simple: 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):

jmp rax

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.

Ver también

Referencias

  1. ^ "Familia de microprocesadores Intel 8008 (i8008)". www.cpu-world.com . Consultado el 25 de marzo de 2021 .
  2. ^ "Intel 8008". MUSEO DE LA CPU - MUSEO DE MICROPROCESADORES Y FOTOGRAFÍA DE Matrices . Consultado el 25 de marzo de 2021 .
  3. ^ abc "CÓDIGOS DE OPCIÓN Intel 8008". www.pastraiser.com . Consultado el 25 de marzo de 2021 .
  4. ^ "Referencia en lenguaje ensamblador". www.ibm.com . Consultado el 28 de noviembre de 2022 .
  5. ^ "Manual de referencia del lenguaje ensamblador x86" (PDF) .
  6. ^ abcde Narayam, Ram (17 de octubre de 2007). "Ensambladores de Linux: una comparación de GAS y NASM". IBM . Archivado desde el original el 3 de octubre de 2013 . Consultado el 2 de julio de 2008 .
  7. ^ "La creación de Unix". Archivado desde el original el 2 de abril de 2014.
  8. ^ Hyde, Randall. "¿Qué ensamblador es el mejor?" . Consultado el 18 de mayo de 2008 .
  9. ^ "GNU Assembler News, v2.1 admite la sintaxis de Intel". 2008-04-04 . Consultado el 2 de julio de 2008 .
  10. ^ "i386-Bugs (usando como)". Documentación de Binutils . Consultado el 15 de enero de 2020 .
  11. ^ "Manual de programación en lenguaje ensamblador Intel 8080" (PDF) . Consultado el 12 de mayo de 2023 .
  12. ^ Mueller, Scott (24 de marzo de 2006). "Procesadores P2 (286) de segunda generación". Actualización y reparación de PC, 17.a edición (libro) (17 ed.). What. ISBN  0-7897-3404-4. Consultado el 6 de diciembre de 2017 .
  13. ^ Prado de Curtis. "Codificación de instrucciones 8086".
  14. ^ Ígor Jolodov. "6. Codificación de operandos de instrucción x86, byte MOD-REG-R/M".
  15. ^ "Instrucciones de codificación x86".
  16. ^ Michael Abrash. "Zen del lenguaje ensamblador: Volumen I, Conocimiento". "Capítulo 7: Direccionamiento de la memoria". Sección "Direccionamiento mod-reg-rm".
  17. ^ Manual del programador de referencia Intel 80386. "17.2.1 Bytes ModR/M y SIB"
  18. ^ "Codificación de instrucciones X86-64: bytes ModR/M y SIB"
  19. ^ "Figura 2-1. Formato de instrucción de las arquitecturas Intel 64 e IA-32".
  20. ^ "Direccionamiento x86 bajo el capó".
  21. ^ ab Stephen McCamant. "Ingeniería inversa binaria manual y automatizada".
  22. ^ "Lista de deseos de instrucciones X86".
  23. ^ Peter Cordes (18 de diciembre de 2011). "Sintaxis de NASM (Intel) versus AT&T: ¿cuáles son las ventajas?". Desbordamiento de pila .
  24. ^ "Acabo de empezar el montaje". daniweb.com . 2008.

Otras lecturas

Manuales

Libros