Documentos de Académico
Documentos de Profesional
Documentos de Cultura
ASM7
ASM7
LENGUAJE ENSAMBLADOR
Elaborado por:
LENGUAJE ENSAMBLADOR.
Temario del Curso.
II
Lenguaje Ensamblador
.
V. Macros
5.1 Definición
5.2 Parámetros y etiquetas.
5.3 Ensamble de macros
5.4 Ventajas y desventajas.
Unidades de Aprendizaje.
UNIDAD I. Conocerá los elementos arquitectónicos del procesador a utilizar, así como las
diferentes formas de acceso a los datos dentro de la computadora.
UNIDAD III. Conocerá y aplicará el uso de rutinas tanto internas como externas y las distintas
formas de llevar a cabo el pase de parámetros.
- Realizar programas utilizando modularidad.
UNIDAD IV. Conocerá los métodos de comunicación con los dispositivos de E/S y la
aplicará en la elaboración de programas.
- Desarrollar programas aplicando las instrucciones e interrupciones para entrada,
salida.
III
Lenguaje Ensamblador
.
Referencias Bibliográficas.
7. MACROASSEMBLER 6
FOR THE MS-DOS OPERATING SYSTEM
- PROGRAMMERS GUIDE
-MICROSOFT CODE VIEW A UTILITIES UP DATE
EDITOR MICROSOFT CORP.
8. INTRODUCCION AL 8086/8088
CRISTOPHER L. MORGAN, MICHELL W.
MC GRAW HILL
IV
Lenguaje Ensamblador
.
I. Introducción.
Aún cuando parezca que las computadoras "entienden" lenguajes de alto nivel como
BASIC o Pascal, todas las computadoras corren actualmente en lenguaje máquina, los bytes
codificados que dirigen la unidad central de proceso de la computadora. Por esta razón código
máquina es un mejor término que lenguaje de computadora de bajo nivel- el único lenguaje que
la computadora conoce. Ya que el CPU de la computadora no puede ejecutar directamente las
sentencias de C y Pascal, los programas en éstos y otros lenguajes de alto nivel deben de ser
compilados (traducidos) a código máquina antes para que los programas puedan ser utilizados.
Similarmente, un programa escrito en un lenguaje intérprete como BASIC o LISP deben de ser
traducidos a código máquina, aunque en estos casos, la traducción sucede invisiblemente
mientras el programa se ejecuta, normalmente una sentencia a la vez.
Los programas de lenguaje ensamblador también deben de ser traducidos a código máquina
por un programa llamado ensamblador.
No igual que las sentencias de C o Pascal, las cuales se pueden traducir a docenas de bytes de
código máquina, las instrucciones de lenguaje ensamblador están directamente relacionadas con
códigos de maquina individuales - la mayor distinción entre los lenguajes de alto nivel y los
ensambladores. Todos los lenguajes tienen sus puntos a favor, pero solamente el lenguaje
ensamblador permite escribir programas directamente en el grupo de instrucciones indivisibles
del CPU.
Un poco de Historia.
Las computadoras IBM-PC, llegaron a revolucionar el mundo de las computadoras personales, a
la vez que se iniciaba una nueva generación de chips procesadores de 16 bits. Fueron ingenios
más grandes y poderosos, destinados a reemplazar o completar a las microcomputadoras de 8
bits de los años 70's, que fueron las que comenzaron la revolución de las microcomputadoras.
Con los procesadores de 8 bits se podían representar hasta 255 números, mientras que a partir
de los procesadores de 16 bits, este número se incremento hasta 65535, un número 256 veces
mayor; así sucesivamente cada vez que se libera una nueva generación dentro de la misma
familia, el numero capaz de representar continúa en aumento, llegando en la actualidad a
manejar números de 64 bits con direccionamientos de hasta 1.844674407371e+019.
La fórmula para obtener el valor máximo de direccionamiento de un procesador es 2 n, donde n
es el numero de bits del procesador.
Es importante resaltar que los micorprocesadores INTEL de la familia 80X86 todavía utilizan un
juego de instrucciones orientado a octetos (bytes);, esto es, que cada instrucción en lenguaje
V
Lenguaje Ensamblador
.
máquina de los procesadores ocupa de 1 a 6 octetos de memoria. En el caso de los
microprocesadores de 16 bits, estos pueden cargar dos instrucciones en un tiempo de reloj. El
hecho de especificar el numero de bits de un microprocesador, también se refiere al numero de
bits que puede tratar en forma simultánea, tanto para enviarlos, recibirlos o procesarlos.
Los microprocesadores 8088, 8086, 80186, 80286, 80386, 80486 y ahora Pentium, son los que
conforman la familia 80X86 de INTEL. Esta familia, desde el 8088, fue una extensión lógica del
popular 8080. Internamente el 8080 y el 8088 son iguales, pero el 8088 está diseñado para
trabajar con un bus de 8 bits, siendo de ésta manera compatible con la mayoría de los buses del
mismo tamaño. El 8086, se conecta a un bus de 16 bits. El 6 del 86 indica el bus de 16 bits; el 8
de 88 significa que el bus en éste caso es de 8 bits. Ambos se refieren a la anchura física del bus
de datos. Internamente ambos poseen el mismo juego de instrucciones y el mismo tamaño de
datos.
El 8088 y 8086, utilizan el concepto de colas de instrucciones para aumentar la velocidad de
proceso. Dentro del propio chip existe una área a la que se le llama cola de instrucciones, que
guarda los octetos (bytes) de las instrucciones. Cuando la computadora está preparada para
obtener la siguiente instrucción, no es necesario tomarla de memoria. De ésta forma el bus de
datos y de direcciones no presentan periodos pico de utilización como es común en los buses de
datos de 8 bits, que necesitan continuamente de accesar a la memoria.
La cola de instrucciones del 8086 es de 6 bytes, y la del 8088 de 4.
El 8086 puede acceder a 1 Mbyte de memoria de lectura/escritura 2 20. Sin embargo, utiliza un
esquema de direccionamiento de memoria llamado de segmentación, en el cual ciertos registros
de segmentación suministran una dirección base, que se añade automáticamente a cada
dirección de 16 bits que define el usuario. Aunque hay cuatro registros de segmentación en el
8086, la dirección base posible puede emplazarse a intervalos de 16 bytes a lo largo de todo el
Megabyte de memoria direccionable.
Parte de la dirección y todo el bus de datos están multiplexados en 16 terminales. Los 4 bits de
dirección restantes se corresponden con los 4 terminales de dirección adicionales que también se
utilizan para el estado. Se requiere de un reloj externo y un controlador de bus también externo
que se utiliza para demultiplexar el bus de direcciones/datos.
El microprocesador 8080 tenia solo tres registros pares de propósito general, HL, BC y DE. Con el
8088 se renombraron y aumentaron y reciben los nombres de AX, BX, CX, DX, estos pueden
tratarse como pares de registros de 8 bits, o como registros de 16 bits. Junto con éstos se
introducen 4 registros nuevos de gran interés. Se trata de los registros de segmentación y se les
designan las siglas CS, DS, SS Y ES. Se utilizan en la segmentación de la familia 80X86. A través
de ellos se puede decir a la computadora separada y dinámicamente, la dirección de un
programa, dato, o pila, dentro del Megabyte de memoria. Hay todavía cuatro registros más de 16
bits (básicamente): el puntero de pila, el puntero base y dos registros índices, el fuente y el
destino.
Los procesadores del Propósito General 80X86 utilizan conceptos de arquitectura avanzada
tales como la gestión de memoria, interrupciones vectorizadas, multinivel y procesamiento
paralelo.
Junto con el 80X86, hay dos procesadores paralelos disponibles. El Procesador de Datos
Numérico 80X87, o coprocesador matemático aumenta el juego de instrucciones del CPU 80X86,
ofreciéndole al programador el equivalente a tener incorporada una calculadora científica
equipada con funciones trigonométricas, logarítmicas y otras de carácter básico. Esto ofrece al
80X86, la capacidad de realizar operaciones muy rápidas de punto flotante. El 80X87, controla el
flujo de instrucciones del 80X86 localizando sus propias instrucciones y ejecutándolas sin ayuda
del 80X86.
VI
Lenguaje Ensamblador
.
El segundo procesador paralelo es el Procesador de Entrada/Salida, IOP-8089. Está diseñado
para una gestión eficiente de los movimientos de los bloques de datos. Tiene dos canales y
puede solapar entradas y salidas fácilmente. Se acopla también con el bus local, y tiene su
propio juego de instrucciones. En resumen, es capaz de realizar inteligentes operaciones de E/S
intercaladas en el canal doble al mismo tiempo que el CPU continúa con el programa principal.
1) de Alimentación
2) de Control
3) de Direcciones
4) de Datos.
2) Control. El sub-bus de control lleva información sobre la temporización (el sistema de señales
de reloj), órdenes (memoria o acceso a la E/S), dirección de los datos (lectura/escritura), señales
de ocupación (línea READY) e interrupciones.
4) Datos.
El sub-bus de datos transporta la información a través de la computadora. El bus de datos de la
8086 por ejemplo, tiene 16 conectores, y es capaz de transportar 16 señales en paralelo. Esto
significa que el bus de datos puede llevar unidades de información de 16 dígitos, es decir 2
bytes, o una palabra.
VII
Lenguaje Ensamblador
.
Cada uno de los dispositivos externos está conectado a uno de estos controladores, que a su vez
se conectan al bus principal del sistema.
El plotter digital y el teclado comparten los 24 bits del controlador 8255.
Observemos que, de hecho, la memoria RAM y ROM no son mas que otro dispositivo de los
procesadores. La ROM se utiliza para almacenar un pequeño programa de autocarga (bootstrap)
para la inicialización del controlador de discos flexibles o duros. El programa carga el primer
sector del disco en memoria. Este primer sector contiene un programa que a su vez carga en
memoria el resto del sistema operativo; éste se encarga de inicializar los diversos controladores,
activándolos y dejándolos en disposición de recibir órdenes.
VIII
Lenguaje Ensamblador
.
Este tema es la base de nuestro estudio. Como hemos escuchado alguna vez, "sin
software una computadora no es nada". A lo que se refiere es que para utilizar un
microprocesador pro completo, se deben tener abundantes programas disponibles que pueden
trabajar con la computadora.
Un punto esencial en el diseño de software para un nuevo microprocesador, es el problema de
los sistemas operativos. En éste caso, para ejecutar programas para los microprocesadores
80X86, se requiere de otro programa llamado "sistema operativo". El sistema operativo es algo
así como un programa "madre" que revisa las peticiones de los programas de los usuarios y
ayuda a cada programa particular a ejecutarse suministrándole vías de acceso a los diferentes
dispositivos de la computadora, como el teclado, discos flexibles, impresora, etc.. El sistema
operativo permite que los programas particulares sean relativamente independientes de la
configuración de la computadora abriendo de ésta manera el amplio mercado para los programas
de aplicación. Esto ha evolucionado tan extensamente, que ahora contamos con ambientes
operativos gráficos, que nos liberan inclusive, de las tareas de reconocimiento de los diferentes
tipos de dispositivos y modelos de los mismos y de su manipulación, como es el caso del
ambiente Windows para los microprocesadores de Intel.
Existen varios sistemas operativos que están diseñados para la familia 8X86 de Intel. Existen
dos grandes divisiones de estos, a los que se les denomina "low-end" y "high-end". Por ejemplo
el CP/M, es un típico sistema operativo "low-end", ya que el control y gestionamiento de los
microcomponentes se realiza en forma poco eficiente y limitada, aunque son muy baratos.
Sistemas operativos más sofisticados, "high-end", como el UNIX, NOVELL, XENIX, SOLARIS son
más caros, pero ofrecen una flexibilidad increíble. Lo cierto es que depende del sistema operativo
es el rango de aplicaciones disponibles que fueron desarrollados para ellos.
Intel tiene también su propio sistema operativo llamado ISI-II.
El sistema operativo más popular de ésta familia de microprocesadores es el MS-DOS,
desarrollado por Microsoft Corp.
IX
Lenguaje Ensamblador
.
Unidades de Memoria.
Bit.
El bit es la unidad más pequeña de información. El término bit proviene de BInary digiT (dígito
binario). Un bit se almacena y transmite como señal que puede estar en dos estados; activa (on)
o inactiva (off). Puede usarse para almacenar variables lógicas o números en aritmética de base
2.
Cuarteto
Un cuarteto nibble son 4 bits, es decir, medio byte. Se utiliza fundamentalmente para
almacenar dígitos en código BCD (decimal codificado en binario).
Octeto
Un octeto o byte son 8 bits, o 2 cuartetos. Puede almacenar un carácter (normalmente
codificado en ASCII), un número de 0 a 255, dos números BCD u ocho indicadores de 1 bit.
Palabra.
Una palabra o word, consta de un número fijo de bits, aunque éste número varíe de una
computadora a otra. Los microprocesadores de las generaciones actuales tienen palabras de 16
bits, 32 bits y 64 bits.
Para nuestro uso, trataremos a las palabras (words) como unidades de 16 bits, a unidades de
32 bits las denominaremos dobles palabras o double word, y al termino “cuadruple palabra”
(quarter word).
Estas unidades de memoria son útiles para almacenar números ordinales y enteros.
Bloque.
Un Block es un grupo de celdas de memoria continua. No tiene tamaño fijo, aunque en ciertos
contextos, como los discos, un bloque significa un tamaño definido. Por ejemplo un sector
consiste de 512 bytes, dentro del sistema operativo MS-DOS.
Tipos de datos.
En éstas unidades de memoria definidas anteriormente se almacenan datos. Cada tipo de dato
tiene un cierto formato o codificación que requiere un cierto número de unidades de memoria.
Sin una descripción del formato de memoria utilizado, los datos se convierten en algo ilegible y
sin sentido, sobre todo cuando utilizamos tipos de datos complicados como puede ser la
representación de los números de punto flotante.
Lógicos.
Un dato lógico es una cantidad que sólo puede tomar uno de dos valores posibles: verdadero o
falso, 0 ó 1, activo o inactivo, etc. Este tipo de dato tiene interés como indicadores condicionales
que definan bifurcaciones de programa dependiendo de su valor, o como indicadores de estado
para cierto dispositivo.
X
Lenguaje Ensamblador
.
Ordinales.
Enteros.
Son números enteros con signo positivo o negativo (+ ó -). Los enteros se representan en forma
binaria de complemento a dos. En ésta representación, el bit de más a la izquierda o más
significativo (MSB) actúa tanto como parte del número y como signo de éste. Una forma de
entender ésta representación es imaginarse que los números están distribuidos alrededor de una
rueda de dos maneras distintas. Supongamos que reservamos n bits para representar cada
n
número. Con n números podemos representar ordinales entre 0 y 2 -1. Coloquemos estos
n
números sobre la rueda de manera que el 0 y el 2 -1 sean adyacentes. Si ahora rompemos la
rueda en dos mitades y asignamos valores negativos a una de las mitades y positivos a la otra,
tendremos la representación del complemento a dos. La rueda se corta precisamente por el
punto en el que el bit más significativo cambia de valor, de manera que para todos los números
negativos éste bit valdrá 1, y para todos los positivos valdrá 0.
En éste sistema de numeración, en un byte se pueden representar enteros entre -128 y +127,
con palabras de 16 bits, entre -32768 y 32767, y con 32 bits (doble palabra), el rango de enteros
varía entre -2147483648 y 2147483647.
Punto flotante.
La representación de punto flotante está pensada para ofrecer una buena aproximación a los
números reales. El sistema de representación de punto flotante se parece mucho a la notación
científica en el cual cada número viene definido por un signo, una magnitud y un exponente. Se
suelen utilizar unidades de memoria de 32 y 64 bits para representarlos.
Cuando se utiliza el formato en 32 bits o 64 bits, se le conoce como reales cortos o reales largos.
Por ejemplo, el formato corto utiliza los 32 bits de la siguiente manera: 1 bit para el signo, 8 para
el exponente, y los 23 restantes para la magnitud.
Los 64 bits del formato real largo se distribuyen como sigue: 1 bit para el signo, 11 para el
exponente, y 52 para la magnitud.
BCD. Decimales-codificados-en-binario
Caracteres.
Los caracteres se utilizan para representar letras del alfabeto u otros símbolos como dígitos (0-
9) o símbolos de puntuación. Lo más común es utilizar el código ASCII, el cual codifica los
caracteres en 8 bits.
Cadenas.
Una cadena es una secuencia de caracteres. Se utiliza para guardar textos. Puesto que las
cadenas tienen longitudes dinámicamente variables, se incluyen frecuentemente algunos bytes
extra con información sobre la longitud máxima y la longitud real de la cadena.
XI
Lenguaje Ensamblador
.
Punteros.
Las CPU Intel utilizan punteros para apuntar a direcciones físicas en memorias. Se usan junto
con la segmentación. Ene le caso del CPU 8086, una dirección física de memoria se guarda como
dos cantidades de 16 bits. Ambas cantidades se combinan de una forma especial, formando una
dirección real de 20 bits. Un puntero es una palabra doble que almacena esas dos cantidades, el
número de segmento y el desplazamiento.
Para direccionar la memoria, las PC-XT's utilizan 20 bits, sin embargo la CPU procesa palabra de
16 bits en sus registros de direcciones. Las direcciones están divididas en dos componentes:
segmentos y desplazamientos (offset). Un segmento es un área continua de memoria que puede
tener una longitud de 64Kbytes. El segmento debe comenzar en una localidad de memoria cuya
dirección sea límite de 16 bytes (párrafo), y puede traslaparse con otros segmentos intercalando
localidades de memoria. La dirección de inicio de un segmento define su localización. Esta
dirección puede estar contenida en uno de cuatro segmentos de registro disponibles en el 8088;
el segmento de código, el de datos, el de stack y el extra. El segmento de código contiene la
dirección del segmento donde residen las instrucciones del programa en ejecución. El segmento
de datos señala la dirección donde inicia el segmento en el que se definen las variables. El
segmento de stack señala hacia el segmento donde se encuentra el stack. Esta última es una
estructura de datos en memoria donde pueden colocarse bytes o words, una después de la otra,
y que posteriormente, se pueden recuperar. El stack tiene como características que la última
palabra, o byte, colocado en ella es la primera en salir (LIFO). Una estructura en memoria de ese
tipo, la establece el programador mediante instrucciones PUSH y POP.
XII
Lenguaje Ensamblador
.
En hexadecimal:
1 0 A F 0 Dirección segmento
+ F 0 F F Dirección desplazamiento
-------------------------------------------------------------
1 F B E F Dirección de 20 bits
Es importante tener en mente que el enlazador LINK es quien define las direcciones de los
segmentos. Así mismo, no todas las combinaciones de estas direcciones no son permisibles. Por
ejemplo, la siguiente combinación no es válida.
F F F F 0 Dirección segmento
+ 0 0 1 0 Dirección desplazamiento
-------------------------------------------------------------
NO DEFINIDO Dirección de 20 bits
XIII
Lenguaje Ensamblador
.
15 8 7 0
15 IP 0 AX AH AL
CS BX BH BL
DS CX CH CL
SS DX DH DL
ES
Registros de Proposito General
Registros de Direcciones
de Segmentos
15 SP 0
BP
O D I T S Z A P C
SI
15 0 DI
Los mismos registros están disponibles en todos los modelos 80X86. Aunque el 386 tiene
registros de longitud mayor y otros adicionales, si nos enfocamos en trabajar en los registros del
8086, se podra asegurar que nuestros programas correrán en todas las PC’s.
Todos los registros del 8086 tienen 16 bits de longitud. En adición, los cuatro registros de
propósito general - ax, bx, cx, y dx - están subdivididos en dos mitades, alta y baja (High, Low) de
8 bits. El registro ax, por ejemplo está compuesto de dos partes de 8 bits, ah y al. Este arreglo
flexible nos permite operar directamente trabajar con los 16 bits completos o trabajar
separadamente con las dos mitades de registros de 8 bits.
Hay que tener siempre en cuenta que si se modifica ax se modificará también las dos mitades
de registro de 8 bits. Al igual, cambiar el valor en cl, cambia también su valor en cx.
XIV
Lenguaje Ensamblador
.
Registros de propósito general.
Los programas de lenguaje ensamblador se refieren a los registros por sus mnemonicos, ax,
cl,ds, y otros. Pero los registros también tienen nombres menos familiares. El acumulador ax es
normalmente utilizado para almacenar el resultado de adiciones, substracciones y otros. El
registro base normalmente apunta a la dirección de inicio de una estructura en memoria. El
registro contador cx, frecuentemente especifica el número de veces que alguna operación se va
a repetir. Y el registro de dato dx, la mayoría de las veces almacena datos, quizá pasada a una
subrutina para procesarse. Estas definiciones no son estrictas, y la mayoría de las veces es
nuestra decisión como usar un registro de propósito genera. Por ejemplo, aunque cx se le
denomine registro contador, no hay ninguna restricción que no me permita contar con bx. En
algunos casos, ciertas instrucciones, requerirán de determinados valores en registros específicos
para efectuar su trabajo.
En contraste con los registros de propósito general, los demás registros del 80X86 están
directamente relacionados a operaciones específicas. El stack pointer sp (puntero de pila), apunta
siempre a la cima del stack del procesador. El base pointer bp (puntero base) normalmente
direcciona a variables almacenadas dentro del stack. Source index si y destination index di
(índice fuente y destino), son conocidos como registros de cadenas. Normalmente si y di sirven
como caballos de trabajo para facilitar la carga de procesar cadenas.
Registros segmento.
Los cuatro registros segmento -cs,ds,ss y es- localizan el inicio de cuatro segmentos de 64K en
memoria. Un programa es libre de ocupar más de cuatro segmentos pero, en ese caso, tiene que
cambiar los valores de uno o más registros segmento para direccionar los segmentos adicionales.
Los registros segmentos están altamente especializados. No se puede realizar directamente
operaciones matemáticas en registros segmento o usarlos para almacenar los resultados de otras
operaciones. El registro de segmento de código (code segment cs), direcciona el inicio de un
código máquina de un programa en memoria. El registro de segmento de datos (data segment
ds) direcciona el inicio de las variables de memoria de un programa. El registro de segmento de
pila (stack segment ss), localiza el inicio del espacio de stack de un programa. El registro de
segmento extra (extra segment es), localiza un segmento de datos adicional si este es necesario,
aunque en muchos programas, es y ds direccionan al mismo segmento de memoria, facilitando
algunas operaciones laboriosas con estos registros. Los segmentos en memoria, pueden
encontrarse en cualquier orden en cualquier lugar de la memoria física de la computadora.
Puntero de Instrucciones.
Banderas.
XV
Lenguaje Ensamblador
.
La bandera de sobreflujo overflow flag indica si el resultado de una adición con signo no puede
ser correctamente representada dentro de un cierto número de bits. Otras instrucciones pueden
tomar acciones en base a los estados de un bit de bandera.
XVI
Lenguaje Ensamblador
.
Direcciones de 20 bits
Inicio Fin Descripción
00000 9FFFF Esta área contiene los primeros 64Kb de memoria de acceso aleatorio
(640K) (RAM).
A0000 A3FFF Área de 16K reservada por IBM
(656K)
A4000 BFFFF Este espacio es un buffer de 112K para gráficas y visualización de vídeo
(768K)
C0000 C7FFF Área de 32K para expansión de memoria ROM
(800K)
C8000 C9FFF 8K de memoria ROM que contiene el programa controlador del Disco Duro.
(808K)
CA000 F3FFF Área de 168K reservada para el ROM de diversas tarjetas adaptadoras
(976K) para soporte de aplicaciones
F4000 F5FFF Área de 8K reservada para memoria ROM del usuario, se relaciona con un
(984K) socket de repuesto
F6000 FDFFF Espacio de 32K para el cassette de BASIC
(1016K)
FE000 FFFFF Área de 8K reservada para el sistema entrada/salida de BASIC.
(1024K)
XVII
Lenguaje Ensamblador
.
Una razón para éste aparente desarreglo es la falta de estructuras de control en lenguaje
ensamblador. No hay construcciones REPEAT, WHILE, UNTIL para agrupar acciones repetitivas. No
hay sentencias IF-THEN-ELSE o CASE para realizar decisiones, no hay símbolos para signar
nombres de variables. Realizar tales acciones de alto nivel requiere que se construyan grupos
con instrucciones de bajo nivel de lenguaje máquina, dándole al texto de código fuente del
lenguaje ensamblador una forma homogénea que tiende a ocultar el significado interno de lo que
el programa hace. El lenguaje ensamblador es orientado a líneas, no orientado a sentencias
como lo es C, Pascal o BASIC. Consecuentemente, muchas líneas de código son normalmente
necesarias para realizar operaciones cualquier operación simple, como sumar o inicializar
variables.
A continuación se muestra un esquema, que se encontrará en casi todos los programas que se
desarrollen. La línea opcional %TITLE, describe el propósito del programa, haciendo con esto que
el texto entre comillas se imprima como cabecera en cada pagina de código que se imprime,
cuando se le pide al Turbo Ensamblador que lo imprima el listado. La directiva IDEAL activa el
modo Ideal del Turbo Ensamblador. Dejando fuera a MASM de poder ensamblarlo.
La directiva DOSSEG, le indica al ensamblador como ordenar los segmentos del programa- áreas
en memoria direccionadas por los registros segmento. Se puede escoger el orden de los
segmentos del programa en diferentes maneras. Aunque conocer el orden actual de los
segmentos es raramente tan importante como muchos programadores creen. Se sugiere utilizar
la directiva DOSSEG para almacenar segmentos en memoria en el mismo orden utilizado por la
mayoría de los lenguajes de alto nivel.
XVIII
Lenguaje Ensamblador
.
En seguida se encuentra la directiva MODEL, opcionalmente precedida por DOSSEG. MODEL
selecciona uno de varios modelos, la mayoría de los cuales son utilizados sólo cuando se
combina lenguaje ensamblador con Pascal o C. En progr amación de lenguaje ensamblador única,
el modelo small es la mejor opción. No hay que dejarse llevar por el nombre. El modelo de
memoria small proporciona 64Kbytes de código mas otros 64Kbytes de datos para hacer un total
máximo del programa de 128K.
Modelos de Memoria
Nombre Descripción
tiny Código, Datos y Stack contenidos en un segmento de 64K. Llamada a subrutinas
y transferencias de datos son cercanas (near). Utilizado solo para programas
*.COM
small Código y Datos en segmentos separados de 64K. Llamada a subrutinas y
transferencias de datos son cercanas (near). Utilizada en programas .EXE de
tamaño small a medium. La mejor opción para programas exclusivamente en
lenguaje ensamblador
medium Tamaño de código ilimitado. Datos limitado a un segmento de 64K. Las llamadas
a subrutinas son de tipo far (lejanas); referencias a datos son de tipo near
(cercanas).
compact Tamaño de código limitado a un segmento de 64k. Datos ilimitado. Las llamadas
a subrutinas son de tipo near (cercanas); referencias a datos son de tipo far
(cercanas). Utilizada por programas de tamaño medium a large con muchas
variables o muy largas.
large Tamaños de Código y Datos ilimitados. Las llamadas a subrutinas y referencias
de datos son de tipo far. Utilizadas para requerimientos de almacenaje de
programas y datos grandes. Tan grandes que las variables no excedan 64K.
huge Tamaños de Código y Datos ilimitados. Las llamadas a subrutinas y referencias
de datos son de tipo far. Utilizadas por programas muy grandes donde una o
más variables exceden los 64K.
tpascal Ensambla con segmentos de Turbo Pascal- similar al modelo small, pero con
múltiples segmentos de código. No utilizado normalmente en la programación
exclusiva en lenguaje ensamblador.
La directiva STACK reserva espacio para el stack del programa, un área de memoria que guarda
dos tipos de datos: valores temporales guardados o pasados por subrutinas y las direcciones a
las cuales las rutinas regresaran el control. Los stacks entran en juego durante las interrupciones.
Manipular el stack es una importante técnica de la programación en lenguaje ensamblador. El
valor después de la directiva STACK indica a l ensamblador cuantos bytes reserve para el
segmento Stack. La mayoría de los programas requieren un stack pequeño, y cualquier programa
de los más largos raramente requieren mas de 8K.
Igualdades, o asociaciones.
Utilizar igualdades de identificadores en lugar de número “mágicos” como 0100h y 0B800h nos
deja referirnos a expresiones, cadenas y otros valores por nombre, haciendo al programa fácil de
leer y modificar. (Los valores literales son mágicos porque es la manera en que pueden ocultar
secretos del programa). A continuación se presentan unos cuantos ejemplos de asociaciones:
XIX
Lenguaje Ensamblador
.
Contador EQU 10
Elemento EQU 5
Tamanio = Contador * Elemento
Texto EQU “Una pequeña constante de cadena”
Tamanio = 0
Después de declarar un símbolo con EQU, no se puede cambiar el valor del símbolo
asociado.
La misma regla no es verdadera para símbolos declarados con el signo de igual, y
puedes cambiar esos valores tan frecuente como se desee.
EQU puede declarar cualquier tipo de igualdades incluyendo números, expresiones y
cadenas de caracteres. El signo de igual (=) solamente puede declarar igualdades
numéricas, las cuales pueden ser valores literales como 10 y 0Fh, o expresiones tales
como y Contador * Elemento Direccion+2.
Los símbolos de igualdades no son variables - mucho menos lo símbolos ni sus valores
asociados son almacenados en el segmento de datos del programa. Las instrucciones
del lenguaje ensamblador nunca pueden asignar nuevos valores a igualdades de
símbolos, aunque EQU o el signo de igual hayan sido utilizados.
Aunque se pueden declarar igualdades en cualquier parte del programa, es
normalmente mejor opción colocarlos cerca del inicio del programa donde pueden ser
mejor visibles.
Las expresiones declaradas con EQU, son evaluadas en forma posterior cuando el
símbolo es utilizado en el programa. Las expresiones declaradas con el signo de igual
(=) son evaluadas en el lugar donde éstas son definidas. El ensamblador almacena el
texto asociado de EQU pero almacena solamente el valor de los símbolos =.
Esta última regla es fácil de comprender examinando unos cuantos ejemplos más. Supóngase
que se tienen las siguientes tres asociaciones:
LineasPorPag = 66
NumPaginas = 100
TotalLineas = LineasPorPag * NumPaginas
XX
Lenguaje Ensamblador
.
Internamente, Turbo Assembler almacena el texto actual, no el resultado calculado, de una
expresión a lo largo con todos los símbolos EQU - en éste caso, el texto de la expresión
LineasPorPag * NumPaginas. Posteriormente en el programa, cuando se utilice TotalLineas, el
ensamblador insertará éste texto como si se hubiese tecleado éstos caracteres dentro del código
fuente. La expresión entonces es evaluada para producir el valor final. Si previamente se
alteraron un o ambos valores en la expresión - tanto LineasPorPag como NumPaginas - el
resultado cambiará de acuerdo a sus valores.
El segmento de datos.
El segmento de datos del programa normalmente aparece entre las asociaciones o igualdades
y las instrucciones del programa. Esto es posible pero raramente utilizado, declarar el segmento
de datos en cualquier otra parte del programa o tener múltiples segmentos de datos declarados
en forma separada a lo largo del programa. Descartando ésta posibilidad. los programas de
lenguaje ensamblador serán mucho más fáciles de leer y modificar si se sigue el plan sugerido
aquí, declarando todas las variables entre las asociaciones y el código.
La sección del segmento de datos del programa inicia con la directiva DATASEG. Esto indica al
Turbo Assembler que almacene variables dentro del segmento de datos del programa.
El segmento de datos tiene dos tipos de variables, inicializadas o sin inicializar. Cuando el
programa se ejecuta, las variables inicializadas tienen valores preasignados, los cuales se
especifican el texto del código fuente y los cuales está guardados en el archivo de código del
programa en disco. Estos valores son automáticamente cargados en memoria y están disponibles
cuando el programa se ejecuta. Las variables sin inicializar son idénticas a las variables
inicializadas en todos los casos excepto que las variables sin inicializar no ocupan espacio en el
archivo de código del programa y consecuentemente, tienen valores desconocidos cuando el
programa se ejecuta. Por lo tanto, declarar variables de valores largos sin inicializar - un arreglo
de valores consecutivos o un buffer grande a ser llenado desde un archivo de disco, por ejemplo -
reducirá el tamaño del archivo de código del programa.
A continuación se muestra una serie de típicas declaraciones dentro del segmento de datos,
las cuales deben aparecer después de la cabecera y las declaraciones de constantes.
DATASEG
numRen db 25
numCol db 80
baseVideo dw 0800h
XXI
Lenguaje Ensamblador
.
Los símbolos asociados con variables - numRen, numCol, y baseVideo en el ejemplo anterior -
son llamadas etiquetas. Una etiqueta apunta al elemento que está etiquetado- en este caso el
espacio de memoria reservado para valores del programa. Los programas pueden hacer
referencia a éste espacio utilizando una etiqueta como apuntador a el valor en memoria. En el
programa ensamblado, las etiquetas son traducidas a direcciones de memoria donde las
variables son almacenadas, un proceso que permite direccionar memoria por nombres que se
inventen mas que literalmente por su dirección de memoria.
Las variables son garantía que siguen una de otra dentro del segmento de datos- conocimiento
que se puede utilizar para realizar varios trucos. Por ejemplo, estas declaraciones:
DATASEG
aTom db 'ABCDEFGHIJKLM'
nToz db 'NOPQRSTUVWXYZ'
Como se aprecia, se crearon dos cadenas de caracteres etiquetadas con aTom y aToz. En
memoria, los caracteres a hasta z, son almacenados consecutivamente, creando una cadena
conteniendo las letras del alfabeto. La etiqueta nToz simplemente apunta a la mitad de la
cadena- estas no son dos realmente dos entidades de memoria.
Se puede pensar que, ¿ Por que, si DB quiere decir "definir byte", que está haciendo
declarando cadenas de caracteres?, buena pregunta, DB tiene la habilidad especial de reservar
espacio para valores multi-byte, desde 1 hasta tantos bytes como sean necesarios. Una cadena
está compuesta de caracteres individuales ASCII, cada uno ocupando un byte; por lo tanto, DB es
una simple herramienta del lenguaje ensamblador para declarar cadenas, las cuales, después de
todo, son meramente series de valores byte ASCII almacenados consecutivamente en memoria.
Se puede utilizar DB para declarar caracteres individuales y valores byte, separados por comas:
DATASEG
dieznumeros db 1,2,3,4,5,6,7,8,9,10
laHora db 9,0 ; 9:00
laFecha db 15,12,93 ; 15/12/1993
También se pueden combinar caracteres y valores byte, creando a una cadena de doble linea
con los códigos ASCII de retorno de carro y salto de linea unidas en una sola declaración.
XXII
Lenguaje Ensamblador
.
Cuerpo del Programa
Después del segmento de datos viene el cuerpo del programa, también conocido como
segmento de código- el pedazo de código que contiene el código de programa ensamblado.
Dentro de ésta área, las lineas de texto del lenguaje ensamblador son a su vez divididas en
cuatro columnas: etiquetas, mnemónicos, operandos y comentarios. Cada columna tiene una
función importante. En el texto del programa, por ejemplo, el monto de espacio entre columnas
no es importante. La mayoría de las personas alinean las columnas simplemente presionando la
tecla TAB una o dos veces en su editor de preferencia.
Primeramente dentro del Segmento de Datos se declara una variable a utilizar dentro del
segmento de código, la variable tipo byte es etiquetada como codSalida.
La cuarta y última columna es siempre opcional y, si es incluida, debe iniciar con un punto y
coma (;), Turbo Assembler ignora todo desde el punto y coma hasta el final de la linea, dando
lugar para escribir un corto comentario, identificando secciones del programa y describiendo
secciones complicadas.
XXIII
Lenguaje Ensamblador
.
El cierre del programa
La parte final de un programa en ensamblador es el cierre, una simple linea que informa al Turbo
Assembler que ha alcanzado el fin del programa. Hay una sola directiva en el cierre: END. Un
cierre típico de un programa es:
La directiva END marca el fin del texto de código fuente del programa. El Ensamblador ignora
cualquier linea escrita por debajo de ésta. A la derecha de END, se debe especificar la etiqueta
donde se desea que el programa comience a ejecutar. Normalmente, esta etiqueta puede ser la
misma que la etiqueta que precede la primer instrucción siguiente a la directiva CODESEG. Se
puede iniciar el programa en cualquier parte, aunque no existe alguna razón para realizar esto.
IDEAL
DOSSEG
MODEL tiny
DATASEG
codSalida db 0
CODESEG
ORG 100h ;Dirección de inicio estándard para un *.COM
Inicio:
Fin:
mov ah, 04Ch ; Función de DOS: Salir del programa
mov al,[codSalida] ; Valor de retorno código-salida
int 21h ; Llama a DOS. Terminar el programa
END Inicio ; Fin del programa
XXIV
Lenguaje Ensamblador
.
IDEAL
DOSSEG
MODEL small
STACK 256
DATASEG
codSalida db 0
CODESEG
Inicio:
mov ax,@data
mov ds,ax
Fin:
mov ah, 04Ch ; Función de DOS: Salir del programa
mov al,[codSalida] ; Valor de retorno código-salida
int 21h ; Llama a DOS. Terminar el programa
END Inicio ; Fin del programa
XXV
Lenguaje Ensamblador
.
Grupos de Instrucciones y Conceptos.
Todas las instrucciones del 8086 están divididas por funciones dentro de seis categorías. Los
seis grupos son:
Instrucciones
Generales Descripción
Mnemónico/Operand
o
mov destino, fuente Mueve (copia) bytes o palabras
pop destino Extrae datos del stack
push inmediato Coloca datos en el stack
xchg destino, fuente Intercambia bytes y palabras
xlat/xlatb tabla Traduce desde una tabla.
Instrucciones de Entrada / Salida
in acumulador, puerto Mueve (copia) bytes o palabras
out puerto, acumulador Extrae datos del stack
Instrucciones de Direccionamiento
lds destino, fuente Carga puntero utilizando ds
lea destino, fuente Carga dirección efectiva
les destino, fuente Carga apuntador utilizando es
Instrucciones de Banderas
lahf Carga ah con (algunas) banderas
popf Extrae el registro de banderas del stack
pushf Coloca el registro de banderas en stack
Sahf Guarda ah dentro de banderas.
(algunas)
El dato fuente se mueve en la dirección de la flecha, de derecha a izquierda. Hay que ser
cuidadoso de no invertir los operandos, un típico y potencialmente desastroso error. En programas
de lenguaje ensamblador, la siguiente instrucción mueve el valor de el registro bx dentro del
registro ax:
Esta instrucción mov mueve el valor guardado en numPagina dentro del registro cx. Los
paréntesis rectangulares encerrando numPagina son importantes. La etiqueta numPagina
especifica una dirección de memoria. Pero con paréntesis rectangulares, [numPaginaError:
Reference source not found indica al valor almacenado en ésa dirección. Este concepto - que
una etiqueta especifica una dirección de un dato guardado en memoria - es muy importante para
comprender la programación en lenguaje ensamblador. Se debe ser cuidadoso siempre en
especificar cuando una instrucción va a operar con valor de dirección o con el valor almacenado
en esa dirección. Los paréntesis rectangulares son una simple herramienta para éste propósito,
pero se debe recordar en utilizarlos correctamente.
Podemos mover datos de registros a memoria, también. Por ejemplo, esta instrucción copia el
valor en el registro de 8 bits dl en la dirección especificada por nivel:
Con los paréntesis, se sabe que el valor de dl se mueve a la localidad en la cual nivel apunta.
Moviendo datos de ésta manera - copiando el valor de un registro a otro y transferir datos desde
un registro a una localidad en memoria- es una de las operaciones más comunes en
programación de lenguaje ensamblador. Solo una cosa no puede hacer mov, lo cual es transferir
datos directamente entre dos localidades de memoria. Los siguiente no es válido:
Para mover el valor almacenado en maximo en la localidad direccionada por contador , requiere
de dos pasos, utilizando un registro como un auxiliar de almacenamiento intermedio.
XXVII
Lenguaje Ensamblador
.
DOSSEG
MODEL small
STACK 256
DATASEG
codSalida DB 0
datoX DB 99 ; variable de un byte
CODESEG
Inicio:
mov ax, @data ; Inicializa la dirección
mov ds, ax ; del segmento de datos
mov ax,1 ; Mueve datos inmediatos en registros
mov bx,2
mov cx,3
mov dx,4
mov ah,[datoX] ; almacena el valor de datoX en al
mov si,offset [datoX] ; Carga la dirección de datoX en si
Fin:
mov ah, 04Ch ; Función de DOS: Salir del programa
mov al, [codSalida] ; Retorno el Código de salida
int 21h ; Llama a DOS. Terminar programa
XXVIII
Lenguaje Ensamblador
.
Uso del Stack
El Stack es un segmento especial de memoria que opera en conjunción con varias instrucciones
del 8086. Como con todos los segmentos, la localización del stack y su tamaño (hasta 64K)
dependen del programador determinarlo. En los programas de lenguaje ensamblador, la forma
más fácil de crear un stack es usar la directiva STACK. Un stack tiene tres propósitos principales:
El último de éstos viene a jugar más frecuentemente en los lenguajes de programación de alto
nivel, donde las variables son pasadas vía stack hacia y desde, funciones y procedimientos.
Similarmente, variables temporales pueden se almacenadas en el stack. Estos usos son raros en
la programación pura en lenguaje ensamblador, aunque se puede almacenar variables de
memoria de ésta manera si se desea.
No como los platos, los valores en la computadora no pueden moverse físicamente hacia arriba
y hacia abajo. Por lo tanto, para simular la acción de un movimiento de los valores del stack,
requiere de utilizar registros para localizar la dirección base del Stack y la dirección OFFSET del
plato superior - que es, la localidad donde el valor superior de la pila es almacenado. En
programación 8086, el registro segmento ss direcciona al segmento de stack base. El registro sp
direcciona el desplazamiento OFFSET tope del Stack en dicho segmento.
Memoria Baja
0F0 0000 0:
0002
0004
0006
ss:sp 0008 200 3
ss:sp2
000A 100
ss:sp 1 Fin del segmento
Stack 000C
XXIX
Lenguaje Ensamblador
.
En referencia al la figura anterior. Varias acciones ocurren si se ejecutan las siguientes
instrucciones:
mov ax,100
push ax ; sp2
mov bx, 200
push bx ; sp3
1. Substrae 2 de sp
2. El valor del registro especificado es copiado a [ss:sp].
El orden de estos pasos es importante. Un push primero substrae 2 (no 1) de sp. La primer
colocación push, deja a sp en sp2, donde el valor del registro ax es almacenado. Nótese que
esta acción deja al puntero del stack direccionado al valor palabra recientemente colocado-
pushed- en el stack.
El punto principal de la manipulación del stack es simple: Para cada push en un programa, debe
haber su correspondiente pop. Igualando pops y pushes mantiene el stack en forma correcta-
en otras palabras, en sincronización con la habilidad del programa para almacenar y recuperar el
valor que necesita.
Considere lo que sucede si se falla en ejecutar un correspondiente pop para cada push. En
éste caso, pushes futuros causarán que el stack crezca mas y más largo, eventualmente
rebasando el espacio segmento permitido para el programa. Este grave error normalmente
termina en un crash sobre escribiendo otras áreas de memoria por el puntero del stack. Un error
similar ocurre si se ejecutan más pops que pushes, causando un bajoflujo y también resultar en
un crash.
Una forma de prevenir éstos problemas es escribir los programas en pequeños módulos, o
subrutinas. En cada modulo, realizar un push con todos los registros que se utilizarán. Entonces,
justo antes de que ésta sección de código termine, realizar un pop a los mismos registros
retirándolos pero en orden inverso.
push ax
push bx
push cx
pop cx
pop bx
pop ax
En éste ejemplo, los registros ax, bx y cx, son posiblemente utilizados; por lo tanto, éstos
registros son almacenados en el stack para preservar los valores de los registros. Por último, los
valores son retirados (pop) del stack en orden inverso, restaurando los valores originales de los
registros y manteniendo el stack en sincronía.
XXX
Lenguaje Ensamblador
.
Instrucciones Aritméticas
Las mayoría de las computadoras son grandiosas para las matemáticas, esto viene a sorprender
que el lenguaje ensamblador solo tiene unos operadores matemáticos relativamente primitivos.
No hay símbolos de exponenciación, no hay punto flotante, no hay raíz cuadradas, y no existen
funciones SENO y COSENO dentro del grupo de instrucciones del 8086. Las instrucciones
Matemáticas en lenguaje ensamblador están restringidas a sumar, multiplicar, dividir y restar
valores enteros con signo o sin signo.
Instrucciones
Generales Descripción
Mnemónico/Operand
o
Instrucciones de Adición
aaa Ajuste ASCII para adición
adc destino, fuente Suma con acarreo
add destino, fuente Suma bytes o palabras
daa Ajuste decimal para adición
inc destino Incremento
Instrucciones de Substracción
aas Ajuste ASCII para substracción
cmp destino, fuente Compara
das Ajuste decimal para substracción
dec destino Decrementa byte o palabra
neg destino Negar (complemento a dos)
sbb destino, fuente Substrae
sub destino, fuente Substrae
Instrucciones de Multiplicación
aam Ajuste ASCII para multiplicación
imul fuente Multiplicación con enteros
mul fuente Multiplicar
Instrucciones de División
aad Ajuste ASCII para división
cbw Convierte bytes a palabras
cwd Convierte palabras a dobles palabras
div fuente Divide
idiv fuente División de Enteros
Existen dos formas de incrementar el poder matemático del lenguaje ensamblador. Primero, se
puede comprar )o escribir) un paquete de funciones matemáticas con rutinas que implementan
las funciones matemáticas de alto nivel que se necesitan. Otra solución es comprar un chip
coprocesador matemático, aunque esto puede ser muy caro. Como una tercera opción , y
probablemente la mejor, es utilizar un lenguaje de alto nivel como Pascal o C para codificar las
expresiones de punto flotante. Estos lenguajes vienen con un detector automático de presencia
de coprocesador matemático o cambiar a un software emulador para sistemas que carezcan del
chip opcional. Después de escribir el programa, se puede combinar el código compilado de alto
nivel, con nuestro programa en lenguaje ensamblador. Ya que el coprocesador matemático tiene
requerimientos estrictos acerca de los datos y formatos de instrucciones, la mayoría de los
compiladores generan código máquina optimizado, y hay poca ventaja en escribir expresiones de
punto flotante directamente en lenguaje ensamblador.
Pero no tome esto como una pronunciada negativa en matemáticas del lenguaje ensamblador.
Aún sin una librería matemática o coprocesador, se pueden utilizar plenamente instrucciones de
enteros.
XXXI
Lenguaje Ensamblador
.
Por ejemplo, no se necesitan números de punto flotante para totalizar los bytes en un directorio
de un disco o contar el número de palabras en un archivo de texto. Para éstas y otras
operaciones, la matemática entera es más que adecuada. En lenguaje ensamblador puro, tales
trabajos frecuentemente trabajan más rápidamente que código equivalente de lenguajes de alto
nivel.
Ejemplo:
DATASEG
codSalida DB 0
contador DW 1
CODESEG
Inicio:
mov ax, @data ; Inicializa la dirección
mov ds, ax ; del segmento de datos
mov ax,4
mov bx,2
add ax,bx ; ax<-ax+bx
mov cx,8
add cx,[contador] ; cx<-cx+[contador]
XXXII
Lenguaje Ensamblador
.
Ejemplo:
DATASEG
codSalida DB 0
opByte DB 8
opWord DW 100
ByteFuente DB 64
WordFuente DW 4000
CODESEG
Inicio:
mov ax, @data ; Inicializa la dirección
mov ds, ax ; del segmento de datos
mov al,[opByte]
mul [ByteFuente] ; ax<- al * [ByteFuente]
mov ax,[opWord]
mul [WordFuente] ; ax,dx<- ax * [WordFuente]
mov ax,[opWord]
mul ax ; ax,dx<- ax * ax
mov ax,[opWord]
div [ByteFuente] ; al<- ax div [ByteFuente]
mov ax,[opWord]
mov dx,0
div [WordFuente] ; ax<-ax,dx div [WordFuente]
Fin:
mov ah, 04Ch ; Función de DOS: Salir del programa
mov al, [codSalida] ; Retorno el Código de salida
int 21h ; Llama a DOS. Terminar programa
Cuando se utilizan valores binarios con signo, normalmente es necesario convertir valores de 8
bits, a palabras de 16 bits, quizá para preparar para una multiplicación o división. Ya que el valor
puede ser un numero negativo en notación complemento a dos.
XXXIII
Lenguaje Ensamblador
.
Instrucciones Lógicas.
Las instrucciones lógicas están agrupadas en dos subdivisiones: lógicas e instrucciones de
rotación/corrimiento. Las instrucciones lógicas combinan bytes y palabras con AND, OR, y otras
operaciones lógicas. Las Instrucciones de rotación/corrimiento, realizan corrimientos y rotaciones
de bits en bytes y palabras.
Instrucciones Lógicas
Mnemónico/Operand Descripción
o
Instrucciones Lógicas
and destino, fuente AND lógico
not destino NOT lógico, complemento a 1
or destino, fuente OR lógico
test destino, fuente Prueba bits
xor destino, fuente OR Exclusivo lógico
Instrucciones de Corrimiento/Rotación
rcl destino, contador Rota hacia la izquierda con acarreo
rcr destino, contador Rota hacia la derecha con acarreo
rol destino, contador Rota hacia la izquierda
ror destino, contador Rota hacia la derecha
sar destino, contador Corrimiento aritmético a la derecha
shl/sal destino, Corrimiento a la izquierda/aritmético
contador
shr destino, contador Corrimiento a la derecha
La instrucción lógica mas simple not, cambia los bits en un byte o palabra de ceros a unos y los
unos a ceros. Esto es llamado complemento a uno, (sumar uno a este resultado forma el
complemento a dos, aunque es más fácil usar neg para este propósito). Una forma de utilizar not
es para cambiar valores de verdadero y falso. Si un valor cero representa falso y un valor no cero
representa verdadero. La siguiente instrucción cambia el registro dh de verdadero a falso y lo
regresa a verdadero.
XXXIV
Lenguaje Ensamblador
.
Ejemplo completo:
DATASEG
codSalida DB 0
WordFuente DW 0ABh ; Valor fuente de 16 bits
WordMascara DW 0CFh ; Mascara de 16 bits
CODESEG
Inicio:
mov ax, @data ; Inicializa la dirección
mov ds, ax ; del segmento de datos
mov ax,[WordFuente]
mov bx, ax
mov cx, ax
mov dx, ax
XXXV
Lenguaje Ensamblador
.
La instrucción and es normalmente utilizada para verificar si uno o más bits son igual a 1 en un
valor byte o word. Pro ejemplo, si se necesita determinar si el bit es 1, se puede utilizar la
máscara de 4:
XXXVI
Lenguaje Ensamblador
.
Realizando corrimiento de bits.
Varias instrucciones de salto y rotación están disponibles en el grupo de instrucciones del
8086. Hay instrucciones para realizar corrimientos de bits a la izquierda y a la derecha y rotar
valores através de la bandera de acarreo cf. Las instrucciones se dividen a su vez en 4
subgrupos:
Msb Lsb
cf 0
Msb Lsb
cf
XXXVII
Lenguaje Ensamblador
.
Msb Lsb
cf
La instrucción sar
Msb Lsb
cf
Para realizar corrimientos de bits por más de un bit a la vez en el 8086 requiere de dos pasos:
primero cargar un valor contador en cl y entonces especificar cl como el segundo operando de la
instrucción de corrimiento:
Se debe utilizar cl para esto - ningún otro registro trabajara como segundo operador. Se puede
también realizar corrimientos de bits dentro de localidades de memoria.
XXXVIII
Lenguaje Ensamblador
.
DATASEG
codSalida DB 0
operando DB 0AAh
CODESEG
Inicio:
mov ax, @data ; Inicializa la dirección
mov ds, ax ; del segmento de datos
XXXIX
Lenguaje Ensamblador
.
La instrucción sar, opera identicamente a shr excepto que el MSB retiene su valor original.
Adicionalmente, el MSB es copiado al bit de la derecha. Esto es más fácil de ver con ejemplos de
valores binarios:
10001000
11000100
11100010
11110001
11111000
Iniciando con el segundo valor, cada linea sucesiva muestra el resultado de aplicar sar al valor
de arriba. Los bits se corren hacia la derecha igual como lo realiza shr, pero el MSB retiene su
valor y es copiado al bit de la derecha. Como resultado, sar es útil para dividir números negativos
en complemento a dos por poderes de 2. Por ejemplo, expresado en hexadecimal, sucesivas
instrucciones sar producen ésta secuencia:
8000 -32768
C000 -16384
E000 -8192
F000 -4096
F800 -2048
:
:
FFFE -2
FFFF -1
Instrucciones sar adicionales no tienen efecto en FFFF hexadecimal - no igual que idiv, el cual
es utilizado para dividir -1 por 2 lo cual resulta 0, como se podría esperar.
sar no tiene una contraparte hacia la izquierda. En su lugar, la instrucción shl se le otorgó un
segundo mnemónico sal, maquillando la deficiencia. La razón de que un corrimiento aritmético a
la izquierda no es diferente de un corrimiento sencillo a la izquierda, es evidente si examinamos
la secuencia en hexadecimal previa en reversa. Si trabajamos desde abajo, estos son los mismos
valores que podría producir el aplicar shl .
XL
Lenguaje Ensamblador
.
Las instrucciones de control de flujo, o instrucciones de salto, son las que permiten a los
programas cambiar la dirección de el siguiente código máquina a ser ejecutado.
Sin instrucciones de control de flujo, un programa simplemente podría iniciar desde el
comienzo del programa y correr sin parar hasta el término del código, sin paros, ciclos, o caminos
alternos através de su vía. Con control de flujo, los programas pueden realizar decisiones,
inspeccionan banderas, y toman aciones basadas en operaciones previas, prueba de bits,
comparaciones lógicas, y aritméticas. También, las instrucciones de control de flujo proporcionan
a los programas la habilidad de repetir instrucciones basadas en ciertas condiciones,
conservando memoria para ciclar continuamente atraves de las mismas secciones de código.
Instrucciones Lógicas
Mnemónico/Operand Descripción
o
Instrucciones de transferencia incondicional
call destino Llama un procedimiento
jmp destino Salta incondicionalmente
ret valor Retorna de un procedimiento
retn valor Retorna de un procedimiento cercano
retf valor Retorna de un procedimiento lejano
Instrucciones de transferencia condicional
ja/jnbe destino-corto Salta si mayor/no menor o igual
jae/jnb destino-corto Salta si mayor o igual/no menor
jb/jnae destino-corto Salta si menor/no mayor o igual
jbe/jna destino-corto Salta si menor o igual/no mayor
jc destino-corto Salta si acarreo
je/jz destino-corto Salta si igual/0
jg/jnle destino-corto Salta si mayor/no menor o igual
jge/jnl destino-corto Salta si mayor o igual/no menor
jl/jnge destino-corto Salta si menor/no mayor o igual
jle/jng destino-corto Salta si menor o igual/no mayor
jnc destino-corto Salta si no acarreo
jne destino-corto Salta si no igual/0
jno destino-corto Salta si no hay sobreflujo
jnp/jpo destino-corto Salta si NO paridad/paridad impar
jns destino-corto Salta si no hay sobreflujo
jo destino-corto Salta si sobreflujo
jp/jpe destino-corto Salta si paridad / paridad par
js destino-corto Salta si signo
Instrucciones de ciclos
jcxz destino-corto Salta si cx es igual a 0
loop destino-corto Ciclar mientras cx<>0
loope/loopz destino Ciclar mientras igual/0
loopne/loopnz destino Ciclar mientras no igual/0
Instrucciones de Control de Interrupciones
int tipo interrupción Interrupción
into Interrupción en sobreflujo
iret Retorno de interrupción
XLI
Lenguaje Ensamblador
.
Transferencias Incondicionales
Llamando subrutinas.
Uno de los mecanismos del lenguaje ensamblador más utilizados es la subrutina, una
colección de instrucciones relacionadas, normalmente realizando una operación repetitiva. Una
subrutina puede desplegar una cadena de caracteres en la pantalla, sumar una serie de valores,
o inicializar un puerto de salida. Algunos programadores escriben largas subrutinas que realizan
múltiples trabajos en la teoría que múltiples subrutinas pueden hacer que un programa rápido
corra lentamente. Se puede obtener un poquito de más de velocidad combinando operaciones
dentro de una rutina masiva, pero se terminará con un programa difícil de mantener.
La mejor rutina hace uno y solo un trabajo. La mejor rutina es la más corta posible y solo tan
larga como sea necesario. La mejor rutina puede ser listada en una o dos páginas de papel
impreso. La mejor rutina no comienza con código, pero sí con comentarios describiendo el
propósito de la rutina, resultados, entradas esperadas, y registros afectados. La mejor rutina
puede ser comprendida fuera del contexto por alguien quien no tenga idea que realiza el
programa completo.
XLII
Lenguaje Ensamblador
.
DATASEG
codSalida DB 0
operando DB 0AAh
CODESEG
Inicio:
mov ax, @data ; Inicializa la dirección
mov ds, ax ; del segmento de datos
XLIII
Lenguaje Ensamblador
.
Saltos Incondicionales.
El 8086 tiene más de una docena de instrucciones de saltos diferentes. Uno de estos, jmp, es
un salto incondicional; todos los demás son saltos condicionales. La diferencia entre los dos tipos
de saltos es muy importante:
- Un salto incondicional siempre causa que el programa inicie corriendo en una nueva
dirección.
- Un salto condicional causa que el programa inicie corriendo a una nueva dirección solo si
ciertas condiciones son satisfechas. De otra manera, el programa continua como si la
instrucción de salto condicional no existiese.
El jmp incondicional trabaja en forma idéntica a call, excepto que la dirección de retorno
no es colocada en el stack. La instrucción jmp toma un solo parámetro: la etiqueta de la localidad
donde el programa transferirá el control.
jmp Salida
Saltos Condicionales.
La tabla siguiente lista las 18 instrucciones de salto condicional del 8086, muchas de las
cuales tienen dos mnemónicos representando la misma instrucción, por ejemplo, je/jz y
jg/jnle haciendo un total de 30 mnemónicos. Esto puede parecer un gran numero de saltos
condicionales que aprender, pero, como las conjugaciones de los verbos, las diferentes formas
son más fáciles de aprender si se separa la raíz (siempre j para saltar), de las terminaciones (a,
nbe, e, z, etc.) Cada una de estas terminaciones representan una condición única. Una vez que
se memorizan sus significados, se tendrán menos problemas diferenciando los muchos tipos de
saltos condicionales.. En la tabla, las terminaciones a la derecha son negaciones de las
terminaciones a la izquierda. (Dos mnemónicos de salto condicional, jpe y jpo no tienen
contrapartes negativas).
Todos los saltos condicionales requieren de una dirección destino - una etiqueta marcando la
localización donde se desea el programa siga corriendo si la condición especificada se cumple.
Por ejemplo, siguiendo a la comparación de dos registros con cmp, se puede utilizar je (salta si
igual) para transferir el control a una localización si los valores de los registros son iguales. Para
demostrar esto, suponga la necesidad de una subrutina para retornar cx igual a 1 si ax = bx o 0
si ax<>bx.
PROC RegIguales
mov cx,1 ; Inicializa cx a 1.
cmp ax,bx ; ax es igual a bx ?
je Continua ; Salta si ax=bx
xor cx,cx ; Sino, inicializa cx a 0000
Continua:
ret ; Retorna al llamador.
ENDP RegIguales
XLIV
Lenguaje Ensamblador
.
Restricciones.
Todos los saltos condicionales tienen una gran restricción: pueden transferir control solo hacia
una distancia muy corta - exactamente 128 bytes hacia atrás (hacia direcciones bajas) o 127
bytes hacia adelante (hacia direcciones altas) desde el primer byte de la siguiente instrucción
inmediata al salto. No hay que preocuparse, el Ensamblador informará si se intenta realizar un
salto que sobrepase este rango.
Nota: Utilice saltos condicionales opuestos a los que normalmente utilizaría si los
destinos están dentro del rango. Entonces continúe con un jmp incondicional a este
destino.
Para aprender más acerca de como operan las instrucciones de salto condicional, trate de correr
algunos de los ejemplos en el Depurador. Más que esto, piense lógicamente y no mecánicamente
como se acostumbra en los lenguajes estructurados. ¿Desea saltar si el resultado es menor o
mayor (con signo), o si el resultado es superior o inferior (sin signo)?. Mantenga los saltos a
mínimas distancias posibles y evite utilizar muchos saltos.
cmp bx,5
jne Not5
mov ax,[contador5]
jmp Continua
Not5:
mov ax,[Contador]
Continua:
: : :
El fragmento anterior requiere de dos etiquetas y dos instrucciones de saltos solo para cargar ax
con un valor diferente dependiendo si bx es igual a 5. No trate de darle muchas vueltas al asunto.
Precarge ax con uno de los dos posibles resultados eliminando una etiqueta y el salto
incondicional.
mov ax,[contador5]
cmp bx,5
je Continua:
mov ax,[contador]
Continua:
: : :
Nos sólo es más corto y fácil de leer, el código opera mas rápidamente cuando bx no es igual a
5. (Una instrucción jmp como se utiliza aquí, toma más tiempo en ejecutarse que un mov entre un
registro y una localidad de memoria; por lo tanto, los dos movs no son una pérdida como se
puede pensar en una lectura superficial).
XLVI
Lenguaje Ensamblador
.
Instrucciones de control del Procesador.
El grupo de instrucciones listadas a continuación operan directamente en el procesador. En
todos los casos, estas instrucciones de control del procesador ensamblan código de un solo byte
y no requieren de operandos. La mayoría de las instrucciones encienden o apagan
individualmente bits de banderas. Otras sincronizan el procesador con eventos externos y, en un
caso, nop realiza absolutamente nada.
PROC PruebaBit3
test dl, 08h ; Prueba el bit 3
jz Exit ; Sale si el bit 3 = 0
stc ; Establece la bandera de acarreo
Exit:
ret ; Retorna al llamador
ENDP PruebaBit3
XLVII
Lenguaje Ensamblador
.
Instrucciones de Cadenas.
Las instrucciones de cadenas del 8086 son pequeños motores para procesar cualquier tiempo
de datos - no solamente cadenas. Recuerde que las cadenas en el leguaje ensamblador, son
secuencias de bytes que puede o no puede representar caracteres ASCII. Descartando sus
nombres sugestivos, las instrucciones de cadenas del 8086 no les importa lo que significan los
bytes. Las instrucciones de cadenas se dividen en tres grupos:
Instrucciones de Cadenas
Mnemónico/Operand Descripción
o
Instrucciones de transferencia de cadenas
lods fuente Carga cadenas de bytes o palabras
lodsb Carga cadenas de bytes
lodsw Carga cadenas de palabras
movs destino, fuente Mueve cadenas de bytes o palabras
movsb Mueve cadenas de bytes
movsw Mueve cadenas de palabras
stos destino Guarda cadenas de bytes o palabras
stosb Guarda cadenas de bytes
stosw Guarda cadenas de palabras
Instrucciones de inspección de cadena
cmps destino, fuente Compara cadenas de bytes o palabras
cmpsb Compara cadenas de bytes
cmpsw Compara cadenas de palabras
scas destino Busca cadenas de bytes o palabras
scasb Busca cadenas de bytes
scasw Busca cadenas de palabras
Instrucciones de Repetición Prefijas
Rep Repite
repe/repz Repite mientras igual/0
repne/repnz Repite mientras no igual/0
Deseche los muchos mnemónicos de la tabla anterior, actualmente sólo existen cinco
instrucciones de cadenas: lods, stos, movs, scas, y cmps. Las otras son nombres alternativos
cortos para estos mismos comandos. Como se puede apreciar en la tabla, los nombre cortos tales
como lodsb y cmpsw no requieren de operandos. Similarmente, solo hay dos prefijos de
repetición: rep es idéntico a repe y repz. Y repne y repnz representan el mismo prefijo. Los
nombres intercambiables son provistos meramente para ayudar a documentar lo que hace el
programa exactamente.
XLVIII
Lenguaje Ensamblador
.
Todas las instrucciones de cadenas utilizan registros específicos para realizar sus acciones. No
como otras instrucciones en que el programador decide cuales registros utilizar, las instrucciones
de cadenas son rígidas en este aspecto, siempre operan con la misma combinación de registros
ds:si y es:di - los registros índice de cadenas fuente y destino, los cuales especifican offsets en
los segmentos de datos y extra.
Las cinco instrucciones cargan, mueven, comparan, y buscan bytes y palabras. Mientras
realizan estas tareas, cada instrucción de cadena también incrementa o decrementa los registros
que utilizan. Operaciones con byte restan o suman 1 a si o di (o ambos); operaciones con
palabras suman o restan 2. Por ejemplo, si si es igual a 0010h, entonces después de una
operación lodsw, si puede ser avanzado a 0012 ( o retardado a 000E, dependiendo de la
dirección de la operación de cadena). Dado éste efecto sobre los registros índice, adicionando un
prefijo de repetición a una instrucción de cadena, los programas pueden procesar secuencias
completas de datos en un simple comando.
La bandera de dirección df especifica si las instrucciones cadena deben incrementar o
decrementar si y di. Si df = 1, entonces los índices son decrementados hacia direcciones
bajas. Si df = 0, entonces los índices son incrementados hacia direcciones altas. Utilizar cld para
limpiar df, automáticamente incrementando si y di hacia direcciones altas. Utilizar std para
establecer df, automáticamente decrementando si y di hacia direcciones bajas.
Cargando cadenas.
La instrucción lods carga datos direccionados por ds:si o es:si en al para operaciones
byte o dentro de ax para operaciones palabra. Después de esto, si es incrementado o
decrementado, dependiendo del estado de la bandera de dirección df. Las operaciones byte
ajustan si por 1; operaciones palabra, por 2. Con esta instrucción, se puede construir un ciclo
simple para buscar por un valor byte:
cld ; Auto-Incrementa si
Repite:
lods [byte ptr ds:di] ; al <- [ds:si]; si <-si+1
or al,al ; al = 0 ?
jne Repite ; Repetir si al<>0
XLIX
Lenguaje Ensamblador
.
Ya que lods normalmente opera sobre los valores direccionados por ds:si, Turbo Assembler
proporciona dos mnemónicos que no requieren operandos, lodsb y lodsw. La sb en este y en los
otros mnemónicos cortos de cadenas está por cadenas de bytes (string byte). La sw esta por
cadenas de palabras (string word). La tabla siguiente lista los formatos largos equivalentes a
todos los mnemónicos de formato corto.
DATASEG
cadena db 'Esta es una cadena',0
CODESEG
mov si, offset cadena ; Asigna la dirección de la cadena a si
lods [cadena] ; Obtiene el primer byte de la cadena
L
Lenguaje Ensamblador
.
Stos y los mnemónicos de formato corto stosb y stosw guardan un byte que está en al o una
palabra en ax a la localidad direccionada por es:di. Como con lods, stos incrementa o
decrementa di por 1 y 2, dependiendo de la inicialización de df y si los datos están compuestos
de bytes o palabras. Combinando lods y stos en un ciclo pueden transferirse cadenas de una
localidad de memoria a otra:
cld ; autoincrementa SI y DI
Repite:
Lodsw ; Carga en el registro AX el word que apunte el registro SI
cmp ax,0FFFFh ;
je Salir
stosw ; Pone en la dirección apuntada por DI, el valor de AX
jmp Repite
Salir:
Moviendo cadenas
Utilice movs o los formatos cortos movsb y movsw para mover bytes y palabras entre dos
localidades de memoria. Ya que estas instrucciones no requieren de un registro intermedio para
almacenar a su manera, datos desde y hacia memoria, esta es la herramienta disponible más
rápida para mover bloques de datos. Como con otras instrucciones de cadenas, se puede utilizar
el formato largo con operandos, o, como prefieren la mayoría de los programadores, utilizar los
mnemónicos simples de formato corto.
Movsb mueve 1 byte de la localidad direccionada por ds:si a es:di a la localidad
direccionada por es:di, incrementando o decrementando ambos registros índices por uno. Movsw
mueve una palabra entre dos localidades de memoria, incrementando o decrementando los
registros por 2. Aun cuando se pueden utilizar estas instrucciones individualmente para
transferir un byte o palabra - o construir ciclos para transferir valores sucesivos- normalmente se
adicionará un prefijo de repetición como en el siguiente ejemplo.
Estas tres instrucciones mueven 100 bytes de memoria iniciando en ds:si a la localidad
iniciando en es:di. El prefijo de repetición rep ejecuta repetidamente movsb, decrementando cx
en 1 después de cada repetición, y terminando cuando cx es igual con 0. Se debe de utilizar cx
para éste propósito. Sin un prefijo de repetición, se tendría que escribir las instrucciones de la
siguiente manera:
LI
Lenguaje Ensamblador
.
Llenando memoria
La instrucción stos realiza el llenado de memoria con un valor byte o palabra fácilmente.
Hay que ser muy cuidadoso con esto. Se puede borrar un segmento entero de memoria en un
flash. Por ejemplo, esto almacena el valor byte 0 en un bloque de memoria de 512 bytes,
iniciando en la etiqueta Buffer:
Rastreando cadenas.
Utilice scas para rastrear cadenas por valores específicos. Como en las otras instrucciones
de cadenas se puede utilizar los formatos largos o los formatos cortos scasb y scasw. Cada
repetición de scasb compara el valor byte en al o el valor palabra en ax con el dato direccionado
por es:di. El registro di es entonces incrementado o decrementado por 1 o 2.
Dado que se puede comparar un sólo byte o palabra con una instrucción cmp, la
instrucciones scan son casi siempre preferidas con repe (repetir mientras igual) - o con los
mnemónicos alternativos repz (repetir mientras zf=1) y repnz (repetir mientras zf=0). Para cada
repetición, estos prefijos decrementan cx en 1, finalizando si cx llega a ser 0. (Recuerde que
repe, repz, y rep son las mismas instrucciones.) Cuando estos prefijos son utilizados con scas o
cmps (o cualquiera de sus formatos cortos equivalentes), las repeticiones también se detienen
cuando la bandera cero zf indica que falló la búsqueda o comparación. Por ejemplo, una simple
secuencia de rastreo de 250 bytes, buscando por un 0:
cld
mov di, OFFSET Inicio
mov cx,250
xor al,al
repne scasb
je SeEncontro
LII
Lenguaje Ensamblador
.
or cx,cx
jz Salto
rep stosb
Salto:
jcxz Salto
rep stosb
Salto:
Comparando cadenas
Para comparar dos cadenas, utilizar cmps o los formatos cortos cmpsb y cmpsw. La instrucción
compara dos bytes o palabras en es:di y ds:si o es:si. La comparación con cmps substrae el
byte o palabra en es:di del byte o palabra en ds:si o es:si, afectando las banderas, pero no el
resultado - similarmente a como trabaja cmp. Después de la comparación, tanto si y di son
incrementados o decrementados por 1 para comparaciones byte y por 2 para comparaciones
palabra. Estas instrucciones casi siempre son precedidas de un prefijo de repetición como en el
ejemplo siguiente:
Esta secuencia asume que la cadena s1 está almacenada en el segmento direccionado por ds y
que la cadena s2 esta almacenada en el segmento direccionado por es. Si ds = es, entonces las
dos cadenas pueden estar guardadas en el mismo segmento.
LIII
Lenguaje Ensamblador
.
De todas las materias de estudio de la programación en lenguaje ensamblador del 8086, las
diferentes formas de direccionamiento de datos en memoria, son probablemente de las más
difíciles de aprender. Se evitarán muchos dolores de cabeza si se recuerda que, todas las
referencias de datos toman una de estas tres formas:
El ensamblador genera una variante de código máquina de la instrucción mov, que carga el valor
inmediato 5 dentro de ax. El 5 es almacenado directamente en el código máquina ensamblado de
la instrucción mov. En la mayoría de los casos, el datos inmediato es el único operando o es el
segundo de dos operandos. (Una excepción es out, la cual permite datos inmediatos como el
primero de dos operandos). Nunca se puede alterar el valor de un dato inmediato cuando el
programa se ejecuta.
Datos en Registros se refiere a datos guardados en los registros del procesador. El código
máquina generado por el ensamblador para datos en registros, incluye valores apropiados
causando que la instrucción opere sobre los registros especificados, como en:
Datos en Memoria es el tercer tipo de referencia de datos, del cual hay muchas
variaciones. Para evitar confusión cuando se aprenden éstas variantes, recordar que el punto
clave es ayudar al procesador a calcular un valor sin signo de 16 bits, llamado dirección efectiva,
o EA (effective address). La EA representa un desplazamiento iniciando desde la base del
segmento direccionado por uno de los cuatro registros de segmento: cs, ds, es y ss. Un
registro segmento y un desplazamiento forman una dirección lógica de 32 bits, la cual el 8086
traduce en una dirección física de 20 bits, para localizar cualquier byte en memoria.
No hay que preocuparse acerca de como calcular una dirección efectiva o formar la dirección
física de 20 bits, estos son trabajos del procesador. La responsabilidad del programador es
proporcionar al procesador los datos necesarios para calcular la dirección efectiva, localizando las
variables en memoria. Para realizar esto, se pueden utilizar uno de siete modos de memoria,
como se describen a continuación.
LIV
Lenguaje Ensamblador
.
Direccionamiento Directo.
Sobre-especificación
- Siempre y cuando se declare una sobre-especificación como parte de una referencia de dato,
una sobre-especificación ocupa un byte de código máquina y es insertado justo antes de la
LV
Lenguaje Ensamblador
.
instrucción afectada. Las sobre-especificaciones son prefijos que cambian el funcionamiento
de la siguiente instrucción a ser ejecutada.
- El efecto de una sobre-especificación es para una sola instrucción. Se debe utilizar una sobre-
especificación en cada referencia a datos en otro segmento, en lugar del segmento por
default para la instrucción.
- En el modo IDEAL de Turbo Assembler, la referencia de dirección completa incluyendo el
segmento sobre-especificado debe estar entre paréntesis rectangulares. Aunque el modo de
MASM permite un estilo de formato mas libre, la sintaxis del modo IDEAL es perfectamente
compatible con el modo MASM.
- Es responsabilidad del programador asegurarse que las variables están actualmente en los
segmentos que se especifican y que los segmentos es y ds son inicializados para direccionar
esos segmentos. El registro de stack ss y el segmento de código cs no requieren
inicialización.
En lugar de referir a variables en memoria por su nombre, se pueden utilizar uno de tres
registros como apuntador a datos en memoria: bx, si y di. Ya que un programa puede modificar
valores de registros para direccionar diferentes localidades de memoria, el direccionamiento
indirecto por registro permite a una instrucción operar en múltiples variables. Después de cargar
una dirección de desplazamiento dentro de un registro apropiado, se puede hacer referencia a
datos almacenados en memoria con instrucciones tales como:
Los operadores WORD y BYTE son requeridos cuando el Turbo Assembler es incapaz de saber
cuando el registro direcciona a un byte o palabra en memoria. En la primer linea del ejemplo
anterior, el datos direccionado por bx es movido dentro del registro cx, por lo tanto el operador
WORD no es necesario ya que Turbo Assembler conoce el tamaño de la referencia del dato por
contexto de la instrucción, no hay problema si se realiza esto. En la segunda linea el operador
BYTE debe ser incluido, ya que el ensamblador no tiene otra forma de saber si dec está
decrementando un byte o una palabra.
El Direccionamiento Indirecto por registro trabaja por default sobre el segmento direccionado
por ds. Como con el direccionamiento directo, se puede usar sobre-especificaciones para cambiar
a cualquiera de los otros tres segmentos.
LVI
Lenguaje Ensamblador
.
Direccionamiento Base
El direccionamiento Base emplea dos registros, bx y bp. Las referencias a bx son relativas al
segmento direccionado por ds. Las referencias a bp son relativas al segmento de stack ss y son
normalmente utilizados para leer y escribir valores almacenados en el stack. Se pueden utilizar
sobre-especificaciones de segmentos como los descritos previamente para referir datos en
cualquiera de los otros segmentos.
El direccionamiento base adiciona un valor de desplazamiento a la localidad direccionada por
bx o bp. Este desplazamiento es un valor con signo de 8 o 16 bits que representa un
desplazamiento adicional por arriba o por abajo del desplazamiento en el registro especificado.
Un uso típico del direccionamiento base es localizar campos en una estructura de datos. Por
ejemplo:
Direccionamiento Indexado.
LVII
Lenguaje Ensamblador
.
Direccionamiento Base Indexado.
mov ax, [bx + si] ; Carga una palabra del segmento de datos en ax
mov ax, [bx + di] ;
mov ax, [bp + si] ; Carga una palabra del segmanto de pila en ax
mov ax, [bp + di] ;
Turbo Assembler permite invertir el orden de los registros, por ejemplo, escribir [si + bx]
y [di + bp]. Pero estos no son modos de direccionamiento diferentes - solo diferentes formas de
referenciar lo mismo. También se puede adicionar un valor de desplazamiento opcional para
cualquiera de las cuatro variaciones previas.
Una directiva ASSUME le informa al Turbo Assembler a que segmento en memoria hace
referencia un registro. El propósito de ASSUME es para permitir al ensamblador insertar
instrucciones de sobre-especificación automáticamente cuando sea necesario. Recuerde siempre
que ASSUME es un comando para el ensamblador que no genera ningún código.
Cuando se utiliza direccionamiento de memoria simplificado, raramente se utiliza ASSUME. Y al
usar explícitamente sobre-especificaciones de segmentos, se puede eliminar la necesidad de
ASSUME.
CODESEG
jmp Continua
v1 db 5
Continua:
mov ah,[cs:v1]
LVIII
Lenguaje Ensamblador
.
El código anterior ilustra una forma de guardar datos dentro del segmento de código - una
práctica inusual pero permisible. La instrucción jmp salta sobre la declaración de una variable
byte v1. (Cuando se mezcla datos y código, no se desea que accidentalmente se ejecuten las
variables como si estas fueran instrucciones.) La instrucción mov utiliza una sobre-especificación
de segmento (cs:), para cargar el valor de v1 dentro de ah. La sobre-especificación es requerida
dado que las referencia directas a datos normalmente las hace sobre el segmento de datos ds.
CODESEG
jmp Continua
v1 db 5
Continua:
mov ax,@code
mov es,ax
ASSUME es:_TEXT
mov ah,[v1]
mov ah,[cs:v1]
Dado que v1 esté almacenada en el segmento de código, por lo tanto, tanto [es:v1] y [cs:v1]
localizan correctamente la misma variable. Todo lo que hace ASSUME es permitir al ensamblador
insertar una instrucción sobre-especificación automáticamente.
LIX
Lenguaje Ensamblador
.
Expresiones y Operadores
Expresiones en lenguaje ensamblador tienen un propósito: hacer que los programas sean más
fáciles de entender y , por lo tanto, fáciles de modificar. Por ejemplo, se pueden tener varias
constantes, asociando valores opcionales con símbolos tales como:
TamReg EQU 10
NumReg EQU 25
Cuando Turbo Assembler procesa ésta directiva, multiplica TamReg por NumReg y guarda la
constante resultante (250) en la variable palabra TamBuffer. Es importante comprender que éste
calculo ocurre durante el ensamble - no cuando corre el programa. Todas las expresiones evalúan
a constantes en lenguaje ensamblador. En lenguajes de alto nivel, expresiones tales como
(NumColumnas *16) son evaluadas en tiempo de ejecución, posiblemente con un nuevo valor
para una variable llamada NumColumnas introducido por un usuario. En lenguaje ensamblador
las expresiones se reducen a valores constantes cuando se ensambla el texto del programa, no
cuando se ejecuta el programa. La diferencia puede ser confusa la primera vez, especialmente si
se está mas acostumbrado a la programación de alto nivel que a la de bajo nivel.
La tabla a continuación, lista expresiones operadores, las cuales se pueden utilizar para
calcular valores constantes de cualquier tipo imaginable.
No hay que confundir operadores tales como AND, OR, XOR y NOT con los mnemónicos del
mismo nombre en lenguaje ensamblador. Los mnemónicos de lenguaje ensamblador son
instrucciones que operan en tiempo de ejecución.
LX
Lenguaje Ensamblador
.
Variables simples
En los ejemplos anteriores se crearon variables simples con las directivas db y dw. Estas
directivas pertenecen a una familia de instrucciones similares, todas con el mismo propósito
general: para definir (significando reservar) espacio para valores en memoria. Las directivas
difieren solo en cuanto espacio pueden definir y los tipos de valores iniciales que se pueden
especificar. La tabla a continuación lista las siete de éstas útiles directivas ordenadas de acuerdo
a mínimo de espacio que reserva cada una. También se listan los ejemplos típicos, aunque las
directivas no están limitadas solamente a la utilización mostrada aquí. Se pueden escribir
cualquiera de estas directivas en mayúsculas o en minúsculas.
Para crear grandes cantidades de espacio, se pueden unir varias db, dw u otras directivas de
memoria, o se puede utilizar el operador DUP, el cual es normalmente más conveniente. DUP tiene
la siguiente forma:
Para crear un espacio multibyte, iniciar con una etiqueta opcional y una directiva de definición
de memoria. Siguiendo a este por un contador igual al número de veces que se quiere duplicar
una expresión, la cual debe están entre paréntesis. La palabra reservada DUP va entre el contador
y la expresión. Por ejemplo, cada una de éstas directivas reservan un área de memoria de 10
bytes, inicializando todos los diez bytes a 0:
Diez1 dt 0
Diez2 db 10 DUP(0)
Separando múltiples expresiones o valores constantes con comas duplica cada valor en turno,
incrementando el total de tamaño de espacio reservado, por el numero de veces el numero de
elementos.
También se pueden anidar expresiones DUP para crear buffers largos inicializados a valores
constantes. Por ejemplo, cada una de las siguientes directivas reservan una área de 20 bytes con
todos los bytes iguales a 255.
LXI
Lenguaje Ensamblador
.
Estos mismos ejemplos trabajan con cualquiera de las directivas de definición de memoria para
reservar diferentes montos de espacio. Normalmente se utilizará db y dw para enteros, cadenas y
variables byte, poniendo las otras directivas a trabajar solo para propósitos especiales. Pero se es
libre de utilizar éstas directivas como mejor nos parezca. Para crear una variable de 20-bytes con
todos en 0, por ejemplo, se puede utilizar db como en el ejemplo anterior o dt como en este:
Veinte4 dt 0
De todas las directivas de definición de memoria, solo db tiene la habilidad especial para
reservar espacio para cadenas de caracteres, almacenando un caracter ASCII por byte en
memoria. Aquí hay un ejemplo, terminando con un byte 0, una típica construcción llamada
cadena ASCIIZ:
Combinando la habilidad de db para las cadenas con el operador DUP es una herramienta útil
para llena un buffer de memoria con texto que es fácil de localizar mediante un depurador.
DUP repite la cadena de 8 bytes entre paréntesis 128 veces, reservando así un total de
1024 bytes.
Cuando se sabe que el programa asignará nuevos valores a las variables, y por lo tanto no nos
preocupan los valores iniciales, se pueden definir variables sin inicializar- aquellas que no tienen
valores específicos cuando el programa se ejecuta. Para hacer esto, hay que usar el signo de
interrogación (?) en lugar de la constante en la definición de memoria:
algo db ?
otro dw ?
valorx dt ?
Para crear grandes espacios de memoria sin inicializar, utilizar el signo de interrogación dentro
de los paréntesis de la expresión DUP, una técnica útil para crear grandes buffers tales como:
La principal razón de crear datos sin inicializar es reducir el tamaño del archivo de código
ensamblado. En lugar de guardar bytes innecesarios en disco, el espacio sin inicializar es
reservado en tiempo de ejecución. Para éste trabajo, se debe seguir una de estas dos reglas:
- Colocar todas las variables sin inicializar al final de la declaración del segmento de datos
- O preceder las variables sin inicializar con la directiva UDATASEG.
LXII
Lenguaje Ensamblador
.
DATASEG
var1 db 1
var2 db 2
UDATASEG
arreglo db 1000 DUP(?)
DATASEG
var3 db 3
Variables cadena
El mayor problema al utilizar las cadenas ASCII$ es obvio, - no hay una forma fácil de desplegar
un signo de pesos. También, es difícil leer caracteres del teclado o desde archivos de disco en
tales cadenas. Por estas razones. Hay quienes raramente utilizan las cadenas ASCII$, en su lugar
es preferible utilizar las cadenas ASCIIZ, cadenas con terminación en un byte con valor 0 - el
mismo formato utilizado por los compiladores de C de alto nivel. Con cadenas ASCIIZ, se puede
crear un mensaje de error escribiendo:
Las cadenas ASCIIZ pueden ser tan largas como se requiera - desde un caracter hasta miles. La
única desventaja de las cadenas ASCIIZ, es que DOS no tiene rutinas estandard para leer o
escribir variables con éste formato.
Para todas las cadenas declaradas con db, se pueden encerrar caracteres tanto con apostrofes (')
o comillas ("), solo hay que iniciar y cerrar con el mísmo símbolo.
Para incluir comillas dentro de una cadena, hay varias opciones. El método más fácil es
utilizando un tipo de simbolo, encerrando la cadena de caracteres conteniendo el otro tipo:
LXIII
Lenguaje Ensamblador
.
Etiquetas locales
Hasta ahora, los programas de ejemplo utilizan etiquetas dentro del segmento de código como
Inicio: y Repite:. Tales etiquetas son globales para todo el programa que los declara. En otras
palabras, si se etiqueta una instrucción etiq1: al inicio del programa, esa etiqueta está
disponible para call, jmp y otras instrucciones en cualquier otra parte del código. Un problema
con esto es que, constantemente hay que pensar en nuevos nombres para evitar conflictos con
etiquetas que ya se utilizaron. En saltos cortos, se presenta el mayor inconveniente:
cmp ax, 9 ; ax = 9
je Salto: ; salta suma siguiente si ax = 9
add cx, 10 ; si no sumar 10 a cx
Salto:
jmp Aqui
@@10:
inc ax
cmp ax,10
jne @@10
Aqui:
cmp ax, 20
je @@10
xor cx,cx
@@10:
El primer jmp salta a la etiqueta global Aqui:- se puede saltar a etiquetas globales desde
cualquier parte del programa. El siguiente jne salta a la etiqueta local @@10:. ¿Pero, cual, si hay
dos?. La respuesta es , a la primer @@10:, la cual se extiende solamente hasta la etiqueta global
@@Aqui:. Consecuentemente, la instrucción jne puede "ver" solamente el primer @@10:. Por la
misma razón, la última instrucción je salta abajo hacia la segunda @@10: dado que la etiqueta
global Aqui: bloquea la visibilidad de la primer etiqueta local.
LXIV
Lenguaje Ensamblador
.
- Etiquetas locales ahorran memoria, permitiendo a Turbo Assembler reusar RAM para otras
etiquetas locales. Las etiquetas globales son permanentemente almacenadas en memoria
durante el ensamble, aunque solamente se utilicen una vez. Las etiquetas locales son
liberadas cada vez que una nueva etiqueta no local es encontrada.
- Las variables locales proveen claridad al programa. Por ejemplo, en una búsqueda rápida de
un programa, fácilmente se diferencian las etiquetas locales y las globales.
- Las etiquetas ayudan a reducir errores haciendo mas difícil de escribir saltos de larga
distancia desde un lugar en el programa a otro. Si se encierran los procedimientos con
directivas PROC y ENDP, no se estará tentado a saltar a una etiqueta temporal en la sección
media de una subrutina - un error de código generalmente encontrado.
LXV
Lenguaje Ensamblador
.
Estructuras
Una estructura es una variable con nombre que contiene otras variables, llamados
campos. La palabra reservada STRUC da inicio a la estructura, siguiendo en la misma linea por
cualquier nombre que identifique a la estructura, por ejemplo, Miestructura. La palabra
reservada correspondiente ENDS, sigue al último campo de la estructura. Se puede colocar la
copia del nombre de la estructura después de ENDS o dejarlo sin nombre. Por ejemplo, esta
estructura contiene tres campos representando la fecha:
STRUCT Fecha
dia db 1 ; campo dia -- valor por default = 1
mes db ? ; campo mes - sin valor predeterminado
anio dw 1995 ; campo año - valor predeterminado = 1991
ENDS Fecha
Se puede insertar campos de cualquier tipo dentro de una estructura, utilizando los
mismos métodos que se utilizan para declarar variables. Este ejemplo tiene tres campos: dia,
mes, y anio. El primero de dos campos son bytes, como el primero de estos valores inicializado a
1. El segundo campo tipo byte esta sin inicializar. El tercer campo es una palabra, inicializada a
1991. La identación de cada campo es solamente por apariencia. Cuando se definen estructuras
tales como ésta, hay que recordar estos puntos importantes:
- Una estructura no es una variable. Una estructura es el esquema para una variable.
- Las estructuras pueden ser declaradas en cualquier parte. La directiva STRUC no tiene que
estar declarada dentro del segmento de datos del programa, aunque esto es posible.
- Una estructura le informa a Turbo Assembler acerca del diseño de variables que se planea
declarar posteriormente o que ya existe en memoria.
- Aunque se utilicen directivas tales como db y dw para definir los tipos de campos de la
estructura, la estructura no reserva espacio en el segmento de datos o causa que cualquier
byte sea escrito en el programa final.
Para utilizar un diseño estructurado, se debe reservar espacio en memoria para los campos de
la estructura. El resultado es una variable que tiene el diseño de la estructura. Iniciando la
declaración de cada una de éstas variables con una etiqueta, seguida por el nombre de la
estructura, y terminando con una lista de valores predeterminados encerrados entre los símbolos
< >. Dejando el espacio vacío entre estos símbolos para utilizar los valores predeterminados ( si
los hay), definidos anteriormente en la definición de la estructura. Retornando nuevamente con el
ejemplo de la estructura Fecha, en el segmento de datos del programa se puede declarar varias
variables fecha:
DATASEG
hoy fecha <5,10,1995> ; 5-10-1995
navidad fecha <24,12,> ; 24-12-1995
agosto fecha <,8,> ; 1-8-1995
LXVI
Lenguaje Ensamblador
.
La variable fecha hoy, reemplaza los tres valores predeterminados - dia, mes y anio- con
5, 10 y 1995. La segunda variable navidad reemplaza dos de los valores predeterminados - dia
y mes- con 24 y 12. El tercer valor faltante de declarar asume el valor predeterminado del diseño
de la estructura, el cual es 1995. La tercer variable agosto especifica un nuevo valor para mes
mientras se utilizan los valores por default para los demás. La primer coma es necesaria para
"obtener" el segundo campo de la estructura. La segunda coma no es necesaria.
STRUC Fecha
dia db 1 ; campo dia -- valor por default = 1
mes db ? ; campo mes - sin valor predeterminado
anio dw 1995 ; campo año - valor predeterminado = 1991
ENDS Fecha
STRUC CiudadEstado
ciudad db "####################",0; 20 caracteres
estado db "###",0 ; 3 caracteres
ENDS CiudadEstado
DATASEG
codSalida DB 0
CODESEG
Inicio:
mov ax,@data
mov ds,ax
Fin:
mov ah, 04Ch
mov al, [codSalida]
int 21h
END Inicio
LXVII
Lenguaje Ensamblador
.
Utilizar los campos en una variable estructurada es solamente un poco más difícil que utilizar
variables simples. Son disponibles todos los mismos modos de direccionamiento. Dado que los
nombres de los campos están contenidos por la definición de la estructura, para referir a un
campo individualmente, se debe escribir tanto el nombre de la variable estructura como la del
campo, separandolos con un punto. Haciendo referencia al programa anterior, para asignar un
nuevo valor al campo dia en fecha, se puede asignar un valor inmediato al campo en memoria
de la siguiente forma:
Otras variaciones son posibles. Se puede sumar, restar, leer, escribir y combinar lógicamente
campos y registros. Recordar que en todos los casos, se tiene que proporcionar el nombre de la
estructura y la variable para que el ensamblador pueda generar la dirección correcta de los
campos.
No hay comandos nativos, estructuras o métodos para declarar y utilizar arreglos en programas
de lenguaje ensamblador. En lenguajes de alto nivel tales como C y Pascal, se pueden declarar
arreglos y entonces referir a elementos del arreglo con un índice variable. Por ejemplo, un
programa en Pascal puede declarar un arreglo de diez elementos, indexado desde 0 a 9:
En el programa, las sentencias pueden entonces referir al arreglo, quizá usando una variable
índice y un ciclo FOR para asignar valores a cada posición del arreglo.
FOR i:=0 to 9 DO
ArregloInt[i]:=i;
Para quienes no están familiarizados con Pascal, esta sentencia asigna los valores de 0 a 9 a los
diez enteros del arreglo. Programadores de C y Pascal tienen formas similares para crear y utilizar
arreglos. En lenguaje ensamblador, el manejo de arreglos es un poco más difícil, pero también
más flexible ya que el programador escribe el código para accesar valores del arreglo. Una forma
de crear un arreglo de enteros, por ejemplo, es utilizando el operador DUP.
unArreglo db 10 DUP(?)
unArreglo db 0,1,2,3,4,5,6,7,8,9
LXVIII
Lenguaje Ensamblador
.
Arreglos de otras estructuras tales como cadenas y variables STRUC toman más tiempo. Por
ejemplo, supóngase que se necesita un arreglo de 4 cadenas de 20 bytes. Dado que éste arreglo
es pequeño, se puede utilizar perfectamente cuatro variables separadamente:
Las cuatro variables son almacenadas consecutivamente en memoria, por lo tanto, las mismas
cadenas de 20 bytes (mas un byte para el terminador de la cadena), pueden ser accesadas como
variables individuales o como una estructura de cuatro cadenas. A menos que nos guste teclear
programas muy largos, esta forma de declarar arreglos puede ser inpráctico para crear arreglos
largos. Considerar como crear espacio para un arreglo de 100 cadenas de 20 bytes. Utilizando
dos nuevas directivas LABEL y REPT, se puede escribir:
La primer linea declara la etiqueta unArreglo de tipo Byte. Otros nombres que se pueden
utilizar aquí son Word, DWord, FWord, PWord, DataPtr, QWord, y TByte. O se puede utilizar un
nombre de estructura. La directiva LABEL informa al Turbo Assembler como direccionar el dato
que sigue - esto no reserva ningún espacio de memoria. En éste ejemplo, el dato que sigue son
cadenas, las cuales son siempre direccionadas como bytes. El comando REPT repite cualquier
sentencia del lenguaje ensamblador por un cierto número de veces, aquí 100. Todo entre REPT y
ENDM (Fin de Macro) es repetido como si se hubiese tecleado esta linea muchas veces.
Un truco útil es cambiar la declaración cada vez en la definición. Por ejemplo, para crear un
arreglo de diez enteros y asignar los valores de 0 a 9 para cada posición del arreglo, se puede
utilizar ésta declaración:
valor = 0
LABEL unArreglo Word
REPT 10
dw valor
valor = valor + 1
ENDM
unArreglo dw 0
dw 1
dw 2
dw 3
dw 4
dw 5
dw 6
dw 7
dw 8
dw 9
LXX
Lenguaje Ensamblador
.
La directiva LABEL es utilizada más frecuentemente para asignar dos o más etiquetas de
diferentes tipos al mismo dato en memoria. Con ésta tecnica, puedes leer y escribir variables
como bytes en STalgunas instrucciones pero como palabras (o de cualquier otro tipo) en otras. La
directiva tiene tres partes:
El identificador es tratado igual como cualquier otra etiqueta. El tipo puede ser near, far,
proc, byte, word, dword, fword, pword, dataptr, qword, o tbyte. El tipo tambien puede
ser el nombre de una estructura declarada con STRU. Utilizando LABEL, se puede declara un valor
de dos bytes, pero visto el valor como una palabra de 16 bits:
El valor hexadecimal 01234h es etiquetado como ValorWord y declarado como una palabra de
16 bits con dw. Pero la anterior directiva LABEL crea una segunda etiqueta ValorByte de tipo
byte, la cual direcciona al mismo valor en memoria. Esto nos permite escribir instrucciones tales
como:
El primer mov carga el valor completo de 16 bits, inicializando ax a 01234h. El segundo mov
almacena solamente los primeros 8 bits del mismo valor, inicializando bl a 034h. El tercer mov
carga los segundos 8 bits, inicializando bh a 012h. Así, las dos últimas instrucciones inicializan bx
al mismo valor que ax. (Recordar que las palabras son almacenadas con bytes en orden inverso-
el valor 01234h es almacenado en memoria como dos bytes 034h y 012h.
Utilizar LABEL para asignar etiquetas de diferentes tipos o variables siempre es mas útil para
direccionar estructuras como colecciones de tipos de campos, pero también como cadenas de
palabras de 16 bits. Utilizando la estructura Fecha anteriormente descrita, se puede escribir:
unDia es una variable estructurada de tipo fecha. La etiqueta Diames direcciona la misma
memoria pero considera el dato de tipo word. En el segmentode código del programa, se pueden
referenciar al primero de dos campos en unDia normalmente como unDia.dia y unDia.mes. O,
dada la etiqueta adicional, se pueden cargar estos dos campos byte directamente en un registr
de 16 bits:
El primer mov realiza la función identica a las utlimas dos intrucciones mov. Algunas veces, como
se muestra aquí, utilizar LABEL puede ayudar a ahorrar una o dos instrucciones y, si la instrucción
es repetida constantemente, esto mejorará el rendimiento del programa.
LXXI
Lenguaje Ensamblador
.
Indexando Arreglos.
Ahora que se sabe como declarar arreglos, el siguiente paso es investigar las formas de leer y
escribir valores en arreglos. Por ejemplo, ¿como se hace referencia al elemento numero 5?. La
clave para contestar, está en que los índices de un arreglo son simples direcciones- como
cualquier otra referencia a variables, por lo tanto, sin importar el tipo de dato almacenado en un
arreglo, el punto es indexar valores individuales reduciéndose esto a dos pasos:
- Multiplicar el tamaño de los elementos del arreglo por el índice i del arreglo.
- Sumar el resultado a la dirección base del arreglo.
000D 10 [0]
000E 20 [1]
000F 30 [2]
0010 40 [3]
0011 50 [4]
0012 60 [5]
0013 70 [6]
0014 80 [7]
0015 90 [8]
0016 100 [9]
Por ejemplo, en un simple arreglo de bytes, si i es 0, entonces i x 2(0) mas la dirección del
arreglo localiza el primer valor en arreglo[0]. El segundo valor (arreglo[1]) es localizado en la
dirección base de arreglo mas 1, y así sucesivamente. Como se muestra en la figura, el punto es
convertir valores de índices de arreglos a estas direcciones en memoria. El índice 0 es
equivalente a la dirección, 00D - la misma que la dirección base del arreglo completo. El índice 1
corresponde a la dirección 000E, el índice 2, a 000F; hasta el índice 9, el cual se localiza el valor
en el desplazamiento 0016. Los arreglos de bytes son los más fáciles de manipular. Para cargar
en al el elemento 64 de un arreglo de 100 bytes, se puede escribir:
DATASEG
unArreglo db 100 DUP(0)
CODESEG
mov al, [unArreglo + 63]
DATASEG
indice dw ?
unArreglo db 100 DUP(0)
CODESEG
mov bx, [indice] ; toma el valor del indice
mov al, [unArreglo + bx] ; al<-unArreglo[indice]
LXXII
Lenguaje Ensamblador
.
Las dos declaraciones de datos reservan espacio para un indice de 16 bits y un arreglo sin
inicializar de 100 bytes. En el segmento de código, el primer mov carga el valor actual de indice
dentro de bx. El segundo mov suma bx a la dirección base del arreglo, localizando el byte correcto
del arreglo dentro de al. Se pueden utilizar también los registros si y di para realizar lo mismo.
Las dos primeras lineas realizan lo mismo que las dos últimas. Técnicamente, este es el modo
de direccionamiento indexado no el modo de direccionamiento base, aunque como se puede
apreciar, no hay mucha diferencia practica entre los dos métodos.
El direccionamiento de arreglos viene a ser mas complejo cuando los elementos de los arreglos
ocupan más de 1 byte. Dada la naturaleza binaria de las computadoras, el calcular la dirección
de elementos de arreglos multibyte es mas simple cuando los tamaños de los elementos son
poderes de 2. En éste caso, se puede utilizar veloces instrucciones de corrimiento de bits para
realizar la multiplicación inicial del indice por el tamaño en bytes del elemento. Adicionando el
resultado de esta multiplicación a la dirección base del arreglo localizando algún elemento, tal y
como lo demuestra el fragmento de código a continuación:
DATASEG
indice dw 1
arreglo dw 100 DUP (?)
CODESEG
mov bx,[indice] ; Obtiene el valor del indice
shl bx,1 ; bx<- indice * tamanio_elemento(2)
mov ax,[bx+arreglo] ; ax<- arreglo[indice]
En este ejemplo, el tamaño del elemento es de 2 bytes; por lo tanto, la forma más fácil ( y
rápida) de multiplicar el indice por 2 es realizar un corrimiento de bits del valor un bit a la
izquierda. Para localizar la dirección del quinto elemento de este arreglo, se debe multiplicar 4 x 2
y sumar el resultado a la dirección base del arreglo para obtener el valor final de desplazamiento.
Calcular la dirección indice cuando los tamaños de los elementos no son poderes de 2, requiere
de mayor esfuerzo para mantener el código corriendo tan rápido como sea posible. Considere un
arreglo de elementos, cada uno ocupando 5 bytes. Para inicializar a bx a la dirección de
desplazamiento del elemento a cierto indice, requiere de varios pasos.
mov ax,[indice]
mov bx,5
mul bx
mov bx, ax
add bx, OFFSET arreglo
LXXIII
Lenguaje Ensamblador
.
mov bx,[indice] ; Obtiene el valor en bx
mov ax, bx ; Guarda el valor en ax
shl bx,1 ; bx<-bx * 2
shl bx,1 ; bx<-bx * 4 (total)
add bx,ax ; bx<- bx * 5 (total)
add bx, OFFSET arreglo ; Inicializa a bx<- dirección del elemento
Trucos como éstos no siempre son posibles. Pero en general, cuando se pueda utilizar
corrimientos de bits, en lugar de multiplicaciones , el resultado será rapidez.
Uniones
Definida con la directiva UNION, una unión tiene la forma idéntica como la estructura STRU. Igual
que las estructuras, las uniones contienen nombres de campos, normalmente de diferentes tipos.
La diferencia entre una unión y una estructura es que los campos de las uniones se traslapan una
a otra dentro de la variable. Una unión con tres campos de bytes, en otras palabras , actualmente
ocupa solo un byte. Como muestra el ejemplo siguiente, se puede utilizar ésta ventaja para
construir variables que el ensamblador pueda referenciar conteniendo mas de un tipo de dato,
similarmente a la forma de utilizar LABEL
UNION ByteWord
unByte db ?
unWord dw ?
ENDS ByteWord
Una directiva ENDS finaliza la unión. En éste ejemplo, unByte traslapa el primer byte de unWord,
Si esta fuera una estructura, entonces unByte y unWord podrían ser almacenadas en localidades
consecutivas. Dada ésta unión, unByte y unWord son almacenadas en la misma localidad de
memoria. Por lo tanto, al insertar un valor en unByte también cambia el LSB de unWord.
Cuando se combinan con estructuras, las uniones dan poderosas formas de procesar variables.
Por ejemplo, el siguiente listado de código:
STRUC DosBytes
ByteAlto db ?
ByteBajo db ?
ENDS DosBytes
UNION ByteWord
cByte DosBytes <>
cWord dw 1
ENDS ByteWord
La estructura DosBytes define dos campos byte, ByteAlto y ByteBajo. La union ByteWord
también define dos campos. Primero es cByte, de la estructura DosBytes previamente definida.
El segundo es cWord como una simple palabra de 16-bits. Variables de tipo ByteWord hacen fácil
de referir a localidades tanto como una palabra o dos valores bytes sin el peligro de olvidar que
LXXIV
Lenguaje Ensamblador
.
las palabras son almacenadas en bytes en orden inverso - un problema del método LABEL. Para
usar la union anidada, primero se declara una variable, en éste caso asignando el valor 0FF00h.
DATASEG
dato ByteWord <,0FF00h>
Ahora se puede referir a dato como una estructura DosBytes o como una palabra de 16-bits. Un
corto ejemplo demuestra como cargar la misma localidad de memoria tanto en registros byte o
word. Dado que la estructura DosBytes está anidada dentro de la union, se requieren dos puntos
para "accesar" los campos byte. Note como los nombres de los campos reducen el peligro de
accidentalmente cargar el byte equivocado de una palabra en un registro de 8 bits.
CODESEG
mov al,[dato.cByte.ByteBajo]
mov ah,[dato.cByte.ByteAlto]
mov ax,[dato.cWord]
Campos de Bits.
or al,00000100b
Cuando se hace esto, normalmente es más útil escribir el valor binario - solamente recordando
colocar la letra b al final del valor. La instrucción and puede enmascarar valores, inicializando uno
o mas bits a 0:
Aunque escribir valores en binario ayuda a clarificar exactamente cuales bits son afectados por
las instrucciones, se tienen que contar los bits y tomar algo de tiempo para visualizar el resultado
lógico.
En programas complejos, se muy fácil encender o apagar bits equivocados - un error muy difícil
de encontrar. Para realizar el procesamiento de bits de una forma mas fácil, Turbo Assembler
ofrece dos mecanismos - El registro RECORD y el enmascaramiento MASK.
RECORD es una directiva que nos permite dar nombres de campos a bits dentro de bytes y
palabras. Simplemente especificando la longitud de cada campo - en otras palabras, el numero
de bits que ocupa el campo. Turbo Assembler calcula entonces la posición del campo. Por
ejemplo, el siguiente registro RECORD define un byteconsigo como un valor de 8 bits con dos
campos:
Después de la directiva RECORD, viene el nombre del registro, seguido por una serie de nombres
de campos. Cada campo termina con una coma y la longitud del campo en bits. El campo signo
en el ejemplo anterior es de 1 bit de longitud. El campo valor es de 7 bits de longitud.
Separando campos múltiples con comas. Si el total de numero de bits e menor o igual a 8, se
asume que el registro es de un byte; de otra forma, se asume que es de una palabra. No se
LXXV
Lenguaje Ensamblador
.
pueden construir registros mas grandes que una palabra, aunque se puedan crear estructuras de
múltiples campos. No es necesario especificar exactamente 8 o 16 bits.
DATASEG
v1 ByteConSigno <> ; Valores por default
v2 ByteConSigno <1> ; signo = 1, valor = default
v3 ByteConSigno <,5> ; signo = default, valor = 5
v4 ByteConSigno <1,127> ; signo = 1, valor = 127
v5 ByteConSigno <3,300> ; signo = 1, valor = 44
En la última linea se intenta insertar rangos de valores mayores a la capacidad que se puede
representar con la longitud de bits especificada para cada campo. Al realizar esto, lo que
realmente se inserta es el residuo de dividir el valor proporcionado entre 2 n, donde n es el
numero de bits de longitud del campo.
Después de declarar un tipo RECORD, y unas cuantas variables de éste tipo, se pueden utilizar
varios métodos para leer y escribir valores de campos de bits en esas variables. Para demostrar
como se hace esto, necesitamos primero un nuevo tipo RECORD.
RECORDS como este pueden empacar mucha información dentro de un espacio pequeño. En éste
ejemplo, solamente 16 bits son necesarios para almacenar 5 datos acerca de una persona. Con el
campo sexo igual a 0 para femenino y 1 para masculino, casado igual a 0 si es falso o 1 si es
cierto, hijos en un rango de 0 a 15, un bit reservado como xxx para un uso futuro, un campo
edad con un rango entre 0 y 127, y escuela desde 0 hasta 3, representando 4 niveles para la
escolaridad de una persona. Como con todos los valores de 16 bits, los dos bytes de 8-bits de
esta variable están almacenados en orden inverso en memoria, con bits 0-7 en direcciones bajas
que 8 bits de 8-15.
El valor es igual al bit posición en el byte o palabra desde el bit menos significativo. En
referencia al registro persona, entonces, sexo = 15, casado = 14, hijos = 10, xxx = 9, edad = 2
y escuela = 0.
Se pueden utilizar estos nombres de campos como las constantes EQU. Almacenar un dato en
la posición de un campo implica un procedimiento inverso, es decir, colocar el valor dentro de un
registro o localidad de memoria y aplicar un corrimiento de bits hacia la izquierda, el número de
posiciones necesarias para colocar el valor hasta su posición especificada.
El utilizar nombres de campos en lugar de contar bits manualmente ahorra tiempo y ayuda a
prevenir errores. Por ejemplo, para incrementa el campo edad, se realiza el corrimiento de bits
requeridos hacia la extrema derecha en un registro palabra, incrementar el registro, y entonces
realizar un corrimiento hacia atrás a su posición. Antes de hacer esto, obviamente, se deben
quitar los otros bits de la variable. Para ayudarnos con este paso, el ensamblador provee un
operador llamado MASK, el cual toma el nombre de un campo bit y genera una mascara and
apropiada con bits igual a 1 en todas las posiciones para este campo. Una buena forma de
organizar las máscaras es utilizar nombres similares a los campos asociados:
LXXVI
Lenguaje Ensamblador
.
Cada nuevo identificador - por ejemplo, maskSexo y maskCasado - es asignada una mascara para
cada campo de bits. Los nombres realizan el propósito de hacer símbolos mas fáciles de recordar,
sin importar el nombre que se utilice. No es necesario colocar la palabra mask como prefijo al
nombre del campo.
DATASEG
empleado persona <>
Para colocar un bit a 1, hay que utilizar la instrucción or combinando la mascara y el valor del
registro.
CODESEG
or [empleado],maskSexo ; inicializa el campo sexo = 1
or [empleado],maskCasado ; inicializa el campo casado = 1
Para colocar los bits a 0, hay que utilizar el operador NOT junto con la máscara de bits para
cambiar el valor de todos los bits de la máscara. El ejemplo siguiente muestra el procedimiento:
Para campos de bits de más de un bit, el proceso es similar pero requiere de pasos adicionales
para extraer los valores. Hay varios métodos posibles que se pueden utilizar, pero estos pasos
siempre tendrán que emplearse:
1. Realizar un corrimiento de bits hacia la derecha por la constante del nombre del campo
2. Aplicar un AND al registro con la mascara del campo
3. Aplicar un AND al valor original aplicando un NOT con la máscara del campo
4. Realizar un OR al regostro dentro del valor original
LXXVIII
Lenguaje Ensamblador
.
Entrada y Salida
Entrada y Salida Estandard
Si se desea que los programas corran en muchos sistemas diferentes DOS, tantos como sea
posible, no solamente en IBM PCs, se deben utilizar métodos estándar para leer la entrada de un
teclado y para escribir la salida a la pantalla - sin mencionar la comunicación con otros
dispositivos tales como impresoras y plotters.
DOS provee varias funciones estándard de E/S, la mas simple de la cuales lee y escribe un
caracter a la vez. Por ejemplo, se puede leer un caracter desde un dispositivo de entrada
estandard dentro del registro al con dos instrucciones simples:
La ventaja de utilizar funciones de DOS para leer datos desde la entrada estandard es que el
programa no tiene que realizar alguna acción especial que permita cambiar a alguien desde
donde viene la entrada o a donde va la salida.
Las funciones de DOS 1 y 2 checan si Ctrl-C fue accionado. Si es así, DOS ejecuta la interrupción
23h, el cual detiene el programa. Para evitar la ruptura inesperada de un programa cuando
alguien presiona Ctrl-C, se tienen tres opciones:
Normalmente, la primera opción es la mejor- otros métodos de entrada están disponibles para
pasar un Ctrl-C de regreso al programa tan solo como cualquier otra tecla presionada. Escribir un
manipulador de la interrupción propio es probablemente un trabajo mas que innecesario. La
tercera opción toma mas trabajo pero es mucho mas útil, en algunos casos. Un manejador de
dispositivo es un programa en una forma altamente especializada que realiza una interface con
los dispositivos físicamente tales como el teclado, impresoras y pantalla.
Hay que recordar siempre que tanto las funciones de entrada estandard y salida, 1 y 2 checan
si se presiona Ctrl-C. Cuando ocurre esto durante la llamada a la función de entrada 1 de DOS, el
programa nunca recibe el Ctrl-C. Cuando un Ctrl-C es detectado durante una llamada a la función
de salida 2 de DOS, el caracter en dl es pasado a la salida estandard antes que el chequeo de
Ctrl-C tome lugar.
Estos chequeos de caracteres especiales son llamados filtros por la manera en que estos evitan
el paso de ciertos caracteres presionados y caracteres de acciones especiales.
Cuando no se desea filtrar el Ctrl-C y otros códigos de control, se puede usar una de dos
funciones:
La función 6 es incluida en DOS mas para acomodar programas convertidos desde el CP/M, los
cuales tienen una función similar para E/S directa a la consola. Dado que hay otros, y
LXXIX
Lenguaje Ensamblador
.
probablemente mejores, formas de accesar una entrada y salida a dispositivos directamente en
DOS, raramente hay alguna razón para utilizar la función 6. En su lugar, normalmente es mejor
utilizar la función 7 para leer caracteres sin realizar un eco en pantalla de la tecla presionada y
sin filtrar el Ctrl-C. Excepto por el número de función, el código para llamar a la función 7 es
idéntico al código para la función 1.
mov ah, 7
int 21h
Este método no checa si se presiona Ctrl-C o Ctrl-Break y por lo tanto, previene de que los
usuarios terminen los programas prematuramente.
Para adicionar filtros a la entrada sin realizar un eco del caracter en el dispositivo de salida
estandard, se utiliza la función 8, la cual genera la interrupción 23h para finalizar el programa.
Como se explicó anteriormente, se pueden escribir cadenas con el formato ASCII$ con la función
9 de DOS. Aparte de que el formato ASCII$ requiere extrañamente de un signo de pesos como
terminador de la cadena, la función 9 detecta Ctrl-C y responde a otros códigos de control. Si se
utilizan éstas funciones para prevenir que usuarios interrumpan un programa, hay que llamar a la
función 44h "Controlador de Dispositivos" o IOCTL- disponible desde la versión 2. Esta función
permite reprogramar la salida del manejador del dispositivo para ignorar el Ctrl-C y el Ctrl-Break.
Primero, llamar a la función 44h con al igual a 0, leyendo los bits del dispositivo de control actual
desde el controlador de dispositivos:
mov ax, 4400h ; Función 44h, elemento 00:obtiene información del dispositivo
mov bx, 1 ; Especifica una salida estandard
int 21h ; Llama a DOS. Retorna el dato en dx.
La configuración en bits del controlador de dispositivos esta ahora en el registro dx. El bit 5 de
la configuración del controlador de dispositivos informa si el manejador procesa todos los datos
(bit =1), o si filtra los caracteres Ctrl-C y Ctrl-Break (bit = 0). Encendiendo el bit 5 desactiva el
filtrado:
mov ax, 4401h ; Función 44h, elemento 01:inicializa información del dispositivo
xor dh, dh ; dh debe ser 0 para esta llamada a función
or dl, 20h ; Inicializa el bit 5 -- procesar datos binarios
int 21h ; Llama a DOS con datos en dx
Esta técnica deshabilita Ctrl-C, Ctrl-S, y Ctrl-P filtrando, no solamente al programa sino
también a cualquier otro programa incluyendo al mismo DOS que llama a las funciones 2 y 9 para
pasar datos al dispositivo de salida estandard. Después de reprogramar el controlador de
dispositivos, no se podrá presionar Ctrl-C para interrumpir el desplegado de un directorio largo
ejecutado por el comando DIR. Por lo tanto antes de que termine el programa, hay que colocar en
0 el bit 5 con las instrucciones mostradas anteriormente reemplazando or dl, 20h con and dl,
0DFh para restaurar el chequeo de Ctrl-C.
LXXX
Lenguaje Ensamblador
.
Esperando y Esperando
Un programa que lee una entrada vía funciones 1, 7 y 8 de DOS, cae en un ciclo sin fin,
esperando por que se opriman teclas. Muchas veces, se querrá que un programa responda a una
entrada de datos mientras realiza otra tarea si ninguna tecla se oprime. Por ejemplo, un
procesador de palabras puede realizar una operación larga de búsqueda y reemplazo,
terminándola si se oprime la tecla ESC. O una simulación puede actualizar la pantalla, tomando
varias acciones en tiempo real dependiendo de las teclas de comando que se opriman. Hay dos
formas de atacar este problema:
En el primer método,
LXXXI
Lenguaje Ensamblador
.
00ccc0dw
El primer byte básicamente dice la operación, pero también informa del tamaño de los
operandos y dice de donde están. Más concretamente se codifica de la siguiente manera:
ccc=000 es un código binario de 3 bits que indica que instrucción se trata, la d es un código
binario de 1 bit que indica la dirección. Si el destino es está en memoria y la fuente es un
registro, d=0; si el destino es un registro y la fuente está en memoria, d=1. La w indica el tamaño
de los datos. Si los datos son de 8 bits entonces w=0, y si son de 16 bits, entonces w=1.
El segundo byte:
mm rrr aaa
LXXXII
Lenguaje Ensamblador
.
La dirección se refiere a una base BX, un índice DI y un decalague de 6. Puesto que la fuente es
un registro y el destino está en memoria, d=0. Como además DX indica un registro de 16 bits, el
tamaño de los datos será de 16 bits, y por tanto, w=1. El decalague es de 8 bits, por lo tanto
podemos ver en la tabla anterior que mm=01. En la misma tabla observamos que aaa=001. El
código para el registro DX es rrr=010. Sólo se necesita un bit adicional para el decalague de 8
bits.
LXXXIII
Lenguaje Ensamblador
.
para:
ADD AX,[SI]
00 000 011 00 000 100
Aquí tenemos que w=1, d=1(de memoria a registro), mm=00 (sin decalague), aaa=100 (modo
índice, con el SI apuntado a los datos) y rrr=000 (registro AX para otro operando). Como no hay
decalague no hay bits adicionales.
Para optimizar ciertas combinaciones utilizadas con mucha frecuencia, especialmente aquellas
que utilizan los registros AX y Al.
La forma más general tiene como fuente un dato inmediato y como destino un registro general
o una posición de memoria. El primer byte es:
100000sw
Aquí, la s es un código binario de 1 bit que indica el tamaño del dato inmediato, y w es otro
código binario de 1 bit que indica el tamaño del dato destino. SI sw es 00, tanto el fuente como el
destino tienen 8 bits; si sw es 11, ambos datos son de 16 bits, y si sw es 01, indica que la fuente
es de 8 bits y s e debe ampliar a 16 bits.
El segundo byte es:
mm 000 aaa
Donde mm y aaa indican el modo de direccionamiento del punto de destino. El 000 que aparece
en el centro del byte no es una referencia a algún registro, sino que es parte del código de
operación, ayudando a especificar que se trata de una suma.
Dependiendo de mm y aaa, puede haber más bits de decalague. Después de la información
sobre el direccionamiento viene el dato inmediato. Si w=0 hay un byte de dato; si w=1 son dos
los bytes del dato.
ADD AX,7
LXXXIV
Lenguaje Ensamblador
.
PAQUETE DE RUTINAS
PARA ENTRADA/SALIDA
DE CADENAS
STRIO.ASM
LXXXV