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 arquitectura Intel, así como la sintaxis del 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 dx. 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 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 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 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 <rept>
+---------------------------+------------------------------------------------------------------------------+
|.asm con TIMES             | .lst con TIMES                                               |.bin con TIMES |
+---------------------------+--------------------------------------------------------------+---------------+
|bits 32                    | 1                              bits 32                       |               |
|NULO_SEL: TIMES 8 db 1     | 2 00000000 01<rept>            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]<rept>  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 

ChristiaN 2023/04/27 11:55

Referencias