======================================================= Introducción a la programación en lenguaje ensamblador para procesadores Intel serie x86 y compatibles (III) ======================================================= Por nmt numit_or@cantv.net ================================= INSTRUCCIONES Y DIRECCIONAMIENTOS ================================= -------------------------------------------------------- CONTENIDO - Intrucciones del i8086 - Identificadores: nombres y etiquetas - Sintaxis de una instrucción · Operandos y direccionamientos. · Registros. · Inmediatos. · Memoria directa. · Memoria indirecta: operador OFFSET, especificadores. de índice. · Desplazamiento de dirección o indexado indirecto. · Direccionamiento relativo a base: operadores aritméticos. · Indexado de base: Indexación en el 80386 y superiores. - Definición de variables · Cadenas de caracteres · Constantes numéricas · Directiva EQU - Instrucciones de Control y el registro de banderas - Ejemplo ------------------------ Instrucciones del i8086 ------------------------ A manera de referencia, presentamos una lista con las instrucciones del procesador 8086, el más simple de la serie que estudiamos: · de transferencia y asignación: mueven datos desde alguna localidad a otra; la localidad puede ser un registro del procesador, un registro de algún dispositivo de entrada/salida del sistema, o una celda en la memoria. También permiten asignar un contenido a cualquier localidad. Las operaciones de transferencia del 8086 son: MOV MOVe: mover datos LEA Load Efective Address: cargar dirección efectiva XCHG eXCHanGe: intercambiar datos XLAT Recorrer una tabla LDS Load Data Segment: cargar segmento de datos LES Load EXended Segment: cargar segmento extendido LAHF, SAHF Load/Store AF flag: cargar/almacenar indicador AF PUSHF, POPF Meter_en\Sacar_de la pila el registro de indicadores · sobre cadenas de caracteres: designan operaciones de transferencia y comparación de cadenas de caracteres CMPS Compare String: comparar cadenas SCAS Scan String: comprar cdenas LODS Load String: Cargar cadenas STOS Store String: Almacenar cadenas · de transformación: designan las operaciones aritméticas, lógicas y de rotación-desplazamiento de bits en localidades. ADD Add: sumar ADC ADd with Car: sumar con acarreo AAA ASCII Adjust then of Add: ajustar ASCII después de la suma DAA Decimal Adjust then of Add: ajustar decimal después de sumar SUB Substract: substraer SBB Substract: substraer con acarreo negativo AAS ASCII Adjust then of Substract: ajustar ASCII después de restar DAS Decimal Adjust then of Substract: ajustar decimal después de restar MUL Multiply IMUL Integer Multiply AAM ASCII Adjust then of Multiply: ajustar ASCII después de multiplicar DIV Divide: dividir IDIV : división con signo AAD ASCII Adjust then of Divide: ajustar ASCII después de Dividir INC Increace: incrementar DEC Decreace: decrecer NEG Negate: negar en complemento a dos CMP Compare: comparar CBW Convert Byte in Word: convertir byte en palabra CWD Convert Word in Dword: convertir palabra en doble palabra . . . . . . NOT Not: negar (los bits 1 los pone en 0 y viceversa) AND And, conjuntor lógico OR Or, disyuntor lógico XOR eXclusive OR, disyuntor lógico exclusivo TEST Test, aplica una conjunción sin cambiar los operandos . . . . . . SHL/SAL Shift Left, desplazar bits a la izquierda SHR Shift Right, desplazar bits a la derecha SAR Shift Artmethic Right, desplazar bits a la derecha ROL, ROR Rote Left/Right, rotar bits a la izquierda/derecha RCL, RCR Rote Left/Right, rotar a bit de acarreo a la izquierda/derecha · de entrada-salida: describen operaciones de transferencia entre el procesador y los dispositivos de entrada/salida. IN Into, Introducir datos en un puerto OUT Out, Obtener entrada desde un puerto · de control de secuencia: controlan el flujo en un procedimiento. JMP Jump, salto incondicional J?, JN? Jump if ?, salto condicional que depende de alguna de las banderas LOOP Loop, bucle JCXZ Jump if CX ?, salto condicional que depende del valor en CX INT INTerrupt, interrumpir IRET INterrupt Return, Regresar de una interrupción · de control de subrutinas: controlan el flujo entre varios procedimientos. CALL Call, llamar una procedimiento RET RETurn, regresar a otro procedimiento PUSH Push, meter en la pila POP Pop, sacar de la pila ENTER Crea una pila para un procedimiento (i386 y posteriores) · de control del procsador: permiten actualizar los bits del registro de los indicadores (flags) y sincronizar el procesador con sucesos externos: CLC, CMC, STC CLear/SeT C flag, Limpiar/establecer indicador de acarreo CLD, STD CLear/SeT D flag, Limpiar/establecer indicador de dirección CLI, STI CLear/SeT Interrupts, Limpiar/establecer interrupciones HALT Halt: parar WAIT Wait: esperar hasta que se niegue la señal busy# ESC Escape LOCK Lock: cerrar bus NOP No OPeration, ninguna operación Ya hemos tenido oportunidad de ver cómo trabajan algunas de las instrucciones de transferencia: MOV, LEA; también una de control de secuencia: INT. Cada instrución tiene sus reglas de uso. Por ejemplo, la instrucción MOV permite · mover datos desde un registro general a otro: MOV AX, BX; · mover datos inmediatos a un registro general MOV AX, 9; · mover datos desde una localidad de memoria a un regisro general: MOV AX, nombre; · mover datos desde un registro regisro a una localidad de memoria: MOV nombre, DX; No permite: · mover datos de una localidad de memoria a otra: MOV nombre1, nombre2 · mover datos desde una localidad de memoria a un registro de segmento: MOV DS, nombre_de _segmento Vistas así en frío, seguro que esta lista tendrá poco sentido. Ya tendremos ocasiones de jugar con ellas y, en ese juego, descubrir su uso y potencia. ------------------------------------- Identificadores: nombres y etiquetas ------------------------------------- Antes de continuar con las instrucciones, es bueno precisar una diferencia en los identificadores usados. Los que se emplean para señalar la dirección de algún dato los llamamos nombres y los empleados para referirse a una dirección de una instrucción, los llamamos etiquetas (labels). En el código presentado, ; -------------------------------------------------------- .data string db 'Hola gente!', 24h ; -------------------------------------------------------- .code main proc far _init: mov ax, @data mov ds, ax _display: lea dx, string mov ah, 1001b int 21h _pause: mov ah, 16 int 16h _exit: mov ax, 0 ret main endp ; -------------------------------------------------------- la palabra "string" refiere a una dirección de un dato; en este caso una constante del tipo cadena de caracteres. Se trata de un nombre. En cambio, todas las palabras que en el segmento de código son seguidas por dos puntos: _init, _display, _pause, _exit (el prefijo '_' no es relevante en este caso, se colocan para evitar poner una palabra reservada del lenguaje ensamblador) son etiquetas que marcan direcciones de instucciones. En un identificador podemos usar las letras desde la A a la Z, caracteres especiales como '?', '_', '$', '@'. Se pueden usar también números y el '.', pero nunca como el primer caracter. Debe tenerse cuidado con el uso de '@', ya que es usado a veces en símbolos especiales. Las letras mayúsculas son tratadas como iguales, a no ser que se indique sensiblidad para la diferencia entre mayúsculas y minúsculas. ---------------------------- Sintaxis de una instrucción ---------------------------- La sintaxis se refiere excusivamente al aspecto formal de un lenguaje, a las reglas para construir fórmulas correctas en él, sin considerar sus significados o contenidos. Desde el punto de vista sintáctico un lenguaje es casi exclusivamente un conjunto de combinaciones que concuerdan con ciertas reglas de arreglo. Para que un enunciado signifique, debe estar bien construído, sus elementos deben estar ordenados correctamente. Si el programa está escrito correctamente, respetado las reglas sintácticas, entonces no debería conducir a error, debería correr perfetamente (corrección de un lenguaje de programación). La sintaxis puede variar de un lenguaje de programación a otro. Inclusive, puede variar de un ensamblador a otro. Tal es el caso, por ejemplo, de la sintaxis de Intel frente a la sintaxis de AT&T para los mismos procesadores. MASM y TASM tienen prácticamente la misma sintaxis: Intel. NASM posee una sintaxis estilo Intel, pero con algunas diferencias respecto a MASM-TASM. El ensamblador de GNU, GAS, sigue la sintaxis de AT&T, la clásica de UNIX. Para ilustrar la diferencia: MOV ECX, 5 ; Intel: mover 5 a ecx. MOV EAX, ECX ; Mover a eax, lo que hay en ecx es en GAS: MOVL $5, %ECX ; AT&T: mover 5 a ecx, MOVL %ECX, %EAX ; Mover a eax, lo que hay en ecx Como puede observarse, cambia el orden de los operandos, cambia la sintaxis de un ensamblador a otro. Pero el significado es el mismo, se conserva la semántica. Tiene que ser así, porque el dominio de aplicación de estos enunciados es el mismo: un procesador ix86. En ambos casos hay también otro aspecto en común: una operación y dos operandos. En general ese es el formato de una intrucción en ensamblador: una operación seguida por sus operandos. Toda operación tiene una ariedad, la propiedad que designa la cantidad de operandos sobre los cuales se aplica. Hay operaciones ceroarias (las constantes, que no se aplican a ningún operando), unarias (que se aplican a un solo operando), binarias (que se aplican a dos operandos), etc. La instrucción MOV y la instrucción LEA, que son operaciones de transferencia, son ambas binarias. La instrucción INT que hemos usado, es una operación unaria: INT 21h ; un único operando Con las citadas diferencias, hay ciertos aspectos sintácticos que respetan los lenguajes ensambladores que he visto hasta ahora: la sintaxis general de una instrucción: [identificador] operación [operando(s)] [; comentario] El identificador designa el nombre de un dato o la etiqueta de una instrucción. La operación puede ser un término que defina un tipo en el área de datos o una operación del procesador, que se halle en el área del código. Los operandos, cuando los hay, proporcionan la información necesaria para una operación. Los comentarios son descripciones en lenguaje natural de los pasos del algoritmo que está realizando nuestro código. Son indicaciones que no procesa el ensamblador, sólo se usan para aclarar lo que se está haciendo. Para convertir una expresión en comentario y evitar que sea procesada, sólo le prefijamos un punto y coma (;). A contiuación dos ejemplos de instrucciones en lenguaje ensamblador; la primera desiga una cadena de caracteres en una dirección del área de datos que identificamos con el nombre "string". La segunda instrucción designa una instrucción en una dirección del área de código que identificamos con la etiqueta "_init", seguida de dos puntos: identificador operación operando comentario string db 'Hola gente!', 24h ; no hay _init: mov ax, @data ; no hay En la sintaxis Intel, que es la que estudiamos, las operaciones binarias en las instrucciones de transferencia consideran el segundo operando, en el extremo derecho, como origen y al primero, inmediatamente después del nombre de la instrucción, como destino: MOV EAX, EDX ; Mover a EAX lo que está en EDX LEA EDX, string ; Cargar en EDX la dirección de string Cada instrucción tiene su limitaciones, las cuales serán explicadas en su momento. Operandos y direccionamientos ----------------------------- El tema de los operandos de las instrucciones está íntimamente relacionado con el de los direccionamientos, debido a que las operaciones se realizan sobre localidades del sistema que debe identificar: todo operando es una localidad o está en una localidad del sistema. Esta localidad no se limita a una dirección de memoria: también un registro del procesador es una localidad del sistema. Por eso, un tipo de operando implica un tipo de dirección y exige un modo de localización. *NOTA* Para localizar un operando, el procesador dispone de una unidad de ejecución, la EU (Execution Unit), donde se hayan los registros de propósito general y la unidad lógico aritmética (ALU: Aritmetic Logic Unit). Su propósito es la ejecución de las instrucciones: EU: : BIU: Unidad de Ejecución : Unidad de Interfaz --------------- : del Bus | ah | al | : --------------- : | bh | bl | : --------------- : | ch | cl | : --------------- : | dh | dl | : --------------- : --------------- | sp | : | CS | --------------- : --------------- | bp | : | DS | --------------- : --------------- | si | : | SS | --------------- : --------------- | si | : | ES | --------------- : --------------- --------- | : | | Unidad | ----------------------------------------->| Control |-> Bus | : | | de BUS | --------------- : | --------- | ALU: Unidad | : ------- |lógico-aritmet.| ---:-----------| 1 | --------------- | : ------- | CU: Unidad de | | : | 2 | Cola de | Control | | : ------- Instrucciones --------------- | : | 3 | | Registro de | | : ------- | Banderas | | : | 4 | --------------- | : ------- | : : --------------- | : ------- | Puntero de |--- : | n | | Instrucciones | : ------- --------------- : : Para ejecutar las instrucciones, la EU debe recibirlas de la unidad de interfaz del bus (BIU), que controla la transferencia de datos de la memoria a la EU. La BIU contiene los registros de segmento, controla el BUS de datos y la cola de instrucciones. La EU pide instrucciones a la BIU, le notifica cuando necesita datos en memoria o en un dispositivo de E/S, o cuando necesita instrucciones en la cola. La BIU pasa la instrucción que se encuentra en el tope de la cola y, mientras la EU ejecuta la instrucción, la BIU va en busca de una nueva instrucción para la cola. La EU queda encargada de interpretar el código de operación y realizar las operaciones respectivas: decide cuál es el operando de origen y cuál el de destino y permite que la ALU lleve a cabo la operación. Como puede observarse, un operando no parece ser más que una localidad en el sistema sobre el cual se ha de realizar una operación. La interpretación del operando es un proceso de ubicación de una localidad, un direccionamiento: los operandos posibles equivalen a modos de direccionamiento posibles. Un operando es una fuente de datos para una instrucción. Algunas instrucciones no tienen operandos. Pero cuando tienen más de uno, decimos (en la sintaxis Intel) que el primero es el destino y el segundo la fuente de la operación. En la operación, la fuente no cambia, pero el destino sí es afectado. Hay varios tipos de operandos o de direccionamientos: · Registros: representado por alguno de los nombres de los registros que ya hemos mencionado. En las operaciones binarias, los registros pueden ocupar cualquiera de los operandos. Adelantamos que las operaciones donde ambos operandos son registros son las más rápidas y cortas, ya que los registros están en el mismo procesador. MOV EBX, EAX · Inmediatos: son valores o expresiones constantes que sólo pueden ocupar el operando fuente, ya que, por ser constantes, no pueden cambiar. La longitud de los datos queda estipulada por el destino. MOV AL, 5 · Memoria directa: son valores que hacen referencia a localidades de memoria. No hay instrucciones donde ambos operandos sean direcciones de memoria, pero un operando de memoria puede ser la fuente o el destino de una instrucción. m1 DW 0 ; dato con un tamaño WORD (16 bits) m2 DB 0 ; dato tamaño BYTE (8 bits) ... MOV m1, 10 ; mover 10 a la localidad llamada m1 MOV AX, m1 ; mover el contenido en m1 a AX LEA DX, m2 ; poner en DX la dirección de m2 MOV CX, DX ; mover a CX la dirección de m2 *NOTA* En este tipo de direccionamiento, la EU debe calcular la dirección efectiva (EA: Efective Address) para una dirección en la memoria que debe ser indicada en la instrucción. Dicho cálculo se efectúa sumando la dirección efectiva (que se interpreta como un desplazamiento [offset]) al valor en el registro del segmento de datos DS, supuestamente la dirección base de dicho segmento. · Memoria indirecta: estos tipos de operandos constan de un registro base y un índice. Como base se usan los registros de segmento y como índice los registros BX, DI, SI y BP. Estos registros índices contienen la dirección efectiva. Para diferenciar un operando de registro de un operando de registro de memoria indirecta, se pone el registro con la dirección efectiva entre corchetes: los especificadores de índice. Los registros índices son asociados a regitros de segmento muy específicos. BX, DI y SI son asociados con DS: DS:BX, DS:DI y DS:SI. De esta manera se procesan objetos en el segmento de datos. El registro BP está asociado al registro del segmento de pila SS: SS:BP. Este tipo de instrucciones son útiles en programas donde hay rutinas remotas y es necesario pasar parámetros. Como la dirección efectiva indica una localidad en la memoria relativa a un segmento, no el contenido de la localidad, se usan instrucciones especiales; una de estas instrucciones es LEA (Load Efective Address): LEA DI, mx También puede usarse el operador OFFSET (desplazamiento): MOV DI, OFFSET mx Las dos instrucciones son equivalentes, salvo cuando la dirección de memoria, en este caso indicada por "mx", refiere a una variable local, las cuales son ubicadas en la pila, no en el segmento de datos. En este caso, debe usarse la instrucción LEA. Un ejemplo de este tipo de operando ya lo hemos presentado: LEA BX, m2 ; poner en DX la dirección de m2 MOV [BX], 80 ; mover 80 a la dirección m2, en DX ; Notar la necesidad de usar los ; corchetes para indicar dirección ; de memoria Si el destino es un operando de memoria indirecta, entonces, el origen debe ser un registro o un operando inmediato. Si la fuente es operando de memoria indirecta, el destino debe ser un registro. *NOTA IMPORTANTE* Especificadores de índice Nótese el uso de corchetes para indicar dirección de memoria. Si hubiésemos escrito: MOV BX, 80 sólo estaríamos poniendo 80 en BX, no en la dirección indicada por el contenido de BX. A los corchetes les llamamos especificadores de índice. Se usan para indicar referencia a memoria. La referencia a memoria se realiza como un desplazamiento (offset) dentro de un segmento. Quiere decir que siempre se apoyan en el valor que contiene un registro de segmento, como dirección base, al que se suma el valor del operando, que sería el desplazamiento dentro del segmento. Respecto a los datos, el registro DS es el tomado por defecto en las instrucciones. Esto quiere decir que la instrucción: LEA BX, m2 MOV [BX], 80 ; pone 80 en DS:BX pone 80 en una localidad igual a la suma del contenido de DS más el el valor correspondiente a la dirección que indica elnombre m2. Cualquier referencia en corchetes a los registros índices, implican operando de memoria indirecta. Luego tendremos que ver cómo se usan correctamente los corchetes especificadores de índice en instrucciones con direcciones de memoria, especialmente en el manejo de arrays. · Desplazamiento de dirección o indexado indirecto: Un tipo de operando complejo usa dirección de memoria directa (generalmente indicadas con nombres) y un registro índice: MOV AL, 1 ; AL = 1 MOV DI, 4 ; DI = 4 MOV m1[DI], AL ; Mueve 1 a m1+4 La última instrucción de la secuencia anterior, mueve el contendio de AL, la cantidad contenida en DI más adelante de la dirección señalada por m1. Aquí, los corchetes, los operadores especificadores de índice, funcionan de manera similar a un sgino de suma "+". Los direccionamientos indexados son muy útiles para referenciar a las entradas de tablas de datos. · Direccionamiento relativo a base: aquí la dirección efectiva del operando se obtiene sumando un desplazamiento a uno de los dos registros base: BP o BX. En este caso, estos registros tienen la dirección del desplazamiento a los que se le suma un escalar que indica el desplazamiento, que puede ser un valor numérico constante: 1, 2, 4 u 8. MOV m2, 80 LEA BX, m2 MOV [BX+2], 80 ; pone 80 en DS:BX+2 Esta instrucción pone 80 dos bytes más adelante de la dirección indicada por m2. *NOTA* Operadores aritméticos Aquí podemos observar el uso de un operador nuevo para indicar suma: "+". Hay varios operadores aritméticos para indicar, además de suma (+), resta (-), multiplicación (*) y división (/). A estos operadores aritméticos habría que añadir operadores lógicos: conjunción (and), disyunción (or), disyunción exclusiva (xor), negación (not). · Indexado de base: la dirección efectiva es la suma del registro base, BX, un registro índice, SI o DI, y un desplazamiento opcional, indicado éste por un escalar. LEA BX, m2 MOV SI, 8 MOV AX, [BX][SI+2] Esta instrución pone en AX lo que se encuentre (SI=8)+2=10 bytes delante respecto a la dirección referenciada por m2. *NOTA IMPORTANTE* Indexación en el 80386 y superiores: A partir del intel 80386, es posible un modo de direccionamiento semejante al indexado de base, pero donde se permite el uso de uno o más registros generales, ya no exclusivamente los registros base o índice, en combinación con un desplazamiento y un factor de escala: MOV EAX, [ECX*2+ESP+4] La instrucción mueve a EAX lo que se encuentra en una localidad de memoria indicada por el contenido en ECX por 2 más el contenido de ESP, que apunta el tope de la pila, más 4. Nótese el uso del operador aritmético de multiplicación '*', que multiplica el contenido de ECX por 2. Ya hablaremos en su turno de los operadores. Se trata de un direccionamiento como el indexado de base, pero que incluye registros generales usados índices. ------------------------ Definición de variables ------------------------ Como ya hemos visto, en el segmento de datos se definen constantes y variables del programa. También pueden definirse regiones de trabajo y áreas para entrada y salida de datos. Para definir variables, seguimos el siguiente formato: [nombre] Dx expresión El nombre es el identificador a través del cual haremos referencia al dato. Este nombre es lo que se usará como referencia a localidad de memoria en los operandos de memoria en las instrucciones del código. Dx es la directiva para definir el tamaño del dato. La "D" significa definir, la "x" corresponde a una letra que nos dice el tamaño del dato: DB (define byte): define un dato con tamaño de un byte (8 bits) DW (define word): define un dato con tamaño de una palabra (word= 2 bytes = 16 bits) DD (define double word) define un dato con tamaño de una palabra doble (double word = 32 bits) QD (define quadruple word) define un dato con un tamaño de cuatro palabras (= 64 bits) DT (define ten byte) define un dato de 10 bytes de longitud (80 bits) La expresión es el valor con el que inicializamos el dato. Si no queremos inicializarlo, podemos usar el signo '?': variable1 db ? variable2 dw 1204h La expresión puede ser definida como un conjunto de valores separados por comas: variable3 db 10, 11, 12, 13 En la definición anterior en realidad hemos definido un array de cuatro datos con un tamaño de un byte cada uno. Para obtener uno de estos valores, necesitamos indexar en el array. mov al, variable3+3 mov ah, variable3+2 mov cl, variable3+1 mov ch, variable3 También podemos usar los _especificadores_de_índice_: mov al, variable3[3] mov ah, variable3[2] mov di, 1 mov cl, variable3[di] dec di ; la instrucción "dec" (decrementar) ; disminuye o decrementa en uno el ; contenido de su operando, sea ; registro u operando de memoria mov ch, variable3[di] Si queremos reservar un buffer grande (área para movilización de datos), podemos usar la directiva DUP (Duplicar), que multiplica tantas veces especefiquemos un valor constante una unidad de dato. El formato de DUP es: [nombre] Dn contador_de_repeticiones DUP expresión buffer1 db 512 dup (?) EFES dw 25 dup (0FFFFh) OCHOS dB 4 dup ( 3 dup (8) ) Cadenas de caracteres --------------------- Para definir constantes del tipo cadenas de caracteres, usamos la la directiva DB (define byte): [nombre] DB 'Cadena de caracteres' Puede preguntarse cómo es posible que se defina como 'byte' una secuencia que supera el tamaño de un byte (recuérdese que un caracter tiene un tamaño de un byte). Debe recordarse también que se puede definir como byte una serie de datos, cada uno con el tamaño de un byte y separado por comas. Es lo mismo con la cadena de caracteres, sólo que ahora los caracteres no están separados por una coma de otro. Obsérvese que la cadena está entre apóstrofos. Si la cadena va a contener un apóstrofo, entonces debe definirse entre paréntesis: string1 DB "numit_or's programming page" Constantes numéricas --------------------- Para definir valores aritméticos y direcciones de memoria, se usan constantes numéricas que no van entre comillas nunca y que van seguidas por un especificador de base. La base puede ser de cuatro tipos: Decimal: correspondiente al formato decimal. Es la base por defecto, pero puede especficarse sufujando la cifra con uuna 'd': 210d Hexadecimal: el más usado, corresponde al formato hexadecimal. Debe ir sufijada la cifra con la letra 'h'. Si la cifra inicia con una letra, entonces debe prefijarse con un 0: 0AB7h 216h Binario: define dígitos binarios 0 y 1 seguidos por la letra 'b'. Se usa normalmente en operaciones lógicas y en el manejo de bits: 0000 0010 0001 0110 b equivale a 216h Real: para definir valores reales se usa una constante decimal o hexadecimal seguida por el especificador de base R. El ensamblador reemplazará este valor en formato de punto flotante para el uso del coprocesador matemático: 450R25 Directiva EQU -------------- Para lograr la definición de constantes numéricas podemos usar la directiva EQU. En realidad la directiva no define un elemento de dato: define un valor que el ensamblador puede usar por un valor numérico o de alguna instrucción. De esa manera, la directiva EQU ayuda en la legibilidad y claridad del programa: BLACK EQU 0 BLUE EQU 1 GREEN EQU 2 CIAN EQU 3 Después de estas líneas, podemos usar la palabra BLACK en vez de 0, BLUE en vez de 1, etc; se ayuda así a memorizar valores constantes. También puede resumirse una expresón en un término simple: precio_a_pagar DW ? ... pap EQU precio_a_pagar invoke EQU call *NOTA* Algunos ensambladores modernos incluyen la directiva TEXTEQU para datos de texto: nombre TEXTEQU --------------------------------------------------- Instrucciones de Control y el registro de banderas --------------------------------------------------- Hay algunos conceptos de programación que son independientes del lenguaje de programación a usar. Dichos conceptos tienen que ver con la forma en que deben diseñarse los programas, en cuanto a estilo y forma. Se han desarrollado varias técnicas para conseguir corrección lógica, eficiencia, comprensibilidad y facilidad de mantenimiento del código de un programa: diseño ascendente, programación modular, código estructurado, programación orientada a objetos. Pareciera una complicación adicional mencionar cuestiones de estilo y problemas de diseño de software en estas circunstancias. Sin embargo, cuando nos referimos a las instrucciones de control es necesario mencionar algunas cuestiones vinculadas con el uso de tales instrucciones, ya que es sobre éstas parece recaer el peso de la problemática referida a la representación de algoritmos a través de lenguajes de programación: la claridad y corrección lógica de un programa parece reposar en un alto grado sobre sus instrucciones de control. En general, un programa es una secuencia de pasos que conduce de una entrada a una salida, y la llegada de un punto a otro reposa sobre condiciones la satisfacción de una regla que controla el flujo del programa: las sentencias que usamos para repesentar algoritmos son reglas que establecen condiciones muy precisas para pasar de un punto a otro. Las instrucciones de control son el principal mecanismo de representación de estas reglas. Cuando en algún momento del programa no se satisface cierta condición, el programa debe tener a mano un mecanismo para abortar la operación. Se trata uno de los principios importantes de la disciplina de la programación y sobre los cuáles reposa cualquier evaluación importante de un programa. Las instrucciones de control consisten en evaluaciones de cuyo resultado dependerá una desición dentro del programa. En lenguaje ensamblador, para los procesadores que estudiamos, existen varias. Hablaremos primero de los saltos condicionales, que establecen que debe tomarse una acción, especificada por una rutina en una dirección determinada --a la que debe saltarse, si se cumple la condición. Como ejemplo, están las siguientes operaciones unarias: je/jz: salta si es igual/salta si es cero jne/jnz: salta si no es igual/salta si no es cero El formato de estas instrucciones es: instrucción dirección El operando "dirección" puede ser un dato directo con indicando una localidad de memoria, puede ser una variable o un registro con la dirección donde el programa pasará el control. También puede ser un operando indirecto (un registro entre corchetes: los especificadores) que indique la dirección donde está otro valor que indica la dirección final donde se ha de pasar el control. Las siguientes instrucciones hacen que se realice al final un salto a la localidad contenida por la variable "dir", dependiendo de si se ha cumplido una condición: lea si, dir je [si] Las instrucciones de salto condicional je/jz y jne/jnz, revisan un bit particular en el registro de banderas: el bit ZF (Zero Flag). Si este bit está activado, la instrucción je/jz pasará el control a la instrucción en la dirección indicada, sino el programa continuará en la siguiente instrucción. La instrucción jne/jnz tiene el comportamiento contrario, pasa el control a la dirección indicada si el bit ZF no está activo. Esta circunstancia nos da oportunidad de introducirnos al registro de banderas o indicadores. Hasta el 80386, el registro de banderas tenía un ancho de 16 bits de los cuales 9 eran comunes con los procesadores anteriores de la serie ix86. Estos bits tienen sus nombres: OF (Overflow Flag, desbordamiento) DF (Dirección) IF (Interrupción) TF (Trampa) SF (Signo) ZF (Cero) AF (Acarreo auxiliar) PF (Paridad) CF (acarreo) Cada bandera será explicada en su momento oportuno. Hablaremos ahora de la bandera de cero. Este indicador señala el resultado de una operación aritmética de comparación. Quiere decir que, si la operación resulta en cero, la bandera se activa, sino se pone en blanco o cero. Aquí tenemos ya un primer mecanismo de evaluación de condiciones: se realiza una operación, si el resultado es cero, activará la bandera de cero y se saltará o no a otra instrucción, dependiendo de si se usa je/jz o jne/jnz. Existen instrucciones especiales para hacer este tipo de comparaciones: test cmp La instrucción test es una operación binaria que somete a ambos operandos a una prueba de conjunción (función and). La conjunción da como resultado verdad o 1 sólo cuando ambos términos son verdad o 1. Quiere decir que si ambos términos de comparación están en cero, el resultado será cero y se activará la bandera de cero: test al, al ; prueba de conjunción jz dir1 ; salta si AL es cero dec al ; si no salta se decrementa AL dir1: inc al ; se incrementa en 1 el contenido de AL Esta instrucción incrementará en 1 el valor en al si durante la prueba contiene cero, en caso contrario se decrementará su valor en uno antes de restablecerse este decremento. La instrucción cmp también es una operación binaria, pero somete a los operadores una operación aritmética: efectúa una substracción sobre el primer operando en la cantidad contenida en el segundo. Esta operación no cambia el contenido de los operandos, pero activa o no alguna bandera dependiendo dependiendo del resultado. Por supuesto, si el contenido de ambos operandos es el mismo, se activará la bandera de cero y se puede tomar una desición al respecto: cmp ax, note ; compara ax con el contenido en note je dir2 ; si son iguales, ir a dir2 dec ax ; si no son iguales decrementar AX jmp dir3 ; e ir a dir 3 dir2: inc ax ; si no eran iguales, incrementar AX dir3: mov bx, ax ; poner en BX el valor en AX Hemos incluido una instrucción de control: el salto incondicional "jmp". Es también un operador unario que pasa incondicionalmente el control a la dirección indicada por el operando. La instrucción tiene ciertas restricciones. Para los procesadores anteriores al 80386, el salto condicional no puede superar el ámbito de un salto corto (-128 a 127 bytes). El salto incondicional puede ser corto (-128 a 127 bytes), cercano (dentro de 32 KB), o lejana (a otro segmento). Otros saltos condicionales serán revisados en su ocasión. -------- Ejemplo -------- Como una muestra del uso de los diversos direccionamientos, escribamos el siguiente 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 mov bx, 000Fh mov ah, 0Eh int 10h inc si jmp _print_string1 _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 _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 mov al, space mov bx, 000Fh mov ah, 0Eh int 10h _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 _end_: mov ah, 10h int 16h mov ax, 4C00h int 21h main endp end main ; ---------------------------------------------------------------------------- ; Ensamblar con: TASM TEST1 TLINK TEST1 ; ---------------------------------------------------------------------------- El programa es demasiado complejo para lo que hace: desplegar la cadena string1, desplegar un espacio, y a medida que vamos pulsando una tecla, despliega dos caracteres de string2 y al final toda string2. Es interesante notar cómo cambian los modos de direccionamiento en cada despliegue. También hay varios detalles que debemos explicar, además de que tenemos la posibilidad y la necesidad de simplificarlo y hacerlo más claro: materia de nuestro siguiente capítulo }:-) -------------------------------------------------- TO BE CONTINUED ---------->