ASM POR AESOFT. (lección 7). -------------------------------------------------------------------- - DUDAS DE LECCIONES ANTERIORES - MAS INFORMACION Y EJEMPLOS ACERCA DEL REGISTRO DE ESTADO (FLAGS) (ampliación de la lección 1). - REPASO AL TEMA DE LOS SEGMENTOS (ampliación de la lección 2). - CONJUNTO DE INSTRUCCIONES DEL 8086 (I). -------------------------------------------------------------------- Hola de nuevo, aplicados alumnos. :-) En esta lección, aparte de repasar algunos temas anteriores, a petición de algunos lectores, empezaremos a estudiar las instrucciones con que contamos en el 8086. Las dividiremos en grupos, y veremos sus características y funcionamiento. Que disfruteis la lección de hoy, que mi trabajo me ha costado. :-) - DUDAS DE LECCIONES ANTERIORES ------------------------------- En este apartado os dejo un mensaje que me parece de utilidad para el curso. En este mensaje se habla sobre la codificación de ciertas instrucciones, y como os decía me parece útil para todos los seguidores del curso. Ahí va: --- Inicio del mensaje FJR> Veamos cómo se codifica esta instrucción: FJR> MOV AX,5 ---> B8 05 00 (Código máquina, siempre FJR> en hexadecimal). FJR> En primer lugar tenemos el primer byte que contiene FJR> el código de FJR> operación (B8). FJR> Debido a que este código de operación(B8) tiene FJR> implícita la utilización FJR> del registro AX como destino, no es necesario el FJR> byte EA ó byte de FJR> direccionamiento, que sí sería necesario para FJR> transferencias con otros FJR> registros. DT> Osea que cada mov que hagas tiene un 'código' distinto si se hace a DT> ax, a cx, etc... ? y se ha seguido algún orden lógico a la hora de DT> asignarle números a las intrucciones?, osea, ¿por qué b8 -> mov ax,? En efecto, ese B8 tiene su razón de ser. En primer lugar, todas las operaciones del tipo MOV registro,dato_inmediato tienen un código de operación cuyo primer dígito hexadecimal es B. Hay 16 códigos de operación diferentes (uno para cada registro, como tú muy bien observabas) para el tipo de operación MOV registro,dato_inmediato. Por supuesto estos código siguen un orden: B0 ---> MOV AL,dato_inmediato_tamaño_byte B1 ---> MOV CL,dato_inmediato_tamaño_byte B2 ---> MOV DL,dato_inmediato_tamaño_byte B3 ---> MOV BL,dato_inmediato_tamaño_byte B4 ---> MOV AH,dato_inmediato_tamaño_byte B5 ---> MOV CH,dato_inmediato_tamaño_byte B6 ---> MOV DH,dato_inmediato_tamaño_byte B7 ---> MOV BH,dato_inmediato_tamaño_byte B8 ---> MOV AX,dato_inmediato_tamaño_word B9 ---> MOV CX,dato_inmediato_tamaño_word BA ---> MOV DX,dato_inmediato_tamaño_word BB ---> MOV BX,dato_inmediato_tamaño_word BC ---> MOV SP,dato_inmediato_tamaño_word BD ---> MOV BP,dato_inmediato_tamaño_word BE ---> MOV SI,dato_inmediato_tamaño_word BF ---> MOV DI,dato_inmediato_tamaño_word Podrás observar que el orden de los registros no es AL, BL, CL, DL... Sino AL, CL, DL, BL. Lo mismo para los registros de 8 bits de mayor peso (AH, CH, DH, BH), Y para los registros de 16 bits (AX, CX, DX, BX, SP, BP, SI, DI). Un saludo --------------------------------- Francisco Jesus Riquelme ------------------ FidoNet 2:341/43.9 MasterNet 17:3468/301.3 --- Fin del mensaje Espero que os haya parecido interesante. - MAS INFORMACION Y EJEMPLOS ACERCA DEL REGISTRO DE ESTADO (FLAGS). (ampliación de la lección 1). -------------------------------------------------------------------- Ya vimos algo acerca del registro de estado (FLAGS) en la lección 1. En esta lección, trataré de desarrollar un poco más para los que no lo entendieron del todo en esa primera lección. El registro de flags ó palabra de estado está compuesto por una serie de banderas (flags en inglés) que no son más que simples bits o dígitos binarios, los cuales pueden tener un valor de uno (bit activo) o un valor de cero (bit no activo). Cada uno de estos bits mantiene una información determinada. Ya dimos en la primera lección una relación de estos bits de estado ó flags, agrupados en categorías según su función. Veamos más detenidamente uno de estos grupos de flags ó banderas: * Flags de estado * Estos flags están íntimamente ligados a las operaciones aritméticas, que son enumeradas y detalladas más abajo. Estos flags nos ofrecen información acerca del resultado de la última operación efectuada. Es decir, si tras realizar una multiplicación se ha producido desbordamiento, el flag ó bit de estado Of (flag de overflow) se pondrá con valor 1. Si fruto de otra operación, como una resta obtenemos un número negativo, el flag Sf (flag de signo), se pondrá a 1, indicando que el resultado de la operación ha dado un número negativo. Si tras una operación, como puede ser una resta, el resultado obtenido es cero, se activará el flag Zf (flag Cero. Zero en inglés). Una operación puede afectar a un sólo flag, a ninguno, o a varios. Es decir, dependiendo del tipo de instrucción de que se trate, el procesador tendrá que actualizar un número determinado de flags. Por ejemplo, las instrucciones de salto, tanto condicionales como incondicionales, no actualizan ningún flag. La instrucción MOV tampoco actualiza ningún flag. Mientras que las instrucciones aritméticas actualizan muchos de los flags, para así indicar el estado de la operación. Tomemos las instrucciones SUB, ADD, ADC, etc. Todas estas instrucciones afectan a los siguientes flags: Of, Sf, Zf, Af, Pf, Cf. En realidad, la mayoría de las instrucciones aritméticas afectan a esos flags. De esta forma, tras realizar cada operación, mediante estos flags sabremos si el resultado es negativo, si es cero, si se ha producido overflow, etc. Como hemos visto, las operaciones modifican los flags para indicar el estado de tal operación. Pero esa no es la única forma de que los flags cambién su valor. En ensamblador disponemos de instrucciones para modificar el valor de un flag determinado. - CLC (Clear Cf. Borrar ó poner a cero el flag de acarreo). Sintaxis: CLC. - STC (Set Cf. Activar ó poner a uno el flag de acarreo). Sintaxis: STC. - CLI ((Clear If. Borrar ó poner a cero el flag de interrupción). Sintaxis: CLI. Esta instrucción la usamos cuando queremos que ninguna interrupción enmascarable nos interrumpa el proceso en el que estamos. (Esto ya lo vimos en la lección 4). - STI (Set If. Activar ó poner a uno el flag de interrupción). Sintaxis: STI. Mediante esta instrucción habilitamos de nuevo las interrupciones. (Visto en la lección 4). - CLD (Clear Df. Borrar ó poner a cero el flag Df). Sintaxis: CLD. Esta instrucción se usa cuando se está trabajando con hileras ó cadenas de caracteres. Ya la estudiaremos entonces. - STD (Set Df. Activar ó poner a uno el flag Df). Sintaxis: STD. Esta instrucción se usa cuando se está trabajando con hileras ó cadenas de caracteres. Ya la estudiaremos entonces. El resto de los flags no puede modificarlos el programador mediante las instrucciones CLx y STx. Pero siempre hay otros métodos. ~~~ A ver si alguien me dice cómo podemos modificar el flag Tf, por ejemplo. Os daré una pista: ¿Recordais las instrucciones PUSHF y POPF? Espero vuestros mensajes. Si a nadie se le ocurre, ya dejaré yo la solución en una próxima lección. - REPASO AL TEMA DE LOS SEGMENTOS (ampliación de la lección 2). -------------------------------------------------------------------- Debido a que no quedó claro para todos el tema de los segmentos, intentaré complementar la información que ya dí acerca del tema de la segmentación con una exposición más coloquial de dicho tema. Tenemos 1 Mbyte de memoria para nuestro uso. 1 Mbyte son 1024 Ks. Y 1024 Ks son a su vez, 1048576 bytes. O sea, que podemos manejar 1048576 bytes de memoria desde nuestro programa. Ahora debemos tener en cuenta que los registros del 8086 son de 16 bits, es decir, tienen capacidad para albergar 16 bits diferentes. Cada uno de estos bits puede tener un valor de 1 o de 0, independientemente del valor que tengan los bits contiguos. Por tanto, tenemos 2^16 combinaciones diferentes para ese registro, es decir, el registro puede tener 2^16 valores diferentes, o lo que es lo mismo, el registro puede representar 65536 valores diferentes. Hemos dicho que los registros en el 8086 son de tamaño de 16 bits (como mucho). Entonces, en teoría, sólo podríamos indicar 65536 posiciones de memoria. Pero sólo en teoría, ya que como vismos en la lección 2, se puede acceder a todas las posiciones de ese Mbyte usando registros de 16 bits. Usamos entonces 2 registros de 16 bits. Por medio del primero, seleccionamos el trozo (segmento) de ese Mbyte donde se encuentra la dirección que nos interesa. Por medio del segundo registro, indicamos cuál es la dirección que nos interesa dentro de ese trozo ó segmento. El primer registro se llamará registro de segmento, y puede ser uno de los que ya conocemos: CS, DS, ES, SS. El segundo registro es lo que se llama offset ó desplazamiento dentro de ese segmento ó trozo de Mbyte. Ya vimos en la lección 2 como se formaba la dirección final a partir de estos dos registros ó direccionamiento segmentado. El valor depositado en el registro de segmento, se multiplica por 16 a la hora de buscar el segmento (trozo de Mbyte actual), de esta forma se puede acceder a todo el Mbyte, ya que 65536*16 = 1048576 (1 Mbyte). Esto es algo que hace internamente el procesador con registros especiales para este propósito. ¿Pero qué pasa con los 15 bytes que quedan entre una dirección y otra? Para eso tenemos el segundo registro: Una vez que ya se sabe dónde comienza el segmento, es decir, una vez que ya sabe el procesador con qué trozo de Mbyte va a trabajar a continuación, lo que hace es sumar al principo de éste, el valor depositado en el segundo registro (offset ó desplazamiento). De esta forma, se produce el acceso a la dirección deseada. Si a pesar de esta explicación alguno no lo entiende, que sea más concreto, y me diga exactamente qué es lo que no entiende. - CONJUNTO DE INSTRUCCIONES DEL 8086 (I). ----------------------------------------- En este apartado vamos a estudiar las operaciones fundamentales para empezar a programar en ensamblador. Una lista completa del conjunto de instrucciones del 8086 se dará más adelante, en otra lección. Por ahora, tendremos suficiente con estudiar las instrucciones más representativas dentro de cada grupo: --- Movimiento de datos. Las instrucciones pertenecientes a este grupo, tienen como objetivo: - Actualizar un registro con un valor. - Copiar la información de un registro a una posición de memoria. - Copiar la información de una posición de memoria a un registro. - Mover la información de un registro a otro. - Intercambiar la información entre dos registros. En este grupo (Movimiento de datos) podíamos incluir varias de las instrucciones que vamos a ver en grupos sucesivos, como por ejemplo cuando hablemos de las instrucciones para el manejo de hileras (cadenas de caracteres), entre otras, estudiaremos las instrucciones para transferir hileras, que bien se podían incluir en este grupo debido a su naturaleza de movimiento de datos. De cualquier modo, se enmarquen en un grupo o en otro, quedará suficientemente claro durante su exposición sus características y finalidad. Como vimos en lecciones anteriores, la instrucción principal usada en movimientos de datos es la instrucción MOV. Con la instrucción MOV, podemos: - Mover el contenido de un registro fuente o una posición de memoria a un registro destino. O bien, mover el contenido de un registro a una posición de memoria. Su sintaxis es como sabemos: MOV destino,fuente. Ejemplo: MOV BX,SI ---> Mueve el contenido del registro SI al registro BX. - Mover un dato (valor inmediato) a un registro o posición de memoria. Sintaxis: MOV destino,valor. Ejemplo: MOV BYTE PTR [SI],7 ---> Introduce el número 7 en la posición de memoria direccionada por SI. Ejemplo: MOV AX,25 ---> Mueve el número 25 al registro AX. Aparte de la instrucción tenemos varias más para realizar movimientos de datos, como pueden ser: - XCHG Intercambia el contenido de dos registros, o bien el contenido de un registro y el de una posición de memoria. Sintaxis: XCHG registro,registro/memoria XCHG viene del inglés EXCHANGE (Cambio). Por tanto es un cambio entre los dos valores dados tras el código de operación de la instrucción. Ejemplo: XCHG AX,WORD PTR [BX] ---> Tras ejecutarse esta instrucción, AX contendrá el valor que hubiera en la posición de memoria direccionada por BX, y viceversa. Ejemplo: CX,DX ---> Intercambia los contenidos de CX y DX. - Todas las relacionadas con la pila: PUSH, POP, PUSHF, POPF. Las cuales las estudiamos en la lección 4. Ejemplo: PUSH AX ---> Introduce en la cima de la pila, el valor contenido en AX. - Además de las instrucciones enumeradas, y como ya hemos dicho arriba, en este grupo existen varias instrucciones más que veremos más adelante, conforme sea necesaria su utilización. --- Transferencia de control. Son un conjunto de instrucciones que permiten al programador romper el flujo secuencial en un programa. Su función consiste en añadir un valor de desplazamiento al puntero de instrucción (IP), y en algunos casos variar también el valor de CS. La finalidad está en permitir ejecutar trozos de código si se cumple una condición, ejecutar trozos de código repetidas veces (bucle), ejecutar trozos de códigos desde diferentes puntos del programa (procedimientos), etc. Son 5 los tipos de instrucciones de transferencia de control. Podemos clasificar las instrucciones de transferencia de control en los siguientes subgrupos: - Saltos incondicionales (JMP). - Bucles (LOOP). - Saltos condicionales (Jnnn). Donde nnn indica la condición. - Llamadas a procedimientos (CALL). - Llamadas a interrupciones o funciones (INT). Vamos a desarrollar cada uno de estos grupos: ... - JMP (salto incondicional). Provoca la transferencia de control a la dirección que se especifica a continuación del código de operación. Su sintaxis es: JMP dirección Donde dirección puede ser una etiqueta (La etiqueta es un nombre que asociamos a una línea de instrucción en ensamblador. Es como una especie de apuntador a esa línea), o una dirección contenida en un registro o variable. Los saltos pueden ser directos o indirectos. Así como también pueden realizarse saltos dentro del mismo segmento (NEAR), y saltos intersegmento (FAR). Directo y NEAR: JMP etiqueta ---> Salto a la dirección etiqueta. Etiqueta puede encontrarse antes o después de la instrucción de salto. Es decir, los saltos se pueden realizar hacia adelante o hacia detrás de la instrucción en curso. Incluso es posible tener una instrucción de salto a esa misma instrucción. Es decir: ;*** porción de código. Etiqueta: JMP etiqueta ;*** fin de la porción de código. Lo cual nos daría como resultado un bloqueo del ordenador, ya que el control no saldría de esa línea. Sería algo así como un bucle sin fin. El salto directo y NEAR, es el salto más común. Raramente se utilizan los que aparecen a continuación. Indirecto y NEAR: JMP [BX] ---> Salto a la dirección indicada por la variable direccionada mediante BX. Es un salto dentro del mismo segmento. Indirecto y FAR: JMP FAR PTR [BX] ---> Salto tipo FAR (a otro segmento) donde BX contiene la dirección de comienzo de una doble palabra con los nuevos valores de IP y CS. Ejemplo de salto directo y NEAR: ;*** Inicio: JMP Sanbit MOV cx,7 Sanbit: MOV cx,6 ;*** Al ejecutar este trozo de código desde la etiqueta Inicio, la instrucción (MOV cx,7) nunca se ejecutará. Por tanto, al final de este trozo de código, la variable CX tendrá valor 6. Obsérvese que las etiquetas pueden tomar cualquier nombre, siempre que éste no pertenezca al lenguaje ensamblador. Al final de la etiqueta debe aparecer el carácter ':' (los dos puntos), el cual le indica al ensamblador que es una etiqueta de tipo NEAR, es decir, que va a ser utilizada para saltos dentro del mismo segmento. Si no apareciesen los dos puntos ':', se consideraría de tipo FAR, utilizada para saltos entre segmentos. Lo más común es utilizar etiquetas tipo NEAR. Para saltos intersegmentos se suelen utilizar otro método diferente al salto a una etiqueta tipo FAR. Yo nunca he usado una etiqueta tipo FAR en los años que llevo con el ensamblador, y seguramente vosotros tampoco la useis nunca. ... - LOOP (Bucle) Esta instrucción sirve para ejecutar un trozo de código un número de veces dado (indicado mediante el registro CX). En cada iteración del bucle se decrementa el valor del registro CX. El bucle finalizará cuando CX tenga valor 0, es decir, cuando se hayan producido tantas iteraciones como indicaba CX antes de entrar al bucle. Veamos un ejemplo: ;*** MOV CX,7 INICIO_BUCLE: ADD WORD PTR [BX],CX INC BX LOOP INICIO_BUCLE MOV SI,BX ;*** En el ejemplo que estamos tratando, tenemos un bucle que se va a repetir 7 veces. En cada una de estas iteraciones se van a realizar dos operaciones aritméticas (echar un vistazo al apartado de operaciones aritméticas, para saber qué hace el cuerpo del bucle). Tras realizar las dos operaciones, llegamos a la instrucción LOOP inicio_bucle. Esta instrucción primero comprueba si CX vale 0, en caso afirmativo, no hace nada y sigue el flujo de control por la siguiente instrucción (en este caso: MOV SI,BX). En caso de que CX tenga un valor distinto de 0, se decrementa su valor, y se bifurca a la dirección inicio_bucle. O sea, que se realiza la siguiente iteración. Del mismo modo que el utilizar variables nos evita tener que indicar posiciones de memoria concretas del modo: [2346h], [7283h], etc, siendo infinítamente más cómodo usar nombres como: coordenada_superior, valor_total, posicion_cursor, modo, etc... Del mismo modo, como os decía, usar etiquetas es la solución que nos ofrece el ensamblador para poder dirigirnos a posiciones de memoria en nuestros saltos, bucles, etc. También se pueden usar las etiquetas para indicar dónde empiezan determinadas estructuras de datos. ... - Saltos condicionales (Jnnn). Los saltos condicionales se usan en ensamblador para ejecutar trozos de código dependiendo del valor de determinado registro o variable. Llegamos a este punto que para realizar un salto condicional, antes hemos de hacer una comparación. Aunque se pueden realizar saltos condicionales sin antes haber hecho una comparación correspondiente, lo usual es hacer la comparación. Por tanto, antes de seguir con la los saltos condicionales, tenemos que saber cómo se realizan las comparaciones en ensamblador, y qué finalidad tiene el que tras cada comparación haya un salto condicional. * COMPARACIONES * Las comparaciones están íntimamente relacionadas con los saltos condicionales. Es más, es raro el programa ensamblador en el que se encuentre una comparación y acto seguido no haya un salto condicional. La sintaxis de la instrucción de comparación es: CMP registro,registro CMP registro,memoria CMP memoria,registro CMP registro,valor CMP valor,registro El orden de los operandos a comparar es muy importante: No es lo mismo la instrucción CMP AX,7 que CMP 7,AX. No es lo mismo, debido a que en la comparación obetenemos más información que un simple 'son iguales' o 'son diferentes'. Fruto de una comparación sabemos qué operando es el mayor. Usaremos una de las 5 sintaxis de arriba dependiendo de lo que vamos a comparar. Si queremos comparar 2 registros, por ejemplo AX con CX, la instrucción apropiada será CMP AX,CX. Los datos a comparar deben ser del mismo tamaño. Es decir, se comparará un dato de tipo byte con otro de tipo byte; Un dato de tipo palabra con otro dato de tipo palabra. Pero nunca se comparará un dato de tipo byte con otro de tipo palabra. Ejemplo de mala utilización de CMP: CMP AX,CL ---> No podemos comparar un dato de tipo palabra (AX) con un dato de tipo byte (CL). Hemos visto que íntimamente ligado a los saltos condicionales están las instrucciones de comparación. Pues bien, el 'medio de comunicación' (por decirlo de alguna manera) entre una comparación y el salto condicional asociado, son las banderas de estado (FLAGS). Para aclarar esto, veamos cómo actúa una instrucción de comparación: Lo que hace la instrucción de comparación es restar al primer operando el segundo, pero eso lo hace mediante unos registros internos del procesador, a los que no tiene acceso el programador. De esta forma, los operandos usados por el programador quedan inalterados. Al realizar esta resta, se actualiza el registro de estado (FLAGS). Es decir, si fruto de la comparación, los dos datos eran iguales, la bandera o flag Zf tendrá valor activo, indicando que fruto de esa resta interna que ha hecho el procesador el resultado es un cero. Es decir, los datos son iguales. Cuando un dato es menor que otro, son otros flags los que se activan, como el flag Cf (flag de acarreo o Carry). Al principio de la lección aparece más desarrollado todo lo relacionado con los FLAGS. Estudiemos más profundamente el tema de los saltos condicionales: Todos los saltos condicionales deben estar dentro del rango (+127, -128) bytes. Es decir, que sólo se pueden saltar 127 bytes hacia adelante y 128 bytes hacia detrás dentro del código del programa. Si sumamos esos 127 bytes y los otros 128, tenemos un valor de 255. Para los que no les suene ese valor, deciros que es el mayor número que puede contener un dato de tipo byte. Es decir, que se reserva un byte para indicar lo grande que va a ser el salto. Como el salto puede ser hacia adelante o hacia detrás, hay que dividir ese 255 en la mitad (más o menos) para los valores positivos (saltos hacia adelante) y otra mitad para los negativos (saltos hacia detrás). ¿Qué hacer cuando se quiere realizar un salto condicional mayor que esos 127/128 bytes? Muy sencillo: Un salto condicional a un salto incondicional. También es útil conocer que existen saltos condicionales empleados cuando se comparan datos con signo, y los saltos condicionales empleados en comparaciones de datos sin signo. Veamos los posibles saltos condicionales que podemos encontrar en el 8086: * Saltos basados en datos sin signo: Instrucción Efecto Flag comprobados -------------------------------------------------------------- JE/JZ (salta si igual) Zf=1 JNE/JNZ (salta si no igual) Zf=0 JA/JNBE (salta si superior) Cf=0 y Zf=0 JAE/JNB (salta si superior o igual) Cf=0 JB/JNAE (salta si inferior) CF=1 JBE/JNA (salta si inferior o igual) CF=1 ó Zf=1 * Saltos basados en datos con signo: Instrucción Efecto Flags comprobados -------------------------------------------------------------- JE/JZ (salta si igual) Zf=1 JNE/JNZ (salta si no igual) Zf=0 JG/JNLE (salta si mayor) Zf=0 y Sf=Of JGE/JNL (salta si mayor o igual) Sf=Of JL/JNGE (salta si menor) Sf<>Of JLE/JNG (salta si menor o igual) ZF=1 ó Zf<>Of Además de estos saltos encontramos una serie de saltos condicionales basados en comprobaciones aritméticas especiales: Instrucción Efecto Flags comprobados -------------------------------------------------------------- JS (salta si negativo) Sf=1 JNS (salta si no negativo) Sf=0 JC (salta si se ha producido acarreo) Cf=1 JNC (salta si no se ha producido acarreo) Cf=0 JO (salta si se ha producido *overflow*) Of=1 JNO (salta si no se ha producido overflow) Of=0 JP/JPE (salta si *paridad par*) Pf=1 JNP/JPO (salta si *paridad impar*) Pf=0 JCX (salta si CX=0) CX=0 (registro CX=0) *overflow* Overflow es lo mismo que desbordamiento, y se produce cuando tras una operación aritmética, el resultado es demasiado grande para que quepa en su destino. Al producirse overflow, se activa el flag Of. *paridad par* , *paridad impar* La paridad indica el número de unos (1) en un registro o variable. Paridad par indica que ese registro tiene un número par de unos. Paridad impar indica que el registro tiene un número impar de unos. Al realizar cada operación aritmética, el procesador comprueba el número de unos del resultado. Si ese número de unos es par (paridad par), activa el flag Pf. Si es impar, lo pone a 0. Veamos la equivalencia entre las sentencias if..then de los lenguajes de alto nivel, y las construcciones CMP..Jnnn. El equivalente a la sentencia: 'If modo=5 then fondo=7', vendría dado en ensamblador por la construcción: ;*** CMP modo,5 jnz no_fon mov fondo,7 no_fon: ;*** Veamos otro ejemplo: El equivalente a: 'If modo=5 then fondo=7 else fondo=6', vendría dado en ensamblador por: ;*** CMP modo,5 jnz no_fon mov fondo,7 jmp short fin_fon ;** a continuación se explica lo de 'jmp short' no_fon: mov fondo,6 fin_fon: ;*** *jmp short* se utiliza cuando se quiere hacer un salto incondicional a una posición de memoria que está dentro del rango (-127 , +128). Es decir, que sobra con un byte para indicar el desplazamiento. de esta forma, nos ahorramos uno de los dos bytes que serían necesarios en caso del salto incondicional normal. El salto incondicional normal (JMP) necesita dos bytes para poder especificar cualquier dirección dentro del segmento actual. Añadiéndole la palabra 'short', como hemos visto, hacemos que sólo necesite un byte para especificar la nueva dirección donde pasar el control. Otro ejemplo: El equivalente de 'If modo <> 24 then fondo=modo' quedaría en ensamblador de la siguiente manera: ;*** suponemos las variables (fondo y modo) de tipo byte. CMP modo,24 jz fin_fon mov al,modo mov fondo,al fin_fon: ;*** Un último ejemplo: El equivalente de 'If modo < 23 then modo=23' quedaría en ensamblador de la siguiente manera: ;*** CMP modo,23 jnb fin_fon mov modo,23 fin_fon: ;*** ... - Llamadas a procedimientos (CALL). Al igual que en los lenguajes de alto nivel, en ensamblador tenemos los llamdos procedimientos, trozos de código que van a ser usados en distintas partes del programa. Los cuales nos permiten tener un código más legible, más estructurado. El formato de un procedimiento en ensamblador es tal como sigue: Tomemos como ejemplo un procedimiento llamado inicializacion. inicializacion PROC . . Cuerpo del procedimiento. . . RET inicializacion ENDP Cuando el procedimiento va a ser llamado desde otro segmento, se dice que es un procedimiento tipo FAR. Y se declara así: inicializacion PROC FAR ;. ;. Cuerpo del procedimiento. ;. ;. RET inicializacion ENDP Cuando el procedimiento se usa sólo en el segmento donde se ha declarado, se denomina procedimiento NEAR. En este caso no es necesario indicar que se trata de NEAR. Es decir, que si no se especifica que es FAR, se supone que es NEAR. O sea, que los dos formatos siguientes, son equivalentes: inicializacion PROC ; Cuerpo del procedimiento. RET inicializacion ENDP ;**** inicializacion PROC NEAR ; Cuerpo del procedimiento. RET inicializacion ENDP Para llamar a un procedimiento y transferirle de este modo el control, usamos la instrucción: CALL nombre_procedimiento. En caso del ejemplo anterior, sería: CALL inicializacion. Se retorna del procedimiento mediante la instrucción RET (Retorno de procedimiento). Existen dos tipos de llamadas a procedimientos: * Llamadas directas: Mediante la instrucción CALL nombre_procedimiento. Donde nombre_procedimiento es el nombre que se le ha dado al procedimiento en cuestión. * Llamadas indirectas: Aquí no se especifica el nombre del procedimiento en cuestión, sino la dirección que contiene la dirección de comienzo del procedimiento que se quiere llamar. Este método se suele usar mucho en programación de utilidades residentes, cuando se llama a una interrupción parcheada (ya veremos todo esto próximamente). En este tipo de llamada, en función de que la llamada sea de tipo NEAR o FAR, las posiciones de memoria donde tengamos almacenada la dirección a la que queremos llamar serán de tipo WORD (palabra) ó DWORD (doble palabra). Pero bueno, por ahora tenemos suficiente con las llamadas directas a procedimientos. ... - Llamadas a Interrupciones o funciones (INT). Ya vimos en lecciones anteriores el funcionamiento de las interrupciones. Vimos que podían ser de tipo hardware, y de tipo software. Pues bien, aquí las que nos interesan son las de tipo software. Que son ni más ni menos que llamadas a procedimientos o subrutinas que se encuentran en la ROM del ordenador, y por otra parte también están las funciones del DOS (sistema operativo) entre otras. Es decir, hay ciertas funciones de muy bajo nivel, como acceso a discos, teclado, etc, que vienen ya programadas en la ROM del ordenador, para así mantener compatibilidad con el resto de PC's, y por otra parte, ayudar al usuario en la programación. También el sistema operativo ofrece al programador una serie de funciones para manejo de ficheros, memoria, etc. Pues bien, la manera de utilizar estas funciones (pasarles el control), es a través de la instrucciín INT. Su sintaxis es la siguiente: INT numero_interrupcion. Donde numero_interrupcion es un número del 0 al 255. Por ejemplo, para acceder al driver de vídeo, se usa la interrupción 10h. INT 10H ---> Provocaría una llamada a la interrupción 10h (16 en decimal). Para acceder a las funciones del DOS, tenemos la interrupción 21h INT 21H ---> Provocaría una llamada a la interrupción 10h (16 en decimal). Estas interrupciones software se dividen en funciones, y éstas a su vez en subfunciones. Para acceder a cada función/subfunción de una interrupción software, existe una convención de llamada. Es decir, para acceder a una determinada función/subfunción, hay que introducir en unos registros determinados un valor adecuado. Por ejemplo, para crear un fichero, accedemos a la función 3Ch de la interrupción 21h. La llamada se realiza así en ensamblador: ;****** MOV AH,3Ch ;Seleccionamos función INT 21H ;pasamos el control a la función. ;***** Otro ejemplo: para leer un carácter desde el teclado, llamamos a la función 00h de la interrupción 16h. La llamada se realiza así en ensamblador: ;****** MOV AH,00h ;Seleccionamos función INT 16H ;pasamos el control a la función. ;***** Hay dos manuales de bolsillo que son prácticamente imprescindibles para un programador en ensamblador. Estos libros son: - Funciones del Ms-Dos (RAY DUNCAN / ANAYA MULTIMEDIA). - La Rom Bios de IBM (RAY DUNCAN / ANAYA MULTIMEDIA). Contienen una gran información acerca de las funciones del DOS y de la ROM. De todas formas, para el que no los pueda o quiera comprar (1000 pelas cada uno, más o menos), próximamente daré una relación de las interrupciones del 8086, junto con información similar a la que viene en estos dos manuales. --- Instrucciones aritméticas. (En un principio sólo trabajaremos con números codificados en binario puro. Es decir, números sin signo.) A diferencia de los lenguajes de alto nivel, en los que existen multitud de instrucciones aritméticas, en ensamblador del 8086 contamos sólo con unas pocas instrucciones básicas fundamentales, como son la suma, la resta, el producto, la división, y poco más. - ADD (Suma en el 8086). Realiza la suma entre dos operandos dados. Estos operandos deben ser del mismo tamaño. Sintaxis: ADD operando1,operando2. Se realiza la suma de los dos operandos, y se deposita en operando1. Tened en cuenta que puede producirse desbordamiento. Tomemos el caso (ADD AX,BX) cuando AX=0F000H y BX=3333H. Al realizarse la suma, se produce overflow (desbordamiento), quedando en AX tras la ejecución, el siguiente valor: 2333H. Con la correspondiente pérdida del dígito más significativo. Esta situación se indica en el registro de estado (FLAGS) activando el flag de overflow (Of). Otro ejemplo: ADD CX,WORD PTR [BX] ---> Suma a CX el valor contenido en la posición de memoria direccionada mediante BX. Otro más: ADD BYTE PTR [SI],7 ---> Introduce el valor 7 en la posición de memoria direccionada por SI. Otro: ADD variable1,2345h ---> Suma a la variable1 (que hemos tenido que definir de tipo palabra) el valor 2345h (tipo palabra). - SUB (Resta en el 8086). Realiza la resta entre dos operandos dados. Estos operandos deben ser del mismo tamaño. Sintaxis: SUB operando1,operando2. Resta del primer operando el segundo. Aquí también se nos pueden plantear situaciones especiales, como cuando restemos a un operando pequeño uno más grande (Recordemos que por ahora sólo trabajamos en binario puro. Es decir, números sin signo). Tomemos el caso (SUB CX,DX) cuando CX vale 0077h y DX vale 8273h. Tras realizarse la operación, CX tendría el valor 7E74h. Esto se debe a que la resta se realiza de derecha a izquierda, y bit a bit, como vamos a ver ahora. Cómo se realiza realmente la resta (basémonos en el ejemplo): El procesador tiene los dos valores en binario: CX = 0000000001110111 DX = 1000001001110011 Acto seguido, procede a realizar la resta, bit a bit (y de derecha a izquierda). CX = 0000000001110111 - DX = 1000001001110011 ----------------------- CX = 0111111001110100 = 7E74H en base hexadecimal. Por tanto, CX=7E74H tras realizar la operación. Otro ejemplo: SUB AX,37h ---> Resta a AX el valor 37h Otro más: SUB BYTE PTR ES:[SI],AL ---> Resta el valor contenido en AL, a la posición direccionada mediante SI, dentro del segmento de datos apuntado por ES. Otro: SUB variable1,word ptr [di] ---> Este ejemlo como podreis deducir por vosotros mismos, es un ejemplo de instrucción no permitida. Como ya vimos en lecciones anteriores, no podemos direccionar dos posiciones de memoria diferentes dentro de la misma instrucción. De tal manera, que esta instrucción habrá que descomponerla en 2 diferentes: MOV AX,WORD PTR [DI] ---> Deposito en AX el valor contenido en la posición de memoria direccionada por DI. De esta manera, en la siguiente instrucción usaré AX y no una dirección de memoria. SUB variable1,AX ---> Ahora sí. Restamos a variable1 (que al fin y al cabo, es una posición de memoria. Tipo palabra en este caso) el contenido del registro AX. - INC (Incremento en una unidad). Se utiliza cuando lo que se quiere hacer es una suma de una unidad. Entonces se utiliza esta instrucción. La sintaxis es: INC operando. Ejempo: INC AX ---> Incrementa el valor de AX en una unidad. Si antes de la instrucción, AX tenía el valor 3656h, ahora tendrá el valor 3657h. Muy importante: Si antes de la instrucción, AX tenía el valor 0FFFFH, ahora tendrá el valor 0000h. Al sumar bit a bit y de derecha a izquierda, queda todo Cero, y al final quedaría un 1, que se pierde porque no cabe en el registro. Aquí pues también se produciría overflow. Otro ejemplo: INC BYTE PTR [BX] ---> Incrementa en una unidad el valor contenido en la posición de memoria direccionada por BX. - DEC (Decremento en una unidad). Se utiliza cuando se quiere restar una unidad a un valor dado. La sintaxis de la instrucción es: DEC operando. Ejemplo: INC AX ---> Decrementa el valor de AX en una unidad. Si antes de la instrucción, AX tenía el valor 3656h, ahora tendrá el valor 3655h. Muy importante: Si antes de la instrucción, AX tenía el valor 0000H, ahora tendrá el valor 0FFFFh. Al restar bit a bit y de derecha a izquierda, queda todo con valor 1, quedando al final 0FFFFH fruto de este DEC. Otro ejemplo: DEC BYTE PTR [BX] ---> Decrementa en una unidad el valor contenido en la posición de memoria direccionada por BX. - ADC (Suma teniendo en cuenta el acarreo anterior). Se utiliza para operaciones cuyos operandos tienen más de un registro de longitud. A la hora de hacer la suma, se tiene en cuenta el posible acarreo de una operación anterior. Esto es posible, gracias al flag Cf ó flag de acarreo. Tanto ésta como la siguiente son instrucciones poco usadas. Yo nunca las uso. - SBB (Resta teniendo en cuenta 'lo que me llevo' de la operación anterior:-)) Se utiliza para operaciones cuyos operandos tienen más de un registro de longitud. A la hora de hacer la resta, se tiene en cuenta 'lo que me llevo' de una operación anterior. Esto es posible, gracias al flag Cf ó flag de acarreo. * MULTIPLICACION Y DIVISION * Estas operaciones aceptan sólo un operando, de forma que según sea su tamaño byte o palabra, asumen que el otro operando está en AL ó AX respectivamente. Esta es una de las instrucciones que os decía (en la lección 1) que tienen el registro acumulador (AX/AH/AL) implícito en la instrucción. De tal manera que no hace falta especificarlo, y sólo es necesario indicar el otro operando involucrado en la operación. - MUL (multiplicación de datos sin signo). Sintaxis: MUL operando. Realiza la multiplicación del operando dado, con el acumulador. Dependiendo del tamaño del operando introducido en la operación, el procesador tomará AL o AX como segundo operando. * Operando de tipo byte: El procesador asume que el otro operando se encuentra almacenado en el registro AL, y el resultado de la operación lo deposita en el registro AX. * Operando de tipo palabra: El procesador asume que el otro operando está almacenado en el registro AX, y el resultado de la operación lo depositará en el par de registros DX,AX. Teniendo DX la parte más significativa ó de mayor peso del resultado. -IMUL (multiplicación de datos con signo). Igual que arriba, pero teniendo en cuenta que se trabaja con números con signo. - DIV (División de datos sin signo). Sintaxis: DIV divisor. Divide el operando almacenado en el registro acumulador por el divisor. Es decir, acumulador/divisor. Dependiendo del tamaño del divisor introducido, el procesador asume que el dividendo se encuentra en AX ó en el par de registros DX,AX. * Divisor de tipo byte: El procesador asume que el dividendo se encuentra almacenado en el registro AX. El resultado de la operación se desompone en AH (resto) y AL (cociente). * Divisor de tipo palabra: El procesador asume que el dividendo se encuentra almacenado en el par de registros DX,AX. Teniendo DX la parte más significativa. El resultado de la operación se descompone en DX (resto) y AX (cociente). - IDIV (División de datos con signo). Igual que arriba, pero teniendo en cuenta que se trabaja con números con signo. Hay que tener muy en cuenta al utilizar estas instrucciones de división, que la ejecución de la operación no desemboque en error. Esto sucede con la famosa división por Cero, entre otras situaciones. También sucede cuando el cociente obtenido en una división no cabe en el registro utilizado para almacenarlo. En estos casos, se produce una INT 0, que origina la terminación del programa en curso. --- Instrucciones de manejo de bits. * Instrucciones de desplazamiento de bits * Son instrucciones que nos permiten desplazar los bits dentro de un regitro o una posición de memoria. Estas instrucciones actúan sobre datos de tipo byte (8 bits) y de tipo palabra (16 bits). - SHL (desplazamiento a la izquierda). Mediante esta instrucción podemos desplazar a la izquierda los bits de un registro o posición de memoria. Esto que puede parecer poco práctico, es muy útil en determinadas situaciones. Por ejemplo, es la manera más rápida y cómoda de multiplicar por 2. Sintaxis: SHL registro,1 SHL registro,CL SHL memoria,1 SHL registro,CL Los desplazamientos pueden ser de una sóla posición o de varias. Cuando queremos realizar un sólo desplazamiento usamos los formatos: SHL registro,1 SHL memoria,1 Pero cuando queremos realizar desplazamientos de más de 1 posición, debemos usar el registro CL para indicar el número de desplazamientos deseados. Veamos algunos ejemplos para aclararlo. Ejemplo: Queremos desplazar a la izquierda una posición los bits del registro AL. La instrucción necesaria sería: SHL AL,1. Veamos el efecto de la instrucción: Supongamos que en un principio, AL = B7h. Tenemos pues, antes de realizar la operación el registro AL de 8 bits, con el siguiente valor en cada uno de estos 8 bits: 10110111. Tras realizar el desplazamiento, el registro quedaría como: 01101110. Hemos desplzado todos los bits una posición a la izquierda. El bit de mayor peso (bit 7), el de más a la izquierda, se pierde. Y el bit de más a la derecha (bit 0) ó de menor peso, toma el valor 0. El registro AL (tras la instrucción) tiene un valor de 6EH. Si volvemos a ejecutar la instrucción (SHL AL,1) con el nuevo valor de AL, tras la ejecución, tendremos los bits del registro de la siguiente manera: AL = 11011100. Si pasamos este número binario a hexadecimal, tenemos que AL = 0DCH. Si seguimos realizando desplazamientos a la izquierda, terminaremos por quedarnos con el registro con todos los bits a Cero, debido a que el valor que entra por la derecha en cada desplazamiento es un cero (0). Otro Ejemplo: Queremos desplazar a la izquierda los bits del registro AL 3 posiciones. Para llevar a cabo el desplazamiento, primero tenemos que introducir en CL el número de 'movimientos' a la izquierda que se van a realizar sobre cada bit. Y luego, ejecutar la instrucción de desplazamiento en sí. MOV CL,3 ---> Indicamos 3 'desplazamientos'. SHL AL,CL ---> Realiza el desplazamiento hacia la izquierda (3 veces). Supongamos que antes de ejecutar la instrucción, AL = 83h. En binario: AL = 10000011. Tras la instrucción, los bits quedarían así: AL = 00011000. En hexadecimal: AL = 18H. Un último ejemplo: Veamos ahora el caso especial en el que se utiliza la instrucción SHL para realizar multiplicaciones por 2. Supongamos que queremos multipicar el contenido del registro AL por 2. Pues bien, sólo podremos multiplicarlo mediante (SHL AL,1) cuando estemos seguros que el bit de mayor peso (de más a la izquierda) valga cero. Es decir, el bit 7 ó de mayor peso no puede ser 1, ya que se perdería al realizar el desplazamiento, con lo cual la multiplicación sería errónea. Siempre que tengamos la certeza que el bit de mayor peso vale cero podremos utilizar (SHL reg/mem,1) para duplicar (multiplicar por 2). Evidentemente, si hacemos 2 desplazamientos, estamos multiplicando por 4, y así sucesivamente: 3 desplazamientos = multiplicar por 8, etc. Veamos el ejemplo: Queremos multiplicar por 8 el registor AL. Previamente en AL hemos depositado un número del 1 al 10. Por lo tanto, sabemos con certeza que el bit 7 vale 0, con lo cual podemos ahorrar tiempo utilizando la multiplicación mediante desplazamientos. La cosa quedaría como: MOV CL,3 SHL AL,CL - SHR (desplazamiento a la derecha). Mediante esta instrucción podemos desplazar a la derecha los bits de un registro o posición de memoria. Es la instrucción opuesta y complementaria a SHL. En este caso, la instrucción puede utilizarse para realizar divisiones por 2. Sintaxis: SHR registro,1 SHR registro,CL SHR memoria,1 SHR registro,CL Los desplazamientos pueden ser de una sóla posición o de varias. Cuando queremos realizar un sólo desplazamiento usamos los formatos: SHR registro,1 SHR memoria,1 Pero cuando queremos realizar desplazamientos de más de 1 posición, debemos usar el registro CL para indicar el número de desplazamientos deseados. Veamos algunos ejemplos. Ejemplo: Queremos desplazar a la derecha una posición los bits del registro DX. La instrucción necesaria sería: SHR DX,1. Veamos el efecto de la instrucción: Supongamos que en un principio, DX = 4251h. Tenemos pues, antes de realizar la operación el registro DX de 16 bits, con el siguiente valor en cada uno de estos 16 bits: 0100001001010001 Tras realizar el desplazamiento, el registro quedaría como: DX = 0010000100101000. Hemos desplzado todos los bits una posición a la derecha. El bit de menor peso (bit 0), el de más a la derecha, se pierde. Y el bit de más a la izquierda (bit 15) ó de mayor peso, toma el valor 0. El registro DX (tras la instrucción) tiene un valor de 2128H, vemos que es prácticamente la mitad de su anterior valor. Si volvemos a ejecutar la instrucción (SHR DX,1) con el nuevo valor de DX, tras la ejecución, tendremos los bits del registro de la siguiente manera: DX = 0001000010010100. Si pasamos este número binario a hexadecimal, tenemos que DX = 1094H, que vuelve a ser la mitad del valor anterior. Si seguimos realizando desplazamientos a la derecha, terminaremos por quedarnos con el registro con todos los bits a Cero, debido a que el valor que entra por la izquierda en cada desplazamiento es un cero (0). En ambas instrucciones SHL y SHR, el valor que entra nuevo en los desplazamientos es un cero. Seguidamente veremos instrucciones similares a éstas que permiten que entre un número distinto de cero al realizar los desplazamientos. Son las instrucciones SAL y SAR. Cuando realizamos divisiones mediante SHR, como el bit que se pierde es el de menor peso (el de más a la derecha), no tenemos el problema que se nos planteaba con la multiplicación mediante SHL. Es decir, aquí como mucho el resultado final pierde el valor media unidad (0.5). - SAL y SAR. Estas instrucciones se diferencian de las anteriores (SHL y SHR) en que el nuevo valor binario que entra al realizar el desplazamiento es igual al bit de mayor peso. De cualquier modo, no pongais demasiada atención en estas instrucciones. Rara vez (por no decir nunca) las tendreis que utilizar. * Instrucciones de rotación de bits * Son instrucciones análogas a las anteriores (de desplazamiento). La diferencia es que aquí no se producen desplazamientos, sino rotaciones en los bits. Es decir, no se pierde ningún bit, sino que entra por el lado opuesto a por donde sale. Estas instrucciones al igual que las anteriores, actúan sobre datos de tipo byte (8 bits) y de tipo palabra (16 bits). - ROL (Rotación a la izquierda). Rota a la izquierda los bits de un registro o posición de memoria. El bit más significativo no se pierde, sino que al rotar, entra por el otro extremo del operando, pasando a ser ahora el bit menos significativo. Sintaxis: ROL registro,1 ROL registro,CL ROL memoria,1 ROL registro,CL Veamos un ejemplo: Tenemos el registro AL con el valor 78h, que en binario es: 01111000. Si ejecutamos la instrucción (ROL AL,1), tendremos acto seguido que AL tiene el valor binario 11110000. Si volvemos a ejecutar esa instrucción con el nuevo valor de AL, tendremos 11100001. Si lo volvemos a hacer repetidas veces, tendremos: 11000011 10000111 00001111 00011110 00111100 01111000 ---> vuelta al valor original. - ROR (Rotación a la derecha). Rota a la derecha los bits de un registro o posición de memoria. El bit menos significativo no se pierde, sino que al rotar, entra por el otro extremo del operando, pasando a ser ahora el bit más significativo. Sintaxis: ROR registro,1 ROR registro,CL ROR memoria,1 ROR registro,CL Ejemplo: Tenemos el registro AL con el valor 78h (igual que en el ejemplo anterior). En binario, AL = 01111000. Si ejecutamos la instrucción (ROR AL,1), tendremos acto seguido que AL tiene el valor binario 00111100. Si volvemos a ejecutar esa instrucción con el nuevo valor de AL, tendremos 00011110. Si lo volvemos a hacer repetidas veces, tendremos: 00001111 10000111 11000011 11100001 11110000 01111000 ---> vuelta al valor original. - RCL y RCR (Rotar a izquierda y derecha con carry ó acarreo). Estas instrucciones son variantes de las anteriores. La diferencia estriba en que la acción de rotar se va a hacer en dos pasos: 1.- El bit que se encuentra en el flag Cf es el utilizado para introducir en el extremo del operando. 2.- El bit que sale por el otro extremo (bit rotado) pasa a la bandera Cf. Ejemplo: Tenemos el registro AL con el valor 78h (igual que en el ejemplo anterior). En binario, AL = 01111000. Tenemos también el falg Cf (flag de Carry ó acarreo) con valor 1. AL = 01111000. Cf=1. Si ejecutamos la instrucción (RCR AL,1), tendremos acto seguido que AL tiene el valor binario 10111100. El valor que ha entrado por la izquierda es el que tenía la bandera Cf. Pero a la vez, la bandera Cf después de ejecutar la instrucción, tendrá valor cero (0), por el bit rotado (el que ha salido por la derecha). No os preocupeis si os parece muy lioso. Este tipo de instrucciones casi nunca se utilizan. Yo nunca las he utilizado en ningún programa. De cualquier manera, cuando hagamos unos cuantos programas, ya tendreis soltura suficiente como para probarlas. --- No hemos visto todos los grupos de instrucciones: Nos queda por ver, principalmente, las operaciones lógicas (AND, OR, etc.) y las operaciones con hileras ó cadenas de caracteres. Esto lo veremos en una próxima lección. Por hoy ya hay demasiadas cosas nuevas. Esto es todo por hoy. El próximo día más. Practicad un poco con las instrucciones que hemos visto hoy. Probad a hacer algún programilla con ellas, aunque no lo ensambleis luego. Lo importante es saber para qué sirven las instrucciones que hemos visto.