lenguaje ensamblador x86 - x86 assembly language
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. Los lenguajes ensambladores x86 se utilizan para producir código objeto para la clase de procesadores x86 . Como todos los lenguajes ensambladores, utiliza mnemónicos cortos para representar las instrucciones fundamentales que la CPU de una computadora puede entender y seguir. Los compiladores veces producen conjunto de código como un paso intermedio en la traducción de un programa de alto nivel en código de 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 de ensamblaje 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. Existen códigos de operación potenciales sin mnemotécnico documentado que diferentes procesadores pueden interpretar de manera diferente, lo que hace que un programa que los usa se comporte de manera inconsistente o incluso genere una excepción en algunos procesadores. Estos códigos de operación a menudo aparecen en concursos de escritura de código como una forma de hacer que el código sea más pequeño, más rápido, más elegante o simplemente para mostrar la destreza del autor.
Sintaxis
x86 lenguaje ensamblador tiene dos principales sintaxis ramas: Intel sintaxis y AT & T de sintaxis . 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 se creó en AT&T Bell Labs . A continuación, se muestra 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.addl $4, %esp
|
Derivado del nombre del registro que se utiliza (por ejemplo , rax, eax, ax, al implican q, l, w, b , respectivamente).add esp, 4
|
| Sigilos | Los valores inmediatos con el prefijo "$", los registros con el prefijo "%". | 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. 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 . Una peculiaridad en la sintaxis de AT&T para x86 es que los operandos x87 están invertidos, un error heredado del ensamblador original de AT&T.
La sintaxis de AT&T es casi universal para todas las demás arquitecturas con el mismo movorden; 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:
- 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 ; copies the value 1234hex (4660d) into register AX
mov bx, ax ; copies the value of the AX register into the BX register
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 usó 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, usaron 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. 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 ; ver x86 segmentación de memoria .
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 usa 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 la 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 de funcionamiento 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
ebpregistros generales (contando ) en el modo de 32 bits y los quincerbpregistros 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, el desplazamiento y el escalado, pero no el 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,xaddy las instrucciones de enteros que se combinan con ellockprefijo) - 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, cally retse 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 enterde 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 opciones de diseño a nivel de puerta y transistor .
sub sp, size
La gama completa de modos de direccionamiento (incluidos inmediato y base + desplazamiento ) incluso para instrucciones como pushy pop, hace que el uso directo de la pila para datos enteros , de punto flotante y de 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, daay 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 ( , , ..., ). Al igual que 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), 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 (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, mm1lleva a cabo 4 paralelo de 16 bits (indicado por el w) número entero añade (indicado por el padd) de mm0los valores a mm1y almacena el resultado en mm0. Streaming SIMD Extensions o SSE también incluye un modo de punto flotante en el que solo se modifica 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, uno 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 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, scasy cmps), que realizar cada operación a un tamaño especificado ( bpor byte de 8 bits, wpor palabra de 16 bits, dde 32 bits de palabra doble) luego incrementa / decrementa (dependiendo de DF, bandera de dirección) el registro de dirección implícito ( sipara lods, dipara stosy scas, y ambos para movsy cmps). Para las operaciones de escaneo de carga, almacenar y, el objetivo / fuente / registro comparación implícita está en el al, axo eaxregistro (dependiendo del tamaño). Los registros de segmento implícitos utilizados son dspara siy espara di. El registro cxo ecxse 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 entery leaveque 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/ leaveson generalmente innecesarias.
Este código al comienzo de una función:
push ebp ; save calling function's stack frame (ebp)
mov ebp, esp ; make a new stack frame on top of our caller's stack
sub esp, 4 ; allocate 4 bytes of stack space for this function's local variables
... es funcionalmente equivalente a solo:
enter 4, 0
Otras instrucciones para manipular la pila incluyen pushf/ popfpara almacenar y recuperar el registro (E) FLAGS. Las instrucciones pusha/ popaalmacenarán y recuperarán todo el estado del registro de enteros hacia y desde 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 de 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 con el apoyo varios saltos condicionales, incluyendo jz(salto en cero), jnz(salto en la no-cero), jg(salto en mayor que, firmado), jl(salto con menos de, firmó), ja(salto en arriba / 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 fcomo ficomque 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, callinserta la dirección de desplazamiento de segmento de la instrucción que sigue calla la 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 retaparece 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. Soft Interrupciones del tipo descrito anteriormente son utilizados por algunos sistemas operativos para llamadas al sistema , y también se puede utilizar en la depuración de manejador de interrupciones duros. 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 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 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 .
.model small
.stack 100h
.data
msg db 'Hello world!$'
.code
start:
mov ah, 09h ; Display the message
lea dx, msg
int 21h
mov ax, 4C00h ; Terminate the executable
int 21h
end start
"¡Hola Mundo!" programa para Windows en montaje estilo MASM
; requires /coff switch on 6.15 and earlier versions
.386
.model small,c
.stack 1000h
.data
msg db "Hello world!",0
.code
includelib libcmt.lib
includelib libvcruntime.lib
includelib libucrt.lib
includelib legacy_stdio_definitions.lib
extrn printf:near
extrn exit:near
public main
main proc
push offset msg
call printf
push 0
call exit
main endp
end
"¡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]
ret
section .data
hello db "Hello world!"
section .idata
dd RVA(msvcrt_LookupTable)
dd -1
dd 0
dd RVA(msvcrt_string)
dd RVA(msvcrt_imports)
times 5 dd 0 ; ends the descriptor table
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)
exit dd RVA(msvcrt_exit)
dd 0
msvcrt_printf:
dw 1
dw "printf", 0
msvcrt_exit:
dw 2
dw "exit", 0
dd 0
"¡Hola Mundo!" programa para Linux en montaje estilo NASM
;
; This program runs in 32-bit protected mode.
; build: nasm -f elf -F stabs name.asm
; link: ld -o name name.o
;
; In 64-bit long mode you can use 64-bit registers (e.g. rax instead of eax, rbx instead of ebx, etc.)
; Also change "-f elf " for "-f elf64" in build command.
;
section .data ; section for initialized data
str: db 'Hello world!', 0Ah ; message string with new-line char at the end (10 decimal)
str_len: equ $ - str ; calcs length of string (bytes) by subtracting the str's start address
; from this address ($ symbol)
section .text ; this is the code section
global _start ; _start is the entry point and needs global scope to be 'seen' by the
; linker --equivalent to main() in C/C++
_start: ; definition of _start procedure begins here
mov eax, 4 ; specify the sys_write function code (from OS vector table)
mov ebx, 1 ; specify file descriptor stdout --in gnu/linux, everything's treated as a file,
; even hardware devices
mov ecx, str ; move start _address_ of string message to ecx register
mov edx, str_len ; move length of message (in bytes)
int 80h ; interrupt kernel to perform the system call we just set up -
; in gnu/linux services are requested through the kernel
mov eax, 1 ; specify sys_exit function code (from OS vector table)
mov ebx, 0 ; specify return code for OS (zero tells OS everything went fine)
int 80h ; interrupt kernel to perform system call (to exit)
"¡Hola Mundo!" programa para Linux en ensamblaje de estilo NASM usando la biblioteca estándar de C
;
; This program runs in 32-bit protected mode.
; gcc links the standard-C library by default
; build: nasm -f elf -F stabs name.asm
; link: gcc -o name name.o
;
; In 64-bit long mode you can use 64-bit registers (e.g. rax instead of eax, rbx instead of ebx, etc..)
; Also change "-f elf " for "-f elf64" in build command.
;
global main ;main must be defined as it being compiled against the C-Standard Library
extern printf ;declares use of external symbol as printf is declared in a different object-module.
;Linker resolves this symbol later.
segment .data ;section for initialized data
string db 'Hello world!', 0Ah, 0h ;message string with new-line char (10 decimal) and the NULL terminator
;string now refers to the starting address at which 'Hello, World' is stored.
segment .text
main:
push string ;push the address of first character of string onto stack. This will be argument to printf
call printf ;calls printf
add esp, 4 ;advances stack-pointer by 4 flushing out the pushed string argument
ret ;return
"¡Hola Mundo!" programa para Linux en modo de 64 bits en ensamblaje de estilo NASM
; build: nasm -f elf64 -F dwarf hello.asm
; link: ld -o hello hello.o
DEFAULT REL ; use RIP-relative addressing modes by default, so [foo] = [rel foo]
SECTION .rodata ; read-only data can go in the .rodata section on GNU/Linux, like .rdata on Windows
Hello: db "Hello world!",10 ; 10 = `\n`.
len_Hello: equ $-Hello ; get NASM to calculate the length as an assemble-time constant
;; write() takes a length so a 0-terminated C-style string isn't needed. It would be for puts
SECTION .text
global _start
_start:
mov eax, 1 ; __NR_write syscall number from Linux asm/unistd_64.h (x86_64)
mov edi, 1 ; int fd = STDOUT_FILENO
lea rsi, [rel Hello] ; x86-64 uses RIP-relative LEA to put static addresses into regs
mov rdx, len_Hello ; size_t count = len_Hello
syscall ; write(1, Hello, len_Hello); call into the kernel to actually do the system call
;; return value in RAX. RCX and R11 are also overwritten by syscall
mov eax, 60 ; __NR_exit call number (x86_64)
xor edi, edi ; status = 0 (exit normally)
syscall ; _exit(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 # without a redirect, your program's stdout is mixed strace's logging on stderr. Which is normally fine
execve("./hello", ["./hello"], 0x7ffc8b0b3570 /* 51 vars */) = 0
write(1, "Hello world!\n", 13) = 13
exit(0) = ?
+++ exited with 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 do_something
; ...
do_something:
; do something here
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 ahusando la lahfinstrucció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 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 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_lineen eax:
call next_line
next_line:
pop eax
Esta secuencia de instrucciones genera código independiente de la posición porque calltoma 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 jmpinstrucció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 eaxen 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
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.