====== Guía de supervivencia de ensamblador ====== ===== Descargo ===== La redacción de una guía que permita aprender el lenguaje ensamblador es una tarea que no escapa al principio de incertidumbre de Heisenberg\\ \\ //Si el lenguaje se enseña rápidamente, no se comprenderá como aplicarlo, mientras que si se detalla exhaustivamente, la enseñanza será extremadamente lenta.//\\ \\ Para los menos amantes de la cuántica se puede decir que una guía de supervivencia de lenguaje ensamblador, es como resumir El Señor de los Anillos en //la historia de un tipo que perdió un anillo y quería recuperarlo porque lo hacia sentir más fuerte.// \\ Por lo expresado de manera irónica en los párrafos anteriores, esta guía no pretende ser un texto que trate de forma exhaustiva los por menores del lenguaje ensamblador detallando el vasto conjunto de instrucciones de la [[http://www.intel.com/content/www/us/en/processors/architectures-software-developer-manuals.html|arquitectura Intel]], así como la sintaxis del [[http://www.nasm.us/|NASM]], sino que empleará un enfoque meramente práctico para la resolución de los ejercicios requeridos por la materia, haciendo inca pie en algunos conceptos útiles que deben ser comprendidos para la formación del futuro ingeniero. ===== Introducción ===== Esta guía solo cubre la sintaxis NASM y el conjunto de instrucciones IA32 ===== Registros de la familia IA ===== ^ Función ^ Registro según la arquitectura ^^^ Instrucciones compatibles ^ ^ principal ^ 16b ^ 32b ^ 64b ^ mas frecuentemente utilizadas ^ ^ Acumulador | ax ; ah ; al | eax ; ax ; ah ; al | rax ; eax ; ax ; al | mov ; in ; out ; add ; and ; bt ; cmp ; cpuid ; mul ; or ; pop ; push ; | ^ Puntero a datos en DS | bx ; bh ; bl | ebx ; bx ; bh ; bl | rbx ; ebx ; bx ; bl | mov ; bt ; cpuid ; in ; out ; pop ; push ; | ^ Contador | cx ; ch ; cl | ecx ; cx ; ch ; cl | rcx ; ecx ; cx ; cl | mov; bt ; inc ; dec; loop ; cpuid ; pop ; push ; | ^ Puntero a I/O | dx ; dh ; dl | edx ; dx ; dh ; dl | rdx ; edx ; dx ; dl | mov ; bt ; cpuid ; pop ; push ; | ^ Indice origen o puntero a datos en DS | si | esi | rsi | mov ; movs ; pop ; push ; | ^ Indice destino o puntero a datos en ES | di | edi | rdi | mov ; movs ; pop ; push ; | ^ Puntero/indice de pila | sp | esp | rsp | mov ; pop ; push ; | ^ Puntero a datos de pila (SS) | bp | ebp | rbp | mov ; pop ; push ; | ^ Uso general | N/A | r8D a r15D | r8 a r15 | mov ; | ^ Segmento/selector de codigo | cs | cs | cs | mov ; | ^ Segmento/selector de datos | ds ; es | ds ; es ; fs ; gs | ds ; es ; fs ; gs | mov ; pop ; push ; | ^ Segmento/selector de pila | ss | ss | ss | mov ; pop ; push ; | ^ Indicador de estado | eflags | eflags | rflags | cli ; sti ; clc ; cld ; pop ; push ; | ^ Puntero a instruccion | ip | eip | rip | No puede ser utilizado como argumento | ^ Registros de control | cr0 a cr4 | cr0 a cr4 | cr0 a cr4 y cr8 | | ^ Punteros a tablas de sistema | gdtr; ldtr; idtr; tr |||l/sgdt ; l/sldt ; l/str ; l/sidt| ^ Multimedia | N/A |mm0 a mm7 yxmm0 a xmm7| mm0 a mm7 y xmm0 a xmm15 | fxsave ; fxrstor | ^ Depuracion | N/A | dr0 a dr7 | dr0 a dr7 | mov ; | ===== Instrucciones básicas ===== * Asignación: Guardan un valor en memoria o registros mov ;Move * Comparación: Utilizadas para comparar un valor con otro, solo afectan las banderas. cmp ;Compare Two Operands test ;Logical Compare * Salto: * Condicionales en función del estado de un bit de bandera particular jxx ;Jump short if ... * Absolutos jmp ;Jump near/long * Binarias: operan bit a bit and ;Logical AND or ;Logical Inclusive OR xor ;Logical Exclusive OR not ;One's Complement Negation * Aritméticas: operaciones matemáticas add ;Add adc ;Add with Carry sub ;Subtract sbb ;Integer Subtraction with Borrow inc ;Increment by 1 dec ;Decrement by 1 clc ;Clear Carry Flag mul ;Unsigned Multiply div ;Unsigned Divide * Stack: operan con la pila, introduciendo y recuperando valores pop ;Pop a Value from the Stack push ;Push Word, Doubleword or Quadword onto the Stack popf ;Pop Stack into FLAGS Register pushf ;Push FLAGS Register onto the Stack popad ;Pop All General-Purpose Registers * Desplazamiento: operan desplazando los bits en un sentido u otro. shr ;Shift shl ;Shift ror ;Rotate rol ;Rotate ===== Constantes, variables y tipos ===== ==== Constantes ==== La definición de un simbolo asociado a una constante se realiza mediante la pseudo instrucción **//EQU//** SEL_CODE EQU 0x08 ;El simbolo SEL_CODE se reemplazara en cada uso por el valor 0x08 ==== Variables ==== En ensamblador el concepto de variable es meramente figurativo, ya que esta no es más que un espacio identificado en memoria. Analicémoslo mediante los siguientes ejemplos: * Tomando la analogía con el bien conocido lenguaje C, si se quiere definir un **//char//**, el ensamblador brinda la pseudo instrucción **//resb//**, la cual reserva un Byte __no inicializado__. El problema es que no se dispone de una identificación de esa memoria que se acaba de reservar, para ello se debe etiquetar dicha posición utilizando la siguiente sintaxis Mi_var_char resb 1 ;Variable no inicializada Si además de definir la variable se quiere inicializar, la pseudo instrucción a utilizar es **//db//**. Mi_var_char db 0xA5 ;Variable inicializada * Aplicando el mismo criterio se utilizan **//resw, resq, rest, dw, dq, dt//** Mi_var_short resw 1 ;Variable no inicializada Mi_var_short resb 2 ;Otra opcion de lo anterior Mi_var_long resq 1 ;Variable no inicializada Mi_var_string db 'Cualquier texto' ;Variable inicializada tipo cadena de caracteres Mi_var_long dq 0xAA55AA55 ;Variable inicializada ==== Tipos ==== En ciertas ocasiones es conveniente disponer de un arreglo de memoria con un formato específico, es decir que se pueda reservar memoria en forma más ágil que un simple //**d**x//. Para tal fín el preprocesador del ensamblador provee el par de macros **//struc//** y **//endstruc//**, las cuales permiten crear tipos de datos complejos, como por ejemplo una descriptor de GDT. struc gdtd_t ;Definicion de la estructura denominada gdtd_t, la cual contiene los siguientes campos .limite: resw 1 ;Limite del segmento bits 00-15. .base00_15: resw 1 ;Direccion base del segmento bits 00-15. .base16_23: resb 1 ;Direccion base del segmento bits 16-23. .prop: resb 1 ;Propiedades. .lim_prop: resb 1 ;Limite del segmento 16-19 y propiedades. .base24_31: resb 1 ;Direccion base del segmento bits 24-31. endstruc Al igual que una variable, una estructura puede ser instanciada con o sin inialización, como se muestra a en los siguientes ejemplos, en ambos casos se debe utilizar **//istruct//** e **//iend//** para la instanciación. Gdt_desc_nulo: istruc gdtd_t ;Instancia Gdt_desc_nulo de la estructura gdtd_t at gdtd_t.limite, dw 0 ;Observar que para inicializar at gdtd_t.base00_15, dw 0 ;cada una de los campos se at gdtd_t.base16_23, db 0 ;se utiliza la macro AT junto at gdtd_t.prop, db 0 ;al nombre del tipo y el campo at gdtd_t.lim_prop, db 0 ; at gdtd_t.base24_31, db 0 ; iend ;Observar el uso de istruc e iend Gdt_desc_extra: istruc gdtd_t ;Instancia Gdt_desc_extra de la estructura gdtd_t. iend ;En este caso se reserva el espacio en memoria pero no se inicializa Debido a que la carga de un valor en un campo de una estructura requiere cierta lógica, la misma se explica a continuación mediante un simple ejemplo, utilizando la estructura instanciada previamente. mov [Gdt_desc_extra+gdtd_t.base00_15], bx Como se explicara anteriormente, una variable no es mas que un espacio identificado en memoria. Este concepto también se aplica a las estructuras, por lo cual como se viera en [[guiasupervivenciaasm:Variables| Variables]], la identificación se realiza mediante una etiqueta, que en este caso es el propio nombre de la instancia de la estructura, es decir //Gdt_desc_extra//. Sabiendo que el inicio del espacio en memoria que se quiere cargar se encuentra //Gdt_desc_code//, si desea aaceder al campo //base00_15//, habrá que desplazarse hasta dicha etiqueta. Este desplazamiento es calculado internamente por el ensamblador, pero el programador debe expresarlo como se indico en el ejemplo. ===== Directivas ===== Se denomina directiva a toda órden que se le da al programa ensamblador propiamente dicho y que no tiene una traducción a código de máquina. En este apartado se describen aquellas directivas que pueden ser catalogadas como de uso general, dejando aquellas de aplicación específica para los apartados correspondientes. ==== Organización del binario ==== Como es bien sabido uno de los objetivos del proceso de ensamblado es generar un fichero binario que sea la copia fiel de la imagen a cargar en la memoria del μP, ya sea esta RAM o ROM, para su ejecución. Para organizar la forma en la cual el fichero binario se debe ubicar en la memoria el ensamblador dispone de las siguientes directivas * **ORG :** acrónimo de origen en inglés, se utiliza para especificar la dirección inicial, a partir de la cual el ensamblador asume que el binario se cargará en memoria. Esta asunción le permite al ensamblador calcular las direcciones de todas las etiquetas, no solo desplazadas respecto al inicio del código, sino tambión teniendo en cuenta la ubicación en memoria del mismo. Para ejemplificar lo comentado, tomemos el caso en el que el binario se ubica en memoria a partir de la dirección 0x7C00. Analizando el fichero //.lst// se observa, como era de esperar, que todas la direcciones se encuentran relativas respecto al inicio del código (recordar que Intel trabaja con formato //little indian//), sin embargo al visualizar el binario resultante mediante algún editor hexadecimal, se observa claramente como las direcciones se encuentran desplazadas 0x7C00, respecto al inicio del programa, para el caso en que se utilizó la directiva **//ORG//** +-----------------------------------------------------------------+ | .lst sin ORG |.bin sin ORG | +--------------------------------------------------+--------------+ | 1 | | | 2 00000000 55AA variablea dw 0xAA55 |55AA | | 3 00000002 55AA variableb dw 0xAA55 |55AA | | 4 | | | 5 00000004 EB03 jmp ETIQUETA |EB03 | | 6 00000006 A3[0000] mov [variablea], ax |A30000 | | 7 ETIQUETA: | | | 8 00000009 A3[0000] mov [variableb], ax |A30200 | +--------------------------------------------------+--------------+ | .lst con ORG |.bin con ORG | +--------------------------------------------------+--------------+ | 1 ORG 0x7C00 | | | 2 00000000 55AA variablea dw 0xAA55 |55AA | | 3 00000002 55AA variableb dw 0xAA55 |55AA | | 4 | | | 5 00000004 EB03 jmp ETIQUETA |EB03 | | 6 00000006 A3[0000] mov [variablea], ax |A3007C | | 7 ETIQUETA: | | | 8 00000009 A3[0000] mov [variableb], ax |A3027C | +--------------------------------------------------+--------------+ * **SECTION :** permite especificar el tipo de memoria que debe ser utilizada, el detalle sobre que se entiende como tipos de memoria se puede encontrar en [[ td3:guiasupervivenciaelf|Guía de supervivencia del formato ELF ]], y los registros que el μP debe utilizar para acceder a la misma. Si bien existen varios tipos de memoria, en los programas a realizar utilizaremos tan solo dos. //.text// la cual corresponde a código y //.data// que como su nombre indica corresponde a datos (inicializados). A modo de ejemplo, en los ejercicios a desarrollar en la cátedra, las estructuras GDT e IDT, se podrían ubicar en la sección de datos, de la siguiente manera section .data align=8 ;La alineacion es opcional, pero recomedable para el caso de la GDT INICIO_GDT: Desc_Nulo resq 1 ... section .text align=0x1000 ;La alineacion (opcional), se utilza para que el codigo comience al inicio de una pagina ;Aqui se deberia colocar la primer instruccion del programa * **BITS :** especifica el tipo de código de operación por el cual el ensamblador debe reemplazar cada instrucción. En el siguiente ejmplo se puede observar claramente como la instrucción que corresponde con el tipo nativo, no presenta ningún prefijo y como si lo hacen aquellas, cuyos operandos no corresponden con la arquitectura especificada 1 00000000 55AA variable dw 0xAA55 2 3 bits 16 4 00000002 A3[0000] mov [variable], ax 5 00000005 66A3[0000] mov [variable], eax 6 7 bits 32 8 00000009 66A3[00000000] mov [variable], ax 9 0000000F A3[00000000] mov [variable], eax 10 11 bits 64 12 00000014 66890425[00000000] mov [variable], ax 13 0000001C 890425[00000000] mov [variable], eax 14 00000023 48890425[00000000] mov [variable], rax ==== Enlace del binario ==== * **%include :** se utiliza de manera muy similar a **//#include//** en C, es decir permite incluir el contenido de un fichero, especificando su nombre y en caso de no estar en el directorio de trabajo, la ruta de acceso. Esta directiva es muy útil para incluir bibliotecas de uso común al código que se este desarrollando. En nuestro caso sería conveniente definir tipos tales como las estructuras de GDT, IDT y constantes que representen cada una de las propiedades que se le asignan a los descriptores, e incluirlas en forma independiente en cada trabajo práctico. * **EXTERN :** permite declarar un símbolo (variable, función) que no se encuentre definido en el código a ser ensamblado. * **GLOBAL :** permite declarar un símbolo (variable, función), que es referenciado por otro módulo, de manera que el enlazador (//linker//) pueda encontrarlo al momento de construir el fichero binario u objeto resultante. Para una mejor comprensión de estas directivas se recomienda leer [[td3:abi64|Consideraciones para integrar C y Assembler en IA-32e]] ===== Secuencias de repetición ===== Las secuencias de repetición son un mecanismo provisto por la herramienta de ensamblaje, para evitar que el programador escriba en forma repetitiva líneas de código idénticas o muy similares. Es muy importante tener en cuenta que la utilización de esta metodología puede incrementar en forma considerable el tamaño del binario resultante. * **TIMES :** esta pseudo instrucción, permite indicarle al ensamblador que repita el código que se presenta a la derecha de la misma, tantas veces como se le pase en el argumento al preprocesador. Analizando la expansión que realiza el ensamblador, se observa que la primitiva de **//TIMES//** para el caso de NASM es **////** +---------------------------+------------------------------------------------------------------------------+ |.asm con TIMES | .lst con TIMES |.bin con TIMES | +---------------------------+--------------------------------------------------------------+---------------+ |bits 32 | 1 bits 32 | | |NULO_SEL: TIMES 8 db 1 | 2 00000000 01 NULO_SEL: TIMES 8 db 1 |01 01 01 01 01 | | | 3 |01 01 01 01 01 | |TIMES 2 mov eax, [NULO_SEL]| 4 00000008 A1[00000000] TIMES 2 mov eax, [NULO_SEL] |A1 00 00 00 00 | | | |A1 00 00 00 00 | +---------------------------+--------------------------------------------------------------+---------------+ * **Par %rep y %endrep :** a diferencia de **//TIMES//**, el par **//%rep//** y **//%endrep//** conforman una directiva que permite repetir varias líneas de código, tantas veces como se le indique en el argumento al preprocesador. Es interesante observar como el ensamblador expende esta directiva en forma completamente diferente respecto a **//TIMES//**, lo cual se visualiza en el fichero //.lst//. +-----------------------------+------------------------------------------------------------------------------+ |.asm con %rep & %endrep | .lst con %rep & %endrep |.bin con %rep | +-----------------------------+--------------------------------------------------------------+---------------+ | 1 bits 32 | 1 bits 32 | | | 2 NULO_SEL: | 2 NULO_SEL: | | | 3 %rep 2 | 3 %rep 2 | | | 4 db 0xA | 4 db 0xA | | | 5 dw 0x5 | 5 dw 0x5 | | | 6 %endrep | 6 %endrep | | | | 7 00000000 0A <1> db 0xA | 0A | | | 8 00000001 0500 <1> dw 0x5 | 05 00 | | | 9 00000003 0A <1> db 0xA | 0A | | | 10 00000004 0500 <1> dw 0x5 | 05 00 | | | 11 | | | 7 %rep 4 | 12 %rep 4 | | | 8 mov eax, [NULO_SEL]| 13 mov eax, [NULO_SEL] | | |10 %endrep | 14 %endrep | | | | 15 00000006 A1[00000000] <1> mov eax, [NULO_SEL] |A1 00 00 00 00 | | | 16 0000000B A1[00000000] <1> mov eax, [NULO_SEL] |A1 00 00 00 00 | | | 17 00000010 A1[00000000] <1> mov eax, [NULO_SEL] |A1 00 00 00 00 | | | 18 00000015 A1[00000000] <1> mov eax, [NULO_SEL] |A1 00 00 00 00 | +-----------------------------+--------------------------------------------------------------+---------------+ ===== Macros ===== En los apartados precedentes se indicó que el preprocesador utilizaba macros para realizar ciertas definiciones, sin embargo el uso de las mismas no está restringido solo para dichos casos, sino que el programador también puede utilizar macros propias. A modo de recordatorio se define como //macro una secuencia de instrucciones que pueden ser invocadas mediante una única sentencia//. A diferencia de las funciones la utilización de macros genera un incremento en el tamaño del código y por ende en el binario final, sin embargo tienen la ventaja de aumentar la legibilidad del código y por lo general aumentar la velocidad de ejecución respecto al mismo código implementado por función. Por lo general existen dos tipos de macros ==== Línea simple ==== Es el caso más básico de macros, el cual permite realizar la definición en una única línea utilizando diferentes directivas según el objetivo a realizar. === %define === Esta macro permite definir no solo valores numéricos, sino también secuencias de operaciones. En este último caso es importante que la secuencia se encuentre entre paréntesis, para evitar errores durante la expansión de la macro realizada por el preprocesador. %define BOOT_DIR 0x07C00 ;Direccion de inicio del bootloader %define PROX_BLOQ_4K (($/0x1000)*0x1000) ;Devuelve la proxima direccion multiplo de 4kB === %assign === Es una opción algo más reducida a **//%define//**, ya que solo admite definiciones numéricas. La macro **//%assign//** suele ser útil para definir tablas estáticas en ROM. En nuestro caso, puede ser una alternativa para inicializar las tablas de paginación en tiempo de compilación, como se muestra en el siguiente ejemplo, sin embargo esta metodología no es una buena práctica ya que el binario final ocupará gran cantidad de espacio en memoria. %assign i PT_BASE ;Asigno a i la direccion de la Page Table %rep 0x100 ;Repito la operacion para paginar 1MB dd i|(PAG_PRES+PAG_WRTEN+PAG_USREN);Asigno las propiedades a la pagina %assign i i+PAG_SIZE ;Incremento i por cada pagina (4kB) %endrep === align === Esta macro, como su nombre en inglés lo indica, permite alinear código o datos, a partir de la dirección múltiplo de 2n inmediata siguiente. En la realización de los trabajos prácticos, esta macro puede ser utilizada para establecer las tablas de descriptores en una página independiente del código. ;Primer instruccion del codigo de inicializacion del sistema ;Ultima intruccion align 0x1000 ;Alineacion cada 4kB INICIO_GDT: Desc_Nulo resq 1 .... FIN_GDT align 65536 ;Alineacion cada 65kB para reservar el tamaño maximo de la GDT INICIO_IDT_32b: IRQ0_CTRL resq 1 .... FIN_IDT_32b ==== Líneas múltiples ==== Este tipo de macros permite realizar secuencias más complejas y emplea las directivas **//%macro//**, **//%endmacro//**. Se ejemplificará este caso por ser el más complejo. Como es costumbre se explicará la utilización de macros mediante un ejemplo, el cual tiene por objetivo inicializar la instancia //Gdt_desc_extra// del ejemplo visto en Tipos. //Vale la pena aclarar que la macro descripta a continuación puede ser y es conveniente, reemplazarla por una función ya que ocupará menos espacio en el binario final, sin embargo el objetivo es mostrar la potencia de las macros incluso para mezclar código y definiciones al momento de operar el preprocesador.// El primer paso es definir la macro * **gdtdescini:** nombre de la macro * **Argumento 1:** Direccion de la base del segmento. Valor maximo soportado 2^32. * **Argumento 2:** Tamaño del segmento. Valor maximo soportado 2^20. * **Argumento 3:** Propiedades P;DPL;S;TIPO. * **Argumento 4:** Propiedades G;D/B;L;AVL * **Argumento 5:** Direccion de inicio del descriptor a ser inicializado. %macro gdtdescini 5 ;Nombre y numero de argumentos push ebx ;Se almacena el valor del registro xor ebx, ebx ;y se borrar para operar con el mismo mov ebx, %1 ;Se carga EL PRIMER ARGUMENTO (la direccion) en ebx mov [%5+gdtd_t.base00_15], bx ;y carga en la instancia indicada por EL QUINTO ARGUMENTO shr ebx, 16 mov [%5+gdtd_t.base16_23], bl xor ebx, ebx mov ebx, %2 ;Se carga EL SEGUNDO ARGUMENTO EN ebx mov [%5+gdtd_t.limite],bx ;del tamaño shr ebx, 16 ;Se adapta el restante nibble y mov [%5+gdtd_t.lim_prop],bl ;se carga en el descriptor xor bl, bl mov bl, %3 ;Se carga el primer byte mov [%5+gdtd_t.prop],bl ;de propiedades EL TERCER ARGUMENTO xor bl, bl mov bl, %4 ;Se carga el nibble EL CUARTO ARGUMENTO shl bl, 4 ;Se desplaza al nibble mas significativo or [%5+gdtd_t.prop],bl ;Se carga en el descriptor pop ebx ;Se restatura el valor del registro %endmacro La utilización es extremadamente sencilla, por ejemplo si se quisiera inicializar el descriptor mencionado anteriormente para direccionar la memoria de video, bastaría con el siguiente codigo gdtdescini 0B8000h, 4000, 10010011b, 0b, Gdt_desc_extra --- //[[cnigri@frba.utn.edu.ar|ChristiaN]] 2023/04/27 11:55// ===== Referencias ===== * [[http://www.nasm.us/|NASM (ensamblador utilizado por defecto en la Cátedra)]] * [[http://www.intel.com/content/www/us/en/processors/architectures-software-developer-manuals.html|Manuales Intel para la familia IA32 y 64]] * [[http://www.gnu.org/software/binutils/|GNU Assembler]]