BREVE RESUMEN DE LENGUAJE ENSAMBLADOR
0.- MOTIVACION.
El lenguaje ensamblador continúa siendo imprescindible para imple-
mentar fragmentos de código donde la velocidad del ejecutable y/o
su tamaño sean críticos. Afortunadamente, muchos compiladores per-
miten la inclusión directa en el código fuente de sentencias en
ensamblador, de forma que ya no es necesario en la mayor parte de
los casos el uso de ensambladores. Como ventaja adicional, no es
necesario conocer las directivas de ensamblador, ya que pueden es-
tablecerse desde el entorno de desarrollo y van implícitas en las
directivas del compilador, por ejemplo, el compilador Pascal de
Borland o el compilador C/C++ de la misma compañía.
Aún así, a muchos programadores les resulta difícil renunciar al
control absoluto de la máquina que permite el lenguaje ensambla-
dor.
1.-REGISTROS DEL 8086
El 8086 dispone de ocho registros de propósito general, que pode-
mos considerar como memorias implementadas sobre la misma CPU de
acceso muy rápido. Estos registros reciben los nombres de AX, BX,
CX, DX, SI, DI, BP, SP. Existe tambien un registro IP (Instruction
Pointer)que apunta a la siguiente instrucción a ejecutar, formando
su dirección junto con el registro CS, que citaremos a
continuación. Aparte de los registros generales y de IP, existen
registros de segmento, llamados CS, DS, SS, ES y un registro de
flags, cuyos bits no son accesibles directamente y que reflejan
los resultados de distintas operaciones.
Cualquiera de los registros de propósito general puede usarse para
escribir a/desde memoria, realizar operaciones, como punteros o
contadores, pero cada uno tiene una personalidad especial, y es
una buena práctica de programación usarlos para lo que fueron
principalmente concebidos.
AX se usa siempre en multiplicaciones y divisiones y es el mas
eficiente para operaciones aritméticas y de movimiento de datos.
BX se usa como puntero, y junto con DS referencia posiciones de
memoria. Por ejemplo, para cargar en AL el contenido de la posi-
ción de memoria número 9:
MOV AX,0
MOV DS,AX
MOV BX,9
MOV AL,[BX]
CX se usa principalmente como contador en los bucles. Estos son
tan frecuentes que existe una instrucción especial, LOOP, que
comprueba su valor, volviendo al principio del bucle si es
distinto de cero:
MOV CX,10
BUCLE:
instrucciones
LOOP BUCLE
DX es el único registro que puede usarse para acceder a puertos.
Por ejemplo, para escribir 62H en la dirección de puerto 1000H:
MOV AL,62H
MOV DX,1000H
OUT DX,AL
SI se usa como puntero. Su nombre proviene de Source Index, y se
usa principalmente con instrucciones de cadena:
CLD
MOV AX,0
MOV DS,AX
MOV SI,20
LODSB
carga en AX el valor de la posición 20 de memoria. SI se incrementa
en una unidad. En combinación con LOOP permite leer posiciones su-
cesivas de memoria.
DI tambien se usa como puntero. Permite escribir en posiciones
sucesivas de memoria cuando se usa con instrucciones de cadena:
CLD
MOV DX,0
MOV ES,DX
MOV DI,2048
STOSB
escribe el contenido de AL en 0000:2048. Mientras que DI usa a ES
como segmento, SI usa a DS.
BX, DI y SI actuan como punteros relativos a DS, o a ES en el caso
de DI cuando se usa con funciones de cadena. BP actua tambien co-
mo puntero, pero relativo al segmento de pila SS.
SP es entre los registros de uso general el mas específico, ya que
no se recomienda su uso, pues apunta al extremo superior de la pi-
la, y por ello no debe modificarse salvo que se sepa exactamente
que se está haciendo.
2.-ESTRUCTURA DE UN PROGRAMA EN ENSAMBLADOR.
Expondremos brevemente la estructura de un programa fuente en en-
samblador. Este está compuesto por una serie de segmentos para la
pila, el código y los datos. Los segmentos no pueden estar anida-
dos.
Cada uno comienza con el nombre del segmento seguido de la pala-
bra SEGMENT y alguna o algunas cadenas exigidas por el enlazador.
Cada segmento termina con el nombre del segmento seguido de la
palabra ENDS.
El programa termina con la palabra END seguida del nombre del pun-
to de entrada al programa.
De esta forma, el armazón de un programa en ensamblador es el si-
guiente:
PILA SEGMENT STACK 'STACK'
.
.
PILA ENDS
DATOS SEGMENT 'DATA'
.
.
DATOS ENDS
CODIGO SEGMENT 'CODE'
.
.
CODIGO ENDS
END [ENTRADA]
donde [ENTRADA] es el nombre del punto donde se inicia la secuen-
cia de instrucciones.
Dentro del segmento de pila, se establece el tamaño de ésta. La
forma habitual de hacerlo es mediante la sentencia DW (define pa-
labra) seguida del número de palabras que vamos a reservar y la
sentencia DUP (?). Así:
PILA SEGMENT STACK 'STACK'
DW 100H DUP(?)
PILA ENDS
establece un segmento de pila con un tamaño de 100H palabras (512
bytes), sin especificar el contenido inicial.
Las directivas que se emplean habitualmente para establecer el
tamaño de la pila son:
DB Define Byte 1 Byte.
DW Define Word 2 Bytes.
DD Define doble palabra 4 Bytes.
DQ Define cuádruple palabra 8 Bytes.
La sintaxis de DUP es expresión1 DUP expresión2. expresión1 indi-
ca el número de veces a incluir consecutivamente expresión2 en el
segmento. Cuando expresión2 es (?) indicamos que no es importante
el contenido inicial. No se debe cometer el error que con (?) la
pila queda inicializada a ceros o a cualquier otro valor particu-
lar.
Pasemos al segmento de datos. La sintaxis de las líneas conteni-
das en este segmento es [nombre] [directiva] expresión(es).
Vayamos por partes: [nombre] es un campo opcional pero absoluta-
mente conveniente, ya que especificándolo nos podremos referir
después a la dirección de la variable simplemente nombrándola.
La [directiva] puede ser DB, DW, DD, DQ. La expresión puede ser:
. Una cadena entre comillas
. Un valor numérico
. Una tabla de valores separados por ","
Así, son válidas las líneas siguientes:
Mensaje DB 'Hola'
Peso DB 70
Nada DW (?)
Vector DB 0,0,0,13,120,76,76,76,76,76,76
La última línea se puede simplificar con DUP en la forma siguien-
te:
Vector DB 3 DUP(0),13,120,6 DUP(76)
Considérese la variable como un puntero a un dato del tipo defi-
nido por D[X], y el valor especificado a continuación como el va-
lor apuntado.
El segmento de código suele tener una primera línea con la direc-
tiva ASSUME, que informa al ensamblador de los segmentos a los que
apuntarán durante la ejecución los diferentes registros de segmen-
to. Por ejemplo:
ASSUME CS:CODIGO, DS:DATOS, SS:PILA
Si deseamos generar un ejecutable de tipo COM, es necesario cum-
plir ciertas normas. La primera es que el programa fuente debe
contener un solo segmento, donde residirán el código, los datos y
la pila. La otra limitación es que el punto de entrada debe estar
en el desplazamiento 100H de dicho segmento. La directiva ORG 100H
indica al ensamblador que continúe el ensamblado a partir de la
dirección dada por el argumento, en este caso 100H.
Lo habitual es que a continuación forzemos un salto al verdadero
punto de entrada, estando el espacio intermedio ocupado por los
datos. De esta forma, el segmento de código de un programa .COM
comenzaría:
CODIGO SEGMENT 'CODE'
ASSUME CS:CODIGO, DS:CODIGO, SS:CODIGO
ORG 100H
Inicio: JMP Entrada
.
. ; espacio para los datos
.
Entrada PROC
.
.
.
Entrada ENDP
CODIGO ENDS
END Inicio
Un dato a tener en cuenta es que somos responsables de iniciali-
zar correctamente los registros para que apnten a los segmentos,
por ejemplo, para inicializar el registro DS:
MOV AX,DATOS
MOV DS,AX
Por otro lado, siempre será necesario llamar a una función del DOS
para terminar el programa, liberando la memoria y devolviendo el
control al sistema operativo. La encargada de realizar esta tarea
es la función 4CH de INT 21H, de manera que al final del segmento
de código será preciso introducir las siguientes dos líneas:
MOV AH,4CH
INT 21H
3.-MODOS DE DIRECCIONAMIENTO.
Existen dieciseis formas de referenciar una posición de memoria.
No nos ocuparemos aquí de sus denominaciones, sino de la forma en
que se construyen, pues, en realidad, todas se forman mediante la
combinación de unos pocos elementos. Así, se podía escribir
simbólicamente
BX SI
+ + despl
BP DI
donde despl es una constante o una expresión resoluble en una
constante y, por defecto, BX indica desplazamientos respecto al
segmento DS, mientras que BP indica desplazamientos respecto al
segmento SS.
Como ilustración, sea una cadena de caracteres definida en el seg-
mento de datos. Se dan una serie de formas equivalentes para refe-
renciar al elemento número ocho de la cadena:
.DATA
CADENA DB 'ABCDEFGHIJKL',0
.CODE
MOV AX,@DATA
MOV DS,AX
...
MOV SI,OFFSET CADENA + 8
MOV AL,[SI]
...
MOV BX,8
MOV AL,[CADENA+BX]
...
MOV BX,OFFSET CADENA
MOV AL,[BX+8]
...
MOV SI,8
MOV AL,[CADENA+SI]
...
MOV BX,3
MOV SI,4
MOV AL,[CADENA+BX+SI+1]
4.- JUEGO DE INSTRUCCIONES
4.1. Instrucciones de transferencia de datos.
Todos los ejemplos anteriores se han puesto con la instrucción MOV
que es con diferencia la mas usada en cualquier programa. Hay que
tener en cuenta sin embargo algunas cosas que no pueden hacerse
con la instrucción MOV.
En primer lugar, no pueden moverse datos directamente entre dos
posiciones de memoria. Así MOV Datos1,Datos2 es una expresión ile-
gal. En su lugar se escriben las dos líneas:
MOV AX,Datos2
MOV Datos1,AX
En segundo lugar, no se pueden mover datos directamente de un re-
gistro de segmento a otro. MOV DS,ES es ilegal. En su lugar pode-
mos escribir:
MOV AX,ES
MOV DS,AX
En tercer lugar, no se puede mover una constante directamente a un
registro de segmento. MOV DS,Datos es ilegal. En su lugar escriba-
mos
MOV AX,Datos
MOV DS,AX
Finalmente, no se puede usar CS como operando.
MOV pertenece a un primer grupo de instrucciones de transferencia
de datos, entre la memoria y los registro y a/de los puertos de
entrada/salida. Las instrucciones mas usadas de este grupo son las
siguientes:
PUSH, POP y sus variantes para almacenar o extraer datos de la pi-
la.
IN, OUT para obtener datos de un puerto o enviarlos.
LEA, LDS, LES para mover direcciones de variables, en lugar de sus
contenidos. Describiremos brevemente cada una de ellas.
PUSH se usa para guardar en la pila un dato, y POP para recuperar-
lo. Como la pila tiene estructura LIFO, las instrucciones POP se
realizarán en orden inverso a las instrucciones PUSH. Por ejemplo,
si se quieren guardar los registro AX y BX y recuperarlos después,
la secuencia correcta es
PUSH AX
PUSH BX
.
.
POP BX
POP AX
PUSH y POP también son adecuados para transferir el contenido de
un registro a otro, por ejemplo
PUSH AX
POP BX
transfiere AX a BX. Este método es sin embargo mas lento. A veces
se necesita guardar todos los registro generales para recuperarlos
después. En lugar de escribir una serie de PUSH y otra serie de
POP, se usan las instrucciones PUSHA y POPA. Además de aportar co-
modidad al programador y legibilidad al programa, estas instruccio-
nes se ejecutan más rápido. Cuando se quiere guardar el registro
de indicadores, se usan las formas PUSHF y POPF.
Las instrucciones de entrada/salida IN/OUT se usan para comunicar-
se con los periféricos del sistema, y tienen el formato
IN acumulador,puerto
OUT puerto,acumulador
donde el acumulador es AX o AL según se trata de bytes o palabras.
Algunos ejemplos pueden ser los siguientes:
IN AL,200 ; pasa a AL el contenido del puerto 200
OUT 20H,AX ; envía al puerto 20H el contenido de AX
Las instrucciones de transferencia de direcciones permiten código
fuente mas compacto sobre todo cuando se trata con direccionamien-
tos indexados. Por ejemplo, consideremos la instrucción
MOV AL,[BX+SI+20]
El micro toma el valor de BX, le suma el valor de SI y al resulta-
do le suma 20, usando el valor obtenido como desplazamiento dentro
del segmento DS para acceder a un byte y transferirlo al registro
AL. Esta instrucción lleva implícitas las operaciones:
MOV DX,BX
ADD DX,SI
ADD SI,20
Existe una instrucción mas rápida y compacta que sustituye a este
conjunto, llamada LEA (Load Effective Address) para cargar un des-
plazamiento. Su sintaxis es
LEA registro,mem16
donde registro es un registro de propósito general de 16 bits y
mem16 es un operando de memoria de tipo palabra. Por ejemplo
LEA DX,[BX+SI+20]
transfiere a DX el desplazamiento BX+SI+20. La variante LDS es si-
milar. Su sintaxis es
LDS reg16,mem32
Lee una palabra doble de 32 bits de la memoria y carga los 16 bits
de orden mas bajo en el registro especificado, y los mas altos en
DS. Similar a la anterior es LES: funciona exactamente igual salvo
que los 16 bits mas altos los guarda en ES.
Supongamos que queremos cargar el segmento de Tabla_, definida co-
mo Etiqueta DD Tabla_ en DX, y su desplazamiento en BX. Una forma
de hacerlo es mediante la serie
MOV BX,DESPL Etiqueta
MOV AX,SEG Etiqueta
MOV DX,AX
La mejor forma de hacerlo es mediante
LDS BX,Etiqueta
4.2. Instrucciones aritméticas.
Las instrucciones aritméticas mas simples son INC y DEC. La prime-
ra incrementa el operando en una unidad. La segunda lo decrementa
también en una unidad. El resulta se trunca al número de bits del
operando. Así, si todos los bits del operando están a 1, el opera-
dor INC los pasará a todos a cero. El bit adicional necesario para
representar el resultado correcto se pierde. De esta forma, INC AL
nunca modificará AH.
Las instrucciones suma y resta son ADD y SUB, y su sintaxis es
ADD destino,fuente
SUB destino,fuente
En el primer caso, se suma fuente a destino, y el resultado queda
almacenado en destino. En el segundo, fuente se resta de destino,
y el resultado queda almacenado en destino. En principio, los ope-
randos fuente y destino han de ser del mismo tamaño, y al tratarse
de aritmética sobre número fijo de bits, los acarreos se pierden.
A veces sin embargo es necesario operar sobre números de distinto
tamaño en bits. Por ejemplo, no es posible escribir directamente
ADD BL,AX. En estos casos se procede a la extensión del operando
de menor tamaño. En este caso pondríamos a cero BH mediante una
instrucción MOV o mediante un XOR. Pero esto es válido solo cuando
estamos tratando con números sin signo. Para extender un número
con signo se recurre a los operadores CBW (Convert Byte to Word) y
CWD (Convert Word to Double word).
Las instrucciones de multiplicación y división son MUL y DIV, y
usan el acumulador en todos los casos. La multiplicación de 8 bits
se hace entre AL y un operando cualquiera de ocho bits, registro o
memoria, y el resultado es de 16 bits, quedando almacenado en AX.
¿Que ocurre cuando se multiplica AX por otro operando de 16 bits ?
El resultado es de 32 bits, de los cuales los 16 menos significa-
tivos se almacenan en AX y los mas significativos en DX.
Cuando se divide un operando de 16 bits entre otro de 8, se obtie-
ne un cociente de 8 bits y un resto también de 8 bits. El dividen-
do se ha de encontrar en AX, mientras que el divisor se indica en
el operando, y puede ser un registro de 8 bits o un operando de
memoria. El cociente se devuelve en AL y el resto en AH. En forma
simbólica podríamos poner
AX ¦ Divisor
+------
AH AL
Cuando se divide un número de 32 bits entre otro de 16 bits, co-
ciente y resto son de 16 bits. Los 16 bits mas significativos han
de encontrarse en DX, y los menos significativos en AX. El divisor
se indica en el operando, y al igual que antes, puede ser un re-
gistro de 16 bits o un operando de memoria de 16 bits. El cociente
de 16 bits se devuelve en AX, y el resto en DX. Simbólicamente:
DX¦AX ¦ Divisor
+------
DX AX
Existen en realidad dos juegos de instrucciones para la multipli-
cación y división. Uno para operandos con signo y otro para ope-
randos sin signo. Es el programador quien decide si los operandos
representan números con signo o sin signo, y por tanto el juego de
instrucciones a utilizar. La sintaxis es:
sin signo MUL operando
DIV operando
con signo IMUL operando
IDIV operando
Otro conjunto de instrucciones son las de manipulación de bits,
que a su vez puede dividirse en operaciones lógicas, de desplaza-
miento y de rotación. Expondremos sólo las primeras. Estas son
AND, OR y XOR y efectúan las mismas operaciones que las funciones
lógicas del mismo tamaño. La sintaxis es
AND destino,fuente
OR destino,fuente
XOR destino,fuente
La instrucción XOR es especialmente útil para poner a cero cual-
quier registro: XOR reg,reg es igual que MOV reg,0 , pero es mas
rápida.
La instrucción AND pone a cero ciertos bits, de forma que se puede
efectuar una determinada operación con el resto de los bits. Por
ejemplo, para comprobar si el bit n-simo de un registro se encuen-
tra a 1, basta hacer AND reg,mascara, donde mascara es un número
binario donde todos los bits son cero, menos el n-simo, que vale 1.
Si el resultado es distinto de cero, el bit n-simo de reg se esta-
ba a uno.
4.3. Instrucciones de transferencia de control
Las instrucciones que conforman un programa se almacenan en orden
correlativo en la memoria, pero, salvo en los casos mas sencillos,
no se ejecutan linealmente, sino que el control se transfiere a
distintas partes de la secuencia. Las instrucciones de control se
pueden dividir en:
. Transferencia incondicional
. Transferencia condicional
. Bucles
Analizaremos cada uno de estos grupos, presentando las instruccio-
nes mas frecuentes.
4.3.1. Transferencia incondicional
Dentro del primer grupo contamos con las instrucciones CALL, RET y
JMP. La primera se usa para llamar a un procedimiento, esto es, un
conjunto de instrucciones que se ejecutan mas de una vez pero que
se escriben una sola. Este conjunto de instrucciones se almacenan
en memoria a partir de una posición dada, y cada vez que se nece-
sita ejecutarlas, se transfiere el control a la primera instruc-
ción de la serie mediante la instrucción CALL [destino], donde
[destino] es la dirección de comienzo. La última instrucción de un
procedimiento es RET, que devuelve el control al programa princi-
pal. Cuando se llama a CALL, la primera tarea que realiza es guar-
dar en la pila la dirección de la siguiente instrucción, para re-
cuperarla y continuar el programa cuando se termine de ejecutar el
procedimiento. El operando de CALL es habitualmente una etiqueta ,
indicando la dirección origen del procedimiento, pero también pue-
den hacerse llamadas indirectas a través de un registro o un ope-
rando de memoria. No entraremos en este punto. El esquema general
de CALL y RET es:
CALL etiqueta
instrucciones siguientes
.
.
etiqueta:
MIPROC PROC
instrucciones siguientes
.
RET
MIPROC ENDP
Es posible anidar procedimientos, de forma que un procedimiento
pueda llamar a otro, y así sucesivamente. El nivel de anidamiento
solo está limitado por el tamaño de la pila, donde se guardan las
direcciones de retorno. Por ejemplo:
CALL etiqueta1
instrucciones siguientes
.
.
etiqueta1:
MIPROC1 PROC
instrucciones siguientes
.
CALL etiqueta2
.
RET
MIPROC1 ENDP
etiqueta2:
MIPROC2 PROC
instrucciones siguientes
.
.
RET
MIPROC2 ENDP
La instrucción JMP (jump, saltar) se usa para transferir el con-
trol al punto del programa que se desee. Cuando la dirección des-
tino se encuentra en el rango -128,127 bytes de la instrucción de
salto, este se llama corto. En caso contrario, cercano, si destino
se encuentra dentro del mismo segmento y lejano si se encuentra en
otro segmento. Los saltos lejanos se generan especificando el seg-
mento y desplazamiento del destino.
4.3.2. Transferencia condicional
El juego de instrucciones para transferencia condicional es espe-
cialmente rico. El ensamblador reconoce algunas instrucciones de
transferencia condicional con varios mnemónicos, con objeto de fa-
cilitar la tarea al programador. Es un buen hábito construir los
programas de forma que, siempre que sea posible, se ejecute el ca-
so esperado si no se efectúa el salto. La razón es que, si no se
efectúa el salto, se consumen solo tres ciclos de reloj. Los sal-
tos condicionales son siempre cortos, y su sintaxis es
J[mnemonico] destino_corto
donde [mnemonico] es de una a tres letras y destino_corto es una
etiqueta situada en el rango -128,127 bytes desde la instrucción
de salto. Las únicas condiciones que pueden comprobarse son el es-
tado de los flags, lo que equivale a decir que los saltos condicio-
nales se refieren a la última operación efectuada. Existen tantas
instrucciones de salto como flags, pero las mas utilizadas son
JZ salta si cero
JNZ salta si no cero
La limitación de los saltos condicionales de que sean saltos cor-
tos se puede superar usando la instrucción JMP. Consideremos las
instrucciones J[x],JN[x], donde J[x] salta si el flag "x" está a
uno y JN[x] salta si el flag "x" está a cero. La técnica consiste
en generar un salto con la condición contraria por encima de un
JMP a la dirección deseada. Por ejemplo, para implementar
J[x] etiqueta_lejana
escribimos
JN[x] etiqueta
JMP etiqueta_lejana
etiqueta:
Casi todas las veces, las condiciones de salto en un programa son
del tipo A]B , A[B , A=B , A[]B , A]=B y A[=B. Por exigencias de
la aritmética de complemento a dos, existen en realidad dos juegos
de instrucciones, según que se comparen números con signo o sin él.
Para comparaciones de números sin signo, se emplean en los mnemó-
nicos las letras A (de Above, por encima) y B (de Below, por deba-
jo), y para las comparaciones de números con signo las letras G
(de Greater, mayor) y L (de Less, menor). Las instrucciones resul-
tantes comprueban los flags de cero, acarreo y desbordamiento, que
han sido previamente establecidos mediante una instrucción CMP. La
sintaxis de esta instrucción es
CMP destino,fuente
y compara destino con fuente. Por ejemplo, si queremos comparar
los números sin signo contenidos en AX y BX y saltar a etiqueta si
BX[AX, las instrucciones adecuadas son
CMP BX,AX
JB etiqueta
y si los números tienen signo, la secuencia es:
CMP BX,AX
JL etiqueta
Los mnemónicos mas usados son
.para números sin signo
JA destino]fuente
JE destino=fuente
JNE destino[]fuente
JB destino[fuente
JBE destino[=fuente
JAE destino]=fuente
.para números con signo
JG destino]fuente
JE destino=fuente
JNE destino[]fuente
JL destino[fuente
JLE destino[=fuente
JGE destino]=fuente
4.3.3. Instrucciones de bucle
Las instrucciones de bucle hacen que se repitan fragmentos de có-
digo un número prefijado de veces. Una forma de hacerlo es median-
te el contador y un salto condicional al final del bucle
MOV CX,repeticiones
comienzo:
.
.
.
DEC CX
JNZ comienzo
Este fragmento puede compactarse usando la instrucción LOOP, di-
señada para este propósito:
MOV CX,repeticiones
comienzo:
.
.
.
LOOP comienzo ; si CX[]0, salta a comienzo
; de lo contrario, sigue en esta línea
4.4. Instrucciones de cadena.
Las instrucciones de cadena se utilizan habitualmente para inicia-
lizar una zona de memoria a un mismo valor, para mover un bloque
contiguo de datos de una zona a otra de memoria, para comparar ca-
denas o para buscar un valor concreto en una zona de memoria.
El funcionamiento de este conjunto de instrucciones depende del
"flag de dirección", cuyo valor puede ser alterado por las dos ins-
trucciones siguientes
CLD: CLear Direction flag
STD: SeT Direction flag
Las instrucciones de cadena tienen mnemónicos que terminan con la
letra "S", y operan sobre los registros que se especificarán en ca-
da caso. Ya que pueden actuar sobre bytes o palabras, es necesario
indicar este extremo, lo que se conseguirá escribiendo la letra "B"
o "W" tras la "S" del mnemónico.
La instrucción LODS (LOaD String), en sus versiones LODSB y LODSW,
se usa para cargar un byte o palabra desde la dirección indica-
da por DS:SI en AL o AX. Si el flag de dirección se encuentra a
cero, SI se incrementa en 1 o 2, y si está a 1 se decrementa en 1
o 2. De esta forma puede recorrerse la memoria en uno u otro sen-
tido. La instrucción complementaria es STOS (STOre String). Se usa
de la misma forma que LODS y almacena el contenido del registro
AL o AX en la dirección de memoria especificada por ES:DI. El re-
gistro DI se altera según la misma lógica que el SI con la ins-
trucción LODS. La instrucción mas usada es MOVS (MOVe String), la
cual sirve para trasladar uno (MOVSB) o dos bytes (MOVSW).
Esta instrucción lee un dato de DS:SI y lo almacena en ES:DI. Des-
pués, los registro SI y DI se modifican según la misma lógica que
para las instrucciones LODS y STOS.
Para efectuar operaciones repetitivas con estas funciones, podemos
implementar un bucle que repitiera un número prefijado de veces la
operacion a realizar con estas instrucciones. No sería necesario
gestionar explícitamente los registro SI y DI porque estos se ac-
tualizan automáticamente. Sin embargo, el lenguaje ensamblador
permite hacer esto mismo de forma mas compacta mediante la ins-
trucción REP.
Esta instrucción se coloca en la misma línea que la instrucción de
cadena que vayamos a usar, antes de la misma. Obsérvese el frag-
mento de código siguiente:
CLD ; incrementa DI cada vez
MOV AX,1234H ; segmento en que escribe
MOV ES,AX ; STOS almacena en ES:DI
MOV DI,5678H ; desplazamiento. Dirección de comienzo
XOR AL,AL ; pone AL a cero
MOV CX,200D ; inicializa CX a 200D
REP STOSB ; ejecuta 200D veces la orden STOSB
; ahora, CX=0 y DI=5678H+200D
En resumen:
+-----------+ STOS
¦ AX ¦ AL ¦ -----------] ES:DI [----+
+-----------+ ¦
MOVS
+-----------+ LODS ¦
¦ AX ¦ AL ¦ [----------- DS:SI -----+
+-----------+
Existen dos instrucciones más que permiten examinar una cadena y
comparar dos cadenas. Sus mnemónicos son SCAS (SCAn String),y CMPS
(CoMPare String). Ambas admiten las "versiones" para byte y pala-
bra añadiendo las terminaciones B y W.
La instrucción SCASB es equivalente a CMP AL,ES:[DI], mientras que
SCASW es equivalente a CMP AX,ES:[DI]. El resultado de estas ope-
ración altera los flags y DI se actualiza según la lógica vista en
las operaciones anteriores.
La sintaxis CMPSB o CMPSW es CMPS mem1,mem2, donde mem1 y mem2 son
operandos de memoria. En concreto, CMPS realiza la comparación en-
tre DS:[SI] y ES:[DI], es decir, CMP DS:[SI],ES:[DI]. Al igual que
en el caso anterior, estas instrucciones alcanzan su máxima utili-
dad al usarlas en combinación con los operadores REPZ (REPeat whi-
le Zero, repetir mientras cero) y REPNZ (REPeat while Not Zero).
REPZ y REPNZ admiten los sinónimos REPE (REPeat while Equal) y
REPNE (REPeat while Not Equal).
Los usos mas comunes de las instrucciones que hemos presentado son
los siguientes:
* Encontrar el primer elemento distinto del contenido del acu-
mulador dentro de un bloque. Para esto se usa REPZ SCAS.
* Encontrar el primer elemento diferente entre dos cadenas. Se
usa REPZ CMPS.
* Encontrar la primera ocurrencia del contenido del acumulador
dentro de un bloque: REPNZ SCAS.
* Encontrar el primer elemento igual entre dos cadenas: REPNZ
CMPS.
5.- USO DE INTERRUPCIONES
La filosofía del PC es ésta: dejar que la BIOS haga el trabajo. En
efecto, el PC almacena en la ROM BIOS una enorme cantidad de proce-
dimientos que podemos usar en nuestro provecho, simplemente invo-
cándolos. El lenguaje ensamblador permite pasar el control a una
determinada interrupción. Cada interrupción tiene un número y ofre-
ce diversos servicios. El número de servicio se especifica en AH,
y en otros registros se especifican otros parámetros. La interrup-
ción puede o no devolver valores.
La instrucción en ensamblador que permite ejecutar interrupciones
es INT, y su sintaxis es INT [nº función]. Por ejemplo, la función
10H tiene muchos servicios útiles para controlar el video. Uno de
estos servicios consiste en posicionar el cursor en una fila y co-
lumna determinadas. En este caso:
ENTRADA: AH=2 (nº de servicio)
BH=página de video en la que fijar posición
DH=fila
DL=columna
SALIDA: No tiene
Por ejemplo, para colocar el cursor en la fila 10, columna 15, el
fragmento de código apropiado sería
MOV AH,2 ; nº de servicio
MOV BH,0 ; suponemos que 0 es el nº de página activa
MOV DH,0AH
MOV DL,0FH
INT 10H
Existe abundante bibliografía donde se recogen todas las funciones
y servicios de la ROM BIOS. La lista de funciones es tan extensa
que puede hacer creer que el emsamblador es mas complicado de lo
que lo es realmente, al menos al nivel que será usado en este
curso. Pero el alumno no deberá confundir la dificultad para
escribir un programa en ensamblador con la dificultad para retener
o incluso solo localizar la función que en un momento dado sirva a
nuestros propósitos.
Solo a modo de ilustración, se exponen a continuación las
funciones mas usuales que aparecen cuando se empieza a programar
en ensamblador:
- Siempre será preciso terminar un programa, para lo que se recurre
a la funcion 4CH de INT 21H:
MOV AH,4CH
INT 21H
- Casi siempre será preciso mandar mensajes a la pantalla, para lo
que se usa la función 9:
MOV DX,OFFSET [MENSAJE]
MOV AH,9
INT 21H
donde [MENSAJE] se ha definido en el segmento de datos mediante
una linea del estilo de
MENSAJE DB 'ESTO ES UN MENSAJE $'
y $ es el carácter que necesita la función 9 para identificar el
final de la cadena.
Cuando hay varios mensajes que mostrar, lo normal es escribir u-
no en cada línea, y por eso se añaden al mensaje los caracteres
10 y 13, que significan respectivamente salto de línea y salto de
carro:
MENSAJE DB 'ESTO ES UN MENSAJE',10,13,'$'
- Cuando se quiere mostrar un mensaje, muchas veces se desea colo-
carlo en una posición determinada de la pantalla. La función 02H
de INT 10H realiza el trabajo. Para ello, BH contendrá el núme-
ro de la página de pantalla, DH la línea y DL la columna, de 0 a
24 y de 0 a 79 respectivamente.
- Ocurre casi siempre que al iniciar un programa deseamos limpiar
la pantalla. Para ello, sobreescribimos con espacios en blanco y
los atributos que deseemos la RAM de video, que en adaptadores a
color comienza en la direccion 0B800H:
MOV AX,0B800H
MOV ES,AX
MOV DI,0 ; apuntamos ES:DI al inicio de la RAM de video
MOV AL,32
MOV AH,07H ; carácter 32, gris sobre negro
MOV CX,2000 ; 2000 caracteres
REP STOSW
- Casi siempre será preciso leer un carácter desde teclado, de lo
que se encarga la función 1. El resultado queda almacenado en AL:
MOV AH,1
INT 21H
- Tambien es frecuente leer una cadena de caractéres de la entrada
standard:
MOV AH,3FH
MOV BX,0
MOV CX,[LONGITUD MAXIMA]
MOV DX,[DESTINO DE LA CADENA]
INT 21H
AND AX,AX
3FH es la función del DOS que permite leer cadenas. Si BX=0, se
leen desde entrada standard, que es el teclado. La longitud
máxima se encuentra en CX, y la cadena leida va a parar a
[DESTINO DE LA CADENA] que es OFFSET [VARIABLE DE CADENA]. Como
AX guarda la longitud de la cadena leida, AND AX,AX comprueba si
se ha leido algún carácter.
- Igualmente frecuente es la escritura de una cadena en la salida
standard:
MOV CX,[LONGITUD CADENA]
MOV AH,40H
MOV BX,1
MOV DX,OFFSET [CADENA]
INT 21H
En el apéndice A se da una relación de las funciones BIOS.
(Nota: tomado de los apuntes de la asignatura Periféricos, Universidad de Alicante,
curso 1995.1996, profesor F.J. Gil-Chica)