El lenguaje ensamblador x86 es el nombre de la familia de lenguajes ensambladores que proporcionan cierto nivel de compatibilidad con las CPU desde el microprocesador Intel 8008 , que se lanzó en abril de 1972. [1] [2] Se utiliza para producir código objeto para la clase de procesadores x86 .
Considerado como un lenguaje de programación , el lenguaje ensamblador es específico de la máquina y de bajo nivel . Como todos los lenguajes ensambladores, el lenguaje ensamblador x86 utiliza mnemotecnia 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 críticas en el tiempo, como pequeños sistemas embebidos en tiempo real , núcleos de sistemas operativos y controladores de dispositivos , pero también se pueden utilizar para otras aplicaciones. A veces, un compilador producirá código ensamblador como paso intermedio al traducir un programa de alto nivel a código de máquina.
Palabras clave reservadas del lenguaje ensamblador x86 [4] [5]
Cada instrucción de ensamblaje x86 se representa mediante un mnemónico 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 a 0x90, por ejemplo, y la instrucción HLT se traduce a 0xF4. [3] Hay códigos de operación potenciales sin mnemónico documentado que diferentes procesadores pueden interpretar de manera diferente, haciendo que un programa que los use se comporte de manera inconsistente o incluso genere una excepción en algunos procesadores. Estos códigos de operación a menudo aparecen en competiciones 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 mostrar la destreza del autor.
El lenguaje ensamblador x86 tiene dos ramas sintácticas principales: la sintaxis Intel y la sintaxis AT&T . [6] La sintaxis Intel es dominante en el mundo DOS y Windows , y la sintaxis AT&T es dominante en el mundo Unix , ya que Unix fue creado en AT&T Bell Labs . [7] 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 Intel , incluidos FASM , MASM , NASM , TASM y YASM. GAS , que originalmente utilizaba la sintaxis AT&T , ha admitido ambas sintaxis desde la versión 2.10 a través de la .intel_syntax
directiva. [6] [8] [9] Una peculiaridad de la sintaxis AT&T para x86 es que los operandos x87 están invertidos, un error heredado del ensamblador AT&T original. [10]
La sintaxis de AT&T es casi universal para todas las demás arquitecturas (mantiene el mismo mov
orden); 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 , que es anterior al x86, también utiliza el orden "destino primero" para mov
. [11]
Los procesadores x86 tienen una colección de registros disponibles para usarse como almacenamiento 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 con los registros generales existen además los siguientes:
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:
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
La arquitectura x86 en modo 8086 real y virtual 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 a qué distancia de esta dirección inicial se encuentra la dirección deseada. En el direccionamiento segmentado, se requieren dos registros para una dirección de memoria completa. Uno para almacenar el segmento, el otro para almacenar el desplazamiento. Para traducir de nuevo a una dirección plana, el valor del segmento se desplaza cuatro bits a la izquierda (equivalente a la multiplicación por 2 4 o 16) y luego se suma 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 solamente, por ejemplo, si DS contiene el número hexadecimal 0xDEAD y DX contiene el número 0xCAFE, juntos apuntarán a la dirección de memoria . Por lo tanto, la CPU puede direccionar hasta 1.048.576 bytes (1 MB) en modo real. Al combinar los 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 expandida 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 grandes 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. [12] 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 2 16 bytes (64 kilobytes ) a la vez. Para acceder a la funcionalidad extendida del 80286, el sistema operativo pondría el procesador en modo protegido, lo que permitiría el direccionamiento de 24 bits y, por lo tanto, 2 24 bytes de memoria (16 megabytes ).
En el 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 la 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 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.
Hay algunas combinaciones especiales de registros de segmento y registros generales que apuntan a direcciones importantes:
El Intel 80386 ofrecía 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 el 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ística 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 del modo protegido) ya que Windows ahora podía ejecutar muchas aplicaciones a la vez, incluidas aplicaciones DOS, mediante el uso de memoria virtual y multitarea simple.
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 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 los 8086, 8088, 80186, 80188 y 80286. Estas instrucciones están disponibles en modo real en todos los procesadores x86, y en el modo protegido de 16 bits ( 80286 en adelante), 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 funciones. El SMM, con algunas de sus propias instrucciones especiales, está disponible en algunas CPU Intel i386SL, i486 y posteriores. Por último, en el 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:
El procesador se ejecuta en modo real inmediatamente después de encenderlo, por lo que el núcleo de un sistema operativo u otro programa debe cambiar explícitamente a otro modo si desea ejecutarse en cualquier otro modo que no sea el real. El cambio de modo 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.
En una computadora que ejecuta BIOS heredado , el BIOS y el cargador de arranque se ejecutan en modo Real . El núcleo 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 Option ROM heredada ), el cargador de arranque UEFI y el kernel del sistema operativo UEFI se ejecutan en modo largo.
En general, las características del conjunto de instrucciones x86 moderno son:
ebp
registros generales (contando) en modo de 32 bits y los quince rbp
registros generales (contando) en modo de 64 bits se pueden usar 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.xchg
( , cmpxchg
/ cmpxchg8b
, xadd
, e instrucciones enteras que se combinan con el lock
prefijo)La arquitectura x86 tiene soporte de hardware para un mecanismo de pila de ejecución . Las instrucciones como push
, pop
, call
y ret
se utilizan con la pila correctamente configurada para pasar parámetros, asignar espacio para datos locales y guardar y restaurar puntos de retorno de llamada. La instrucción ret
size es muy útil para implementar convenciones de llamada que ahorran espacio (y son rápidas) donde el receptor de la 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 enter
instrucció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
; ). Que sea 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 de programa en particular; la mayoría del código x86 está destinado a 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 opciones de diseño a nivel de compuerta y transistor variables .sub sp, size
La gama completa de modos de direccionamiento (incluidos inmediato y base+desplazamiento ), incluso para instrucciones como push
y pop
, hace que el uso directo de la pila para datos enteros , de punto flotante y de dirección sea simple, además de mantener las especificaciones y los mecanismos de ABI relativamente simples en comparación con algunas arquitecturas RISC (requieren detalles de pila de llamadas más explícitos).
El ensamblaje x86 tiene las operaciones matemáticas estándar, add
, sub
, neg
, imul
y idiv
(para enteros con signo), con mul
y div
(para enteros sin signo); los operadores lógicos and
, 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
, un complemento de las instrucciones aritméticas BCDaaa
, , aad
, daa
y otras.
El lenguaje ensamblador x86 incluye instrucciones para una unidad de coma flotante (FPU) basada en pila. La FPU era un coprocesador independiente opcional para los modelos 8086 a 80386, era una opción integrada en el chip para la serie 80486 y es una característica estándar en todas las CPU x86 de Intel 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 números 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, coma flotante de 32 bits, coma flotante de 64 bits o coma flotante de 80 bits (al cargar, el valor se convierte al modo de coma flotante utilizado actualmente). x86 también incluye una serie de funciones trascendentales , entre ellas seno, coseno, tangente, arcotangente, exponenciación con base 2 y logaritmos con base 2, 10 o e .
El formato de registro de pila a registro de pila de las instrucciones es usualmente o , donde es equivalente 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 de destino. y debe ser señalado como el primero en intercambiar 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 hacen estallar la parte superior de la pila después de que su operación se completa. Entonces, por ejemplo, realiza el cálculo , luego elimina de la parte superior de la pila, haciendo así que lo que era el resultado en la parte superior de la pila en .fop st, st(n)
fop st(n), st
st
st(0)
st(n)
st(0)
st(1)
st(7)
fsubr
fdivr
faddp st(1), st
st(1) = st(1) + st(0)
st(0)
st(1)
st(0)
Las CPU x86 modernas contienen instrucciones SIMD , que realizan en gran medida la misma operación en paralelo en muchos valores codificados en un amplio registro SIMD. Varias tecnologías de instrucciones admiten diferentes operaciones en diferentes conjuntos de registros, pero tomadas como un todo completo (desde MMX hasta SSE4.2 ) incluyen cálculos generales sobre 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). Por ejemplo, paddw mm0, mm1
realiza 4 sumas de números enteros (indicadas por ) de 16 bits en paralelo 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 realmente el primer valor de los registros (expandido en SSE2 ). Se han agregado algunas otras instrucciones inusuales, incluida una suma de diferencias absolutas (usada 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 la combinación alfa basada en software y el filtrado digital ). Las extensiones SSE (desde SSE3 ) y 3DNow! 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 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).
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. Por ejemplo, se puede codificar mov eax, [Table + ebx + esi*4]
como una única instrucción que carga 32 bits de datos desde la dirección calculada como (Table + ebx + esi * 4)
desplazamiento desde el ds
selector y los almacena en el eax
registro. En general, los procesadores x86 pueden cargar y utilizar 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 de 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 de modo de direccionamiento, esas instrucciones son ortogonales porque cualquiera de esos códigos de operación se puede 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 byte 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
, stos
, movs
, scas
y cmps
) que realizan cada operación en un tamaño especificado ( b
para byte de 8 bits, w
para palabra de 16 bits, d
para palabra doble de 32 bits) y luego incrementan/decrementan (dependiendo de DF, indicador de dirección) el registro de dirección implícito ( si
for lods
, di
for stos
y scas
, y ambos for movs
y cmps
). Para las operaciones de carga, almacenamiento y escaneo, el registro de destino/origen/comparación implícito está en el registro al
, ax
o eax
(dependiendo del tamaño). Los registros de segmento implícitos utilizados son ds
for si
y es
for di
. El registro cx
or ecx
se utiliza como un 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, 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 de ellas ahora tienen un rendimiento muy respetable, por lo que se recomienda que el programador lea artículos de evaluación comparativa recientes y respetados antes de elegir utilizar una instrucción particular de este grupo.
La pila es una región de memoria y un 'puntero de pila' asociado, que apunta a la parte inferior de la pila. El puntero de pila se decrementa cuando se agregan elementos ('push') y se incrementa después de que se eliminan cosas ('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 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
/ pop
/ call
/ ret
. También se incluyen las instrucciones enter
y leave
que reservan y eliminan datos de la parte superior de la pila mientras se configura un puntero de marco de pila en bp
/ ebp
/ rbp
. Sin embargo, también se admite la configuración directa, o la adición y resta al registro sp
/ esp
/ rsp
, por lo que las instrucciones 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:
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 del sub de pila de nuestro llamador 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 puede hacer referencia a ellas en relación con rbp, ; nuevamente, lo mejor para facilitar la depuración, pero para un mejor rendimiento, ; rbp no se usará en absoluto, y las variables locales se referenciarían en relación con rsp ; porque, además de guardar el código, rbp queda libre para otros usos. … … ; Sin embargo, si rbp se altera aquí, su valor debe conservarse para el llamador. mov [ rbp - 8 ], rdx ; Ejemplo de acceso a una variable local, desde la ubicación de 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/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 SSE requieren una alineación de 16 bytes para funcionar correctamente. Los conjuntos de instrucciones SIMD también incluyen instrucciones de "prefetch" que realizan la carga pero no apuntan a ningún registro, que se utilizan para la carga de caché. Los conjuntos de instrucciones SSE también incluyen instrucciones de almacenamiento no temporal 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 almacenamiento normal).
La mayoría de las instrucciones genéricas de números enteros y de punto flotante (pero no SIMD) pueden utilizar un parámetro como dirección compleja como segundo parámetro de origen. Las instrucciones de números enteros también pueden aceptar un parámetro de memoria como operando de destino.
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
(saltar en cero), jnz
(saltar en distinto de cero), jg
(saltar en mayor que, con signo), jl
(saltar en menor que, con signo), ja
(saltar en mayor/mayor que, sin signo), jb
(saltar en menor/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 activan, borran o complementan estos indicadores según su resultado. Las instrucciones de comparación cmp
(compare) y testactivan 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 funcionan directamente en los indicadores. Las comparaciones de punto flotante se realizan mediante instrucciones fcom
o ficom
que eventualmente deben convertirse en indicadores 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 lejano es aquel que utiliza el valor base:offset del segmento completo como una 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 la subrutina). Antes de transferir el control a la subrutina, call
inserta la dirección de desplazamiento del segmento de la instrucción que sigue a la call
en 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 far call
, se inserta la base del segmento después del desplazamiento; far ret
extrae el desplazamiento y luego la base del segmento para regresar.
También hay dos instrucciones similares, int( interrupción ), que guarda el valor del registro (E)FLAGSfar call
actual en la pila, luego realiza una , excepto que en lugar de una dirección, utiliza un vector de interrupción , un índice en una tabla de direcciones de manejadores de interrupciones. Normalmente, el manejador de interrupciones guarda todos los demás registros de CPU que utiliza, a menos que se utilicen para devolver el resultado de una operación al programa que realiza la llamada (en software llamado interrupciones). El retorno correspondiente de la instrucción de interrupción es iret
, que restaura los indicadores después de regresar. Algunos sistemas operativos utilizan interrupciones suaves del tipo descrito anteriormente para llamadas del sistema , y también se pueden utilizar para depurar manejadores 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.
Los siguientes ejemplos utilizan la denominada sintaxis Intel , tal como la utilizan los ensambladores Microsoft MASM, NASM y muchos otros. (Nota: también existe una sintaxis AT&T alternativa en la que se intercambia el orden de los operandos de origen y destino, entre muchas otras diferencias). [23]
Uso de la instrucción de interrupción de software 21h para llamar al sistema operativo MS-DOS para la salida a la pantalla; otros ejemplos utilizan la rutina printf() de C 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 de Intel 386 en modo de 32 bits. El código moderno estará en modo de 64 bits. [24]
.modelo pequeño .pila 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 alto del registro ax, en 9, para ; seleccionar un número de subfunción de una rutina MS-DOS llamada a continuación ; a través de la interrupción de software int 21h para mostrar un mensaje lead dx , msg ; Toma la dirección de msg, almacena la dirección en el registro de 16 bits dx int 21h ; Varias rutinas MS-DOS se pueden llamar mediante la interrupción de software 21h ; Nuestra subfunción requerida se estableció en el registro ah anterior mov ax , 4C00h ; Establece el registro ax en el número de subfunción del software MS-DOS ; interrupción int 21h para el servicio 'terminar programa'. int 21h ; Llamar a este servicio MS-DOS nunca retorna, ya que finaliza el programa. fin inicio
; requiere el interruptor /coff en 6.15 y versiones anteriores .386 .model small , c .stack 1000h .data msg db "¡Hola mundo!" , 0 .código includelib libcmt.lib includelib libvcruntime.lib includelib libucrt.lib includelib legacy_stdio_definitions.lib extrn printf : cerca de extrn exit : cerca de público principal principal proc push offset msg llamada printf push 0 llamada salida principal endp fin
; Base de la imagen = 0x00400000 %define RVA(x) (x-0x00400000) sección .text push dword hola call dword [ printf ] push byte + 0 call dword [ exit ] ret sección .data hello db "¡Hola mundo!" sección .idata dd RVA ( msvcrt_LookupTable ) dd - 1 dd 0 dd RVA ( msvcrt_string ) dd RVA ( msvcrt_imports ) veces 5 dd 0 ; finaliza la tabla de descriptores msvcrt_string dd "msvcrt.dll" , 0 tabla de búsqueda 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
.data ; sección para datos inicializados str: .ascii "Hola, mundo!\n" ; define una cadena de texto que contiene "¡Hola, mundo!" y luego una nueva línea. str_len = . - str ; obtiene la longitud de str restando su dirección .text ; sección para funciones del programa .globl _start ; exporta la función _start para que pueda ejecutarse _start: ; comienza la función _start movl $4 , %eax ; especifica la instrucción para 'sys_write' movl $1 , %ebx ; especifica la salida a la salida estándar, 'stdout' movl $str , %ecx ; especifica el texto de salida a nuestra cadena definida movl $str_len , %edx ; especifica la cantidad de caracteres a escribir como la longitud de nuestra cadena definida. int $0x80 ; llama a una interrupción del sistema para iniciar la llamada al sistema que hemos creado. movl $1 , %eax ; especifica la instrucción 'sys_exit' movl $0 , %ebx ; especifica el código de salida a 0, lo que significa éxito int $0x80 ; llama a otra interrupción del sistema para finalizar el programa
; ; 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 el modo long de 64 bits puede usar registros de 64 bits (por ejemplo, rax en lugar de eax, rbx en lugar de ebx, etc.) ; Cambie también "-f elf" por "-f elf64" en el comando de compilación. ; sección .data ; 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 de inicio de la cadena ; de 'aquí, esta dirección' (símbolo '$' que significa 'aquí') sección .text ; esta es la sección de código (texto del programa) en 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 ; especifica el código de la función sys_write (de la tabla de vectores del SO) mov ebx , 1 ; especifica el descriptor de archivo stdout --en gnu/linux, todo se trata como un archivo, ; incluso los dispositivos de hardware mov ecx , str ; mueve la _dirección_ inicial del mensaje de cadena al registro ecx mov edx , str_len ; mueve la longitud del mensaje (en bytes) int 80h ; interrumpe el núcleo para realizar la llamada al sistema que acabamos de configurar - ; en gnu/linux los servicios se solicitan a través del núcleo mov eax , 1 ; especifica el código de función sys_exit (de la tabla de vectores del SO) mov ebx , 0 ; especifica el código de retorno para el SO (cero le indica al SO que todo salió bien) int 80h ; interrumpe 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.
; ; Este programa se ejecuta en modo protegido de 32 bits. ; gcc vincula la biblioteca C estándar de forma predeterminada; build: nasm -f elf -F stabs name.asm ; link: gcc -o name name.o ; ; En el modo long 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 definirse, ya que se está compilando ; contra la biblioteca estándar de C extern printf ; declara el uso de un símbolo externo, como printf ; printf se declara en un módulo de objeto diferente. ; El enlazador resuelve este símbolo más tarde. segmento .data ; 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 de byte cero 'NUL' ; 'cadena' ahora se refiere a la dirección de inicio ; en la que se almacena 'Hola, mundo'. segmento .text principal: inserta cadena ; Inserta la dirección de 'cadena' en la pila. ; Esto reduce esp en 4 bytes antes de almacenar ; la dirección de 4 bytes 'cadena' en la memoria en ; el nuevo esp, la nueva parte inferior de la pila. ; Este será un argumento para la llamada printf () ; llama a la función printf() de C. add esp , 4 ; Aumenta el puntero de pila en 4 para volver a colocarlo ; en donde estaba antes del 'push', que ; lo redujo en 4 bytes. ret ; Regresa a nuestro llamador.
Este ejemplo está en modo moderno de 64 bits.
; compilación: nasm -f elf64 -F dwarf hola.asm ; enlace: ld -o hola hola.oREL PREDETERMINADO ; utiliza modos de direccionamiento relativos a RIP de manera predeterminada, 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 Hola: db "¡Hola mundo!" , 10 ; Terminando con un byte 10 = nueva línea (ASCII LF) len_Hello: equ $ - Hola ; Consigue 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 .rodata ; los datos de solo lectura pueden ir en la sección .rodata en GNU/Linux, como .rdata en Windows Hola: db "Hello world!" , 10 ; 10 = `\n`. len_Hello: equ $ - Hola ; hace que NASM calcule la longitud como una constante de tiempo de ensamblaje ;; write() toma una longitud, por lo que no se necesita una cadena de estilo C terminada en 0. Sería para puts SECCIÓN .texto global _start _start: mov eax , 1 ; __NR_write número de llamada al sistema desde Linux asm/unistd_64.h (x86_64) mov edi , 1 ; int fd = STDOUT_FILENO lea rsi , [ rel Hello ] ; x86-64 usa LEA relativo a RIP para poner direcciones estáticas en regs mov rdx , len_Hello ; size_t count = len_Hello syscall ; write(1, Hello, len_Hello); llamada al núcleo para hacer realmente la llamada al sistema ;; valor de retorno en RAX. RCX y R11 también son sobrescritos por syscall mov 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 idioma común preferido para poner a cero un registro, y siempre es, por lejos, 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 a cero automáticamente en todos los casos. Esto le ahorra tener que configurar 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, stracese verifica que no se realicen llamadas adicionales al sistema en el proceso. La versión printf realizaría muchas más llamadas al sistema para inicializar libc y realizar enlaces dinámicos . Pero este es un ejecutable estático porque vinculamos usando ld sin -pie ni ninguna biblioteca compartida; 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 su 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) = ? +++ exited with 0 +++
En la arquitectura x86, los indicadores se utilizan mucho para realizar comparaciones. Cuando se realiza una comparación entre dos datos, la CPU establece el indicador o indicadores pertinentes. A continuación, se pueden utilizar instrucciones de salto condicional para comprobar los indicadores y pasar al código que se debe ejecutar, por ejemplo:
cmp eax , ebx jne hacer_algo ; ... hacer_algo: ; haz algo aquí
Aparte de las instrucciones de comparación, hay una gran cantidad de instrucciones aritméticas y de otro tipo que establecen bits en el registro de indicadores. Otros ejemplos son las instrucciones sub, test y add, y hay muchas más. Las combinaciones comunes, como cmp + salto condicional, se "fusionan" internamente (" macrofusión ") en una única microinstrucción (μ-op) y son rápidas siempre que el procesador pueda adivinar en qué dirección irá el salto condicional, salto o continuación.
Los registros de indicadores también se utilizan en la arquitectura x86 para activar y desactivar determinadas funciones o modos de ejecución. Por ejemplo, para desactivar todas las interrupciones enmascarables, puede utilizar la instrucción:
Clí
También se puede acceder directamente al registro de indicadores. Los 8 bits inferiores del registro de indicadores se pueden cargar ah
mediante la lahf
instrucción. El registro de indicadores completo también se puede mover dentro y fuera de la pila mediante las instrucciones pushfd/pushfq
, popfd/popfq
, int
(incluidas into
) y iret
.
El subsistema de matemáticas de punto flotante x87 también tiene su propio registro independiente de tipo "flags", la palabra de estado fp. En la década de 1990, era un procedimiento complicado y lento acceder a los bits de bandera en este registro, pero en los procesadores modernos hay instrucciones de "comparación de dos valores de punto flotante" que se pueden usar con las instrucciones de salto/bifurcación condicionales normales directamente sin ningún paso intermedio.
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 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 32 bits, pero se puede escribir una secuencia como la siguiente para poner la dirección next_line
en eax
(código de 32 bits):
llamar a la siguiente línea siguiente línea: pop eax
Escribir en el puntero de instrucción es simple: una jmp
instrucció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 rax
en 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.