======================================================= Introducción a la programación en lenguaje ensamblador para procesadores Intel serie x86 y compatibles (IV) ======================================================= Por nmt numit_or@cantv.net ======================================= MACROS, ESTRUCTURAS DE CÓDIGO Y CADENAS ======================================= -------------------------------------------------------- CONTENIDO - Análisis el programa anterior: Introducción a los arrays - Macros y subrutinas: · Inicialización de un programa · Directiva PROC · El PSP NOTA SOBRE LOS SEGMENTOS NOTA SOBRE LAS DIRECTIVAS SIMPLIFICADAS PARA SEGMENTO · Despliegue de cadenas de caracteres · Macros: primera aproximación · Subprogramas · Directiva PROC y la instrucción call - Estructuras de Control en Lenguaje Ensamblador · Comparaciones: segunda aproximación - Instrucciones de salto o ramificación: 2da, aproximación · Instrucciones para bucles - Estructuras de control estándar en ensamblador - Tratando con cadenas de caracteres - Línea de órdenes · Análisis -------------------------------------- Análisis el programa anterior: arrays -------------------------------------- Antes de proceder a la simplificación de nuestro programa, hagamos su análisis: ; -------------------------------------------------------- TITLE TEST1.ASM: Programa para probar direccionamientos ; -------------------------------------------------------- .model small ; -------------------------------------------------------- space EQU 32 ; -------------------------------------------------------- .stack 32 - Inicialización - En este pasaje indicamos el título, en la primera línea, usando la directiva TITLE, que comenta el resto de la línea, como ";". Luego indicamos el modelo de memoria, SMALL en este caso, que como ya dijimos indica que asignamos un segmento para código, uno para datos y uno para la pila. La directiva ".model" se emplea para indicar el modelo de memoria y que debemos usar directivas simplificadas para indicar los segmentos. Aclararemos esto en su oportunidad. El detalle nuevo es la instrucción "space EQU 32"; lo que hace esta línea es asignar al término "space" el valor 32. Esto significa que el término "space" puede usarse como equivalente al número 32 ¿Por qué no usar mejor 32? porque "space" ilustra mejor el uso para el cuál está destinado este valor en nuestro programa: 32 es el equivalente ASCII del caracter espacio. Finalmente, declaramos el tamaño de la pila con la directiva .stack y su tamaño: 32 bytes. - Cadenas terminadas en cero - A continuación se declaran los datos del programa: ; -------------------------------------------------------- .data string1 db 'Hola gente', 0 string2 db 'Programa_TEST1.ASM', 0 ; -------------------------------------------------------- Como puede observarse, se trata de dos cadenas de caracteres, declaradas como ya lo hemos especificado: usando la instrucción "db"; lo interesante ahora es que terminamos las cadenas con el número "0", y ya no con el signo "$". - Directiva PROC - Sigue ahora el código, un poco más complejo, y encerrado entre las directivas "main proc" y "main endp". Estas directivas señalan los límites o el ámbito de un procedimiento, en esta ocasión llamado "main". .code main proc _init: mov ax, @data mov ds, ax ; ... main endp Las instruciones de una rutina de un programa es "empacada" en un bloque que llamamos procedimiento. Para indicar el comienzo de un procedimiento, usamos la directiva PROC y usamos ENDP para indicar el final. El formato es: nombre_del_procedmiento PROC distancia La distancia puede ser FAR o NEAR. Se usa FAR en el procedimieto principal de un programa .EXE, el punto de entrada principal de la aplicación. La distancia NEAR se reserva para los procedimientos que se hallan en un mismo segmento. Cuando se omite el indicador de distancia, por defecto, el ensamblador interpreta el procedimiento como NEAR. - El PSP - Es importante explicar estas líneas. Cuando el programa se inicia, el propio sistema operativo le antepone un área de 256 bytes con datos relevantes sobre el programa. Esta área se conoce como PSP (Prefijo de Segmento del Programa), se ubica en el desplazamiento cero desde la base del programa en memoria. Así que el programa propiamente dicho se ubicará en el desplazamiento 256 o 100h. El PSP incluye infomación valiosa, como un buffer donde DOS pone el texto que introdujo el usuario para ejecutar el programa y desde donde se pueden extraer los parámetros pasados al programa. Cuando el programa es cargado, los registros DS y ES poseen la dirección del PSP, es decir, la dirección de la base del programa en la memoria. Todavía el registro DS no tiene la dirección del segmento de datos. Las líneas anteriores se encargan de eso: como no se puede mover directamente datos en memoria a los registros de segmento, entonces se pasa primero la dirección del segmento de datos a AX y luego desde AX a DS. Nótese que la dirección del registro de datos está contenida en la variable especial "@data", válida solamente cuando se usan las directivas simplificadas de segmento (.model, .stack, .data, .code). *NOTA SOBRE LOS SEGMENTOS* Para declarar un segmento se emplea la directiva SEGMENT. Recordemos que los i386+ permiten segmentar la memoria: dividir el espacio disponible en bloques de diferentes tamaños, llamados segmentos. El formato .EXE de los ejecutables de DOS se adapta a esta posibilidad: la información que contiene se divide en segmentos cuyo contenido puede variar. Hemos visto que los segmentos pueden ser reservados para datos (el segmento de datos), código (el segmento de código), o para ser empleado como pila (el segmento de pila). También habíamos mencionado que la segmentación del .EXE aporta una primera clasificación de los objetos de un programa. Para definir un segmento en un programa, se emplean dos directivas solidarias, una para indicar comienzo del segmento, SEGMENT, y otra para indicar el final, ENDS. El formato sería el siguiente: NOMBRE OPERACIÓN OPERANDO COMENTARIO nombre SEGMENT [opciones] ; inicio del segmento · · · nombre ENDS En DOS, el tamaño máximo de un segmento es 64KB, excepto en modelo de memoria HUGE, donde puede ser mayor. En el modelo FLAT, usado en sistemas de 32 bits, el ejecutable está constituido por un único segmento que puede ser, en teoría, de hasta 4 GB. Son tres los tipos de opciones posibles para SEGMENT: sintaxis: nombre SEGMENT alineación combinar 'clase' alineación: indica el límite de inicio del segmento. El segmento iniciará en una dirección inmediata a la indicación, que puede variar: BYTE: Inicio en la siguiente dirección. WORD: Siguiente dirección par. Divisible entre 2. DWORD: Siguiente dirección de palabra doble. Divisible entre 4. PARA: Siguiente párrafo. Un párrafo equivale a 16 bytes. PAGE: Siguiente dirección de página. Divisible entre 256. combinar: esta opción se incluye si se quiere combinar un segmento con otros en tiempo de enlazado, después de haber sido ensamblados. Los tipos combinar son: NONE: Segmento separado lógicamente de los otros. El segmento tendrá su propia dirección base. PUBLIC: el segmento será cargado junto a otros del mismo nombre y clase adyacente. Todos estos segmentos tendrán una sola dirección base común para todos. STACK: LINK de Microsoft trata STACK como PUBLIC. Indica que el segmento se reserva para la pila. COMMON: El segmento será agrupado con los que tienen el mismo nombre y la misma clase. Todos tendrán una misma dirección de base. En la ejecución del programa, los segmentos se traslaparán y el más grande determinará el tamaño del área común. AT dirección: Permite definir etiquetas y variables en desplazamientos fijos en áreas de memoria como la tabla de interrupciones o el área de datos del BIOS. Lo que hace el ensamblador, es crear un segmento mudo o ficticio que sólo proporciona una imagen de las localidades de memoria. clase: la clase de un segmento se pone entre apóstrofos e indica al enlazador cómo deben agruparse los segmentos en tiempo de enlace. Los nombres recomendados son 'code', 'data' y 'stack' para código, datos y pila, respectivamente. *NOTA SOBRE LAS DIRECTIVAS SIMPLIFICADAS PARA SEGMENTO* TASM y MASM proporcionan la directiva .MODEL para simplificar la declaración de segmentos. Su formato es: .MODEL modelo_de_memoria Al usar .MODEL ya no tiene que emplearse la directiva SEGMENT, ya que .MODEL hará que el ensamblador acomode los segmentos de acuerdo a los modelos preestablecidos: TINY reservado para generar ejecutables .COM: 1 segmento para datos y código, todo en un único segmento. SMALL 1 segmento de datos, 1 seg. de código. FLAT 1 segmento para datos y código. El segmento puede ser, en teoría, de hasta 4 GB. MEDIUM Varios segmentos de datos, 1 seg. de código. COMPACT 1 seg. de datos, varios de código. LARGE Varios segs. de datos, varios de código. HUGE Varios segmentos para datos y código. Los segmentos pueden ser superiores a los 64 KB. Después de indicarla, sólo hay que señalar los segmentos con sus respectivas directivas simplificadas: .MODEL modelo_de_memoria .STACK tamaño .DATA [elementos] .CODE [instrucciones] END Las directivas simplificadas para segmentos son: .STACK define la pila. .CONST definición de un segmento de datos de clase 'const'. .DATA un segmento para inicializar datos cercanos. .DATA? define un segmento para datos cercanos no inicializados. .FARDATA define un segmento para datos lejanos inicializados. .FARDATA? define un segmento para datos lejanos no inicializados. .CODE define un segmento de código - Despliegue de cadenas de caracteres - El siguiente fragmento despliega la cadena "string1" mov si, 0 _print_string1: lea bx, string1 mov al, [bx][si] test al, al jz _print_string21 mov bx, 000Fh mov ah, 0Eh int 10h inc si jmp _print_string1 Aquí ya no usamos el servicio 9 de la interrupción 21h de DOS para desplegar cadenas de caracteres. Usamos el servicio 0Eh de la interrupción 10h del BIOS que despliega un caracter, que ponemos en AL, en el monitor. En la rutina utilizamos SI como un índice al array "string1" y lo inicializamos con cero, para que apunte al primer caracter. La instrucción "lea bx, string1" pone en BX la dirección de "string1". En la instrucción "mov al, [bx][si]", donde empleamos direccionamiento indirecto de base-índice (base del array en BX, índice en SI), movemos un caracter del array "string1" a AL. La instrucción "test al, al" prueba para verificar si AL tiene cero. La operación TEST somete a los operandos a una operación lógica de conjunción AND, que resulta en 0 si sus operandos son cero, lo que activaría la bandera ZF. La siguiente instrucción, "jz _print_string21", revisa la bandera ZF y si está activada se pasa el control a la dirección indicada por la etiqueta "_print_string21". Por lo tanto, se trata de un bucle que terminará de ejecutarse cuando en el array se encuentre un cero. Luego, las siguentes tres líneas, despliegan el contenido en AL aprovechando el servicio 0Eh (mov ah, 0Eh) de la interrupción 10h. Nótese que el servicio de interrupción siempre se indica en la parte alta del registro AX, es decir, en AH. La instrucción "mov bx, 000Fh", antes de la interrupción 10h, debería indicar el color del caracter a desplegar y el color de su fondo, de acuerdo a un código preestablecido que en su oportunidad explicaremos. Después de ejecutada la interrupción 10h y desplegado el caracter, se incrementa SI (inc si), para apuntar al siguiente caracter en el array "string1", y se obtiene este nuevo caracter en AL para desplegarlo, proceso que se repetirá hasta que, como señalamos, se ubique un cero que señala el final de la cadena. Luego se despliegan cuatro espacios: _print_string21: mov cl, 4 _next1: push cx mov al, space mov bx, 000Fh mov ah, 0Eh int 10h pop cx dec cx jne _next1 El número de espacios se indican con "mov cl, 4". Luego guardamos el contenido original de CX en la pila (push cx), ya que al ejecutarse la interrupción puede cambiarse el valor en CX. Se despliega el espacio, como cualquier otro caracter, se recupera el valor original de CX guardado en la pila (pop cx), decrementamos su valor y si se ha llegado a cero, se activará la bandera ZF; mientras esta bandera no se active la rutina volverá a ejecutarse, desplegando de nuevo un espacio. La instrucción "jne _next1", pasa el control a "_next1" siempre que la bandera ZF no esté activada. El siguiente fragmento es medio bizarro. Se incluye simplemente para mostrar un direccionamiento indirecto usando base, índice y escalar. _print_space: mov si, 0 _next2: mov bx, offset string2 mov al, [bx+si+2] mov bx, 000Fh mov ah, 0Eh int 10h mov ah, 10h int 16h inc si cmp si, 2 jnz _next2 De nuevo usamos SI como índice, inicializándolo en cero, y cargamos en BX la dirección de "string2". Nótese que no se usa la instrucción LEA sino MOV, con el operador "OFFSET". Luego movemos a AL el segundo caracter en el array "string2" (mov al, [bx+si+2], bx tiene la base del array, si un índice y 2 es un escalar), que será desplegado usando el servicio 0Eh de la interrupción 10h. Luego usamos el servicio 10h de la int 16h para detener momentáneamente el programa en espera a que el usuario pulse una tecla. Cuando se hace, se incrementa SI y se revisa luego si contiene 2 (cmp si, 2). Si SI contiene 2, se activará la bandera ZF, en caso contrario se ejecutará un salto que hará que se ejecute de nuevo el bucle (jnz _next2), pasando el control de nuevo a _next2. Cuando el bucle vuelve a ejecutarse, se desplegará el tercer caracter; cuando se incremente SI contendrá 2 y ya no se ejecutará un salto a _next2. Las siguientes líneas despliegan un espacio: mov al, space mov bx, 000Fh mov ah, 0Eh int 10h Luego desplegamos toda la cadena "string2". Esta vez lo hacemos de manera que cada caracter se vaya desplegando uno a uno, cada vez que el usuario pulse una tecla, operación que continuará hasta alcanzar el final de la cadena, indicado por un cero: _print_string22: lea si, string2 _next3: mov al, [si] test al, al jz _end_ mov bx, 000Fh mov ah, 0Eh int 10h mov ah, 10h int 16h inc si jmp _next3 En este caso, para mover a AL el caracter indicado, se usa direccionamiento indirecto por índice: se carga en SI la dirección de "string2" (lea si, string2), así que SI contiene en este momento la base del array "string2"; luego movemos directamente el caracter a AL (mov al, [si]). Se revisa si ese caracter es cero (test al, al) y si lo es salimos del bucle (jz _end_). Si no se ha alcanzado aún el final de la cadena, se despliega el caracter usando de nuevo el servicio 0Eh de la interrupción 10h, se detendrá la ejecución hasta que el usuario pulse una tecla y se incrementará SI para que apunte al siguiente caracter. - Salir del programa - Cuando se alcanza el final de la cadena, en AL debe haber un cero y la instrucción "test al, al" activará la bandera ZF, haciendo que la siguiente instrucción, "jz _end_", pase el control a la rutina final: _end_: mov ah, 10h int 16h mov ax, 4C00h int 21h main endp end main Ahora se detiene la ejecución hasta que el usuario pulse una tecla y, cuando lo haga, se ejecutará la rutina de salida a DOS, usando el servicio 4Ch de la interrupción 21h. La instrucción "mov ax, 4C00h", pone 4Ch en AH y 00 en AL. Como puede observarse, se trata de un primer programa que manipula arrays y cadenas de caracteres. El 8086 incluye en su repertorio instucciones que facilitan este trabajo que aún no usamos pero que ya veremos. Para señalar el final del programa se indica con la directiva END que indica el final del programa. Su formato es: END [dirección del inicio del código] Si se escribe un programa en un módulo secundario, donde no se halla el punto de entrada, entonces debe terminarse el programa escrito en este fichero con END, sin indicar nombre de inicio. ==================== Macros y subrutinas ==================== ----------------------------- Macros: primera aproximación ----------------------------- Después de ejecutada la interrupción 10h y desplegado el Cualquier programador entrenado en lenguaje ensamblador, notará de inmediato que el programa anterior puede mejorarse. En primer lugar, hay muchos pasajes que hacen lo mismo y que bien podrían ser reunidos en un sólo bloque de código. Por ejemplo, siempre se usa el mismo código para desplegar un caracter: mov bx, 000Fh mov ah, 0Eh int 10h El servicio 0Eh de la interrupción 10h imprime sobre el monitor el caracter que está en AL. Una manera de aligerar el trabajo puede ser escribir una macro para esta rutina: Display_char macro mov bx, 000Fh mov ah, 0Eh int 10h endm Luego sólo escribimos algo como: mov al, "A" Display_char Las directivas "macro" - "endm" nos permiten encapsular varias líneas de código que serán desplegadas por el ensamblador en tiempo de ensamblado cada vez que encontramos el nombre de la macro. La directiva macro tiene el siguiente formato: nombre macro ... ; contenido de la macro ... endm Con las macros se abren posibilidades muy interesantes, pero tienen un problema, si su contenido es muy amplio y se usan mucho, el código se agrandará más de lo deseado. Asi que en algunos casos es mejor otra opción. Lo ideal sería capturar mentalmente lo común que encontramos en diversas rutinas y buscar cómo sintetizarlas en una sola rutina, en un concepto, en una función. Una vez apresado un fragmento que puede ser encapsulado, que ya podríamos hacerlo mediante macros, lo mejor sería colocarlo en una pare aparte del programa y pasarle el control cada vez que sea necesario. Esto puede hacerse usando subrutinas. Para ello, escribimos las rutinas aparte usando la directiva "proc - endp". Luego le pasamos el control cuando sea necesario usando la intrucción "call". ------------- Subprogramas ------------- Un subprograma es una unidad independiente de código que puede ser usada desde diferentes partes de un programa. En otras palabras, un subprograma es como una función en lenguaje C. Para invocar subprogramas se podría usar algún salto, pero el regreso a la rutina que hizo la invocación representa un problema. Así que la dirección de regreso debe ser indicada. El 8086 tiene dos instrucciones para facilitar la llamada a subprogramas: la instrucción CALL, que realiza un salto incondicional a un subprograma y mete en la pila la direccón de la siguiente instrucción; otra instrucción, RET saca de la pila la dirección y salta a esa dirección. Cuando se usan estas instrucciones, es importante que uno maneje bien la pila para que el valor correspondiente a la dirección de regreso, sea sacado de ella por la instrucción RET. El uso de CALL y RET tene varias ventajas: · Es más simple · Permite que las llamadas a subprogramas sea anidada fácilmente. Directiva Proc y la instrucción call ------------------------------------ La directiva PROC se emplea para encapsular rutinas. Indica un "procedimiento" que debe realizar el sistema: un programa en ensamblador, como los escritos en C, generalmente consta de una rutina o procedimiento principal ("main ()" en C) y una serie de subrutinas o procedimientos secundarios. Tenemos entonces que las instruciones que conforman una rutina de un programa es "empacada" en un bloque que llamamos procedimiento. Para indicar el comienzo de un procedimiento, usamos la directiva PROC y usamos ENDP para indicar el final. El formato es: nombre_del_procedmiento PROC distancia La distancia puede ser FAR o NEAR. Se usa FAR para indicar el procedimieto principal de un programa .EXE, que es el punto de entrada principal de la aplicación. La distancia NEAR se reserva para los procedimientos que se hallan en un mismo segmento. Para pasar el control de un procedimiento a otro, se emplea la instrucción unaria "CALL" (Llamar), la cual tiene el siguiente formato: CALL nombre_del_procedimiento La instrucción CALL guarda en la pila la dirección de la instrucción que le sigue y pasa el control a la driección indicada por el nombre_del_procedimiento. El valor en la pila será usado por el procedimiento llamado para devolver el control al procedimiento que hizo la llamada. La instrucción CALL equivale a: PUSH offset return_here ; Meter en la pila ; la dirección de la ; instrucción que sigue ; a la llamada. JMP nombre_del_procedimiento ; Pasar el control ; a otro procedimiento return_here: ; y regresar aquí Para devolver el control al procedimiento original, un procedimiento llamado debe usar la instrucción RET (return: regresar). Esta instrucción devuelve el control a la dirección indicada por el puntero de pila (SP). Ahí debería estar el valor puesto por la intrucción CALL que pasó el control a la subrutina actual. Usando estas instrucciones podemos reescribir nuestro programa: ; -------------------------------------------------------- TITLE TEST1.ASM: Programa para probar direccionamientos ; -------------------------------------------------------- .model small ; -------------------------------------------------------- space EQU 32 ; -------------------------------------------------------- .stack 32 ; -------------------------------------------------------- .data string1 db 'Hola gente', 0 string2 db 'Programa_TEST1.ASM', 0 ; -------------------------------------------------------- .code main proc _init: mov ax, @data mov ds, ax mov si, 0 _print_string1: lea bx, string1 mov al, [bx][si] test al, al jz _print_string21 call _display_char inc si jmp _print_string1 _print_string21: mov cl, 4 _next1: push cx mov al, space call _display_char pop cx dec cx jne _next1 _print_space: mov si, 0 _next2: mov bx, offset string2 mov al, [bx+si+2] call _display_char call _pause inc si cmp si, 2 jnz _next2 mov al, space call _display _print_string22: lea si, string2 _next3: mov al, [si] test al, al jz _end_ call _display_char call _pause inc si jmp _next3 _end_: call _pause mov ax, 4C00h int 21h main endp end main __display_char proc NEAR mov bx, 000Fh mov ah, 0Eh int 10h ret __display_char endp _pause proc NEAR mov ah, 10h int 16h ret _pause endp ; :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::: Este programa tendrá un tamaño menor que el anterior, aunque no tanto: nuestras subrutinas son tan pequeñas que igual vale usar macros, pero igual vale para ilustrar el uso de subrutinas. Veamos otra posibilidad. El pasaje: mov si, 0 lea bx, string1 _print_string1: mov al, [bx][si] test al, al jz _print_string21 call _display_char inc si jmp _print_string1 _print_string21: es una rutina que despiega una cadena de caracteres que termina en cero. Se carga en BX la dirección efectiva de string1, se pone en AL el caracter apuntado por BX+SI. La instrucción "test al, al" revisa si ese caracter es cero: si este fuera el caso, se activa la bandera ZF, lo que indicaría que se llegó al final de la cadena. La instrucción "jz _print_string21" pasa el control a "_print_string21" si encuentra activada la bandera ZF, en caso contrario se ejecuta "call _display_char", que pasa el control a la función "_display_char" que despliega el caracter en pantalla; al regreso de la llamada, se incrementa en uno el valor de SI ("inc si") y se repite de nuevo la rutina para el siguiente caracter. Tenemos así una rutina que puede ser muy útil, que no usa interrupciones del sistema operativo. La llamaremos _szDisplay y despliega cadenas de caracteres que terminan en cero: _szDisplay proc near mov si, 0 _print_string: mov al, [bx][si] test al, al jz _exit_szDisplay call _display_char inc si jmp _print_string _exit_szDisplay: ret _szDisplay endp El procedimiento espera que BX posea la dirección de la cadena a desplegar. Es un buen reemplazo para el servicio 9 de la int 21h, especialmente cuando no tenemos DOS a nuestra disposición. *NOTA* El empleo de "PROC" para designar bloques de rutinas es significativo del estilo de programación favorecido por el lenguaje ensamblador: programación procedimental. El paradigma de este estilo es "decidir qué procedimientos quieres y usar los mejores argumentos que consigas". El programador debe concentrarse en la implementación del algoritmo necesario para realizar el cómputo deseado. Para soportar este paradigma, los lenguajes deben suministrar facilidades para el paso de parámetros a subrutinas o funciones y para regresar valores de éstas. La discusión reposa en las maneras de pasar argumentos, las diferentes maneras de distinguirlos, los diferentes tipos de funciones. =============================================== Estructuras de Control en Lenguaje Ensamblador =============================================== ------------------------------------ Comparaciones: segunda aproximación ------------------------------------ Las estructuras de control deciden qué hacer basándose en una comparación de datos. Vimos que en ensamblador el resultado de la comparación se almacena en el registro de banderas para ser usado luego. El registro de banderas hace las veces de un registro de estado cuyos bits informan sobre el estado del procesador. En este caso, estos bits indican el estado del procesador después de realizada alguna operación. Ya mostramos que para hacer comparaciones, el 8086 proporciona la instrucción CMP. Esta operación realiza una substracción y, de acuerdo al resultado, se activa alguna de los indicadores o banderas del registro FLAGS sin que el resultado sea almacenado en alguno de los operandos. Otra instrucción hace casi lo mismo que CMP, pero guardando el resultado en el operando de destino: SUB. En ambos casos, si los operadores son iguales el resultado será cero, por lo que se activará la bandera de cero ZF del registro FLAGS. Otras operaciones revisarán esta bandera para tomar alguna decisión. Para operaciones con enteros sin signo, hay dos banderas: Cero (ZF) y carro (CF). ZF se activa (se pone en 1) si el resultado de la diferencia es cero; CF se usa para indicar que en una substracción que se ha intentado substraer a una cantidad otra mayor; si este es el caso, entonces se activa la bandera CF. Un ejemplo: cmp v1, v2 Al computarse la diferencia, se activa o no la bandera en correspondencia al resultado. Si el resultado es cero, porque v1 = v2, ZF se activa y CF se desactiva. Si v1 < v2, entonces se activará CF y ZF se desactivará. Pero si v1 > v2, ambas banderas se desactivarán. Para enteros con signo hay tres banderas significativas, de nuevo ZF, la de desbordamiento (overflow) OF y la de signo SF. Para indicar que un entero es negativo, el bit más alto, en la extrema izquierda, considerado el bit de signo, está activo. En su momento hablaremos sobre operaciones aritrméticas en ensamblador. La bandera de desbordamiento OF se activa cuando el resultado de una operación de comparación produce un desbordamiento, condición que se da cuando la cantidad supera los límites de espacio de un operando; por ejemplo, cuando se intenta poner en un registro de 8 bits (como AL), cuyo máximo es 0FFh = 256 (0FFh = 11111111), una cantidad como 257 o superior, se activará la bandera de desbordamiento. La bandera de signo SF se activa cuando el resultado de la operación es negativo. Si v1 = v2, se activa ZF, como en el caso de enteros sin signo. Si v1 > v2, ZF se desactiva y SF = OF. Si v1 < v2, ZF se desactiva y SF difiere de OF. ¿Por qué SF = OF si v1 > v2? si no hay desbordamiento, la diferencia tendrá el valor correcto y debe ser positivo. Así que SF = OF = 0. Pero si hay un desbordamiento, el resultado no será correcto, y será negativo. Así que SF = OF = 1. --------------------------------------------------------- Instrucciones de salto o ramificación: 2da, aproximación --------------------------------------------------------- Estas instrucciones transfieren el control a puntos arbitrarios del programa. Es decir, actúan como "goto". Hay dos tipos: incondicionales y condicionales. Las incondicionales siempre efectúan el salto; las condicionales lo efectúan dependiendo del estado de las banderas. Si no se efectúa el salto, el control es pasado a la siguiente instrucción. La instrucción JMP se usa para saltos incondicionales. Es unaria, de un único operando que generalmente es la etiqueta de una instrucción a donde se pasará el control. El ensamblador o el enlazador reemplazará la etiqueta por la dirección correcta de la dirección. La instrucción siguiente, no será ejecutada a no ser que alguna otra instrucción le pase el control. Hay variaciones para la instrucción JMP: Salto corto (SHORT) Este salto es muy limitado en rango. Sólo puede moverse hasta o por debajo de 128 bytes en memoria. La ventaja de este tipo es que usa menos memoria que los otros. Usa un byte con signo para almacenar el desplazamiento del salto respecto a la dirección de la propia instrucción. (El desplazamiento es agregado a IP). Para especificar un salto corto, se usa la palabra clave SHORT inmediatamente antes de la etiqueta en la instrucción JMP. Salto cercano (NEAR) Este el tipo por defecto para saltos incondicionales y condicionales, puede ser usado para saltar a cualquier localidad en un segmento. Realmente, el 80386 soporta dos tipos de instrucciones cercanas. Una usa dos bytes para el desplazamiento. Esto permite moverse hacia arriba o hacia abajo 32,000 bytes. El otro tipo usa cuatro bytes para el desplazamiento, que permite moverse a cualquier dirección en el segmento de código. El tipo de cuatro bytes es el tipo por defecto en el modo protegido 386. Salto lejano (FAR) Este salto permite pasa el control a otro segmento de código. Es muy raro en modo protegido 386. Las etiquetas de código siguen las mismas reglas que los nombres de los datos: se definen colocándolas en el segmento de código frente de la expresión que etiquetan. Pero hay una diferencia: hay que colocar dos puntos inmediatamente después de la etiqueta. A continuación un ejemplo de salto incondicional: jmp etiqueta ; aquí puede haber cualquier código etiqueta: ; el control es pasado a esta dirección mov ax, 0 ; primera instrucción ejecutada después ; del salto Hay varias instrucciones para saltos condicionales. Ya dijimos que estas instrucciones revisan si se da cierta condición de la cual depende la realización del salto: el procesador revisa el registro de banderas en busca de las banderas activas para decidir si hay que saltar. A continuación una lista de saltos condicionales simples y una descripción de cómo se ejecutan: JZ salta sólo si ZF se activa JNZ salta sólo si ZF se desactiva JO salta sólo si OF se activa JNO salta sólo si OF se desactiva JS salta sólo si SF se activa JNS salta sólo si SF se desactiva JC salta sólo si CF se activa JNC salta sólo si CF se desactiva JP salta sólo si PF se activa JNP salta sólo si PF se desactiva (PF es la bandera de paridad, que indica si el resultado de una operación es par o impar.) Las instrucciones de control SON complejas: una instrucción revisa algún dato; el resultado de la revisión activa o no una o más banderas dependiendo del resultado de la evaluación; luego la instrucción de salto condicional efectuará o no un salto dependiendo de las banderas activadas. Como ejemplo, escribamos en ensamblador el siguiente pseudo-código: if ( AX == 0 ) BX = 1; else BX = 2; (si AX es igual a cero, poner 1 en BX, sino poner 2 en BX) Este pseudo-código podría escribirse así en ensamblador: cmp ax, 0 ; activar banderas: ZF se activa si ax - 0 = 0) jz thenblock ; si ZF se activó, pasar el control a thenblock mov bx, 2 ; Parte ELSE: sino mover 2 a BX jmp next ; salir hacia next thenblock: mov bx, 1 ; Parte THEN: entonces mover 1 a BX next: Otras comparaciones no son tan fácil de implementar con los saltos condicionales simples presentados en la pasada lista. Veamos el siguiente pseudo-código: if ( AX >= 5 ) BX = 1; else BX = 2; (Si AX es mayor o igual que cinco, poner 1 en BX, sino poner 2 en BX) Si AX es mayor o igual que cinco, ZF puede activarse o no y SF será igual que OF. A continuación el código en ensamblador, asumiendo que AX tiene signo: cmp ax, 5 js signon ; ir a signon si SF = 1 jo elseblock ; ir a elseblock si OF = 1 y SF = 0 jmp thenblock ; ir a thenblock si SF = 0 y OF = 0 signon: jo thenblock ; ir a thenblock si SF = 1 y OF = 1 elseblock: mov ebx, 2 jmp next thenblock: mov ebx, 1 next: Es un código complejo y confuso para el cual, afortunadamente, el 8086 suministra instrucciones que facilitan estos tipos de pruebas, pensadas para datos sin signo o con signo. Estas instrucciones generalmente revisan las banderas en busca del resultado de un operación de comparación com CMP, pero también responden al resultado de operaciones aritméticas, que también activan las banderas de acuerdo a su resultado. Suponiendo que ejecutamos una instrucción que compara dos valores lv y rv (lv = left_value o valor izquierdo; rv = right_value o valor derecho): cmp lv, rv los saltos condicionales se efectuarán según los siguientes criterios: Saltos sin signo: JE salta si lv es igual a rv JNE salta si lv no es igual a rv JB, JNAE salta si lv es menor que rv JLE, JNG salta si lv no es menor que rv JG, JNLE salta si lv es mayor que rv JGE, JNL salta si lv no es mayor que rv Saltos con signo: JE salta si lv es igual a rv JNE salta si lv no es igual a rv JL, JNGE salta si lv es menor que rv JBE, JNA salta si lv no es menor que rv JA, JNBE salta si lv es mayor que rv JAE, JNA salta si lv no es mayor que rv El salto igual (equal) y no igual (not equal), JE y JNE, son los mismos para enteros con signo y sin signo. En realidad son instrucciones idénticas a JZ y JNZ, respectivamente. Cada una de las otras instrucciones de salto tienen dos sinónimos. Por ejemplo, JL (jump less than: saltar si es menor que) y JNGE (jump not greater than or equal to: saltar si no es más grande que). Estas instrucciones son las mismas porque: x < y = not(x > y) Las saltos sin signo usan A para "above" (encima) y B para "below" (debajo) en vez de L y G. Gracias a estas nuevas instrucciones, nuestro pseudo-código de arriba puede escribirse en un código ensamblador más claro y simple: cmp ax, 5 jge thenblock mov bx, 2 jmp next thenblock: mov bx, 1 next: Instrucciones para bucles ------------------------- El 8086 incluye en su repertorio algunas instrucciones para bucles estilo "for", y que toman una etiqueta como operando. El formato de estas instrucciones es: instrucción etiqueta La etiqueta indica donde la instrucción devuelve el control hasta que se cumpla una condición que finaliza el bucle. La instrucción usa el registro CX como operando implícito: en él debe incluirse el número de veces que se ejecutará el bucle de acuerdo a las siguientes reglas: LOOP Decrementa CX, si CX difiere de 0, y salta donde indica la etiqueta. LOOPE, LOOPZ Decrementa CX (El registro de bandera es modificado); si CX no vale 0 y ZF = 1, efectúa un salto. LOOPNE, LOOPNZ Decrementa CX (El registro de bandera es modificado); si CX no vale 0 y ZF = 0, efectúa un salto. Las últimas dos instrucciones son útiles en bucles de búsqueda secuencial. El siguiente pseudo-código: sum = 0; for ( i=10; i>0; i) { sum+=i; --i; } puede escribirse así en lenguaje ensamblador: mov eax, 0 ; eax is sum mov ecx, 10 ; ecx is i loop_start: add eax, ecx loop loop_start A partir del 80486, estas instrucciones son poco recomendables debido a que la nueva arquitectura hace inconveniente su uso,: requieren más tiempo de ejecución que otras instrucciones más complejas que hacen lo mismo, pero en menos tiempo. ----------------------------------------------- Estructuras de control estándar en ensamblador ----------------------------------------------- Hemos estado viendo un poco cómo empleamos las estructuras de control en lenguaje ensamblador. Podemos concluir que se trata de ejecutar una operación de prueba (hemos usado CMP y TEST) que "pregunta" si se ha satisfecho una condición; el programa nos dice sobre el cumplimiento de esa condición activando o no un bit especial del registro de banderas, como ZF, al cual responde una instrucción de salto condicional. Teniendo esto encuenta, podemos diseñar algunas estructuras de control estándar usadas en lenguajes de alto nivel, como C: Constructo IF-ELSEIF-THEN ------------------------- El siguiente pseudo-código: if ( condición ) then bloque1 ; else bloque2 ; (Si se cumple una condición entonces ejecutar bloque1, sino ejecutar bloque2) podría ser implementado así: ; ; Activar banderas ; cmp v1, v2 ; (puede ser otra instrucción de prueba ; como test) ; ; Revisar banderas activadas ; jxx else_block ; selecciona xx para que haya un salto ; si se activó o no la bandera ; consultada ; ; Bloque "then" ; jmp endif else_block: ; ; Bloque "else" ; endif: Si no hay bloque "else", puede reemplazarse el salto a este bloque por un salto a "endif". ; ; Activar banderas ; cmp v1, v2 ; (puede ser otra instrucción de prueba como test) jxx endif ; selecciona xx para que haya un salto ; ; código para el bloque else ; endif: Constructo WHILE ---------------- El bucle while es una estructura con prueba en el comienzo del bloque: while( condición ) { cuerpo del bucle; } Esto podría traducirse así: while: ; ; Activar banderas de acuerdo a la comparación ; cmp v1, v2 ; (puede ser otra instrucción de prueba como test) jxx endwhile ; seleccionar xx para saltar si la condición es falsa ; ; cuerpo del bucle: debe cambiar los valores de v1 o v2. ; jmp while endwhile: Constructo DO WHILE ------------------- Un bucle do while es una estructura con prueba en el final del bloque: do { cuerpo del bucle; } while( condición ); Podría traducirse como: do: ; ; cuerpo del bucle ; ; ; código para poner las banderas de acuerdo a una condición ; jxx do ; seleccionar xx para saltar si la condición es verdadera Luego veremos cómo usar macros para simular estas estructuras de alto nivel en lenguaje ensamblador. ---------------------------------- Tratando con cadenas de caracteres ---------------------------------- El ix86+ dispone de instrucciones para facilitar el trabajo con cadena de caracteres: MOVS MOVe String: CMPS CoMPare String: comparar cadenas SCAS SCAn String: comprar cdenas LODS LOaD String: Cargar cadenas STOS STOre String: Almacenar cadenas Veamos una por una estas instrucciones. En realidad el formato de estas instrucciones es un poco más complejo, ya que necesitan un sufijo que indica el tamaño de los datos. Ya veremos cómo es esto. Todas estas instrucciones trabajan con los registros de índice SI y DI, usando SI (Source Index = Índice al origen) para una dirección donde deberían estar los datos que sirven de origen para la operación, y DI (Destiny Index = Índice al destino) para la dirección de destino de los datos. Estos registros se usan en combinación con los registros de segmento de datos DS y el extra ES. Además, algunas de estas instrucciones usan como prefijo el operador REP que indica la repetición de una instrución tantas veces como se indique en el registro CX. Em adición a REP, hay otros operadores semejantes que no trabajan con CX sino que repiten la operación hasta que se satisfaga una condición que será indicada por la bandera de cero ZF. Ya las revisaremos. Otra situación que observar es que una operación con cadenas siempre tiene una dirección: se puede realizar de izquierda a derecha (desde el comienzo al final de la cadena) o de derecha a izquierda (desde el final al comienzo). Para especifiar la dirección hay que activar o desactivar la bandera de dirección DF. En condiciones normales, esta bandera está en cero para el proceso de cadenas de izquierda a derecha. Si vamos a procesar cadenas de derecha a izquierda, debemos activar esta bandera con la operación ceroaria (sin operandos) STD (SeT Df). Para restablecer la bandera DF usaremos CLD (CLear Df), que limpia la banderam es decir, la pone en cero. - MOVS - Esta operación se usa en combinación con el prefijo REP para mover caracteres desde una dirección indicada por DS:SI hasta otra dirección indicada por ES:DI. El número de carateres que serán movidos debe indicarse en el registro contador CX. El formato es: REP MOVSn Automáticamente se moverán los caracteres desde DS:SI hasta ES:DI. La "n" que hemos usado de sufifo en MOVS, indica el tamaño del dato a mover: b para byte, w para word, d para dword, etc. Así que una instrucción como: lea si, str1 lea di, str2 mov cx, size_of_str1 here: mov al, [si] mov [di], al inc di inc si dec cx jne here que mueve los caracteres desde str1 hasta str2 puede escribirse así: lea si, str1 lea di, str2 mov cx, size_of_str1 rep movsb - LODS - Esta operación arga en AL, AX o EAX, dependiendo del sufijo (B para byte, W para word o D para dword) los caracteres que se encuentran en una dirección indicada por DS:SI. La instrucción incrementa el valor en SI, dependiendo del sufij en LODS. Así que una instrucción equivalente para LODSW, sería: lea si, str1 mov ax, [si] inc si inc si - STOS - La operación almacena (store) en la dirección indicada por ES:DI el contenido en AL, AX o en EAX, dependiendo del sufijo. Incrementa el valor de DI de acuerdo al tamaño indicado por el sufijo. Un código equivalente para STOSB podría ser: mov al, "A" lea di, str2 mov [si], al inc di La instrucción mueve a str2 la letra A. Usando LODS y STOS se puede mover con facilidad un cadena de caracteres de un array a otro, sin necesidad de saber su tamaño, dato necesario para el uso de MOVS: str1 db "hola gente", 0 str2 db 20 DUP (?) lea si, str1 ; en DS:SI la dirección de str1 lea di, str2 ; en ES:DI la dirección de str2 otro: lodsb ; poner en AL un caracter en DS:SI test al, al ; ¿es cero este caracter? je ya ; si es cero, salir del bucle stosb ; sino, poner el caracter en ES:DI jmp otro ya: - REP - Como vimos, REP es un prefijo para repetición de una operación con cadenas de caracteres: dice al procesador que repita una operación de este tipo tantas veces como se indique en el registro contador CX. Por ejemplo, MOV CX, 4 REP MOVSB repetirá 4 veces MOVSB, moviendo cuatro caracteres desde DS:SI hasta DS:DI. Hay variaciones de REP: · REPE o REPZ: repite la operación mientras la ZF esté activa; la operación se detendrá cuando se desactive ZF o CX sea cero. · REPNE o REPNZ: repite la operación mientras la ZF esté desactivada; la operación se detendrá cuando se sactive ZF o CX sea cero. - CMPS - Esta instrucción compara el contenido en DS:SI con el de ES:DI. Dependiendo de la bandera de dirección DF, se incrementará o disminuirá el valor en SI y DI con cada operación: .data string1 db "hola", 0 string2 db "hole", 0 .code ; .... mov cx, 4 lea si, string1 lea di, string2 repe cmpsb test cx, cx je are_equals mov ax, 0 jmp _exit_ are_equals: mov ax, 1 _exit_: El código compara las cadenas de cuatro caracteres string1 y string2, si son iguales CX debe ser cero, ya que cada comparación decrementa el valor de CX. Se revisa si es el caso, si no lo es se pone 0 en ax, si es el caso se pone 1 en ax. - SCAS - Similar a CMPS, SCAS busca uno o más caracteres en una localidad indicada por ES:DI, donde debe haber una cadena de caracteres. El caracter a buscar se indica en AL. Si se buscan una pareja de caracteres se usará AX; si es una secuencia de cuatro caracteres, se usará EAX. Cada ejecución de la instrucción incrementa o disminuye el valor de DI, dependiendo de la bandera de dirección DF. .data string db "hola amigo", 0 .code ; .... push edi mov cx, -1 ; cx = infinito mov al, "m" lea di, string repne scasb neg cx dec cx mov ax, cx pop edi El código anterior devuelve en AX dónde se encuentra el caracter "m" en la cadena "hola amigo". Primero indicamos en CX que la revisión sea infinita; luego buscamos en la cadena repetidas veces hasta hallar el caracter buscado; luego negamos del contenido de CX para obtener el número de comparaciones realizadas. Finalmente restamos 1 al contenido en CX para obtener el lugar del caracter de la cadena. Aquí hay que tener cuidado, porque si la cadena no contiene el caracter buscado, la búsqueda se extenderá más allá de lo necesario, realizando lecturas sobre segmentos de memoria que posiblemente estén protegidos contra lectura, produciéndose un grave error. Pero valga como un ejemplo. ----------------- Línea de órdenes ----------------- Para ejercitarnos con las cadenas de caracteres, escribamos un pequeño intérprete de órdenes. Pensemos en tres instrucciones arbitrarias: · clean: limpia el monitor · quit: sale del programa · exit: reinicializar el sistema El intérprete consiste en un bucle que sólo debe terminar por indicación del ususario. Una vez ejecutado, espera por una indicación del usuario; cuando el usuario escribe algo, el intérprete lo lee, determina si es alguna de las órdenes que maneja y si lo es, pasa el control a la rutina correspondiente a la acción requerida por el usuario. La secuencia de acciones para un programa así debería ser algo como: 1. Desplegar mensaje de inicio del programa 2. Inicio del bucle: Desplegar puntero que indica que el programa está en espera 3. Leer línea 4. Interpretar la línea: Si es "clean": Limpiar el monitor Si es "quit": Salir del programa Si es "exit": Reiniciar el sistema Si no es uno de los anteriores: Desplegar mensaje de aviso 5. Final del bucle: Volver a 2 El código en ensamblador: ; ==================================================================== TITLE CONSOLE.ASM: Pequeña cónsola para procesor órdenes ; ==================================================================== .model small ; ================================================================================ .stack 64 ; ================================================================================ .data msg1 db "Int",82h,"rprete de ",0A2h,"rdenes", 10, 13, 0 msg2 db 10, 13, "No es una orden del programa!", 10, 13, 0 msg3 db 10, 13, "Introdujo m",0A0h,"s de 12 caracteres", 10, 13 puntero db 10, 13, "> ", 0 _clean_ db "clean", 0 _quit_ db "quit", 0 _exit_ db "exit", 0 command db 12 dup (?) ; ================================================================================ .code main proc far _init: mov ax, @data mov ds, ax mov es, ax call clean_ lea si, msg1 call _szDisplay _loop: lea si, puntero call _szDisplay lea di, command call _get_command _is_clean: lea si, _clean_ call _compare test ax, ax je _is_exit call clean_ jmp _loop _is_exit: lea si, _exit_ call _compare test ax, ax je _is_quit int 19h _is_quit: lea si, _quit_ call _compare test ax, ax je _no_order _quit: mov ax, 4C00h int 21h _no_order: lea si, msg2 call _szDisplay jmp _loop main endp ; ================================================================================ _szDisplay proc near push si _print_string: lodsb test al, al jz _exit_szDisplay call __display_char jmp _print_string _exit_szDisplay: pop si ret _szDisplay endp __display_char proc NEAR mov bx, 000Fh mov ah, 0Eh int 10h ret __display_char endp ; -------------------------------------------------------------------- _get_command proc near push di mov cx, 12 _loop_: mov ah, 10h int 16h filt_extended_function_keys: test al, al je _loop_ cmp al, 0E0h je _loop_ is_enter_key: cmp al, 0Dh je _exit_get_command dec cx test cx, cx je _excess get_character: stosb call __display_char jmp _loop_ _exit_get_command: xor al, al stosb pop di ret _excess: lea si, msg3 call _szDisplay mov cx, 12 pop di push di jmp _loop_ _get_command endp ; -------------------------------------------------------------------- _compare proc near push si push di get_string_lenght: xchg di, si call _strlen xchg si, di _compare_strings: repe cmpsb test cx, cx jne _not_equals mov ax, 1 jmp _exit_compare _not_equals: mov ax, 0 _exit_compare: pop di pop si ret _compare endp _strlen proc near push di mov al, 0 mov cx, -1 ; cx = -1 = FFFFh = 65535 = infinito repne scasb neg cx dec cx dec cx pop di ret _strlen endp ; -------------------------------------------------------------------- clean_ proc near mov dx, 0 call set_cursor mov cx, 80*25 _print: mov al, 32 ; 32 = 20h = espacio call __display_char loop _print mov dx, 0 call set_cursor ret clean_ endp set_cursor: mov ah, 2 int 10h ret ; -------------------------------------------------------------------- end main ; -------------------------------------------------------------------- ; Para ensamblar: ; tasm console ; tlink console ; ==================================================================== Análisis -------- La verdad que este programa es un reto: como pienso usarlo luego como shell para un pequeño sistema operativo, se evita usar interrupciones de DOS: sólo se usa la int21 para salir del programa, ya que pensamos correrlo en DOS para probarlo. El programa responde, por ahora, a cuatro órdenes: clean: limpia la pantalla time: despliega el tiempo quit: sale a DOS exit: reinicia el sistema Si se introduce una orden ajena, se despliega el mensaje: "No es una orden del programa!" Ahora veamos cómo trabaja el programa. Después de inicilizar los registros de los segmentos de datos, ES y DS: mov ax, @data mov ds, ax mov es, ax Se limpia el monitor: call clean_ El código de esta subritina es: clean_ proc near mov dx, 0 call set_cursor mov cx, 80*25 _print: mov al, 32 call __display_char loop _print mov dx, 0 call set_cursor ret clean_ endp Las primeras tres líneas colocan el cursor en la esquina izquierda superior del monitor: mov dx, 0 call set_cursor llamando a set_cursor: set_cursor: mov ah, 2 int 10h ret que ejecuta el servicio 2 de la interrupción 10h, que pone el cursor en la columna indicada en DL y en la fila o línea indicada por DH. Luego se escriben espacios (20h) por todo el monitor. Para ello, se ponen espacios desde el comienzo todas las localidades del monitor. mov cx, 80*25 _print: mov al, 32 call __display_char loop _print En modo de texto, que es el usado, el monitor ofrece 80 espacios en cada línea y 25 líneas. Así que lo que hacemos es escribir 80*25 espacios sobre el monitor comenzando desde la esquina superior izquierda del monitor. Para ejecutar 80*25 veces la llamada (call) a __display_char, ponemos esta cantidad en CX, y ejecutamos un bucle controlado por la instrucción "loop", que va decrementando en uno a CX cada vez que se ejecuta. Cuando CX llega a cero, termina el bucle y se coloca de nuevo el cursor en la esquina superior izquierda del monitor, llamando de nyevo a set_cursor con DX=0. Luego se despliega el mensaje inicial lea si, msg1 call _szDisplay se coloca el puntero que señala que ya pueden ingresarse las órdenes _loop: lea si, puntero call _szDisplay y se espera por la orden, que será puesta en el buffer "command" lea di, command call _get_command Analicemos _get_command. Primero esperamos por que el usuario ingrese una tecla: _loop_: mov ah, 10h int 16h El servicio 10h de la interrupción 16h espera que el usuario pulse una tecla. Ingresada la tecla, su contenido está en AL. Entonces revisamos si este valor es cero: filt_extended_function_keys: test al, al je _loop_ si es 0E0h, que es el código de rastreo generado por las teclas de función extendida (inicio, fin, repág, avpág, insert, supr, flechas, etc.): cmp al, 0E0h je _loop_ En el caso que sean estos valores, se regresa al inicio del bucle. Si ese no es el caso, se revisa si el usuario tecleo ENTER: is_enter_key: cmp al, 0Dh je _exit_get_command si este es el caso, se sale de la rutina, sino se revisa cuántos caracteres han sido ingresados, para evitar que se sobrepase el tamaño del buffer. Al comienzo de la rutina habíamos indicado el tamaño del buffer en CX: mov cx, 12 Para revisar si ya se han ingresado los 12 caracteres: dec cx test cx, cx je _excess se decrementa en uno el contenido en CX, luego se comprueba si ha llegado a cero el decremento, si este es el caso se pasa el control a la rutina "_excess": _excess: lea si, msg3 call _szDisplay pop di push di mov cx, 12 jmp _loop_ que despliega la cadena en msg3, restaura el valor original en DI, que es la dirección de inicio del buffer "command"; se pone en CX el tamaño del buffer, 12, y se vuelve a ejecutar el bucle. Si no se ha presionado la tecla ENTER o no se han escrito los doce caracteres para la orden, entonces se coloca el caracter en el buffer, en este caso "command": get_character: stosb se usa "stosb" que pone en la dirección indicada por DI el caracter en AL. Luego desplegamos el caracter introducido: call __display_char y ejecutamos de nuevo el bucle, jmp _loop_ el cual se repetirá hasta que el usuario teclee ENTER, como hemos visto: Antes de salir colocamos al final de la cadena introducida un cero, que será nuestro marcador de final de cadena: _exit_get_command: mov al, 0 stosb pop di ret _get_command endp Luego revisamos si el usuario ha escrito alguna de las órdenes que debe ejecutar nuestro programa. El esquema general es el siguiente: lea si, _clean_ call _compare test ax, ax je _is_exit call clean_ jmp _loop ponemos en SI la dirección de la cadena que queremos revisar. En DI debe estar la otra que se va a comparar, en este caso es la introducida por el usuario. Luego llamamos a "compare", que compara las cadenas y si son iguales devuelve en AX un valor distinto de cero, en caso contrario devuelve cero. Si devuelve cero se pasa el control al código que revisa si se introdujo alguna otra orden. Si no se devuelve cero, entonces se ejecuta la orden correspondiente, "clean" en este caso, que ya hemos analizado, y ejecutamos de nuevo el bucle. Analicemos la función "_compare". Primero obtenemos el tamaño de la cadena en DI, que en este caso será la orden que se va a probar: _compare proc near push si push di get_string_lenght: xchg di, si call _strlen Las primeras dos instrucciones salvan el contenido de SI y DI en la pila. La rutina que cuenta los caracteres es _strlen, que lee en la dirección en DI en busca del caracter cero. Como la dirección de la orden a verificar está en SI, tenemos que pasarla a DI, pues es en DI donde debe pasarse la dirección de la cadena cuyo número de caracteres queremos contar. Para hacer esto, hemos usado la instrucción "xchg" (eXCHanGe: intercambiar) que mueve al primer operando el contenido del segundo y el del segundo operando al primero. Analicemos el procedimiento _strlen. Para la búsqueda, colocamos en AL el caracter a buscar, cero en este caso: mov al, 0 Ponemos en CX -1: mov cx, -1 ; cx = -1 = FFFFh = 65535 = infinito que es igual a 0FFFFh = 65535, el número más grande que puede colocarse en un dato de tamaño de una palabra (word). Luego efectuamos la búsqueda: repne scasb La instrucción repetirá la búsqueda mientras no se encuentre 0, y con cada lectura se debe redudir el valor de CX. Para recuperar el número exacto de caracteres cambiamos el signo del valor en CX y restamos dos a ese valor: neg cx dec cx dec cx Restauramos los valores originales de los registros: xchg si, di y comparamos las dos cadenas en un número de caracteres indicado en CX: _compare_strings: repe cmpsb Si esta instrucción encuentra un caracter diferente o si CX llega a cero antes, entonces termina la comparación. Si las cadenas son iguales, al finalizar la comparación, CX debe ser cero y ponemos 1 en ax, en caso contrario se pone 0 en AX. test cx, cx jne _not_equals mov ax, 1 jmp _exit_compare _not_equals: mov ax, 0 _exit_compare: Las otras dos órdenes son "quit" y "exit". Primero revisamos si el usuario introdujo la orden "exit": _is_exit: lea si, _exit_ call _compare test ax, ax je _is_quit int 19h Si es el caso, se ejecuta la interrupción 19h que reinicializa el sistema. Por supuesto, esta interrupción no funcionará en Windows. *NOTA* Pude haber usado otra instrucción para reinicializa como: push 0ffffh ; Meter el segmento en la pila push 0000h ; Meter el desplazamiento en la pila retf ; Saltar ahí o como reboot: db 0EAh ; Código de operación para JMP dw 0000h ; Saltar a FFFF:0000h, que reinicia dw 0FFFFh ; el sistema pero son instrucciones de mucha complejidad que requieren explicaciones adicionales. Luego se revisa si se trata de la orden "quit": _is_quit: lea si, _quit_ call _compare test ax, ax je _no_order _quit: mov ax, 4C00h int 21h si es el caso, se ejecuta el servicio 4Ch de la int21, que sale del programa a DOS. Si no es el caso, se pasa el control a una rutina que despliega el mensaje "No es una orden del programa!" y ejecuta de nuevo el bucle. Es necesario aclarar algunas cosas todavía. Hemos declarado dos cadenas de caracteres, en el segmento de datos, así: msg2 db 10, 13, "No es una orden del programa!", 10, 13, 0 msg3 db 10, 13, "Introdujo m",0A0h,"s de 12 caracteres", 10, 13 puntero db 10, 13, "> ", 0 Hemos antecedido ambas declaraciones con dos números separados por una coma: "10, 13,". Ambos números son interpretados por nuestra rutina de despliegue como caracteres de control. El número 13 indica retorno de carro (CR) y es interpretado como un restablecimiento del cursor a la posición de la extrema izquierda. El número 10 indica avance de línea (LF), que se interpreta como avance a la línea siguiente. Al colocar estas indicaciones al comienzo de la cadena, esta se desplegará al comienzo de la siguiente línea desde donde estaba originalmente el cursor. Obsérvese que no se colocó un cero al final de la cadena msg3; se hizo así para que cuando se ordene desplegar esta cadena, también se desplegará un par de líneas por debajo el indicador del puntero de órdenes. En la declaración de la cadena "Int",82h,"rprete de ",0A2h,"rdenes", se han reemplazado los caracteres "é" y "ó" por 82h y 0A2h respectivamente, ya que usar directamente estos caracteres provoca un despliegue errado. Para declarar un buffer de 12 caracteres usamos la instrucción: command db 12 dup (?) Aquí el operador DUP DUPLica el contenido entre parénteses que le sigue en la cantidad indicada por el número entero que le antecede. Otra observación importante tiene que ver con unas instrucciones que hemos agregado al comienzo y al final de muchas subrutinas: algunas comienzan salvando en la pila los registros SI y DI: push si push di Estas subrutinas también incluyen en su salida las instrucciones: pop di pop si ret que restauran los valores originales en los registros SI y DI en el momento que se llamó a la rutina. Este va a ser una convención que vamos a seguir en nuestras subrutinas: siempre preservarán el contenido en SI, DI y BX cada vez que la subrutina haga uso de ellos. ----------------------------------------------- TO BE CONTINUED -----------> emailme: numit_or@cantv.net webZ: http://mipagina.cantv.net/numetorl869/ http://oberon.spaceports.com/~tutorial/aks/