El lenguaje ensamblador x86 es una familia de lenguajes ensambladores compatibles con versiones anteriores , que proporcionan cierto nivel de compatibilidad desde el Intel 8008 introducido en abril de 1972. [1] [2] Los lenguajes ensambladores x86 se utilizan para producir código objeto para el x86 clase de procesadores. Como todos los lenguajes ensambladores, utiliza mnemónicos cortos para representar las instrucciones fundamentales que la CPU de una computadora puede entender y seguir. [3] Los compiladores a veces producen código ensamblador como un paso intermedio al traducir un programa de alto nivel a código máquina. . Considerada como un lenguaje de programación , la codificación de ensamblaje es específica de la máquina y de bajo nivel . Los lenguajes ensambladores se utilizan más habitualmente para aplicaciones detalladas y críticas en cuanto al tiempo, como pequeños sistemas integrados en tiempo real o núcleos de sistemas operativos y controladores de dispositivos .
Mnemónicos y códigos de operación
Cada instrucción en ensamblador x86 está representada por 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] Existen códigos de operación potenciales sin mnemotécnico documentado que diferentes procesadores pueden interpretar de manera diferente, haciendo 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
x86 lenguaje ensamblador tiene dos principales sintaxis ramas: Intel sintaxis y AT & T de sintaxis . [4] 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 . [5] Aquí hay un resumen de las principales diferencias entre la sintaxis de Intel y la sintaxis de AT&T :
AT&T | Intel | |
---|---|---|
Orden de parámetros | Origen antes del destino. movl $ 5 , % eax | Destino antes que origen. mov eax , 5 |
Tamaño del parámetro | Los mnemónicos tienen como sufijo una letra que indica el tamaño de los operandos: q para qword, l para long (dword), w para word yb para byte. [4]addl $ 4 , % esp | Derivado del nombre del registro que se utiliza (por ejemplo , rax, eax, ax, al implican q, l, w, b , respectivamente). añadir esp , 4 |
Sigilos | Los valores inmediatos con el prefijo "$", los registros con el prefijo "%". [4] | El ensamblador detecta automáticamente el tipo de símbolos; es decir, si son registros, constantes o algo más. |
Direcciones efectivas | Sintaxis general de DISP (BASE, INDEX, SCALE) . Ejemplo: movl mem_location ( % ebx , % ecx , 4 ), % eax | Expresiones aritméticas entre corchetes; Además, las palabras clave de tamaño como byte , word o dword deben usarse si el tamaño no se puede determinar a partir de los operandos. [4] Ejemplo: mov eax , [ ebx + ecx * 4 + mem_location ] |
Muchos ensambladores x86 usan la sintaxis Intel , incluidos NASM , FASM , MASM , 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 directiva .intel_syntax . [4] [6] [7] 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. [8]
La sintaxis de AT&T es casi universal para todas las demás arquitecturas con el mismo mov
orden; originalmente era una sintaxis para el ensamblaje de 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.
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]
- AX multiplicar / dividir, cargar y almacenar cadenas
- Registro de índice BX para MOVE
- Recuento de CX para operaciones y turnos de cadenas
- Dirección de puerto DX para IN y OUT
- SP apunta 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 las operaciones de transmisión
Junto a los registros generales se encuentran además:
- Puntero de instrucción IP
- BANDERAS
- registros de segmento (CS, DS, ES, FS, GS, SS) que determinan dónde comienza un segmento de 64k (sin FS y GS en 80286 y anteriores)
- registros de extensión adicional ( MMX , 3DNow! , ESS , etc.) (Pentium y posteriores).
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 de 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
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 que se usa 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 KB (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, el otro para mantener el desplazamiento. Para traducir nuevamente 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 agrega al desplazamiento para formar la dirección completa, lo que permite romper la barrera de 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í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. Al combinar los valores de segmento y desplazamiento , encontramos una dirección de 20 bits.
El IBM PC original restringió los programas a 640 KB, pero se utilizó una especificación de memoria ampliada para implementar un esquema de cambio de banco que dejó de usarse 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.
El modo protegido, comenzando con Intel 80286, fue utilizado por OS / 2 . 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. [9] 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 configuraría el procesador en modo protegido, habilitando 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 el GDT o LDT y un nivel de privilegio solicitado de 2 bits ; consulte la segmentación de la memoria x86 .
Cuando se hace referencia a una dirección con un segmento y un desplazamiento, se usa 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 segmento y un par de registros de desplazamiento; DS: DX.
Hay algunas combinaciones especiales de registros de segmento y registros generales que apuntan a direcciones importantes:
- CS: IP (CS es segmento de código , IP es puntero de instrucción ) apunta a la dirección donde el procesador buscará el siguiente byte de código.
- SS: SP (SS es segmento de pila , SP es puntero de pila ) apunta a la dirección de la parte superior de la pila, es decir, el byte introducido más recientemente.
- DS: SI (DS es segmento de datos , SI es índice de origen ) se utiliza a menudo para señalar datos de cadena que están a punto de copiarse en ES: DI.
- ES: DI (ES es segmento adicional , DI es índice de destino ) se utiliza normalmente para señalar el destino de una copia de cadena, como se mencionó anteriormente.
El Intel 80386 presentó tres modos de funcionamiento: modo real, modo protegido y modo virtual. El modo protegido que debutó en el 80286 se amplió para permitir al 80386 direccionar hasta 4 GB de memoria, el modo 8086 virtual completamente nuevo ( 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 (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 modo protegido) ya que Windows ahora puede 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 compatibles con cinco modos de funcionamiento para x86 código, de modo real , el modo protegido , el modo largo , Virtual 86 Modo y el modo de administración del sistema , en el que algunas instrucciones están disponibles y otros no lo son. Hay disponible un subconjunto de instrucciones de 16 bits 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 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 memoria y el tamaño de la palabra varían, lo que requiere diferentes estrategias de programación.
Los modos en los que se puede ejecutar el código x86 son:
- Modo real (16 bits)
- Espacio de direcciones de memoria segmentada de 20 bits (lo que significa que solo se puede direccionar 1 MB de memoria, en realidad, un poco más), acceso directo de software al hardware periférico y ningún concepto de protección de memoria o multitarea a nivel de hardware. Las computadoras que usan BIOS se inician en este modo.
- Modo protegido (16 bits y 32 bits)
- Expande la memoria física direccionable a 16 MB y la memoria virtual direccionable a 1 GB . Proporciona niveles de privilegios y memoria protegida , lo que evita que los programas se corrompan entre sí. El modo protegido de 16 bits (utilizado durante el final de la era de DOS ) utilizaba un modelo de memoria complejo y multisegmentado. El modo protegido de 32 bits utiliza un modelo de memoria simple y plano.
- Modo largo (64 bits)
- Principalmente 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 descartaron en el modo de 64 bits. Desarrollado por AMD .
- Modo 8086 virtual (16 bits)
- Un modo operativo híbrido especial que permite que los programas y sistemas operativos en modo real se ejecuten mientras están bajo el control de un sistema operativo supervisor en modo protegido.
- Modo de gestión del sistema (16 bits)
- Maneja funciones de todo el sistema como administración de energía, control de hardware del sistema y código patentado diseñado por OEM. Está diseñado para ser utilizado únicamente por el firmware del sistema. Se suspende toda la ejecución normal, incluido el sistema operativo . Luego, se ejecuta un sistema de software alternativo (que generalmente reside en el firmware de la computadora o en un depurador asistido por hardware ) con altos privilegios.
Modos de cambio
El procesador se ejecuta en modo real inmediatamente después del encendido, por lo que un kernel del sistema operativo u otro programa debe cambiar explícitamente a otro modo si desea ejecutar cualquier cosa que no sea el modo real. Los modos de cambio se logran modificando ciertos bits de los registros de control del procesador después de alguna preparación, y es posible que se requiera alguna configuración adicional después del cambio.
Ejemplos de
Con una computadora que ejecuta 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 código de 64 bits.
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 instrucción
En general, las características del conjunto de instrucciones x86 moderno son:
- Una codificación compacta
- Longitud variable y alineación independiente (codificado como little endian , al igual que todos los datos en la arquitectura x86)
- Principalmente instrucciones de una dirección y de dos direcciones, es decir, el primer operando también es el destino.
- Se admiten operandos de memoria como origen y destino (se utilizan con frecuencia para leer / escribir elementos de la pila direccionados mediante pequeñas compensaciones inmediatas).
- Uso de registro tanto general como implícito ; Aunque los siete
ebp
registros generales (contando ) en el modo de 32 bits y los quincerbp
registros generales (contando ) en el modo de 64 bits se pueden utilizar libremente como acumuladores o para direccionamiento, la mayoría de ellos también son utilizados implícitamente por ciertos (más o menos menos) instrucciones especiales; Por lo tanto, los registros afectados deben conservarse temporalmente (normalmente apilados), si están activos durante tales secuencias de instrucciones.
- Produce banderas condicionales implícitamente a través de la mayoría de las instrucciones ALU enteras .
- Admite varios modos de direccionamiento, incluidos el índice inmediato, de desplazamiento y escalado, pero no relativo a la PC, excepto los saltos (introducido como una mejora en la arquitectura x86-64 ).
- Incluye punto flotante a una pila de registros.
- Contiene un soporte especial para atómicas de lectura-modificación-escritura instrucciones (
xchg
,cmpxchg
/cmpxchg8b
,xadd
y las instrucciones de enteros que se combinan con ellock
prefijo) - Instrucciones SIMD (instrucciones que realizan instrucciones simples simultáneas en paralelo en muchos operandos codificados en celdas adyacentes de registros más amplios).
Apilar instrucciones
La arquitectura x86 tiene soporte de hardware para un mecanismo de pila de ejecución. Instrucciones tales como push
, pop
, call
y ret
se utilizan con la pila correctamente configurado para pasar parámetros, para asignar espacio para los datos locales, y para guardar y restaurar los puntos de llamada de retorno. La instrucción de ret
tamaño es muy útil para implementar convenciones de llamada eficientes (y rápidas) en el espacio donde el destinatario es responsable de recuperar 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; el alto nivel enter
de instrucciones (introducido con el 80186) realiza un procedimiento que anidan en profundidad argumento así como un tamaño local de argumento, y puede ser más rápido que la manipulación más explícita de los registros (tales como push bp
; mov bp, sp
; ). Si es más rápido o más lento depende de la implementación del procesador x86 en particular, así como de la convención de llamada utilizada por el compilador, el programador o el código de programa en 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 variadas , así como diferentes opciones de diseño a nivel de puerta y transistor .sub sp, size
La gama completa de modos de direccionamiento (incluidos inmediato y base + desplazamiento ) incluso para instrucciones como 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 más explícitos de la pila de llamadas).
Instrucciones de ALU de enteros
montaje x86 tiene las operaciones matemáticas estándar, add
, sub
, mul
, con idiv
; los operadores lógicos and
, or
, xor
, neg
; aritmética y lógica de desplazamiento de bits , sal
/ sar
, shl
/ shr
; Girar con y sin equipaje, rcl
/ rcr
, rol
/ ror
, un complemento de BCD instrucciones aritméticas, aaa
, aad
, daa
y otros.
Instrucciones de punto flotante
El lenguaje ensamblador x86 incluye instrucciones para una unidad de punto flotante (FPU) basada en pilas. El FPU era un coprocesador separado opcional para el 8086 al 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 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, coma flotante de 32 bits, 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, que incluyen seno, coseno, tangente, arcotangente, exponenciación con la base 2 y logaritmos en las bases 2, 10 o e .
El registro de pila de formato de registro de pila de las instrucciones es por lo general o , donde es equivalente a , y es uno de los 8 registros de pila ( , , ..., ). Como los enteros, el primer operando es tanto el primer operando fuente como el operando destino. y debe destacarse como el primer intercambio de 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 después de que se completa su operación. Entonces, por ejemplo, realiza el cálculo , luego quita de la parte superior de la pila, haciendo así cuál fue 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)
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 (de MMX a SSE4.2 ) incluyen cálculos generales en 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). Así, por ejemplo, paddw mm0, mm1
lleva a cabo 4 paralelo de 16 bits (indicado por el w
) número entero añade (indicado por el padd
) de mm0
los 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 que incluyen una suma de diferencias absolutas (utilizadas 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 mezcla alfa basada en software y filtrado digital ) . SSE (desde SSE3 ) y 3DNow! Las extensiones incluyen instrucciones de suma y resta para tratar valores de coma 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 enteros y los registros XMM (usado en SSE) / FPU (usado en MMX).
Instrucciones de manipulación de datos
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 un 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 ds
selector y los almacena en el eax
registro. En general, los procesadores x86 pueden cargar y usar una memoria que coincida con el tamaño de cualquier registro en el que estén operando. (Las instrucciones SIMD también incluyen instrucciones de media carga).
El conjunto de instrucciones x86 incluye carga cadena, almacenar, mover, exploración y instrucciones de comparación ( lods
, stos
, movs
, scas
y cmps
), que realizar cada operación a un tamaño especificado ( b
por byte de 8 bits, w
por palabra de 16 bits, d
de 32 bits de palabra doble) 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 escaneo de carga, almacenar y, el objetivo / fuente / registro comparación implícita está en el al
, ax
o eax
registro (dependiendo del tamaño). Los registros de segmento 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 escaneos y comparaciones) cuando se detecta desigualdad.
La pila se implementa con un puntero de pila implícitamente decreciente (push) e incremental (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 de funcionamiento 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 los datos de reserva y retirar de la parte superior de la pila, mientras que la creación de un puntero del marco de pila en bp
/ ebp
/ rbp
. Sin embargo, también se admite la configuración directa o la suma y resta al registro sp
/ esp
/ rsp
, por lo que las instrucciones enter
/ leave
son generalmente innecesarias.
Este código al comienzo de una función:
empujar ebp ; guardar el marco de pila de la función de llamada (ebp) mov ebp , esp ; hacer un nuevo marco de pila encima de la pila de nuestro llamador sub esp , 4 ; asignar 4 bytes de espacio de pila para las variables locales de esta función
... es funcionalmente equivalente a solo:
ingrese 4 , 0
Otras instrucciones para manipular la pila incluyen pushf
/ popf
para almacenar y recuperar el registro (E) FLAGS. Las instrucciones pusha
/ popa
almacenarán y recuperarán todo el estado del registro de enteros desde y hacia la pila.
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 almacenamiento y carga de SSE requieren una alineación de 16 bytes para funcionar correctamente. Los conjuntos de instrucciones SIMD también incluyen instrucciones de "captación previa" que realizan la carga pero no apuntan a ningún registro, utilizado para la carga de la caché. Los conjuntos de instrucciones SSE también incluyen instrucciones de almacenamiento no temporales que realizarán almacenes 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 entero y punto 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 de 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 el salto).
También se admiten varios saltos condicionales, que incluyen jz
(saltar sobre cero), jnz
(saltar sobre un valor distinto de cero), jg
(saltar sobre mayor que, firmado), jl
(saltar sobre menor que, firmado), ja
(saltar sobre / mayor que, sin signo) , jb
(saltar por 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 establecen, borran o complementan estos indicadores según su resultado. La comparación cmp
(comparar) y las testinstrucciones establecen 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 bandera de acarreo) y cmc
(complemento de bandera de acarreo) que funcionan directamente en las banderas. Las comparaciones de coma flotante se realizan mediante instrucciones fcom
o ficom
que eventualmente deben convertirse en indicadores de números 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 la base del segmento completo: valor de desplazamiento 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
inserta la dirección de desplazamiento de segmento de la instrucción que sigue call
a la pila; ret
saca 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 ret
aparece el desplazamiento y luego la base del segmento para volver.
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, usa 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 CPU que utiliza, a menos que se utilicen para devolver el resultado de una operación al programa que realiza la llamada (en un software llamado interrupciones). El retorno coincidente de la instrucción de interrupción es iret
, que restaura las banderas después de regresar. 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 desencadenan por eventos de hardware externos y deben conservar todos los valores de registro, ya que se desconoce el estado del programa en ejecución. 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 de
"¡Hola Mundo!" programa para DOS en ensamblaje estilo MASM
Usando la interrupción 21h para la salida - otras muestras usan printf de libc para imprimir en stdout . [10]
.modelo pequeño .stack 100h.data msg db '¡Hola mundo! $'.code inicio: mov ah , 09h ; Muestra el mensaje lea dx , msg int 21h mov ax , 4C00h ; Terminar el ejecutable int 21hfin, comienzo
"¡Hola Mundo!" programa para Windows en montaje 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.libextrn printf : cerca de extrn salida : cercapublic main main proc push offset msg call printf push 0 call exit main endpfinal
"¡Hola Mundo!" programa para Windows en montaje estilo NASM
; Image base = 0x00400000 % define RVA (x) (x-0x00400000) section .text push dword hello call dword [ printf ] push byte + 0 call dword [ exit ] retsecció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 ) veces 5 dd 0 ; termina la tabla de descriptoresmsvcrt_string dd "msvcrt.dll" , 0 msvcrt_LookupTable: dd RVA ( msvcrt_printf ) dd RVA ( msvcrt_exit ) dd 0msvcrt_imports: printf dd RVA ( msvcrt_printf ) salida dd RVA ( msvcrt_exit ) dd 0msvcrt_printf: dw 1 dw "printf" , 0 msvcrt_exit: dw 2 dw "salir" , 0 dd 0
"¡Hola Mundo!" programa para Linux en montaje estilo NASM
; ; Este programa se ejecuta en modo protegido de 32 bits. ; build: nasm -f elf -F puñaladas name.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 .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 ; desde esta dirección (símbolo $)sección .texto ; esta es la sección de código global _start ; _start es el punto de entrada y necesita un alcance global para ser 'visto' por ; enlazador --equivalent a main () en C / C ++ _start: ; la definición del procedimiento _start comienza aquí mov eax , 4 ; especificar el código de función sys_write (de la tabla de vectores del SO) mov ebx , 1 ; especificar el descriptor de archivo stdout - en gnu / linux, todo se trata como un archivo ; incluso dispositivos de hardware mov ecx , str ; mover la _dirección_ de inicio del mensaje de cadena al registro ecx mov edx , str_len ; mover la 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 ; especificar el código de función sys_exit (de la tabla de vectores del SO) 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)
"¡Hola Mundo!" programa para Linux en ensamblaje de estilo NASM usando la biblioteca estándar de C
; ; Este programa se ejecuta en modo protegido de 32 bits. ; gcc vincula la biblioteca estándar-C de forma predeterminada; build: nasm -f elf -F puñaladas name.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. ; main global ; main debe definirse ya que se compila contra la biblioteca C-Standard extern printf ; declara el uso de un símbolo externo ya que printf se declara en un módulo de objeto diferente. ; El vinculador resuelve este símbolo más tarde. segmento .data ; sección para la cadena de datos inicializada db '¡Hola mundo!' , 0Ah , 0h ; cadena de mensaje con carácter de nueva línea (10 decimal) y el terminador NULL ; la cadena ahora se refiere a la dirección inicial en la que se almacena 'Hello, World'.segmento .texto principal: empujar cadena ; empujar la dirección del primer carácter de la cadena a la pila. Este será un argumento para printf llamar a printf ; llama a printf add esp , 4 ; avanza el puntero de pila en 4 eliminando el argumento de cadena empujado ret ; return
"¡Hola Mundo!" programa para Linux en modo de 64 bits en ensamblaje de estilo NASM
; compilación: nasm -f elf64 -F enano hola.asm ; enlace: ld -o hola hola. o REL PREDETERMINADO ; utilizar modos de direccionamiento relativos a RIP de forma predeterminada, por lo que [foo] = [rel foo]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_Hello: equ $ - Hola ; conseguir 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 por putSECCIÓN .texto_start global _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 Hola ] ; 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, Hola, len_Hello); llamar al kernel para hacer realmente la llamada al sistema ;; valor de retorno en RAX. RCX y R11 también se sobrescriben con syscallmov eax , 60 ; __NR_exit número de llamada (x86_64) xor edi , edi ; status = 0 (salir normalmente) syscall ; _salir (0)
Ejecutarlo straceverifica 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 enlazamos 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 redirección, la salida estándar de su programa es el registro de strace mixto en stderr. Lo que normalmente está bien execve ("./ hello", ["./hello"], 0x7ffc8b0b3570 / * 51 vars * /) = 0 write (1, "Hello world! \ N", 13) = 13 exit (0) =? +++ salió con 0 +++
Usando el registro de banderas
Los indicadores se utilizan mucho para realizar 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 bifurcar al código que debería ejecutarse, por ejemplo:
cmp eax , ebx jne hacer_algo ; ... hacer_algo: ; haz algo aquí
Los indicadores también se utilizan en la arquitectura x86 para activar y desactivar determinadas 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 bajos del registro de banderas se pueden cargar ah
usando la lahf
instrucción. Las banderas enteras registro también se pueden mover dentro y fuera de la pila utilizando las instrucciones pushf
, popf
, int
(incluyendo into
) y iret
.
Usando el 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 de puntero de instrucción apunta a la dirección de memoria que el procesador intentará ejecutar a continuación; 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 next_line
en eax
:
llamar next_line next_line: pop eax
Esta secuencia de instrucciones genera código independiente de la posición porque call
toma un operando inmediato relativo al puntero de instrucción que describe el desplazamiento en bytes de la instrucción de destino desde la siguiente instrucción (en este caso 0).
Escribir en el puntero de instrucción es simple: una jmp
instrucción establece el puntero de instrucción en la dirección de destino, por lo que, por ejemplo, una secuencia como la siguiente colocará el contenido de eax
en eip
:
jmp eax
En el modo de 64 bits, las instrucciones pueden hacer referencia a datos relacionados con el 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
- Lenguaje ensamblador
- Listados de instrucciones X86
- Arquitectura X86
- Diseño de CPU
- Lista de ensambladores
- Código auto modificable
- DOS
Referencias
- ^ "Familia de microprocesadores Intel 8008 (i8008)" . www.cpu-world.com . Consultado el 25 de marzo de 2021 .
- ^ "Intel 8008" . MUSEO DE LA CPU - MUSEO DE MICROPROCESADORES Y FOTOGRAFÍA DE MATRIZ . Consultado el 25 de marzo de 2021 .
- ^ a b c "Intel 8008 OPCODES" . www.pastraiser.com . Consultado el 25 de marzo de 2021 .
- ^ a b c d e Narayam, Ram (17 de octubre de 2007). "Ensambladores de Linux: una comparación de GAS y NASM" . Archivado desde el original el 3 de octubre de 2013 . Consultado el 2 de julio de 2008 .
- ^ "La creación de Unix" . Archivado desde el original el 2 de abril de 2014.
- ^ Hyde, Randall. "¿Qué ensamblador es el mejor?" . Consultado el 18 de mayo de 2008 .
- ^ "GNU Assembler News, v2.1 es compatible con la sintaxis Intel" . 2008-04-04 . Consultado el 2 de julio de 2008 .
- ^ "i386-Bugs (usando como)" . Documentación de Binutils . Consultado el 15 de enero de 2020 .
- ^ Mueller, Scott (24 de marzo de 2006). "P2 (286) Procesadores de segunda generación" . Actualización y reparación de PC, 17ª edición (libro) (17 ed.). What. ISBN 0-7897-3404-4. Consultado el 6 de diciembre de 2017 .
- ^ "Acabo de comenzar la Asamblea" . daniweb.com . 2008.
Otras lecturas
Manuales
- Manuales para desarrolladores de software Intel 64 e IA-32
- Manual del programador de la arquitectura AMD64 (Volumen 1-5)
Libros
- Ed, Jorgensen (mayo de 2018). Programación en lenguaje ensamblador x86-64 con Ubuntu (PDF) (1.0.97 ed.). pag. 367.
- Dennis Yurichev: comprensión del lenguaje ensamblador