Programación en assembler bajo emu8086 Organización de Computadoras Depto. de Cs. e Ing. de la Computación
Copyright Copyright © 2010 A. G. Stankevicius Se asegura la libertad para copiar, distribuir y modificar este documento de acuerdo a los términos de la GNU Free Documentation License, Version 1.2 o cualquiera posterior publicada por la Free Software Foundation, sin secciones invariantes ni textos de cubierta delantera o trasera. Una copia de esta licencia está siempre disponible en la página http://www.gnu.org/copyleft/fdl.html http://www.gnu.org/copyleft/fdl.html.. La versión transparente de este documento puede ser obtenida de la siguiente dirección: http://cs.uns.edu.ar/~ags/teaching
Contenidos Entorno de programación. Partes de un programa. Directivas al compilador. Arquitectura Arquitectura i8086. Estructuras de control. Accediendo al sistema operativo. Definición de procedimientos. Dispositivos virtuales.
Entorno de programación El lenguaje assembler nos permite hacer un uso de la computadora a muy bajo nivel. nivel. Para mantener a raya la posibilidad de dañar al hardware y/o al software, haremos uso del simulador emu8086 emu8086.. Este simulador no solo recrea al hardware de la PC sino también parte del software de la misma (su BIOS, algunas funciones del SO, etc.).
Ciclo de desarrollo bajo emu8086 Escribir el código fuente. Podemos hacer uso del editor propio del entorno o bien editar el código fuente en cualquier otro editor externo.
Simular la ejecución del programa. El entorno de programación compila el código fuente al momento de solicitar su ejecución. De no haber errores, el código máquina resultante es cargado en el CPU simulado y queda listo para su ejecución.
Pasos para depurar un programa En la mayoría de los lenguajes de programación es complicado escribir código 100% libre de errores. Peor aun... en assembler, es todavía más difícil. Por suerte el propio entorno de ejecución, hace las veces de un poderoso entorno de depuración (debugger).
Estructura de un archivo fuente Los archivos fuente son esencialmente archivos de texto. Están compuestos de diferentes secciones, dependiendo del tipo de ejecutable que se desea obtener. En general trabajaremos en el marco de la plantilla para obtener .com, por lo que no va a hacer falta distinguir las distintas secciones de nuestro código fuente.
Directivas al compilador Las directivas instruyen al compilador, sin generar código máquina. name fija el nombre del programa que estamos escribiendo: name “holamundo”
include nos permite incorporar código de otros archivos fuente: include “mismacros.inc”
equ asocia el resultado de una expresión constante a una etiqueta: TOPE equ 25*4
Directivas al compilador Existen dos directivas para reservar locaciones de memoria: db, para reservar un byte: contador db 0
dw, para reservar un word: resultado dw 0
Si no interesa inicializar su contenido, se puede hacer uso de un “ ?”. Cada locación de memoria puede tener una etiqueta asociada.
Etiquetas Las etiquetas son básicamente direcciones de memoria. Pueden apuntar a locaciones que contienen datos o bien código. A la izquierda del dato al cual se asocian: arreglo db 16 dup(?)
A la izquierda de la instrucción a la cual se asocian, pero separada por un “ :”: lazo: mov ax, 1234h
Arquitectura i8086 La arquitectura i8086 fue introducida en 1978. Su éxito dio a lugar a la serie de procesadores 8086, 8088, 80186, 80286, 80386, 80486, Pentium, etc.
Registros disponibles La arquitectura i8086 tiene apenas 8 registros de 16 bits. SP y BP: suelen tener un rol específico. AX, BX, CX, DX, SI, DI: registros de propósito general.
También cuenta con cuatro registros auxiliares de segmento: CS (code segment), DS (data segment), ES (extra segment) y SS (stack segment).
Registros disponibles Algunos de estos registros permiten a su vez acceder a los dos campos de 8 bits que lo componen. Por caso: AX se compone de AH (los 8 bits más significativos) y AL (los 8 bits menos significativos).
El procesador está en modo real: A diferencia de los nuevos procesadores, no brinda protección de memoria. Los distintos procesos comparten el mismo espacio de direcciones.
Organización de la memoria i8086 cuenta con un espacio de direcciones de 20 bits. Esto es, ¡¡¡apenas 1 megabyte!!! (menos que la memoria disponible en un teléfono celular berreta).
Cómo los registros son de sólo 16 bits, se apela a la segmentación para poder acceder a la totalidad de la memoria. Para obtener la dirección efectiva se debe hacer uso de un registro de segmento.
Organización de la memoria El registro de segmento que se debe usar se determina según el contexto. Por caso, si BX contiene al valor 1234h y DS al valor 5678h, la referencia [BX] denota en realidad a la locación de memoria 579B4h. Matemáticamente: EA = BX + DS * 10h 579B4h = 1234h + 5678h * 10h
Instrucción MOV Sintaxis: mov dest, origen mov bx, 3 mov ax, bx mov ax, CONT
; guarda un 3 en bx ; copia bx en ax ; guarda CONT en ax
Restricciones: El destino no puede ser constante. Puede referencia a lo sumo una dirección de memoria. Origen y destino deben ser compatibles, esto de, de igual cantidad de bits.
Accediendo a memoria Los corchetes denotan el modo de direccionado indirecto: mov bx, CONT mov cx, [bx]
; ; ; mov cx, [CONT] ;
guarda CONT en bx guarda el contenido de CONT en cx inst. equivalente
¡No todos los registros pueden ser utilizados para acceder a la memoria! mov ax, [bx] mov bx, [ax]
; válido ; inválido
Accediendo a memoria La arquitectura i8086 sólo permite acceder a la memoria mediante los registros BX, SI, DI y BP. Estos registros se combinan entre sí para obtener un modo de direccionado base-indexado. También es posible incluir un desplazamiento constante:
[
bx ó
bp
+
si ó
di
+ offset
]
Accediendo a memoria Ya que ambos operandos tienen que ser compatibles, en ocasiones debemos explicitar el tamaño: mov mov mov mov
word prt [CONT], 1 bx, CONT ax, [CONT] byte ptr [bx], FFh
Los modificadores son byte ptr y word ptr.
Instrucciones aritméticas La arquitectura i8086 cuenta con las operaciones aritméticas usuales: add sub inc dec not neg mul div
destino, operando destino, operando destino destino destino destino operando / imul operando divisor / idiv divisor
Estructuras de control Los lenguajes de alto nivel hacen uso de diversas estructuras de control. Assembler es más básico, sólo cuenta con las siguientes facilidades: La instrucción cmp: para comparar magnitudes entre sí. Los flags: para recordar el resultado de la última comparación u otras operaciones. Los saltos: para cambiar el flujo de ejecución de forma condicionada o no.
Principales Flags La arquitectura i8086 cuenta con diversos flags, los cuales reflejan el resultado de la última comparación. Para los enteros no signados se usan los flags zero (ZF) y carry (CF). Para los enteros signados se usan los flags overflow (OF) y sign (SF).
La mayoría de las operaciones aritméticas también afectan los flags.
Instrucción CMP Sintáxis: cmp primero, segundo Computa primero – segundo y luego modifica los flags de manera acorde: Si resultado = 0 (primero == segundo): ZF = 1; CF = 0
Si resultado > 0 (primero > segundo): ZF = 0; CF = 0 SF = OF
Si resultado < 0 (primero < segundo): ZF = 0; CF = 1 SF != OF
Instrucción JMP Sintaxis: jmp dest / jmp short dest jmp infinite-loop jmp short label-cercano
Los saltos incondicionales siempre transfieren el control a una cierta dirección de memoria sin tener en cuenta el estado de los flags. La instrucción a continuación de un jmp jamás se ejecuta... ...salvo que sea el destino de un salto.
Saltos condicionales En los saltos condicionales no siempre se produce la transferencia de control. El salto se realiza o no dependiendo del estado de uno o más flags. Los saltos más sencillos dependen de sólo un flag (no son frecuentes). Los saltos más complejos dependen de múltiples flags a la vez (son más frecuentes).
Saltos condicionales Saltos simples: jz jo js jc jp
dest dest dest dest dest
/ / / / /
jnz jno jns jnc jnp
dest: depende dest: depende dest: depende dest: depende dest: depende
de de de de de
ZF. OF. SF. CF. PF.
Si el flag en cuestión está activo se produce el salto. Caso contrario, la ejecución continúa en la instrucción que sigue al salto.
Saltos condicionales Saltos condicionales para magnitudes signadas (suponiendo que se acaba de ejecutar cmp ax, bx): je dest: salta si ax == bx. jne dest: salta si ax != bx. jl dest / jnge dest: salta si jle dest / jng dest: salta si jg dest / jnle dest: salta si jge dest / jnl dest: salta si
ax ax ax ax
< bx. <= bx. > bx. >= bx.
Saltos condicionales Saltos condicionales para magnitudes no signadas (suponiendo que se acaba de ejecutar cmp ax, bx): je dest: salta si ax == bx. jne dest: salta si ax != bx. jb dest / jnae dest: salta si jbe dest / jna dest: salta si ja dest / jnbe dest: salta si jae dest / jnb dest: salta si
ax ax ax ax
< bx. <= bx. > bx. >= bx.
IF-THEN-ELSE La estructura de control condicional se codifica fácilmente: ; if (a > 15) { b = 32; } ; else { a = a + 1; } mov ax, [a] cmp ax, 15 ; comparo a con 15 jng else ; ir a “else” si <= mov word ptr [b], 32 ; brazo “then” jmp seguir ; else: inc ax ; brazo “else” seguir: … ;resto del programa
Estructuras de control Las restantes estructuras de control también se pueden codificar de una manera similar. Ejercicios para ir pensando: ¿Cómo se codifica una secuencia? ¿Cómo se codifica una repetición estilo while o repeat-until? ¿Y una estilo for?
En este punto, estamos en condiciones de comenzar a resolver el Práctico 8.
Nuestro primer ¡hola mundo! Para poder encarar nuestro primer “hola mundo” en assembler, sólo resta resolver un aspecto: la entrada/salida. El sistema operativo es el encargado de gestionar ese aspecto. El entorno de programación también emula una parte de un antiguo sistema operativo llamado DOS. También se emula una parte del propio BIOS de la máquina.
Llamadas al sistema La invocación a los servicios provistos por el sistema operativo se realiza mediante las interrupciones. Por caso, el DOS brinda muchos de sus servicios mediante la interrupción 21h.
El servicio solicitado se suele especificar mediante alguno de los registros de la máquina. Bajo DOS, el valor del registro AH al momento de la interrupción indica el servicio solicitado.
Llamadas al sistema Análogamente se pueden acceder a los servicios provistos por el BIOS. El propio DOS implementa el conjunto de servicios que brinda apelando a los servicios provistos por el BIOS. El listado preciso de los servicios emulados por el entorno se encuentra disponible en la ayuda del mismo.
Ejemplo concreto Por ejemplo, para mostrar una cadena de texto por pantalla podemos usar el servicio 09h de la interrupción 21h: Cargar el número de servicio solicitado en el registro AH. Cargar el offset del mensaje que se quiere mostrar por pantalla en el registro DX. Invocar a la interrupción correspondiente.
Cabe acotar que DOS asume que toda cadenas de texto termina con un “ $”.
Nuestro primer programa org 100h jmp comienzo ; ¿por qué hará falta? texto db 'Hola mundo!',10, 13, '$' comienzo: mov ah, 09h mov dx, offset texto int 21h ; el texto aparecerá en ; donde esté el cursor int 20h ; indicamos al DOS que ; la ejecución ha finalizado
Invocación a procedimientos La invocación a un procedimiento no difiere en gran medida de los saltos convencionales: Se adopta la convención de dejar en el tope la pila la dirección a la cuál se debe retornar. Las instrucciones call y ret permiten simplificar la invocación a los procedimientos y el posterior retorno.
Invocación a procedimientos Existen dos alternativas a la hora de evitar que se pierda información al invocar a un procedimiento: Quien invoca puede encargarse de almacenar los registros que no desea se vean afectados por el procedimiento. O bien, quien es invocado puede ocuparse de guardar sólo los registros que serán usados por el procedimiento.
Ambas soluciones tienen ventajas y desventajas... ¿cuáles son?
Ejemplo de “quien invoca guarda” mov ax, [unvalor] mov bx, [otrovalor] push bx ; alcena bx call triplicar ; procedimiento para ; triplicar ax pop bx ; restaura bx ...
triplicar: mov cx, 3 mul cx ret
; multiplica por 3 ; retorna
Ejemplo de “quien es invocado guarda” mov ax, [unvalor] mov bx, [otrovalor] call triplicar ; procedimiento para ... ; triplicar ax
triplicar: push cx push dx mov cx, 3 mul cx pop dx pop cx ret
; almacena cx ; almacena dx ; multiplica por 3 ; recupera dx ; recupera cx retorna
Definición de un procedimiento A fin de mejorar la legibilidad del código fuente se puede hacer uso de las directivas proc y endp. Por caso, la rutina del ejemplo anterior puede ser reescrita como el siguiente procedimiento: triplicar proc ... ret
; cuerpo del procedimiento
triplicar endp
Macros predefinidas El entorno de programación brinda un conjunto de macros predefinidas para simplificar la gestión de la interacción con el usuario. Consultar el documento “ emu8086.inc”, presente en la carpeta “ inc”.
Es posible definir nuestras propias macro mediante las directivas al compilador macro y endm. La sintáxis es análoga a proc y endp.
Dispositivos virtuales El entorno de programación emu8086 brinda acceso al programador a un conjunto de dispositivos virtuales. printer (simula una impresora). display (simula a un display de siete segmentos de varios dígitos). thermostate (simula a un termómetro calentado por un mechero bunsen) stepper_motor (simula a un motor eléctrico paso a paso)