Download Tema 10. Arquitectura y programación de un
Document related concepts
no text concepts found
Transcript
Centro Asociado Palma de Mallorca Arquitectura de Ordenadores Tutor: Antonio Rivero Cuesta Unidad Didáctica 3 El Lenguaje Ensamblador Tema 10 Arquitectura y Programación de un Procesador 16 bits II MC68000 En este capítulo se aborda la programación en ensamblador del MC68000 de forma introductoria, desde un punto de vista práctico a través de ejemplos. Introducción Un programa escrito en ensamblador es un texto escrito siguiendo ciertas reglas sintácticas. Este archivo de texto es procesado por un programa denominado programa ensamblador. Produce como salida un archivo que contiene una secuencia de ceros y unos agrupados en una columna de bytes. Este archivo no es directamente ejecutable y todavía debe ser procesado por el programa enlazador. Al archivo de texto o programa escrito ensamblador se le conoce por programa fuente. en Al archivo obtenido por ensamblado se le conoce por programa objeto. Es preferible dividir el programa en módulos que se programan y se procesan con el ensamblador de forma independiente. A continuación se procesa por un programa especializado llamado enlazador, para obtener el código máquina ejecutable por el procesador MC68000. El programa ensamblador reconoce en el programa fuente ciertas palabras que tienen un significado especial y que se denominan palabras reservadas. Reconoce en el texto del programa fuente una serie de separadores de campos. Estos separadores son o espacios en blanco o signos de puntuación como el punto y coma. Por otro lado, el programa ensamblador toma cada línea del programa fuente como una orden o instrucción, salvo que se trate de una línea de comentario. La sintaxis de una línea completa del programa fuente tiene la siguiente sintaxis: [<etiqueta>] [<mnemotécnico de instrucción> [<operandos>]];[<comentarios>] Una etiqueta es un alfanumérico que facilita la lectura del programa y que el programa ensamblador traducirá por la dirección en la que se deberá ubicar la instrucción que le sigue. El campo de operandos puede estar formado por un operando, por dos operandos separados por una coma, o puede no existir. Eso depende del número de operandos que necesite cada instrucción. ADD.L D0, D1 Esta instrucción utiliza dos operandos, los registros D0 y D1, separados por una coma. NOP ; instrucción que no hace nada RTS ; instrucción de retorno de subrutina Las dos anteriores instrucciones no utilizan el campo de operandos, ya que son instrucciones con modo de direccionamiento implícito. El campo de mnemotécnico puede estar constituido por: Una instrucción perteneciente al conjunto de instrucciones del procesador. Una pseudoinstrucción ensamblador. o directiva de Una directiva de ensamblador, a diferencia de una instrucción no produce código máquina ejecutable. Es utilizada por el programador para dar instrucciones de ensamblado al programa ensamblador. Un ejemplo lo constituye la directiva ORG que sirve para indicarle al programa ensamblador la dirección en la que debe comenzar un fragmento de programa objeto. ORG $4000 ; dirección de memoria $4000 ADD.L D0, D1 Las dos instrucciones anteriores indican al programa ensamblador que la instrucción ADD.L debe almacenarse en la dirección $4000 de la memoria principal. Como se ha comentado antes, esas dos instrucciones de ensamblador sólo generan una única instrucción en código máquina. La directiva ORG tiene gran interés para delimitar zonas de programa y subrutinas, aparte de zonas de datos. Al realizar un programa para el MC68000 no se deben utilizar las direcciones inferiores a 1024 ya que en ellas se encuentran los vectores de excepción. Conviene reservar, desde el primer momento, ciertas zonas para las diferentes partes del programa. Otro detalle a destacar en el fragmento anterior lo constituyen los comentarios que ocupan una única línea. El programa ensamblador ignora cualquier texto que aparezca entre el signo «;» y el salto de línea, por lo que se pueden poner líneas enteras de comentarios con sólo iniciarlas con punto y coma. Esos comentarios sólo quedan en el texto del programa fuente, no ocupan espacio en el programa objeto y sólo tienen interés para el programador. Otras directivas de ensamblador que son muy habituales son las siguientes: EQU. DC. DS. EVEN. END. Otros elementos importantes de un programa ensamblador lo constituyen los símbolos y las expresiones. Los símbolos son, al igual que las etiquetas, nombres que substituyen a constantes, variables y direcciones de memoria. Permiten su utilización en expresiones y son traducidos automáticamente por el programa ensamblador facilitando el trabajo al programador. El programa fuente debe finalizar con la directiva de ensamblador END. Sirve para indicarle al programa ensamblador que ahí es donde finaliza el programa y no tiene que seguir procesándolo. Directivas de Ensamblador o Pseudoinstrucciones más Utilizadas La Directiva ORG Sirve para indicarle al programa ensamblador la dirección en la que debe comenzar un fragmento de programa objeto. Su sintaxis es la siguiente: ORG <dirección o expresión> [;<comentarios>] Indica el origen absoluto (ORiGen) o dirección absoluta de las instrucciones de programa que le sigan. La Directiva END Sirve para indicar al programa ensamblador que el programa fuente ha finalizado. Esta directiva no requiere de operandos. Su sintaxis es la siguiente: END [;<comentarios>] La Directiva EQU Se utiliza para definir un símbolo que se va a utilizar posteriormente en un campo de operando, en una expresión o en una etiqueta. Su sintaxis es la siguiente: <etiqueta> EQU <valor o expresión> [;<comentarios>] Todos los campos, excepto el de comentarios, son requeridos en la pseudoinstrucción. El programa ensamblador crea una tabla de símbolos donde anota todos los símbolos definidos o encontrados en forma de etiquetas y los asocia a un valor. Posteriormente revisa el programa fuente substituyendo los símbolos o expresiones por los valores correspondientes. Un detalle importante reside en que esta directiva no utiliza memoria ya que no da lugar a ninguna instrucción en código máquina. La Directiva DS Se utiliza para reservar posiciones de memoria con vista a utilizarlas como variables. Su sintaxis es la siguiente: <etiqueta> DS.t <número de variables> [;<comentarios>] La etiqueta va a dar lugar a que el programa ensamblador la utilice como un símbolo y le adjudicará la dirección de memoria que apunta a la variable definida. El programa ensamblador traducirá ese símbolo por esa dirección cada vez que lo encuentre en el programa fuente. Esta directiva sí da lugar a un incremento del tamaño de la memoria utilizada por el programa. Esta directiva permite utilizarla para definir variables de varios tamaños: DS.B reserva tantos bytes como variables se indiquen a continuación. DS.W reserva palabras DS.L reserva palabras largas. La Directiva DC Se utiliza para definir datos constantes. No debería sufrir modificaciones durante la ejecución del programa. Indica al programa ensamblador que debe fijar una o varias posiciones de memoria como datos y almacena en ellas los valores indicados. La sintaxis es la siguiente: <etiqueta> DC.t <valor o valores> [;<comentarios>] Tiene el mismo efecto que la directiva DS.t con la diferencia de que aquí no sólo se reserva espacio en memoria, también se le asigna un valor. Ejemplo El siguiente programa se corresponde con el código máquina, mostrando las direcciones de memoria en las que se encuentra almacenado, que se muestra posteriormente. Ese programa no hace nada, pero sirve de ejemplo de utilización de las directivas vistas anteriormente. El programa anterior, al ensamblarlo da lugar al siguiente programa en código máquina. Ejemplos de Realización de Estructuras de Datos En este apartado se abordará de forma introductoria algunas estructuras de datos típicas en los problemas de programación. En los lenguajes de alto nivel, este problema suele estar resuelto ya que es suficiente que el programador defina un tipo de datos para que el compilador se encargue de reservar la memoria necesaria. En lenguaje ensamblador se trabaja directamente sobre posiciones de memoria y el único apoyo que el programa ensamblador presta al programador es el de permitir símbolos y etiquetas que eviten el tener que trabajar directamente con valores numéricos de las direcciones de memoria. Por otro lado la forma de realizar estructuras de datos o de programa no siempre es única y las que aquí se exponen no deben tomarse como excluyentes de otras posibilidades sino más bien como orientaciones. Definición de Constantes Una instrucción de alto nivel como: CONST coef = 34; Se trataría en ensamblador como coef DC.t 34 ;se reservan posiciones de memoria inicializándolas Aquí se ha dejado deliberadamente sin especificar el tamaño del dato. En el caso del MC68000 se permiten los tamaños: byte. palabra. palabra larga. En el caso de un compilador de un lenguaje de alto nivel este dato suele estar definido, por defecto, a 16 bits y debe especificarse mediante la directiva correspondiente en caso de que se quiera modificar. Definición de Variables coches DS.L 1 ;se reserva una palabra larga de memoria El programa ensamblador se encarga de asignar la dirección de memoria reservada a la etiqueta coches que puede ser utilizada posteriormente como nombre de variable en el programa fuente en ensamblador, pero siempre con tamaño palabra larga y teniendo en cuenta el tipo de dato que contiene. Definición de Vectores de Datos El tratamiento de este caso es similar al anterior pero reservando un dato, del tamaño oportuno, por cada componente del vector. La reserva se produce en posiciones de memoria contiguas. Este detalle es de gran importancia ya que en el programa hay que tener mucho cuidado con leer correctamente cada posición o con no salirse de la zona reservada para el vector. En ensamblador, el programador debe de tener en cuenta este detalle. Por ejemplo, si se define un vector de cuatro componentes tipo entero de 16 bits, en lenguaje de alto nivel se puede hacer referencia a la componente vector[3] sin prestar más atención. Sin embargo en ensamblador sólo se dispone de la etiqueta de la cabecera del vector y cualquier referencia hay que realizarla especificando el desplazamiento oportuno. Es el caso del ejemplo del apartado (1.1.6). La asignación de lenguaje de alto nivel: D0 := vector[3] equivale a una transferencia de un dato tamaño palabra mediante la instrucción: MOVE.W vector+4, D0 cuatro bytes por delante de vector. Por lo tanto, la definición de una variable tipo vector en un lenguaje de alto nivel, de cuatro componentes se realizaría como sigue: VAR vector=ARRAY[1..4] OF integer; Esta definición de un vector en un lenguaje de alto nivel se puede trasladar a lenguaje ensamblador mediante una reserva de cuatro posiciones de memoria, asignando la dirección de inicio de la primera componente a una etiqueta. Ésta sería una posibilidad: vector DS.B 4 reservaría espacio para 4 componentes de 8 bits; vector DS.W 4 reservaría espacio para 4 componentes de 16 bits, y vector DS.L 4 reservaría espacio para 4 componentes de 32 bits. Cadenas de Caracteres Se trata de situar en memoria variables de tipo cadena de caracteres. No se trata del tipo carácter (CHAR) que se almacena corrientemente en un único byte. Sino de una cadena de éstos y cuyo tamaño puede ser variable. Normalmente se reserva una cantidad determinada de bytes en el momento de definir la cadena. En ensamblador puede codificarse realizando una reserva de memoria para la variable cadena de 50 bytes: cadena DS.B 50 ;se reservan 50 bytes en memoria Para manejar cadenas se pueden seguir dos enfoques: Procesar los 50 bytes de cadena. Procesar sólo aquellos que contengan algún carácter. El enfoque usual es este último para lo que conviene utilizar un marcador que permita identificar el final de la cadena. Los marcadores más utilizados son: $0A. $00. Podría utilizarse cualquier otro a condición de no coincidir con ningún carácter ASCII imprimible. El marcador $00 presenta ventajas ya que la instrucción MOVE.B también modifica el bit Z de estado y permite implementar algoritmos de procesamiento de cadenas de una forma muy sencilla. Cargando un registro interno desde memoria MOVE.B (An)+, Dn ; se recorre la cadena de caracteres Se va actualizando el bit Z que se pone a 1 cuando se lea el marcador $00 y señala sin comparaciones el final de la cadena. Al crear cadenas también es útil el aumentar en una posición el tamaño de la cadena e inicializarla con el valor $00. Así un bucle que recorra la cadena tiene asegurada su terminación: El bloque anterior representa el fragmento de memoria utilizado por la variable cadena cuando se ha inicializado con el valor ‘HOLA’, y utilizando una posición adicional. En este ejemplo se han utilizado caracteres pero lo que realmente se almacena en memoria son los números de código correspondientes a estos caracteres según ASCII. Si para este ejemplo suponemos, además, que la etiqueta “cadena” representa a la dirección $3A00, entonces se tendría la siguiente situación en memoria. Los valores ASCII correspondientes con la palabra “HOLA” son los siguientes: ‘H’ = $48. ‘O’ = $4F. ‘L’ = $4C. ‘A’ = $41. Pilas Una pila es un espacio de datos de tamaño infinito en el que, a partir de una dirección base, se van almacenando los datos. Unicamente se necesita de un puntero o registro que almacene la dirección del dato que ocupa la cabeza de la pila. En el MC68000 existe ya una pila definida por USP o registro A7. Cada vez que se almacena un dato en esta pila, el puntero se dirige a direcciones decrecientes. Cada vez que se extrae un dato de la pila el puntero incrementa su valor. Sin embargo se pueden crear otras pilas por programación o pilas de usuario. Las instrucciones adecuadas para el manejo de estas pilas son: Como es lógico, la cantidad de memoria disponible no es infinita sino que depende de cada sistema físico y nunca puede exceder de la memoria direccionable por el procesador. Esta característica muestra que en la práctica una pila se declara reservando una tamaño finito de palabras y programando una subrutina que compruebe, en cada acceso, que la pila no se desborde. Un ejemplo podría ser: En el fragmento anterior se reservan 512 bytes en memoria que van desde la dirección etiquetada como pila y que finaliza 512 posiciones después. A continuación se define el puntero para manejar esa pila creciendo hacia direcciones inferiores. Para ello se define una variable denominada punt_pila y se inicializa con un valor que es precisamente la dirección que ocupa. Esto es posible porque se utiliza como valor el símbolo de la propia etiqueta y que el programa ensamblador sustituirá por la dirección correspondiente a la etiqueta punt_pila. La representación gráfica de la memoria del resultado es la siguiente (suponiendo que pila = $4000): Si ahora se emplea, por ejemplo, el registro A6 para almacenar momentáneamente este puntero, entonces para almacenar un dato contenido en D3 se podría ejecutar el fragmento siguiente: Para extraer un elemento de la pila el procedimiento a seguir es justo el contrario: Estructuras de Programa En este apartado se tratarán, de forma muy básica, las estructuras de programa más habitualmente utilizadas para realizar un algoritmo. Las estructuras básicas son: La secuencia de instrucciones. Las bifurcaciones. Las iteraciones. Los saltos a subrutinas. Secuencia de Instrucciones Se trata de situar en un orden determinado una instrucción tras otra. Por ejemplo, un simple problema del cálculo del área de rectángulo involucra tres variables: base, altura y área. Una vez definidas estas variables como posiciones de memoria, es posible utilizarlas en lenguaje ensamblador como etiquetas: En el MC68000 no se puede operar directamente sobre memoria y es preciso que los contenidos de estas variables y que se hallan en memoria sean transferidos a los registros internos del procesador para su procesamiento. La transferencia de memoria a un registro suele denominarse carga. La transferencia en sentido contrario se denomina almacenamiento. Esta nomenclatura procede de las instrucciones LOAD y STORE. Para realizar la carga se seleccionan unos registros no utilizados y cuyos contenidos no necesiten ser conservados. Por ejemplo seleccionemos los registros R0 y R1. Se aprecia que el resultado ha modificado R0, por lo que si se desea volver a operar con la variable altura entonces es preciso recurrir a una nueva carga. Se aprovecha este ejemplo para advertir que no es necesaria la carga del primer operando del producto, base, ya que la instrucción MULU permite el direccionamiento directo. El programa anterior quedaría como sigue: La asignación entre variables en alto nivel es muy sencilla aunque en ensamblador requiere la carga previa en uno de los registros internos. Se realizaría como sigue (ejemplo para tamaño W): Conviene tener presente que si una variable se utiliza varias veces en una secuencia de instrucciones siempre es susceptible de no ser cargada o almacenada más que en una única ocasión. Bifurcaciones Las bifurcaciones se basan en comparaciones que determinarán el camino a seguir por la ejecución del programa. En ensamblador estas bifurcaciones se apoyan en instrucciones de salto condicional. Como las condiciones necesitan de la evaluación de los códigos de condición también suelen necesitar de una instrucción de comparación. Existen varios tipos de bifurcaciones condicionales. La bifurcación más sencilla es la representada por la instrucción de alto nivel: La comparación modifica el CCR y no debe modificarse antes de ejecutar la instrucción de salto condicional, por lo que lo mejor es que ésta se encuentre inmediatamente después de la comparación. A veces es más sencillo negar la comparación de variables buscando el código de condición alternativo y eliminar el salto incondicional BRA: Iteraciones Por iteraciones entendemos aquellos fragmentos de programa que se ejecutan repetidamente varias veces. Las iteraciones también se denominan bucles ya que al ejecutarse la última instrucción de ese fragmento se vuelve a ejecutar la primera. Bucles FOR En estas iteraciones el fragmento de programa contenido en el bucle se repite un número de veces predeterminado y, por lo tanto, requiere de un contador que debe ser evaluado para conocer cuándo se finaliza la ejecución del mencionado bucle. Se necesita crear una variable para utilizarla como contador que se comparará con el valor n. Puede ser preferible alojar dicho contador en un registro interno que se incrementa. Aquí seguiremos esta última orientación y elegiremos, por ejemplo, D5. Bucles WHILE Estos bucles se caracterizan por evaluar la condición de finalización de bucle justo antes de ejecutar las instrucciones del cuerpo del bucle. De esta forma el bucle puede no ejecutarse nunca si la condición es falsa al evaluarla por primera vez. En un lenguaje de alto nivel estos bucles se pueden codificar como sigue: Bucles REPEAT‐UNTIL Como sucede en los bucles while aquí también está indefinido el número de veces que se ejecuta la iteración. También debe analizarse detenidamente para poder asegurar que se ejecuta un número finito de veces, esto es, que finalice la iteración y no se entra en un bucle sin fin. La característica que diferencia este tipo de bucles del anterior consiste en que la condición de finalización se evalúa al final del bucle y se asegura que se ejecuta al menos en una ocasión. Bucles LOOP Aunque se trata de iteraciones menos habituales, en ocasiones se tiene la necesidad de evaluar la condición de terminación de una iteración en uno o en varios puntos del cuerpo del bucle distintos del inicio y del final. En estos bucles al llegar al final se salta al comienzo del mismo y este proceso se repite indefinidamente. En el interior del bucle debe existir entonces al menos una comparación de terminación que determine si se abandona el bucle o no. Subrutinas Facilitan mucho la programación ya que permiten simplificar mucho cualquier programa ya que cada uno de estos subprogramas pueden ser ejecutados mediante una llamada desde cualquier punto del programa o desde cualquier módulo. El único requerimiento consiste en que se le deben suministrar los datos a utilizar de una forma más o menos explícita. Una subrutina es, por lo tanto, un fragmento de código que interesa realizar fuera del código principal. Un motivo para esto es que se trata de realizar labores concretas que pueden repetirse muchas veces en otros programas y puede ser conveniente aislar de un programa principal para ensamblarlo aparte e incluirlo en una librería. La conexión entre ambos fragmentos se realiza con instrucciones de salto especializadas: BSR o JSR. RTS o RTE. La llamada a esta subrutina se produce en el programa principal mediante la instrucción BSR subrutina ; salta a subrutina Esta instrucción no sólo realiza el salto a la dirección aquí etiquetada como etiqueta sino que, además guarda el contenido del contador de programa en la pila. De esta forma se permite al procesador continuar con la instrucción siguiente una vez finalizada la ejecución de la subrutina. Sin embargo para que suceda esto la instrucción RTS debe ser la última de dicha subrutina. Esta instrucción realiza el salto contrario volviendo a la instrucción siguiente a la que se había realizado la llamada a la subrutina. Si la subrutina se encuentra a una distancia del punto de llamada mayor a 256 bytes entonces se utiliza la instrucción JSR. No siempre se necesita llamar explícitamente a la subrutina ya que en ocasiones la llamada la realiza el propio procesador. Esto es lo que ocurre cuando un dispositivo externo al procesador activa una línea de petición de interrupción. En este caso las subrutinas suelen denominarse rutinas de atención a una interrupción. El mecanismo consiste en que se genera un evento que da lugar a que el procesador guarde información de su estado y pase a realizar un salto a la dirección donde se encuentra la subrutina que puede gestionar ese evento. En estos casos la instrucción de retorno es RTE. En la operación con subrutinas se aprecia que deben existir dos partes diferenciadas en el programa: una parte principal que se conoce como programa principal. un subprograma o subrutina. La estructura podría ser la siguiente: Otro detalle fundamental a tener en cuenta es cómo transfiere los datos el programa principal a una subrutina. Una forma muy sencilla de traspasar los datos u operandos a una subrutina consiste en utilizar los registros internos del procesador, D0, D1, ... D7. En este caso se suele hablar de paso de datos por valor. Este método es muy rápido y sencillo pero presenta importantes limitaciones. Dos características muy importantes de las subrutinas son la posible recursividad o la posible reentrada de la propia subrutina. Conjunto de Instrucciones Ver páginas 506 y siguientes de libro base.