Fundamentos Sistemáticos
Fundamentos Sistemáticos
Este documento está específicamente diseñado para servir de material de estudio a los alumnos de
la asignatura «Fundamentos de los Sistemas Telemáticos» de la Escuela Técnica Superior de Ingenieros
de Telecomunicación de la Universidad Politécnica de Madrid.
Parte del documento es un autoplagio: casi la mitad de los capítulos 1 y 2 están adaptados de
textos publicados en el libro «Curso de Ordenadores. Conceptos básicos de arquitectura y sistemas
operativos», 5ª ed., Fundación Rogelio Segovia para el Desarrollo de la Telecomunicaciones, Madrid,
2004.
La licencia cc-by-sa significa que usted es libre de copiar, de distribuir, de hacer obras derivadas y
de hacer un uso comercial del documento, con dos condiciones: reconocer el origen citando los datos
de la portada, y utilizar la misma licencia.
1 Terminología y objetivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.1 Ordenador, procesador, microprocesador, microcontrolador, SoC... . . . . . . . . . . . 1
1.2 Niveles y modelos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.3 Arquitectura de ordenadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.4 Objetivos y contenido del Tema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
3 El procesador BRM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
3.1 Modelo estructural . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
3.2 Modelo procesal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
3.3 Modelo funcional . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
3.4 Instrucciones de procesamiento y de movimiento . . . . . . . . . . . . . . . . . . . . 33
3.5 Instrucciones de transferencia de datos . . . . . . . . . . . . . . . . . . . . . . . . . . 38
3.6 Instrucciones de transferencia de control . . . . . . . . . . . . . . . . . . . . . . . . . 41
3.7 Comunicaciones con los periféricos e interrupciones . . . . . . . . . . . . . . . . . . . 42
4 Programacion de BRM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
4.1 Primer ejemplo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
4.2 Etiquetas y bucles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
4.3 El pool de literales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
4.4 Más directivas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
4.5 Menos bifurcaciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
4.6 Subprogramas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
4.7 Módulos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
4.8 Comunicaciones con los periféricos e interrupciones . . . . . . . . . . . . . . . . . . . 64
4.9 Software del sistema y software de aplicaciones . . . . . . . . . . . . . . . . . . . . . 69
4.10 Dos programas para Android . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
iii
iv ÍNDICE GENERAL
Bibliografía . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
Capítulo 1
Terminología y objetivos
Conviene aclarar el significado de algunos términos e introducir varios conceptos generales antes
de concretar los objetivos de este Tema.
Estando estas notas dirigidas a lectores de España, utilizaremos siempre el término «ordenador»1 ,
entendiendo, además, que es de naturaleza digital.
En otro artículo enmendado para la 23ª edición, se define así «procesador»2 :
Unidad funcional de una computadora que se encarga de la búsqueda, interpretación y ejecución de
instrucciones.
~ de textos.
Programa para el tratamiento de textos.
Dos comentarios:
• Se deduce de lo anterior que un ordenador tiene dos partes: una memoria, donde se almacenan pro-
gramas y datos, y un procesador. Los programas están formados por instrucciones, que el procesa-
dor va extrayendo de la memoria y ejecutando. En realidad, la mayoría de los ordenadores contienen
varios procesadores y varias memorias. A esto hay que añadir los dispositivos periféricos (teclado,
1
En España, en los años 60 se empezó a hablar de «cerebros electrónicos» o «computadoras», pero muy pronto se impuso
la traducción del francés, «ordenador», que es el término más habitual, a diferencia de los países americanos de habla cas-
tellana, en los que se usa «computador» o «computadora». También se prefiere «computador» en los círculos académicos
españoles: «Arquitectura de Computadores» es el nombre de un «área de conocimiento» oficial y de muchos Departamentos
universitarios y títulos de libros. Creemos que tras esa preferencia solo se esconde un afán elitista, por lo que utilizamos el
nombre más común.
2
En la edición actual, la 22ª, se define de este curioso modo: «Unidad central de proceso, formada por uno o dos chips.»
1
2 Capítulo 1. Terminología y objetivos
ratón, pantalla, discos...). Los procesadores pueden ser genéricos o especializados (por ejemplo, el
procesador incorporado en una tarjeta de vídeo). Al procesador o conjunto de procesadores genéri-
cos se le llama unidad central de procesamiento (UCP, o CPU, por las siglas en inglés), como ya
habíamos visto en la introducción del Tema 1.
• El procesador de textos es un programa. Pero hay otros tipos de programas que son también pro-
cesadores: los que traducen de un lenguaje de programación a otro, los que interpretan el lenguaje
HTML para presentar las páginas web en un navegador, los que cifran datos, los que codifican datos
de señales acústicas en MP3....
Podemos, pues, hablar de dos tipos de procesadores:
• Procesador hardware: Sistema físico que ejecuta programas previamente almacenados en una me-
moria.
• Procesador software: Programa que transforma unos datos de entrada en unos datos de salida.
«Microprocesador» tiene una definición fácil3 : es un procesador integrado en un solo chip. Pero
hay que matizar que muchos microprocesadores actuales contienen varios procesadores genéricos (que
se llaman «cores») y varias memorias de acceso muy rápido («caches», ver apartado 2.6).
Hasta aquí, seguramente, está usted pensando en lo que evoca la palabra «ordenador»: un apara-
to (ya sea portátil, de sobremesa, o más grande, para centros de datos y servidores) que contiene la
memoria y la UCP, acompañado de discos y otros sistemas de almacenamiento y de periféricos para
comunicación con personas (teclado, ratón, etc.). Pero hay otros periféricos que permiten que un pro-
cesador hardware se comunique directamente con el entorno físico, habitualmente a través de sensores
y conversores analógico-digitales y de conversores digitales-analógicos y «actuadores». De este modo,
los programas pueden controlar, sin intervención humana, aspectos del entorno: detectar la temperatura
y actuar sobre un calefactor, detectar la velocidad y actuar sobre un motor, etc. Para estas aplicaciones
hay circuitos integrados que incluyen, además de una UCP y alguna cantidad de memoria, conversores
y otros dispositivos. Se llaman microcontroladores y se encuentran embebidos en muchos sistemas:
routers, discos, automóviles, semáforos, lavadoras, implantes médicos, juguetes...
SoC significa «System on Chip». Conceptualmente no hay mucha diferencia entre un microcon-
trolador y un SoC, ya que éste integra también en un solo chip uno o varios procesadores, memoria,
conversores y otros circuitos para controlar el entorno. Pero un microcontrolador normalmente está
diseñado por un fabricante de circuitos integrados que lo pone en el mercado para que pueda ser utili-
zado en aplicaciones tan diversas como las citadas más arriba, mientras que un SoC generalmente está
diseñado por un fabricante de dispositivos para una aplicación concreta. Por ejemplo, varios fabricantes
de dispositivos móviles y de tabletas utilizan como «core» el mismo procesador (ARM) y cada uno le
añade los elementos adecuados para su dispositivo. Podemos decir que un microcontrolador es un SoC
genérico, o que un SoC es un microcontrolador especializado.
órdenes, llamadas instrucciones, que se expresan en un lenguaje binario, el lenguaje de máquina. Así,
«1010001100000100» podría ser, para un determinado procesador, una instrucción que le dice «envía
un dato al puerto USB». Y, como hemos visto en el Tema 2, los datos también están representados en
binario. Hacemos abstracción de cómo se materializan físicamente esos «0» y «1»: podría ser que «0»
correspondiense a 5 voltios y «1» a -5 voltios, u otros valores cualesquiera. Estos detalles son propios
de niveles de abstracción más «bajos», los niveles de micromáquina, de circuito lógico y de circuito
eléctrico, que se estudian en la Electrónica digital y la Electrónica analógica.
El lenguaje de máquina es el «lenguaje natural» de los procesadores hardware. Pero a la mente hu-
mana le resulta muy difícil interpretarlo. Los lenguajes de programación se han inventado para expresar
de manera inteligible los programas. Podríamos inventar un lenguaje en el que la instrucción anterior
se expresase así: «envía dato,#USB». Este tipo de lenguaje se llama lenguaje ensamblador.
que facilitan el uso de los recursos (las memorias, los procesadores y los periféricos), ofreciendo ser-
vicios al nivel de máquina simbólica. Este es el nivel estudiado (aunque muy superficialmente) en el
Tema 1.
Un hito importante en la historia de los ordenadores fue la introducción del concepto de progra-
ma almacenado: previamente a su ejecución, las instrucciones que forman el programa deben estar
guardadas, o almacenadas, en una memoria. También se dice que el programa tiene que estar cargado
en la memoria. Esto condiciona fuertemente los modelos estructurales, funcionales y procesales de los
procesadores en todos los niveles.
Modelos estructurales que contenían unidades de memoria, de procesamiento, de control y de entra-
da/salida, se habían propuesto tiempo atrás, pero la idea de programa almacenado y el modelo procesal
que acompaña a esta idea, se atribuyen generalmente a John von Neumann. Hay un documento escrito
en 1946 por Burks, Goldstine y von Neumann (cuando aún no se había construido ninguna máquina
de programa almacenado) que mantiene hoy toda su vigencia conceptual. Haciendo abstracción de los
detalles ligados a las tecnologías de implementación y de realización, esa descripción no ha sido supe-
rada, pese a que seguramente se habrán publicado decenas de miles de páginas explicando lo mismo.
Por eso, nos serviremos de ese texto, extrayendo y glosando algunos de sus párrafos.
7
8 Capítulo 2. La máquina de von Neumann y su evolución
Memoria
«... Hemos diferenciado, conceptualmente, dos formas diferentes de memoria: almacenamiento de da-
tos y almacenamiento de órdenes. No obstante, si las órdenes dadas a la máquina se reducen a un código
numérico, y si la máquina puede distinguir de alguna manera un número de una orden, el órgano de
memoria puede utilizarse para almacenar tanto números como órdenes.»
Es decir, en el subsistema de memoria se almacenan tanto las instrucciones que forman un programa
como los datos. Esto es lo que luego se ha llamado «arquitectura Princeton». En ciertos diseños se
utiliza una memoria para datos y otra para instrucciones, siguiendo una «arquitectura Harvard».
«... Planeamos una facilidad de almacenamiento electrónico completamente automático de unos 4.000
números de cuarenta dígitos binarios cada uno. Esto corresponde a una precisión de 2−40 ≈ 0, 9 × 10−12 ,
es decir, unos doce decimales. Creemos que esta capacidad es superior en un factor de diez a la requerida
para la mayoría de los problemas que abordamos actualmente... Proponemos además una memoria sub-
sidiaria de mucha mayor capacidad, también automática, en algún medio como cinta o hilo magnético.»
Por tanto, las direcciones (números que identifican a las posiciones) eran de doce bits, y, como las
palabras identificadas por esas direcciones tenían cuarenta bits, la capacidad de la MP, expresada en
unidades actuales, era 40/8 × 212 = 5 × 4 × 210 bytes, es decir, 20 KiB. Obviamente, los problemas
«actuales» a los que se dirigía esa máquina no eran los que se resuelven con los procesadores de hoy...
Se habla de una memoria secundaria de «cinta o hilo». La cinta y el hilo son reliquias históricas; las
memorias secundarias actuales son de discos o de estado sólido (memorias flash).
«... Lo ideal sería [...] que cualquier conjunto de cuarenta dígitos binarios, o palabra, fuese accesible
inmediatamente –es decir, en un tiempo considerablemente inferior al tiempo de operación de un multi-
plicador electrónico rápido–[...] De aquí que el tiempo de disponibilidad de una palabra de la memoria
2.1 Modelo estructural 9
debería ser de 5 a 50 µseg. Asimismo, sería deseable que las palabras pudiesen ser sustituidas por otras
nuevas a la misma velocidad aproximadamente. No parece que físicamente sea posible lograr tal capaci-
dad. Por tanto, nos vemos obligados a reconocer la posibilidad de construir una jerarquía de memorias,
en la que cada una de las memorias tenga una mayor capacidad que la precedente pero menor rapidez
de acceso...»
La velocidad también se ha multiplicado por varios órdenes de magnitud: el tiempo de acceso en las
memorias de semiconductores (con capacidades de millones de bytes), es alrededor de una milésima
parte de esos 5 a 50 µs que «no parece que sea posible lograr».
Aquí aparece una característica esencial de la memoria principal: la de ser de acceso aleatorio
(«sería deseable que las palabras pudiesen ser sustituidas por otras nuevas a la misma velocidad»), así
como el concepto de jerarquía de memorias.
La figura 2.2 ilustra que para extraer una palabra de la memo-
ria (operación de lectura) se da su dirección y la señal («micro- microórdenes de la UC
esc
lec
orden») lec; tras un tiempo de acceso para la lectura se tiene en
la salida el contenido de esa posición. Para introducir una pala-
bra en una posición (operación de escritura), se indica también dirección palabra direccionada (n bits)
la dirección y la microorden esc; tras un tiempo de acceso para
la escritura queda la palabra grabada. Como vimos en el Te-
ma 2 (apartado 1.3), decir que la memoria tiene acceso aleatorio
(o que es una RAM) significa simplemente que los tiempos de palabra palabra
obtenida a grabar
acceso (los que transcurren desde que se le da una microorden (caso de lectura) (caso de escritura)
Las unidades de entrada y salida, o dispositivos periféricos (que, abreviadamente, se suelen llamar
periféricos), representadas en la figura 2.1 como un simple bloque (UE/S), en aquella época eran muy
básicas: simples terminales electromecánicos. Actualmente son muchas y variadas. Periféricos son los
que permiten la comunicación con las personas (teclado, pantalla, ratón, micrófono, altavoz...), pero
también las memorias secundarias, los puertos de comunicaciones, y los sensores y actuadores para la
interacción con el entorno.
Unidad de control
«... Si la memoria para órdenes es simplemente un órgano de almacenamiento, tiene que haber otro
órgano que pueda ejecutar automáticamente las órdenes almacenadas en la memoria. A este órgano le
llamaremos el control ...»
microórdenes dirección de dato dirección de la La UC examina cada instrucción («orden») y la ejecuta
(a la UAL, la MP (a la MP instrucción
y/o las UE/S) o las UE/S) siguiente (a la MP) emitiendo las señales de control, llamadas microórdenes,
adecuadas para activar a las otras unidades y, si es nece-
sario, las direcciones de MP o de entrada/salida oportunas
UC instrucción (figura 2.4).
(de la MP)
«... Tiene que ser posible extraer números de cualquier
parte de la memoria en cualquier momento. Sin embargo,
Figura 2.4 Unidad de control. en el caso de las órdenes, el tratamiento puede ser más
metódico, puesto que las instrucciones se pueden poner,
por lo menos parcialmente, en secuencia lineal. En consecuencia, el control se construirá de forma que
normalmente proceda de la posición n de memoria a la posición (n+1) para su siguiente instrucción...»
Tras la ejecución de una instrucción que está almacenada en la palabra de dirección d de la memoria,
la siguiente a ejecutar es, normalmente, la almacenada en la dirección d + 1 (las excepciones son las
instrucciones de bifurcación, que explicaremos enseguida).
2.2 Modelo procesal 11
El modelo funcional es lo que se necesita conocer para usar (programar) la máquina: los convenios
para la representación de números e instrucciones.
Representación de números
«... Al considerar los órganos de cálculo de una máquina computadora nos vemos naturalmente obli-
gados a pensar en el sistema de numeración que debemos adoptar. Pese a la tradición establecida de
construir máquinas digitales con el sistema decimal, para la nuestra nos sentimos más inclinados por el
sistema binario...»
12 Capítulo 2. La máquina de von Neumann y su evolución
Luego de unas consideraciones para justificar esta elección, muy ligadas a la tecnología de la época
(aunque podrían aplicarse también, en esencia, a la tecnología actual), continúa con las ventajas del
sistema binario:
«... La principal virtud del sistema binario frente al decimal radica en la mayor sencillez y velocidad
con que pueden realizarse las operaciones elementales...»
«... Un punto adicional que merece resaltarse es éste: una parte importante de la máquina no es de
naturaleza aritmética, sino lógica. Ahora bien, la lógica, al ser un sistema del sí y del no, es fundamen-
talmente binaria. Por tanto, una disposición binaria de los órganos aritméticos contribuye de manera
importante a conseguir una máquina más homogénea, que puede integrarse mejor y ser más eficiente...»
«... El único inconveniente del sistema binario desde el punto de vista humano es el problema de la
conversión. Sin embargo, como se conoce perfectamente la manera de convertir números de una base a
otra, y puesto que esta conversión puede efectuarse totalmente mediante la utilización de los procesos
aritméticos habituales, no hay ninguna razón para que el mismo computador no pueda llevar a cabo tal
conversión.»
Formato de instrucciones
«... Como la memoria va a tener 212 = 4.096 palabras de cuarenta dígitos [...] un número binario de
doce dígitos es suficiente para identificar a una posición de palabra...»
«... Dado que la mayoría de las operaciones del computador hacen referencia al menos a un número
en una posición de la memoria, es razonable adoptar un código en el que doce dígitos binarios de cada
orden se asignan a la especificación de una posición de memoria. En aquellas órdenes que no precisan
extraer o introducir un número en la memoria, esas posiciones de dígitos no se tendrán en cuenta...»
«... Aunque aún no está definitivamente decidido cuántas operaciones se incorporarán (es decir, cuán-
tas órdenes diferentes debe ser capaz de comprender el control), consideraremos por ahora que probable-
mente serán más de 25 , pero menos de 26 . Por esta razón, es factible asignar seis dígitos binarios para el
código de orden. Resulta así que cada orden debe contener dieciocho dígitos binarios, los doce primeros
para identificar una posición de memoria y los seis restantes para especificar una operación. Ahora po-
demos explicar por qué las órdenes se almacenan en la memoria por parejas. Como en este computador
se va a utilizar el mismo órgano de memoria para órdenes y para números, es conveniente que ambos
tengan la misma longitud. Pero números de dieciocho dígitos binarios no serían suficientemente precisos
para los problemas que esta máquina ha de resolver [...] De aquí que sea preferible hacer las palabras
suficientemente largas para acomodar dos órdenes...»
«... Nuestros números van a tener cuarenta dígitos binarios cada uno. Esto permite que cada orden ten-
ga veinte dígitos binarios: los doce que especifican una posición de memoria y ocho más que especifican
una operación (en lugar del mínimo de seis a que nos referíamos más arriba)...»
Por ejemplo, tras leer y descodificar una instrucción que tuviese el código de operación correspon-
diente a «sumar» y 0b000000101111 = 0x02F = 47 en el campo CD, la unidad de control, ya en la
fase de ejecución, generaría primero las microórdenes oportunas para leer el contenido de la dirección
47 de la MP, luego llevaría este contenido a una de las entradas de la UAL y el contenido actual del
acumulador a la otra, generaría la microorden de suma para la UAL y el resultado lo introduciría en el
acumulador.
Programas
«... La utilidad de un computador automático radica en la posibilidad de utilizar repetidamente una
secuencia determinada de instrucciones, siendo el número de veces que se repite o bien previamente
determinado o bien dependiente de los resultados de la computación. Cuando se completa cada repetición
hay que seguir una u otra secuencia de órdenes, por lo que en la mayoría de los casos tenemos que
especificar dos secuencias distintas de órdenes, precedidas por una instrucción que indique la secuencia
a seguir. Esta elección puede depender del signo de un número [...] En consecuencia, introducimos una
orden (la orden de transferencia condicionada) que, dependiendo del signo de un número determinado,
hará que se ejecute la rutina apropiada de entre las dos...»
Es decir, los programas constan de una secuencia de instrucciones que se almacenan en direccio-
nes consecutivas de la MP. Normalmente, tras la ejecución de una instrucción se pasa a la siguiente.
Pero en ocasiones hay que pasar no a la instrucción almacenada en la dirección siguiente, sino a otra,
almacenada en otra dirección. Para poder hacer tal cosa están las instrucciones de transferencia de
control, o de bifurcación (éstas son un caso particular de las primeras, como veremos enseguida).
Para completar el modelo funcional sería preciso especificar todas las instrucciones, explicando
para cada una su código de operación y su significado. Esto es lo que haremos en el capítulo 3, ya con
un procesador concreto y moderno.
2.4. Evolución
En el año 1946 la tecnología electrónica disponible era de válvulas de vacío. El transistor se inventó
en 1947, el primer circuito integrado se desarrolló en 1959 y el primer microprocesador apareció en
1971. Y la ley de Moore, enunciada en 1965 («el número de transistores en un circuito integrado se
duplica cada dos años»)1 , se ha venido cumpliendo con precisión sorprendente.
Ese es un brevísimo resumen de la evolución en los niveles inferiores al de máquina convencional,
que le habrán explicado con más detalle en la asignatura «Introducción a la ingeniería». Pero también
en este nivel se han ido produciendo innovaciones. Veamos, de manera general, las más importantes.
bus de control
bus de direcciones
bus de datos (e instrucciones)
Bus del sistema
• En lugar de un solo registro acumulador (figura 2.3), hay un conjunto de registros, R0, R1...Rn,
que forman una memoria local (ML). Esta memoria es muy rápida pero de muy poca capacidad,
en relación con la de la MP: como mucho tiene 64 o 128 registros (y en cada registro se puede
almacenar una sola palabra).
• Hay un registro especial, el registro de estado (RE), que, entre otras cosas, contiene los indicadores
Z, N, C y V, cuya función hemos visto en el Tema 2 (apartado 3.3), y uno o varios bits que indican
el modo en que se encuentra el procesador: usuario, supervisor, etc.
• Hay un nuevo componente, la unidad de desplazamiento (UD) que permite realizar sobre cual-
quier registro las operaciones de desplazamientos y rotaciones que también hemos estudiado en el
apartado 3.3 del Tema 2.
• Los datos y las instrucciones se transfieren de una unidad a otra a través de buses. Un bus es un
conjunto de «líneas», conductores eléctricos (cables o pistas o de un circuito integrado) al que pue-
den conectarse varias unidades. El ancho del bus es su número de líneas2 . Se trata de un recurso
compartido: en cada momento, solo una de las unidades puede depositar un conjunto de bits en el
bus, pero varias pueden leerlo simultáneamente.
• Los periféricos se conectan al bus a través de registros incluidos en los controladores, que se llaman
puertos.
Solo es cuestión de convenio considerar que hay un único bus, el bus del sistema (la zona som-
breada) formado por tres subuses, o que realmente hay tres buses:
• El bus de direcciones transporta direcciones de la MP, o de los puertos, generadas por la UC.
• Por el bus de datos viajan datos leídos de o a escribir en la MP y los puertos, y también las instruc-
ciones que se leen de la MP y van a la UC.
• El bus de control engloba a las microórdenes que genera la UC y las que proceden de peticiones
que hacen otras unidades a la UC. Generalmente este bus no se muestra explícitamente: sus líneas
están agrupadas con las de los otros dos (o en un solo bus del sistema).
En la figura 2.7 no se han incluido otros registros que tiene el procesador y que son «transparentes»
en el nivel de máquina convencional. Esto quiere decir que ninguna instrucción puede acceder directa-
mente a ellos. El contador de programa (apartado 2.2) es un caso especial: en algunas arquitecturas es
también transparente, pero en otras (como la que estudiaremos en el capítulo 3) es uno de los registros
de la ML.
2
Esto es suponiendo un bus paralelo, en el que se transmite un flujo de bytes o de n bytes. También hay buses serie, en
los que se transmite un flujo de bits serializados (USB), o 2n flujos de bits, si el bus es bidireccional y tiene n enlaces (PCIe).
2.6 Modelos procesales 15
Un detalle que no refleja la figura es que, aunque toda la MP es «RAM» (es decir, de acceso
aleatorio), una pequeña parte de ella es «ROM» (es decir, de solo lectura). En esta parte están grabados
los programas que se ejecutan al iniciarse el sistema.
En sistemas muy sencillos, como algunos microcontroladores, éste es un modelo estructural que
refleja bastante bien la realidad. Pero incluso en ordenadores personales, el tener un solo bus es muy
ineficiente. En efecto, las transferencias entre la UCP y la MP son muy rápidas, pero hay una gran va-
riedad de periféricos, más o menos lentos. Esto conduce a separar los buses y a establecer una jerarquía
de buses, como puede apreciarse en la figura 2.8, que es el modelo estructural de un pecé del año 2010.
C0 C1 C2 C3
MP
L1i,L1d L1i,L1d L1i,L1d L1i,L1d
L2 1b,
480 Mbps
bus de memoria puerto
bus del sistema 64b, 8512 MB/s cámara
puente sur USB
(FSB: Front−Side Bus)
64b, 8512 MB/s (southbridge, o puerto escáner
USB
puente norte ICH: I/O
Controller Hub) puerto impresora
PCIe x16 (northbridge, o USB
2x16b, 2x8000 MB/s MCH: Memory
controlador puerto puerto ratón
Controler Hub) audio USB
grafico (GPU)
PCIe x1 puerto smart card
VGA DVI HDMI 2x500 MB/s USB
gigabit
ethernet
bus PCI
32b, 133MB/s switch
puente LPC 4b, 16 MB/s
PCI
Encadenamiento (pipelining)
Uno de los inventos que más ha contribuido a aumentar la velocidad de los procesadores es el enca-
denamiento (en inglés, pipelining). Es, conceptualmente, la misma idea de las cadenas de producción:
si la fabricación de un producto se puede descomponer en etapas secuenciales e independientes y si
se dispone de un operador para cada etapa, finalizada la etapa 1 del producto 1 se puede pasar a la
etapa 2 de ese producto y, simultáneamente con ésta, llevar a cabo la etapa 1 del producto 2, y así
sucesivamente.
El modelo procesal de la figura 2.5 supone que hay tres fases, o etapas, y que se llevan a cabo una
detrás de otra. En la primera se hace uso de la MP, pero no así en la segunda ni (salvo excepciones)
en la tercera. En la segunda se descodifica y se usa la ML para leer registros, y en la tercera se usa
la UAL y la ML para escribir. Suponiendo que en la ML se pueden leer dos registros y escribir en
otro al mismo tiempo, en cada fase se usan recursos distintos, por lo que pueden superponerse. El
resultado, gráficamente, es el que ilustra la figura 2.9. Con respecto a un modelo procesal estrictamente
secuencial, la velocidad de ejecución se multiplica (teóricamente) por tres.
tiempo
en instrucciones por segundo, no se multiplicaría por tres, sino por un número ligeramente superior a
uno3 .
En realidad, sí que hay tecnologías de memoria muy rápi-
das, pero son demasiado costosas para implementar con ellas una bus de direcciones
Esto significa (aunque la salida del programa lscpu no es muy explícita) que el microprocesador
contiene cuatro procesadores4 , cada uno de ellos acompañado de una MO de datos y una MO de
3
Concretamente (es un sencillo ejercicio comprobarlo), e ignorando los posibles conflictos, por 1,02.
4
A los procesadores, o «cores», el programa lscpu les llama «CPU». Nosotros llamamos UCP (CPU) al conjunto formado
18 Capítulo 2. La máquina de von Neumann y su evolución
instrucciones, de 32 KiB cada una, y que hay una MO de nivel 2 de 3 MiB común a los cuatro (es el
ordenador de la figura 2.8).
En todo caso, como decíamos al principio de este apartado, la existencia de las memorias ocultas
es transparente en el nivel de máquina convencional. Es decir, las instrucciones de máquina hacen
referencia a direcciones de la MP, y el programador que las utiliza no necesita ni siquiera saber de la
existencia de tales memorias ocultas. Es en el nivel de micromáquina en el que el hardware detecta los
aciertos o fracasos y los resuelve. En el peor (y poco frecuente) caso ocurrirá que habrá que acceder
hasta la MP, y la única consecuencia «visible»5 será que la ejecución de esa instrucción tardará más
tiempo.
Direcciones y contenidos de la MP
En la máquina de von Neumann las palabras tenían cuarenta bits, lo que se justificaba, como hemos
leído, porque en cada una podía representarse un número entero binario con suficiente precisión. Y en
cada acceso a la MP se leía o se escribía una palabra completa, que podía contener o bien un número
o bien dos instrucciones. Pero pronto se vio la necesidad de almacenar caracteres, se elaboraron varios
códigos para su representación y se consideró conveniente poder acceder no ya a una palabra (que podía
contener varios caracteres codificados), sino a un carácter. Algunos códigos primitivos eran de seis bits
y se llamó byte a cada conjunto de seis bits. Más tarde se impuso el código ASCII de siete bits y se
redefinió el byte como un conjunto de ocho bits (el octavo, en principio, era un bit de paridad), por lo
que también se suele llamar octeto.
En todas las arquitecturas modernas la MP es accesible por bytes, es decir, cada byte almacenado
tiene su propia dirección. Pero los datos y las instrucciones ocupan varios bytes, por lo que también es
accesible por palabras, medias palabras, etc. El número de bits de una palabra es siempre un múltiplo
de ocho, y coincide con el ancho del bus de datos6 . La expresión «arquitectura de x bits» se refiere
al número de bits de la palabra, normalmente 16, 32 o 64. Cuando inicia una operación de acceso, el
procesador pone una dirección en el bus de direcciones y le comunica a la MP, mediante señales de
control, si quiere leer (o escribir) un byte, o una palabra, o media palabra, etc.
En una arquitectura de dieciséis bits, la palabra de dirección d está formada por los bytes de direc-
ciones d y d + 1. Si es de treinta y dos bits, por los cuatro bytes de d a d + 3, etc. Y como hemos visto
en el Tema 2 (apartado 1.4), el convenio puede ser extremista mayor (el byte de dirección más baja
contiene los bits más significativos de la palabra) o el contrario, extremista menor.
Junto con el convenio de almacenamiento, hay otra variante: el alineamiento de las palabras. En
algunas arquitecturas no hay ninguna restricción sobre las direcciones que pueden ocupar las palabras,
pero en otras se requiere que las palabras no se «solapen». Así, en una arquitectura de dieciséis bits
sería obligatorio que las palabras tuviesen siempre dirección par, en una de treinta y dos bits, que fuesen
múltiplos de cuatro (figura 2.11(a)), etc. En este segundo caso, se dice que las direcciones de palabra
deben estar alineadas.
dir. byte dir. palabra dir. byte dir. palabra
0 0
1 1
0 0
2 2
3 3 1
2
4
5 4
6
7
27 27
28 28
29 29 27
30 28 30 28
29
Si el ancho del bus de datos está ligado a la longitud de palabra, el ancho del bus de direcciones está
ligado al espacio de direccionamiento, es decir, el número de direcciones diferentes que puede generar
el procesador. Un bus de direcciones de diez líneas puede transportar 210 direcciones distintas, por lo
que el espacio de direccionamiento es de 1 KiB; con dieciseis líneas el espacio es de 216 = 64 KiB, etc.
Observe que hablamos de «espacio de direccionamiento», no de «capacidad de la MP». La MP no
necesariamente alcanza la capacidad que es capaz de direccionar el procesador. Si las direcciones son
de 32 bits, la capacidad de direccionamiento es 232 = 4 GiB. No todos los sistemas tienen una memo-
ria de esta capacidad. Por este motivo, las direcciones generadas por el procesador se suelen llamar
direcciones virtuales. Un mecanismo que combina hardware (la «unidad de gestión de memoria») y
software (el «sistema de gestión de memoria») puede ocuparse de que los programas «vean» ese espa-
cio de direccionamiento como si realmente tuviesen disponible una MP de esa capacidad, ayudándose
de memoria auxiliar en disco. Este es el principio de funcionamiento de la llamada memoria virtual.
CISC y RISC
A un ordenador cuyo diseño sigue la tendencia «tradicional» se le llama CISC (Complex Instruc-
tion Set Computer), por contraposición a RISC (Reduced Instruction Set Computer).
A pesar de su nombre, lo que mejor caracteriza a los RISC no es el tener pocas instrucciones, sino
otras propiedades
• Tienen arquitectura «load/store». Esto quiere decir que los únicos accesos a la MP son para extraer
instrucciones y datos y para almacenar datos. Todas las operaciones de procesamiento se realizan en
registros del procesador.
• Sus instrucciones son «sencillas»: realizan únicamente las operaciones básicas, no como los CISC,
que tienen instrucciones sofisticadas que facilitan la programación, pero cuyo porcentaje de uso es
muy bajo.
• Su formato de instrucciones es «regular»: todas las instrucciones tienen la misma longitud y el nú-
mero de formatos diferentes es reducido. En cada formato, todos los bits tienen el mismo significado
para todas las instrucciones, y esto permite que la unidad de control sea más sencilla.
Actualmente las arquitecturas CISC y RISC conviven pacíficamente. Los procesadores diseñados
a partir de los años 1980 generalmente son RISC, pero otros (incluidos los más usados en los ordena-
dores personales), al ser evoluciones de diseños anteriores y tener que conservar la compatibilidad del
software desarrollado para ellos, son CISC.
Modos de direccionamiento
En la máquina de von Neumann, las instrucciones que hacen referencia a memoria (almacenar el
acumulador en una dirección de la MP, sumar al acumulador el contenido de una dirección, bifurcar a
una dirección...) indican la dirección de memoria directamente en el campo CD. Hay diversos meca-
nismos mediante los cuales la dirección real, o dirección efectiva no se obtiene directamente de CD,
2.7 Modelos funcionales 21
sino combinándolo con otras cosas, o, incluso, prescindiendo de CD, lo que facilita la programación y
hace más eficiente al procesador. Dos de ellas son:
• El direccionamiento indirecto consiste, en principio, en interpretar el contenido de CD como la
dirección de MP en la que se encuentra la dirección efectiva. Pero esto implica que para acceder al
operando, o para bifurcar, hay que hacer dos accesos a la MP, puesto que primero hay que leer la
palabra que contiene la dirección efectiva. A esta palabra se le llama puntero: apunta a el operando
o a la instrucción siguiente.
Como normalmente el procesador tiene una ML, es más eficiente que el puntero sea un registro. En
tal caso, la instrucción no tiene campo CD, solo tiene que indicar qué registro actúa como puntero.
• En el direccionamiento relativo a programa el contenido de CD es una distancia («offset»): un
número con signo que se suma al valor del contador de programa para obtener la dirección efectiva.
La utilidad es, sobre todo, para las instrucciones de bifurcación, ya que la dirección a la que hay
que bifurcar suele ser cercana a la dirección de la instrucción actual, por lo que el campo CD puede
tener pocos bits.
Veamos un ejemplo de uso de un modelo funcional que disponga de estos modos: sumar los cien
elementos de un vector que están almacenados en cien bytes de la MP, siendo D la dirección del
primero. Supongamos que todas las instrucciones ocupan una palabra de 32 bits (cuatro bytes), y que
la primera se almacena en la dirección I. Utilizando el lenguaje natural, esta secuencia de instrucciones
resolvería el problema, dejando el resultado en el registro R2:
Dirección Instrucción
I Poner D en el registro R0 (puntero)
I+4 Poner 100 en el registro R1 (contador de 100 a 1)
I+8 Poner a cero el registro R2 (suma)
I + 12 Copiar en R3 el contenido de la palabra de la MP
apuntada por R0 e incrementar el contenido de R0
I + 16 Sumar al contenido de R2 el de R3
I + 20 Decrementar el contenido de R1 en una unidad
I + 24 Si el contenido de R1 no es cero,
bifurcar a la instrucción que está en la dirección de ésta menos 12
Observe que:
• El programa escrito en lenguaje natural se llama pseudocódigo. No hay ningún procesador softwa-
re que traduzca del lenguaje natural al lenguaje de máquina. Las mismas instrucciones se pueden
escribir así en el lenguaje ensamblador que estudiaremos en el capítulo 4:
ldr r0,=D
mov r1,#100
mov r2,#0
bucle: ldrb r3,[r0],#1
add r2,r2,r3
subs r1,r1,#1
bne bucle
Subprogramas
Ocurre con mucha frecuencia que en varios puntos de un programa hay que realizar una determi-
nada secuencia de operaciones, siempre la misma, con operandos distintos cada vez. Por ejemplo, en
un mismo programa podríamos necesitar el cálculo de la suma de componentes de distintos vectores,
almacenados en distintas zonas de la MP y con distintas dimensiones. Una solución trivial es repetir esa
secuencia cuantas veces sea necesario, pero más razonable es tenerla escrita una sola vez en una zona
de la MP y «llamarla» cuando sea necesario mediante una instrucción que permita saltar a la primera
instrucción de la secuencia.
Ahora bien, para que esta solución (la «razonable») funcione correctamente deben cumplirse dos
condiciones:
• Pasar los parámetros: antes de saltar a esa secuencia (que en adelante llamaremos subprograma)
hay que indicarle los datos sobre los que ha de operar («parámetros de entrada»), y después de su
ejecución el subprograma ha de devolver resultados al que lo llamó («parámetros de salida»).
• Preservar la dirección de retorno: una vez ejecutado el subprograma hay que volver al punto
adecuado del programa que lo llamó, y este punto es distinto cada vez.
0
El mapa de memoria de la figura 2.12 corresponde a una situación en la
Programa
que hay un programa cargado a partir de la dirección 0 que llama dos veces
d1 llamada 1 a un subprograma cargado a partir de la dirección s. En este ejemplo, «lla-
mada 1» y «llamada 2» son instrucciones almacenadas en las direcciones
d2 llamada 2 d1 y d2 . Al final del subprograma habrá que poner alguna instrucción para
volver a la instrucción siguiente a la de llamada, que es distinta cada vez.
Para el paso de parámetros, lo más sencillo es colocarlos en registros
s Subprograma antes de la llamada, de donde los puede recoger el subprograma. Así, utili-
zando de nuevo pseudocódigo, las llamadas desde un programa que prime-
retorno ro tiene que sumar los elementos de un vector que empieza en la dirección
D1 y tiene L1 elementos y luego los de otro que empieza en D2 y tiene L2
elementos serían:
Figura 2.12. Llamadas
y retornos.
2.7 Modelos funcionales 23
Dirección Instrucción
I1 Poner D1 en el registro R0 (puntero)
I1 + 4 Poner L1 en el registro R1
I1 + 8 Llamar al subprograma (transferencia a s)
I1 + 12 [Instrucciones que operan con la suma del vector 1, en R2]
... ...
... ...
I2 Poner D2 en el registro R0 (puntero)
I2 + 4 Poner L2 en el registro R1
I2 + 8 Llamar al subprograma (transferencia a s)
I2 + 12 [Instrucciones que operan con la suma del vector 2, en R2]
... ...
... ...
Los parámetros de entrada son la dirección del vector y el número de elementos, que se pasan,
respectivamente, por R0 y R1. El parámetro de salida es la suma, que el subprograma ha de devolver
por R2. Las direcciones I1 + 8 e I2 + 8 son las que en la figura 2.12 aparecen como d1 y d2 .
El subprograma contendrá las instrucciones I + 8 a I + 24 del ejemplo inicial, pero tras la última
necesitará una instrucción para volver al punto siguiente a la llamada:
Dirección Instrucción
s Poner a cero el registro R2 (suma)
s+4 Copiar en R3 el contenido de la palabra de la MP
apuntada por R0 e incrementar el contenido de R0
s+8 Sumar al contenido de R2 el de R3
s + 12 Decrementar el contenido de R1 en una unidad
s + 16 Si el contenido de R1 no es cero,
bifurcar a la instrucción que está en la dirección que resulta de
restar 20 al contador de programa (s + 16 + 8 − 20 = s + 4)
s + 20 Retornar a la instrucción siguiente a la llamada
Las instrucciones de llamada y retorno son, como las bifurcaciones, instrucciones de transferencia
de control, ya mencionadas al final del apartado 2.3: hacen que la instrucción siguiente a ejecutar no
sea la que está en la dirección ya preparada en el contador de programa (CP), sino otra distinta. Pero
en una bifurcación la dirección de la instrucción siguiente se obtiene de la propia instrucción, mientras
que las instrucciones de llamada y retorno tienen que trabajar de forma conjunta: la de llamada, además
de bifurcar tiene que dejar en algún sitio la dirección de retorno, y la de retorno tiene que acceder a
ese sitio para poder volver.
En el ejemplo, al ejecutarse s + 20 se debe introducir en CP un valor (la dirección de retorno) que
tendrá que haber dejado la instrucción de llamada (I1 +8 o I2 +8; dirección de retorno: I1 +12 o I2 +12).
La cuestión es: ¿dónde se guarda ese valor?
Algunos procesadores tienen un registro especial, llamado registro de enlace. La instrucción de
llamada, antes de poner el nuevo valor en CP, introduce la dirección de la instrucción siguiente en
ese registro. La instrucción de retorno se limita a copiar el contenido del registro de enlace en CP. Es
una solución sencilla y eficaz al problema de preservación de la dirección de retorno. Pero tiene un
inconveniente: si el subprograma llama a su vez a otro subprograma, la dirección de retorno al primero
se pierde. Una solución más general es el uso de una pila.
24 Capítulo 2. La máquina de von Neumann y su evolución
Pilas
Una pila es un tipo de memoria en la que solo se pueden leer los datos en el orden inverso en
que han sido escritos, siguiendo el principio de «el último en entrar es el primero en salir». Las dos
operaciones posibles son push (introducir o apilar: escribir un elemento en la pila) y pop (extraer o
desempilar: leer un elemento de la pila). Pero, a diferencia de lo que ocurre con una memoria de acceso
aleatorio, no se puede introducir ni extraer en cualquier sitio. Únicamente se tiene acceso a la cima de la
pila, de modo que un nuevo elemento se introduce siempre sobre el último introducido, y la operación
de extracción se realiza siempre sobre el elemento que está en la cima, como indica la figura 2.13. Por
tanto, en una memoria de pila no hay direcciones. La primera posición es el fondo de la pila. Si la pila
está vacía, no hay cima; si solo contiene un elemento, la cima coincide con el fondo.
PUSH POP
cima
p
n+1 cima
cima
p p p
n n n
cima
p p p p
n−1 n−1 n−1 n−1
p p p p
0 0 0 0
PILA INICIAL PILA DESPUES PILA INICIAL PILA DESPUES
DE INTRODUCIR DE EXTRAER
Una memoria de pila se puede implementar en hardware, y hay procesadores con arquitectura
diseñada para trabajar con operandos en la pila. En ellos, la instrucción «sumar», por ejemplo, no hace
referencia a ninguna dirección de memoria ni utiliza ningún registro: lo que hace es extraer de la pila el
elemento que está en la cima y el que está debajo de él, sumarlos, y almacenar el resultado en el lugar
que ocupaba el segundo. Pero estos procesadores son raros8 . Lo que sí tienen todos los procesadores
hardware es un mecanismo para «simular» una pila en una parte de la MP, que consiste en lo siguiente:
El procesador tiene registro llamado puntero de pila,
PP, que contiene en todo momento la primera dirección de
MP
la MP libre por encima de la pila (figura 2.14) (o bien la
dirección de la cima). En la instrucción de llamada a sub-
PP programa, la UC guarda la dirección de retorno (el conte-
(PP) = d nido del contador de programa en ese momento) en la pila,
haciendo un push. Esta operación push se realiza en dos pa-
d
+k −k cima sos: primero se escribe la dirección de retorno en la direc-
(pop) (push)
zona
ción apuntada por PP y luego se decrementa el contenido
reservada de PP en tantas unidades como bytes ocupe una dirección
para la pila
(si el convenio es que PP apunta a la cima, se invierten los
fondo
pasos).
La ejecución de la instrucción de retorno se hace con un
Figura 2.14 Una pila en una RAM. pop para extraer de la pila la dirección de retorno y ponerla
8
Un ejemplo es la «máquina virtual Java», que se utiliza como paso intermedio en la ejecución de programas escritos en
el lenguaje Java, que estudirá usted en la asignatura «Programación». Normalmente se implementa con software.
2.8 Comunicaciones con los periféricos e interrupciones 25
• Las operaciones push y pop mencionadas son transparentes al programador. Es decir, éste sola-
mente tiene que saber que existe una instrucción (en ensamblador se suele llamar CALL) que le
permite transferir el control a una dirección donde comienza un subprograma, y que la última
instrucción de este subprograma debe ser la de retorno (en ensamblador, RET).
• El «anidamiento» de subprogramas (que un subprograma llame a otro, y éste a otro... o, incluso,
que un subprograma se llame a sí mismo de manera recursiva) no plantea ningún problema: las
direcciones de retorno se irán apilando y recuperando en el orden adecuado. (El único problema
práctico radica en que la pila no es infinita).
• La pila puede utilizarse también para transmitir parámetros entre el programa y el subprograma:
antes de CALL, el subprograma utiliza instrucciones «PUSH» para introducirlos en la pila, y el
subprograma puede acceder a ellos con un modo de direccionamiento que sume una distancia al
puntero de pila.
La forma de programar estas operaciones depende de las características de cada periférico, funda-
mentalmente, de su tasa de transferencia. Hay dos tipos (son los que el sistema de gestión de ficheros
conoce como «ficheros especiales de caracteres» y «ficheros especiales de bloques», Tema 2, aparta-
do 5.1):
• Periféricos de caracteres: son los «lentos», en los que la tasa de transferencia de datos es sustan-
cialmente inferior a la velocidad con que el procesador puede procesarlos (teclado, ratón, sensor
de temperatura, etc.). Cada vez que el periférico está preparado para enviar o recibir un dato
se ejecuta una instrucción que transfiere el dato (normalmente, un byte o «carácter») entre un
registro del procesador y el puerto correspondiente.
26 Capítulo 2. La máquina de von Neumann y su evolución
• Periféricos de bloques: son aquellos cuya tasa de transferencia es comparable a la velocidad del
procesador (disco, pantalla gráfica, USB, controlador ethernet, etc.), y se hace necesario que el
controlador del periférico se comunique directamente con la memoria, leyendo o escribiendo en
cada operación no ya un byte, sino un bloque de bytes. Es la técnica de DMA (acceso directo a
memoria, Direct Memory Access).
Interrupciones
El mecanismo de interrupciones, presente en todos los procesadores hardware de uso general actua-
les, permite intercambiar datos entre el procesador y los periféricos, implementar llamadas al sistema
operativo (mediante instrucciones de interrupción por software) y tratar situaciones excepcionales (ins-
trucciones inexistentes, fallos en el acceso a la memoria, instrucciones privilegiadas, etc.).
El tratamiento de las interrupciones es uno de los asuntos más fascinantes y complejos de la arqui-
tectura de procesadores y de la ingeniería en general. Fascinante, porque gracias a él dos subsistemas de
naturaleza completamente distinta, el hardware (material) y el software (intangible) trabajan conjunta-
mente, y de esa simbiosis diseñada resultan los artefactos que disfrutamos actualmente. Y es complejo,
sobre todo para explicar, porque los diseñadores de procesadores han ingeniado distintas soluciones.
En el capítulo siguiente concretaremos los detalles de una de esas soluciones. De momento, seña-
lemos algunos aspectos generales:
• La atención a una interrupción consiste en ejecutar una rutina de servicio, un programa que,
naturalmente, tiene que estar previamente cargado en la MP.
• Atender a una interrupción implica abandonar temporalmente el programa que se está ejecutando
para pasar a la rutina de servicio. Y cuando finaliza la ejecución de la rutina de servicio, el
procesador debe volver al programa interrumpido para continuar con su ejecución.
• Esto recuerda a los subprogramas, pero hay una diferencia fundamental: la interrupción puede no
tener ninguna relación con el programa, y puede aparecer en cualquier momento. Por tanto, no
basta con guardar automáticamente la dirección de retorno (dirección de la instrucción siguiente a
aquella en la que se produjo la atención a la interrupción), también hay que guardar los contenidos
del registro de estado y de los registros que use la rutina (para que el programa continúe como si
nada hubiese pasado). Naturalmente, al volver al programa interrumpido deben recuperarse.
• Como puede haber muchas causas que provocan la interrupción, el mecanismo debe incluir la
identificación para poder ejecutar la rutina de servicio que corresponda. En algunos diseños esta
identificación se hace por software, en otros por hardware, o, lo que es más frecuente, mediante
una combinación de ambos.
2.9. Conclusión
Hemos visto los conceptos más importantes del nivel de máquina convencional que se implementan
en los procesadores hardware actuales. En este sentido, ya hemos cubierto uno de los objetivos plan-
teados. Pero es un hecho que estos conceptos no se asimilan completamente si no se materializan y se
practican sobre un procesador concreto. Eso es lo que haremos en los dos capítulos siguientes.
Capítulo 3
El procesador BRM
ARM (Advanced RISC Machines) es el nombre de una familia de procesadores, y también el de una
compañía, ARM Holdings plc. A diferencia de otras, la principal actividad de esta empresa no consiste
en fabricar microprocesadores, sino en «licenciar» la propiedad intelectual de sus productos para que
otras empresas puedan incorporarlos en sus diseños. Por este motivo, aunque los procesadores ARM
sean los más extendidos, la marca es menos conocida que otras. En 2010 se estimaba en 25 millardos
el número total acumulado de procesadores fabricados con licencia de ARM, a los que se sumaban 7,9
millardos en 20111 . Estos procesadores se encuentran, sobre todo, en dispositivos móviles (el 90 % de
los smartphones contienen al menos un SoC basado en ARM), pero también en controladores de discos,
en televisores digitales, en dispositivos de ayuda al frenado, en cámaras digitales, en impresoras, en
consolas, en routers etc.
Parece justificado basarse en ARM para ilustrar los principios de los procesadores hardware ac-
tuales. Ahora bien, se trata de una familia de procesadores que comparten muchas características, tan
numerosas que sería imposible, en los créditos asignados a este Tema, describirlas por completo. Es por
eso que nos hemos quedado con los elementos esenciales, esquematizando de tal modo la arquitectura
que podemos hablar de un «ARM simplificado», al que hemos bautizado como «BRM» (Basic RISC
Machine).
BRM es totalmente compatible con los procesadores ARM. De hecho, todos los programas que se
presentan en el capítulo siguiente se han probado con un simulador de ARM.
En este capítulo, tras una descripción somera de los modelos estructural y procesal, veremos con
todo detalle el modelo funcional, es decir, todas las instrucciones, sus modos de direccionamiento y
sus formatos. Como la expresión binaria es muy farragosa, iremos introduciendo las expresiones en
lenguaje ensamblador (apartado 1.2). Hay varios lenguajes para la arquitectura ARM, y en el capítulo
siguiente seguiremos la sintaxis del ensamblador GNU, que es también el que se utiliza en el simulador
ARMSim# que nos servirá para practicar. De todas formas, en este capítulo no hay programas, solo
instrucciones sueltas, y los convenios (nemónicos para códigos de operación y registros) son los mismos
en todos los ensambladores.
1
Fuentes: http://www.arm.com/annualreport10/20-20-vision/forward-momentum.html
http://financialreports.arm.com/downloads/pdfs/ARM_AR11_Our_Story_v1.pdf
27
28 Capítulo 3. El procesador BRM
En la parte superior izquierda, el registro de direcciones, RA, contendrá una dirección de memoria
o de puerto de entrada/salida de donde se quiere leer o donde se pretende escribir. Tanto este registro
como el bus A tienen 32 bits, por lo que el espacio de direccionamiento es de 232 bytes (4 GiB). Este
espacio está compartido entre la MP y los puertos de entrada/salida. Es decir, algunas direcciones co-
rresponden a puertos. Pero estas direcciones no están fijadas: será el diseñador de los circuitos externos
que conectan la MP y los periféricos a los buses el que, mediante circuitos descodificadores, determine
qué direcciones no son de la MP sino de puertos.
Abajo a la izquierda hay dos registros de datos. En RDL se introducirán los datos que se lean del
exterior, y en RDE los que vayan a escribirse.
Si hay un registro, RDL, en el que se introducen los datos leídos (de la MP o de los puertos pe-
riféricos), hace falta otro distinto, RI (registro de instrucción), para introducir las instrucciones que
se leen de la MP. Pero no solo hay uno, sino tres: RIL, RID y RIE. La explicación se encuentra en el
modelo procesal de BRM, en el que las fases de ejecución de las instrucciones se solapan en el tiempo
(figura 2.9): mientras una se ejecuta (la que está en RIE) la siguiente (en RID) se está descodificando y
la siguiente a ésta (en RIL) se está leyendo.
Cuando la UC ha de ejecutar una transferencia con el exterior (ya sea en el momento de la lectura
de una instrucción o como respuesta a determinadas instrucciones que veremos en el apartado 3.5),
pone la dirección en el registro RA, si es una salida de datos pone también el dato en RDE, y activa las
señales req, r/w y size. Si ha sido una petición de lectura, después de un ciclo de reloj la UC introduce
2
Para comprender el nivel de máquina convencional no es estrictamente necesario, pero sí instructivo, conocer la funcio-
nalidad de estas señales de control:
clk es la entrada de reloj, una señal periódica que provoca las transiciones de cada fase a la siguiente en el proceso de ejecu-
ción de las instrucciones.
reset fuerza que se introduzca 0 en el contador de programa.
irq es la petición de interrupción externa.
req es la petición del procesador a la memoria o a un periférico, y va acompañada de r/w, que indica si la petición es de
lectura (entrada, r/w = 1) o de escritura (salida, r/w = 0) y de size (dos líneas) en las que el procesador codifica si la petición
es de un byte, de media palabra (16 bits) o de una palabra (32 bytes) (aunque en BRM, versión simplificada de ARM, no hay
accesos a medias palabras).
wait la puede poner un controlador externo (el árbitro del bus) para que el procesador se abstenga, durante el tiempo que
está activada, de acceder a los buses A y D.
Estas señales son un subconjunto de las que tienen los procesadores ARM.
3.1 Modelo estructural 29
VAddr[31:0]
RA incrementador
(+4)
bus UAL
clk
reset
irq
UAL R0 UC
req
operando 2 operando 1 R1
r/w
size
RID
bus B
WData[31:0] RData[31:0]
La memoria local contiene dieciséis registros, R0 a R15. Los trece primeros son de propósito ge-
neral (pueden contener operandos, resultados o direcciones), pero los tres últimos tienen funciones
especiales que ya hemos avanzado en el capítulo anterior:
• R13, que también se llama SP (Stack Pointer), es el puntero de pila.
• R14, también conocido como LR (Link Register) es el registro de enlace.
• R15, o PC (Program Counter) es el contador de programa3 .
3
Venimos traduciendo muchas siglas al castellano (UCP por CPU, UAL por ALU, etc.), pero para estos tres registros, y
para algún otro, es preferible conservar sus nombres en inglés, porque son los que se utilizan en el lenguaje ensamblador.
30 Capítulo 3. El procesador BRM
Hay dos R13 y dos R14. El motivo es que el procesador en todo momento está en uno de entre dos
modos de funcionamiento: modo usuario o modo supervisor. Normalmente está en modo usuario, pero
cuando pasa a atender a una interrupción y cuando recibe la señal reset cambia a modo supervisor.
Y en cada modo se reserva una zona de la MP distinta para la pila. El procesador, según el modo en
que se encuentre, elije uno u otro registro. Por ejemplo, ante una instrucción que diga «sacar de la
pila (pop) y llevar a R5», el procesador, si está en modo usuario, copiará en R5 el contenido en la
dirección apuntada por R13 (e incrementará R13), mientras que si está en modo supervisor lo hará con
la dirección apuntada por R13_SVC (e incrementará R13_SVC). Con el registro de enlace, R14 pasa
algo parecido: la instrucción «bifurca y enlaza» (BL) guarda la dirección de retorno en R14 si está en
modo usuario, o en R14_SVC si está en modo supervisor.
Registro de estado
El registro de estado se llama «CPSR» (Current Program Status Register), y contiene las informa-
ciones esenciales del estado del procesador que hay que guardar al suspender el programa para atender
a una interrupción. Precisamente se guardan en SPSR (Saved Program Status Register). Al volver del
servicio de la interrupción se copia el contenido de SPSR en CPSR.
31 28 27 7 4 0
N Z C V 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 I 0 0 Modo
reloj
(clk)
Así pues, las instrucciones de bifurcación retrasan la ejecución, tanto más cuanto menos instruc-
ciones tenga el bucle. En el apartado 4.5 veremos que en BRM es posible, en ciertos casos, prescindir
de instrucciones de bifurcación, gracias a que todas las instrucciones son condicionadas.
5
En algunos modelos de ARM la cadena tiene profundidad 4 o más, por lo que la penalización es de tres o más ciclos. Y
mucho mayor en los procesadores que tienen cadenas de gran profundidad. Generalmente la UC incluye una pequeña memoria
(«prefetch buffer») en la que se adelanta la lectura de varias instrucciones y unos circuitos que predicen si la condición de un
bifurcación va a cumplirse o no, y actúan en consecuencia. Pero esto es propio del nivel de micromáquina, que no estudiamos
aquí.
32 Capítulo 3. El procesador BRM
Las instrucciones aritméticas de BRM operan sobre enteros representados en treinta y dos bits.
• El convenio para número negativos es el de complemento a 2.
• El convenio de almacenamiento en memoria es extremista menor.
• Las direcciones de la MP deben estar alineadas (figura 2.11(a)): un número representado en una
palabra (32 bits) tiene que tener su byte menos significativo en una dirección múltiplo de cuatro.
Representación de instrucciones
Todas las instrucciones ocupan treinta y dos bits y también deben estar almacenadas en la MP en
direcciones alineadas: toda instrucción tiene que estar en una dirección múltiplo de cuatro (la dirección
de una palabra es la de su byte menos significativo).
Una particularidad de esta arquitectura es que todas las instrucciones pueden ser condicionadas, es
decir, ejecutarse, o no, dependiendo del estado de los indicadores. En la mayoría de los procesadores
solo las bifurcaciones pueden ser condicionadas.
Hay cuatro tipos de instrucciones, y cada tipo tiene su formato. Pero los cuatro formatos comparten
dos campos (figura 3.4):
31 28 27 26 0
Cond Tipo
• En los cuatro bits más significativos, «Cond» codifica la condición para que la instrucción se ejecute.
La tabla 3.1 resume las dieciséis condiciones. Para cada una se da también un código nemónico,
que es el que se utiliza como sufijo del código de la instrucción en el lenguaje ensamblador. Por
ejemplo, el código de la instrucción de bifurcación es «B». Pero «BEQ» es «bifurcación si cero»
(Z = 1). «BAL» («branch always») es lo mismo que «B»: «bifurcación incondicional». En la tabla,
«significado», cuando contiene un signo de comparación, se refiere a la condición expresada en
términos de lo que resulta si previamente se ha realizado en la UAL una operación de comparación
de dos operandos (instrucción CMP, que veremos en el siguiente apartado). Puede usted comprobar
que estos significados son coherentes con lo que vimos sobre los indicadores en el apartado 3.3 del
Tema 2.
• Los dos bits siguientes, «Tipo», indican el tipo de instrucción:
Tipo 00: Instrucciones de procesamiento y de movimiento de datos entre registros
(dieciséis instrucciones).
Tipo 01: Instrucciones de transferencia de datos con el exterior
(cuatro instrucciones).
Tipo 10: Instrucciones de bifurcación (dos instrucciones).
Tipo 11: Instrucción de interrupción de programa.
A diferencia de otros procesadores, éste no tiene instrucciones específicas para desplazamientos y
rotaciones: estas operaciones se incluyen en las instrucciones de procesamiento y movimiento.
3.4 Instrucciones de procesamiento y de movimiento 33
Según esto, BRM tendría un número muy reducido de instrucciones: veintitrés. Pero, como en-
seguida veremos, la mayoría tienen variantes. De hecho, desde el momento en que todas pueden ser
condicionadas, ese número ya se multiplica por quince (la condición «nunca» convierte a todas las
instrucciones en la misma: no operación).
Describimos a continuación todas las instrucciones, sus formatos y sus variantes. Para expresar
abreviadamente las transferencias que tienen lugar usaremos una notación ya introducida antes, en la
figura 2.5: si Rd es un registro, (Rd) es su contenido, y «(Rf) → Rd» significa «copiar el contenido de
Rf en el registro Rd».
Debe usted entender esta descripción como una referencia. Lo mejor es que haga una lectura rápida,
sin pretender asimilar todos los detalles, para pasar al capítulo siguiente y volver a éste cuando sea
necesario.
Estas instrucciones realizan operaciones aritméticas y lógicas sobre dos operandos (o uno, en el
caso de la complementación), y de «movimiento» de un registro a otro, o de una constante a un re-
gistro. El entrecomillado es para resaltar que el nombre es poco afortunado: cuando algo «se mueve»
desaparece de un lugar para aparecer en otro. Y aquí no es así: una instrucción de «movimiento» de R0
a R1 lo que hace, realmente, es copiar en R1 el contenido de R0, sin que desaparezca de R0.
El formato de estas instrucciones es el indicado en la figura 3.5.
Uno de los operandos, el «operando1», es el contenido de un registro (R0 a R15) codificado en
los cuatro bits del campo «Rn». El otro depende del campo «Op2» y del bit I. El resultado queda
(en su caso) en el registro codificado en el campo «Rd» (registro destino). Las dieciséis operaciones
codificadas en el campo «Cop» son las listadas en la tabla 3.2.
34 Capítulo 3. El procesador BRM
31 28 25 24 21 20 19 16 15 12 11 0
Los «nemónicos» son los nombres que reconoce el lenguaje ensamblador. Por defecto, entenderá
que la instrucción se ejecuta sin condiciones, o sea, el ensamblador, al traducir, pondrá «1110» en
el campo «Cond» (tabla 3.1). Para indicar una condición se añade el sufijo correspondiente según la
tabla 3.1. Así, ADDPL significa «sumar si N = 0».
El bit «S» hace que el resultado, además de escribirse en Rd (salvo en los casos de TST, TEQ, CMP
y CMN) afecte a los indicadores N, Z, C y V (figura 3.2): si S = 1, los indicadores se actualizan de
acuerdo con el resultado; en caso contrario, no. Para el ensamblador, por defecto, S = 0; si se quiere
que el resultado modifique los indicadores se añade «S» al código nemónico: ADDS, ADDPLS, etc. Sin
embargo, las instrucciones TST, TEQ, CMP y CMN siempre afectan a los indicadores (el ensamblador
siempre pone a uno el bit S).
Nos falta ver cómo se obtiene el operando 2, lo que depende del bit «I»: si I = 1, se encuentra
inmediatamente en el campo «Op2»; si I = 0 se calcula a través de otro registro.
Operando inmediato
En los programas es frecuente tener que poner una constante en un registro, u operar con el con-
tenido de un registro y una constante. Los procesadores facilitan incluir el valor de la constante en la
propia instrucción. Se dice que es un operando inmediato. Pero si ese operando puede tener cualquier
valor representable en 32 bits, ¿cómo lo incluimos en la instrucción, que tiene 32 bits?6
6
Recuerde (apartado 2.7) que BRM es un RISC. En los CISC no existe este problema, porque las instrucciones tienen un
número variable de bytes.
3.4 Instrucciones de procesamiento y de movimiento 35
Para empezar, la mayoría de las constantes que se usan en un programa son números pequeños.
Según el formato de la figura 3.5, disponemos de doce bits, por lo que podríamos incluir números com-
prendidos entre 0 y 212 − 1 = 4,095 (luego veremos cómo los números pueden ser también negativos).
Concretemos con un primer ejemplo. A partir de ahora vamos a ir poniendo ejemplos de instruccio-
nes y sus codificaciones en binario (expresadas en hexadecimal) obtenidas con un ensamblador. Debería
usted hacer el ejercicio, laborioso pero instructivo y tranquilizador, de comprobar que las codificaciones
binarias son acordes con los formatos definidos.
Para introducir el número 10 (decimal) en R11, la instrucción, codificada en hexadecimal y en
ensamblador es:
E3A0B00A MOV R11,#10
(Ante un valor numérico, 10 en este ejemplo, el ensamblador lo entiende como decimal. Se le puede
indicar que es hexadecimal o binario anteponiéndole los prefijos habituales, «0x» o «0b»).
Escribiendo en binario y separando los bits por campos según la figura 3.5 resulta:
1110 - 00 - 1 - 1101 - 0 - 0000 - 1011 - 0000000000001010
Operando en registro
11 7 6 5 4 3 0 Cuando el bit 25, o bit I, es I = 0, el operando 2 se ob-
despl_inmed D 0 Rm tiene a partir de otro registro, el indicado en los cuatro bits
menos significativos del campo Op2 (Rm). Si el resto de
11 8 7 6 5 4 3 0 los bits de ese campo (de 4 a 11) son ceros, el operando es
Rs 0 D 1 Rm exactamente el contenido de Rm. Pero si no es así, ese con-
tenido se desplaza o se rota de acuerdo con los siguientes
Figura 3.7 Operando en registro.
convenios (figura 3.7):
• bit 4: si es 0, los bits 7 a 11 contienen el número de bits que ha de desplazarse Rm.
si es 1, el número de bits está indicado en los cinco bits menos significativos
de otro registro, Rs.
El ensamblador GNU entiende ADD R1,#-5 y lo traduce igual que SUB R1,#5. Sin embargo, ARMSim# da error (es
7
• bits 5 y 6 (D): tipo de desplazamiento (recuerde el apartado 3.3 y la figura 3.2 del Tema 2):
D = 00: desplazamiento lógico a la izquierda (LSL).
D = 01: desplazamiento lógico a la derecha (LSR).
D = 10: desplazamiento aritmético a la derecha (ASR).
D = 11: rotación a la derecha (ROR).
A continuación siguen algunos ejemplos de instrucciones de este tipo, con la sintaxis del ensam-
blador y su traducción, acompañada cada una de un pequeño comentario (tras el símbolo «@»). Debería
usted comprobar mentalmente que la instrucción hace lo que dice el comentario. Cuando, después de
estudiar el siguiente capítulo, sepa construir programas podrá comprobarlo simulando la ejecución.
Ejemplos con operando inmediato:
E2810EFA ADD R0, R1, #4000 @ (R1) + 4000 → R0
E2500EFF SUBS R0, R0, #4080 @ (R0) - 4080 → R0,
@ y pone indicadores según el resultado
E200001F AND R0, R0, #0x1F @ 0 → R0[5..31]
@ (pone a cero los bits 5 a 31 de R0)
E38008FF ORR R0, R0, #0xFF0000 @ 1 → R0[24..31]
@ (pone a uno los bits 24 a 31 de R0)
E3C0001F BIC R0, R0, #0x1F @ 0 → R0[0..4]
@ (pone a cero los bits 0 a 4 de R0)
La última operación sería equivalente a AND R0, R0, #0xFFFFE0, pero ese valor inmediato no se
puede generar.
Ejemplos con operando en registro:
E1A07000 MOV R7, R0 @ (R0) → R7
E0918000 ADDS R8, R1, R0 @ (R1) + (R0) → R8, y pone indicadores
E0010002 AND R0, R1, R2 @ (R1) AND (R2) → R0 y pone indicadores
E1A01081 MOV R1, R1, LSL #1 @ 2×(R1) → R1
E1A00140 MOV R0, R0, ASR #2 @ (R0)÷4 → R0
E1B04146 MOVS R4, R6, ASR #5 @ (R6)÷32 → R4, y pone indicadores
E0550207 SUBS R0, R5, R7, LSL #4 @ (R5) + 16×(R7) → R0,
@ y pone indicadores
Movimientos con los registros de estado MRS (copia CPSR o SPSR en Rd)
31 28 27 23 22 21 16 15 12 11 0
Cond 0 0 0 1 0 Ps 0 0 1 1 1 Rd 0 0 0 0 0 0 0 0 0 0 0 0
Como hay dos registros de estado, CPSR y
Ps = 0: CPSR; Ps = 1: SPSR
SPSR (apartado 3.1), que no están incluidos en MSR (copia Rm en CPSR o SPSR)
31 28 27 23 22 21 12 11 0
la memoria local, se necesitan cuatro instruccio- 4 3
Cond 0 0 0 1 0 Pd 1 0 1 0 0 1 1 1 1 1 0 0 0 0 0 0 0 0 Rm
nes para copiar su contenido a un registro o a la
Pd = 0: CPSR; Pd = 1: SPSR
inversa. MSR (copia Rm o inmediato en CPSR o SPSR, sólo indicadores)
31 28 27 23 22 21 12 11 0
Estas cuatro instruccciones son también de
Cond 0 0 I 1 0 Pd 1 0 1 0 0 0 1 1 1 1
este grupo (T = 00) y comparten los códigos de I = 0: Rm; I = 1: inmediato
operación con los de TST, TEQ, CMP y CMN, pero 11 4 3 0
11 8 7 0
MRS simplemente hace una copia del contenido de un registro de estado en un registro de la ML,
pero hay que tener en cuenta que en modo usuario SPSR no es accesible:
E10F0000 MRS R0, CPSR @ (CPSR) → R0
E14F0000 MRS R0, SPSR @ (SPSR) → R0 (solo en modo supervisor)
Para copiar en un registro de estado hay dos versiones. En una se modifica el registro completo
(pero en modo usuario solo pueden modificarse los indicadores, o «flags»: bits N, Z, C y V, figura 3.2),
y en otra solo los indicadores. Esta segunda tiene, a su vez, dos posibilidades: copiar los cuatro bits más
significativos de un registro (Rm), o los de un valor inmediato de 8 bits extendido a 32 bits y rotado a
la derecha.
Ejemplos:
E129F005 MSR CPSR, R5 @ Si modo supervisor, (R5) → CPSR
@ Si modo usuario, (R5[28..31]) → CPSR[28..31]
E128F005 MSR CPSR_flg, R5 @igual que la anterior en modo usuario
E328F60A MSR CPSR_flg, #0xA00000 @ 1 → N, C; 0 → Z, V
31 28 25 24 22 20 19 16 15 12 11 0
Cond 0 1 I P U B WL Rn Rd Dist
A pesar de la aparente complejidad del formato, hay solo cuatro instrucciones, que se distinguen
por los bits 20 (L: load) y 22 (B: byte). En la tabla 3.5 se dan sus códigos en ensamblador y se resumen
sus funciones.
L B Nemónico Acción Significado
0 0 STR (Rd) → M[DE] Copia el contenido de Rd en la
palabra de dirección DE
0 1 STRB (Rd[0..7]) → M[DE] Copia los ocho bits menos
significativos de Rd en el
byte de dirección DE
1 0 LDR (M[DE]) → Rd Copia la palabra de dirección DE en el
registro Rd
1 1 LDRB (M[DE]) → Rd[0..7] Copia el byte de dirección DE en los
0 → Rd[8..31] ocho bits menos significativos de Rd, y
pone los demás bits a cero
Tabla 3.3 Instrucciones de transferencia de datos.
«DE» es la dirección efectiva (apartado 2.7), que se obtiene a partir del contenido de un registro de
base, Rn (que puede actuar como registro de índice). «M[DE]» puede ser una posición de la memoria
o un puerto de entrada/salida, y «(M[DE])» es su contenido.
3.5 Instrucciones de transferencia de datos 39
El cálculo de la dirección efectiva depende de los bits I (25) y P (24). Si I = 0, el contenido del
campo «Dist» es una «distancia» (offset), un número entero que se suma (si el bit 23 es U = 1) o se resta
(si U = 0) al contenido del registro de base Rn. Si I = 1, los bits 0 a 3 codifican un registro que contiene
la distancia, sobre el que se puede aplicar un desplazamiento determinado por los bits 4 a 11, siguiendo
el mismo convenio que para un operando en registro (figura 3.7). Por su parte, P determina cómo se
aplica esta distancia al registro de base Rn. W (write) no influye en el modo de direccionamiento, indica
solamente si el resultado de calcular la dirección efectiva se escribe o no en el registro de base.
Resultan así cuatro modos de direccionamiento:
Si R3 apunta inicialmente al primer elemento del vector y cada elemento ocupa una palabra (cuatro
bytes), cada vez que se ejecute la instrucción R3 quedará apuntando al elemento siguiente, suponiendo
que estén almacenados en direcciones crecientes. Si estuviesen en direcciones decrecientes pondría-
mos:
E4135004 LDR R5, [R3], #-4 @ (M[(R3)]) → R5; (R3) - 4 → R3
Podemos escribir la misma instrucción como LDRB R6, [R0], pero en este caso el ensamblador la
traduce a una forma equivalente, la que tiene P = 1
E5D06000 LDRB R6, [R0] @ Igual que LDRB R6, [R0, #0]
E5235004 STR R5, [R3, #-4]! @ como antes, pero a R3 se le resta cuatro
En los modos «registro» se utiliza un registro auxiliar, Rm, cuyo contenido se puede desplazar o
rotar siguiendo los mismos convenios de la figura 3.7.
• distancia = despl(Rm)
• DE = (Rn)
• (Rn)±distancia → Rn (independientemente de W)
Ejemplos:
Instrucciones de bifurcación
Las dos instrucciones de bifurcación, B (branch) y BL (branch and link), tienen el formato de la
figura 3.10. El bit 24 (L) distingue entre B (L = 0) y BL (L = 1).
31 28 27 26 25 24 23 0
Cond 1 0 1 L Dist
Ambas usan direccionamiento relativo: el campo «Dist» se interpreta como un número con signo
que se suma al valor que tenga en el momento de su ejecución el contador de programa (dirección de la
instrucción más ocho). Pero antes de sumarse, y como todas las instrucciones tienen que estar en direc-
ciones alineadas (múltiplos de cuatro), se desplaza dos bits a la izquierda. De este modo, la dirección
efectiva resulta: DE = dirección de la instrucción + 8 + 4×Dist. Con 26 bits se puede representar un
número con signo entre −225 y 225 − 1, es decir, se puede bifurcar a una dirección comprendida en un
rango aproximado de ±32 MiB sobre la dirección de la instrucción, lo que es más que suficiente para
la mayoría de los casos.
La instrucción B simplemente modifica el contenido del registro PC (R15) copiando en él la di-
rección efectiva. BL, además, modifica el contenido del registro de enlace, LR (R14) copiando en él la
dirección de la instrucción más cuatro. De este modo, se bifurca a un subprograma del que se vuelve
con MOV PC, LR (o MOV R15, R14).
42 Capítulo 3. El procesador BRM
Veremos ejemplos de uso de estas instrucciones en los apartados 4.2 y 4.6, cuando ya conozcamos
cómo el ensamblador nos puede liberar de la tediosa tarea de calcular la distancia.
Esta instrucción, de código ensamblador «SWI», tiene un formato muy sencillo, el de la figura 3.11.
31 28 27 26 25 24 23 0
El efecto de la instrucción es, simplemente, provocar una interrupción del programa en el que
está incluida (siempre que la condición se cumpla, como en todas las instrucciones). Como indica la
figura, el contenido de los 24 bits menos significativos es indiferente para el procesador, pero la rutina
de servicio puede recuperar el contenido de la dirección en la que está almacenada la instrucción y
analizar esos bits para realizar la acción oportuna, de acuerdo con algún convenio establecido.
En el apartado 2.8 decíamos que unos procesadores siguen el convenio de que el espacio de direc-
cionamiento para los puertos de periféricos es independiente del de la memoria, mientras que en otros
el espacio de direccionamiento es compartido. Pues bien, BRM sigue el segundo convenio. Ciertas
direcciones están reservadas para los puertos: para leer de un puerto se utiliza una instrucción LDRB,
y para escribir en un puerto una STRB. ¿Qué direcciones son esas? No están fijadas en el diseño del
procesador. Los circuitos externos que decodifican las direcciones que el procesador pone en el bus A
se ocuparán de activar a la memoria o a un periférico. Por tanto, para programar las operaciones de
entrada/salida no basta con conocer el modelo funcional del procesador, es necesario tener el cuenta el
diseño de los circuitos externos para saber qué direcciones son de puertos y la función de cada puerto.
Veremos un ejemplo concreto en el apartado 4.8.
También nos referíamos a las interrupciones en términos generales en el apartado 2.8: la atención
a una interrupción implica salvaguardar el estado del procesador, identificar la causa y ejecutar una
rutina de servicio.
Y al final del apartado 3.1 ya veíamos algo del caso concreto de BRM: al pasar a atender a una
interrupción el procesador hace una copia del contenido del registro CPSR (Current Program Status
Register) en el registro SPSR (Saved Program Status Register). En realidad, hace algo más:
• Copia CPSR en SPSR.
• En CPSR pone el modo supervisor e inhibe las interrupciones (modo = 10011, I = 0, figura 3.2).
• Copia el valor actual del contador de programa, PC, en el registro R14_SVC = LR_SVC.
• Introduce en PC la dirección de comienzo de la rutina de servicio.
Todas estas operaciones las hace automáticamente el hardware.
3.7 Comunicaciones con los periféricos e interrupciones 43
En el caso de reset el procesador abandona la ejecución del programa actual sin guardar CPSR ni
la dirección de retorno, pero poniendo el modo supervisor e inhibiendo interrupciones. La rutina de
servicio consiste en realizar las operaciones de inicialización, normalmente incluidas en un programa
grabado en ROM (apartado 2.5).
Las otras causas solo se atienden si I = 1. Cuando se trata de instrucción desconocida o de SWI
el procesador, en cuanto la descodifica, realiza inmediatamente las operaciones descritas. En ese mo-
mento, el registro PC está apuntando a la instrucción siguiente (dirección de la actual más cuatro). Sin
embargo, cuando atiende a una petición de interrupción externa (IRQ) el procesador termina de ejecutar
la instrucción en curso, por lo que el valor de PC que se salva en LR_SVC es el de la instrucción de la
siguiente más ocho.
Retorno de interrupción
Al final de toda rutina de servicio, para volver al programa interrumpido son necesarias dos opera-
ciones (salvo si se trata de reset):
• Copiar el contenido del registro SPSR en CPSP (con esto se vuelven a permitir interrupciones y al
modo usuario, además de recuperar los valores de los indicadores para el programa interrumpido).
• Introducir en el registro PC el valor adecuado para que se ejecute la instrucción siguiente a aquella
en la que se produjo la interrupción.
De acuerdo con lo dicho en los párrafos anteriores, lo segundo es fácil: si la causa de interrupción
fue de programa (SWI) o instrucción desconocida, basta copiar el contenido de R14_SVC = LR_SVC
8
En otros procesadores, por ejemplo, los de Intel y los de Motorola, los vectores de interrupción no son instrucciones, sino
punteros a los comienzos de las rutinas.
9
El «hueco» de 12 bytes que hay entre las direcciones 0x0C y 0x18 se debe a que en el ARM hay otras causas no
consideradas en BRM.
44 Capítulo 3. El procesador BRM
en PC con una instrucción MOV PC,LR (recuerde que estamos en modo supervisor, y el registro LR
afectado por la instrucción es LR_SVC). Y si fue IRQ hay que restar cuatro unidades: SUB PC,LR,#4.
Pero antes hay que hacer lo primero, y esto plantea un conflicto. En efecto, podríamos pensar en
aplicar las instrucciones que vimos al final del apartado 3.4 para, utilizando un registro intermedio,
hacer la copia de SPSR en CPSR. Sin embargo, en el momento en que esta copia sea efectiva habremos
vuelto al modo usuario, y habremos perdido la dirección de retorno, puesto que LR ya no será LR_SVC.
Pues bien, los diseñadores de la arquitectura ARM pensaron, naturalmente, en este problema e
ingeniaron una solución elegante y eficiente. ¿Recuerda la función del bit «S» en las instrucciones
de procesamiento y movimiento (figura 3.5)? Normalmente, si está puesto a uno, los indicadores se
modifican de acuerdo con el resultado. Pero en el caso de que el registro destino sea PC es diferente:
automáticamente copia SPSR en CPSR. Por tanto, la última instrucción de una rutina de servicio debe
ser:
• MOVS PC,LR (o MOVS R15,R14) si la interrupción era de programa o de instrucción desconocida.
• SUBS PC,LR,#4 (o SUBS R15,R14,#4) si la interrupción era IRQ.
De este modo, con una sola instrucción se realizan las dos operaciones.
Programacion de BRM
Ya hemos dicho en el apartado 1.2 que los lenguajes ensambladores, a diferencia de los lenguajes
de alto nivel, dependen de la arquitectura (ISA) del procesador. Pero no solo es que cada procesador
tenga su lenguaje, es que para un mismo procesador suele haber distintos convenios sintácticos para
codificar las instrucciones de máquina y otras informaciones en ensamblador.
Los programas de este capítulo están escritos en el ensamblador GNU, que es también el que se
utiliza en el simulador ARMSim#. En el apartado A.3 del apéndice se mencionan otros lenguajes en-
sambladores para la arquitectura ARM. El ensamblador GNU es insensible a la caja (case insensitive).
Es decir, se pueden utilizar indistintamente letras mayúsculas o minúsculas: «MOV» es lo mismo que
«mov», o que «MoV»... «R1» es lo mismo que «r1», etc. En este capítulo usaremos minúsculas.
Los listados se han obtenido con un ensamblador cruzado (apartado A.3): un programa ensam-
blador que se ejecuta bajo un sistema operativo (Windows, Linux, MacOS...) en una arquitectura (x86
en este caso) y genera código binario para otra (ARM en este caso). Los mismos programas se pueden
introducir en ARMSim#, que se encargará de ensamblarlos y de simular la ejecución. Debería usted no
solo probar los ejemplos que se presentan, sino experimentar con variantes.
La palabra «ensamblador» tiene dos significados: un tipo de lenguaje (assembly language) y un tipo
de procesador (assembler). Pero el contexto permite desambiguarla: si decimos «escriba un programa
en ensamblador para...» estamos aplicando el primer significado, mientras que con «el ensamblador
traducirá el programa...» aplicamos el segundo.
45
46 Capítulo 4. Programacion de BRM
Si le entregamos ese programa fuente (guardado en un fichero de texto cuyo nombre debe acabar
en «.s») al ensamblador y le pedimos que, aparte del resultado principal (que es un programa objeto
En el ensamblador GNU se puede usar, en lugar de «@», «#» (siempre que esté al principio de la línea), y en ARMSim#
1
en un fichero binario), nos dé un listado (en el apartado A.3 se explica cómo hacerlo), obtenemos lo
que muestra el programa 4.1.
/***********************************************
* *
* Primer ejemplo *
* *
***********************************************/
.equ cte1,10
.equ cte2,8
00000000 E3A0000A mov r0,#cte1 @ Carga valores en R0
00000004 E3A01008 mov r1,#cte2 @ y R1
00000008 E0802001 add r2,r0,r1 @ y hace (R0) + (R1) → R2
0000000C EF000011 swi 0x11
.end
Los «números de Fibonacci» tienen una larga tradición en matemáticas, en biología y en la litera-
tura. Son los que pertenecen a la sucesión de Fibonacci:
f0 = 0; f1 = 1; fn = fn−1 + fn−2 (n ≥ 2)
Es decir, cada término de la sucesión, a partir del tercero, se calcula sumando los dos anteriores.
Para generarlos con un programa en BRM podemos dedicar tres registros, R0, R1 y R2 para que con-
tengan, respectivamente, fn , fn−1 y fn−2 . Inicialmente ponemos los valores 1 y 0 en R1 ( fn−1 ) y R2
( fn−2 ), y entramos en un bucle en el que a cada paso calculamos el contenido de R0 como la suma de
R1 y R2 y antes de volver al bucle sustituimos el contenido de R2 por el de R1 y el contenido de R1
por el de R0. De este modo, los contenidos de R0 a cada paso por el bucle serán los sucesivos números
de Fibonacci.
Para averiguar si un determinado número es de Fibonacci basta generar la sucesión y a cada paso
comparar el contenido de R0 ( fn ) con el número: si el resultado es cero terminamos con la respuesta
«sí»; si ese contenido es mayor que el número es que nos hemos pasado, y terminamos con la respuesta
«no».
El programa 4.2 resuelve este problema. Como aún no hemos hablado de comunicaciones con
periféricos, el número a comprobar (en este caso, 233) lo incluimos en el mismo programa. Y seguimos
el convenio de que la respuesta se obtiene en R4: si es negativa, el contenido final de R4 será 0, y si el
número sí es de Fibonacci, el contenido de R4 será 1.
/****************************************************
* *
* ¾Número de Fibonacci? *
* *
****************************************************/
.equ Num, 233 @ Número a comprobar
00000000 E3A02000 mov r2,#0 @ (R2) = f(n-2)
00000004 E3A01001 mov r1,#1 @ (R1) = f(n-1)
00000008 E3A030E9 mov r3,#Num
0000000C E3A04000 mov r4,#0 @ saldrá con 1 si Num es Fib
00000010 E0810002 bucle: add r0,r1,r2 @ fn = f(n-1)+f(n-2)
00000014 E1500003 cmp r0,r3
00000018 0A000003 beq si
0000001C CA000003 bgt no
00000020 E1A02001 mov r2,r1 @ f(n-2) = f(n-1)
00000024 E1A01000 mov r1,r0 @ f(n-1) = f(n)
00000028 EAFFFFF8 b bucle
0000002C E3A04001 si: mov r4,#1
00000030 EF000011 no: swi 0x11
.end
• Las direcciones efectivas se calculan en tiempo de ejecución, es decir, con el programa ya cargado
en memoria. Venimos suponiendo que la primera dirección del programa es la 0 (el ensambla-
dor no puede saber en dónde se cargará finalmente). Pero si se carga, por ejemplo, a partir de
la dirección 0x1000 (como hace ARMSim#) todo funciona igual: b bucle (en binario, por su-
puesto) no estará en 0x28, sino en 0x1028, y en la ejecución bifurcará a 0x1020, donde estará
add r0,r1,r2.
• Una desilusión provisional: la utilidad de este programa es muy limitada, debido a la forma
de introducir el número a comprobar en un registro: mov r3, #Num. En efecto, ya vimos en
el apartado 3.4 que el operando inmediato tiene que ser o bien menor que 256 o bien poderse
obtener mediante rotación de un máximo de ocho bits.
Vamos a ver cómo se puede operar con una constante cualquiera de 32 bits. Pero antes debería
usted comprobar el funcionamiento del programa escribiendo el código fuente en un fichero de texto y
cargándolo en el simulador. Verá que inicialmente (R4) = 0 y que la ejecución termina con (R4) = 1,
porque 233 es un número de Fibonacci. Pruebe con otros números (editando cada vez el fichero de
texto y recargándolo): cuando es mayor que 255 y no se puede generar mediante rotación ARMSim#
muestra el error.
Moraleja: en este ensamblador, en general, para cargar una constante en un registro, lo mejor es
utilizar la pseudoinstrucción LDR.
Si modifica usted el programa 4.2 cambiando la instrucción mov r3,#Num por ldr r3,=Num verá
que el ensamblador ya no genera errores para ningún número.
Si ha seguido usted los consejos no debe haber tenido problemas para ejecutar en el simulador los
ejemplos anteriores. Pero para poder ejecutarlos en un procesador real los programas fuente tienen que
procesarse con un ensamblador, que, como ya sabemos, genera un código objeto binario. Es binario,
pero no es ejecutable. Esto es así porque los sencillos ejemplos anteriores son «autosuficientes», pero
en general un programa de aplicación está compuesto por módulos que se necesitan unos a otros pero
se ensamblan independientemente. Cada módulo puede importar símbolos de otros módulos y exportar
otros símbolos definidos en él.
La generación final de un código ejecutable la realiza otro procesador software llamado montador
(linker), que combina las informaciones procedentes del ensamblaje de los módulos y produce un fiche-
ro binario que ya puede cargarse en la memoria. Una de las informaciones que necesita es una etiqueta
que debe estar en uno de los módulos (aunque solo haya uno) que identifica a la primera instrucción a
ejecutar. Esta etiqueta se llama «_start», y el módulo en que aparece tiene que exportarla, lo que se
hace con la directiva «.global _start»
En el apartado 4.7 volveremos sobre los módulos, y en el 5.2 sobre el montador. La explicación
anterior era necesaria para justificar que los ejemplos siguientes comienzan con esa directiva y tienen
la etiqueta _start en la primera instrucción a ejecutar.
Hasta ahora nuestros ejemplos contienen solamente instrucciones. Con frecuencia necesitamos in-
cluir también datos. Enseguida veremos cómo se hace, pero otra información que necesita el montador
es dónde comienzan las instrucciones y dónde los datos. Para ello, antes de las instrucciones debemos
escribir la directiva .text, que identifica a la sección de código y antes de los datos, la directiva .data,
que identifica a la sección de datos.
Las directivas que sirven para introducir valores en la sección de datos son: .word, .byte, .ascii,
.asciz y .align.
.word y .byte permiten definir un valor en una palabra o en un byte, o varios valores en varias
palabras o varios bytes (separándolos con comas). Como ejemplo de uso, el programa 4.3 es autoexpli-
cativo, pero conviene fijarse en tres detalles:
• El listado se ha obtenido con el ensamblador GNU, que pone tanto la sección de código (.text)
como la de datos a partir de la dirección 0. Será luego el montador el que las coloque una tras otra.
Sin embargo, el simulador ARMSim# hace también el montaje y carga el resultado en la memoria a
partir de la dirección 0x1000, de modo que el contenido de «palabra1» queda en la dirección 0x1024.
Ahora bien, si, como es de esperar, está usted siguiendo con interés y atención esta explicación,
debe estar pensando: «aquí tiene que haber un error». Porque como la última instrucción ocupa las
52 Capítulo 4. Programacion de BRM
/**************************************************
* Este programa intercambia los contenidos *
* de dos palabras de la memoria *
**************************************************/
.text
.global _start
00000000 E59F0014 _start: ldr r0,=palabra1
00000004 E59F1014 ldr r1,=palabra2
00000008 E5902000 ldr r2,[r0]
0000000C E5913000 ldr r3,[r1]
00000010 E5812000 str r2,[r1]
00000014 E5803000 str r3,[r0]
00000018 EF000011 swi 0x011
.data
00000000 AAAAAAAA palabra1: .word 0xAAAAAAAA
00000004 BBBBBBBB palabra2: .word 0xBBBBBBBB
.end
direcciones 0x1018 a 0x101B, ese contenido debería estar a partir de la dirección 0x101C. Veamos
por qué no es así, y no hay tal error.
• La pseudoinstrucción ldr r0,=palabra1 no carga en R0 el contenido de la palabra, sino el valor
de la etiqueta «palabra1», que es su dirección. En los listados no se aprecia, pero mire usted en el
simulador los contenidos de la memoria a partir de la instrucción SWI:
Dirección Contenido
00001018 EF000011
0000101C 00001024
00001020 00001028
00001024 AAAAAAAA
00001028 BBBBBBBB
Lo que ha hecho el ensamblador en este caso al traducir las pseudoinstrucciones LDR es crear un
«pool de literales» con dos constantes, que después del montaje han quedado en las direcciones
0x101C y 0x1020, con las direcciones de las palabras que contienen los datos, 0x1024 y 0x1028.
• En tiempo de ejecución, R0 se carga con la dirección del primer dato, y R1 con la del segundo. Por
eso, para el intercambio (instrucciones LDR y STR) se usan R0 y R1 como punteros.
Otro ejemplo: el programa 4.4 es la implementación de la suma de las componentes de un vector
(apartado 2.7). En el simulador puede verse que con los datos puestos el resultado de la suma, 210,
queda en R2.
Dos comentarios:
• Al cargar en la memoria un número de bytes que no es múltiplo de cuatro, la dirección siguiente
al último no está alineada. En este ejemplo no importa, pero si, por ejemplo, después de los diez
primeros bytes ponemos una .word la palabra no estaría alineada y la ejecución fallaría al intentar
acceder a ella. Para evitarlo, antes de .word se pone otra directiva: .align. El ensamblador rellena
con ceros los bytes hasta llegar a la primera dirección múltiplo de cuatro.
4.4 Más directivas 53
/**************************************************
* Suma de las componentes de un vector en la MP *
* (suma bytes, pero solo si son positivos) *
* *
**************************************************/
.text
.global _start
.equ N, 20 @ El vector tiene 20 elementos
@ (bytes)
_start:
00000000 E59F0018 ldr r0,=vector @ R0 = puntero al vector
00000004 E3A01014 mov r1,#N @ R1 = contador
00000008 E3A02000 mov r2,#0 @ R2 = suma
bucle:
0000000C E4D03001 ldrb r3,[r0],#1 @ Carga un elemento en R3
@ y actualiza R0
@ (cada elemento ocupa 1 byte)
00000010 E0822003 add r2,r2,r3
00000014 E2511001 subs r1,r1,#1
00000018 1AFFFFFB bne bucle
0000001C EF000011 swi 0x11
.data
vector:
00000000 01020304 .byte 1,2,3,4,5,6,7,8,9,10
05060708
090A
0000000A 0B0C0D0E .byte 11,12,13,14,15,16,17,18,19,20
0F101112
1314
.end
• Otra desilusión provisional: pruebe usted a ejecutar el programa poniendo algún número negativo.
El resultado es incorrecto. En el apartado 4.6 veremos una solución general con un subprograma,
pero de momento le invitamos a pensar en arreglarlo con solo dos instrucciones adicionales que
hacen desplazamientos.
Las directivas .ascii y .asciz, seguidas de una cadena de caracteres entre comillas, introducen
la sucesión de códigos ASCII de la cadena. La diferencia es que .asciz añade al final un byte con el
contenido 0x00 («NUL»), que es el convenio habitual para señalar el fin de una cadena.
El programa 4.5 define una cadena de caracteres y reserva, con la directiva .skip, 80 bytes en la
memoria. En su ejecución, hace una copia de la cadena original en los bytes reservados.
En la ventana principal del simulador no se muestran los contenidos de la sección de datos. Es
interesante que los mire con la vista de memoria por bytes a partir de la dirección 0x1018 (siga las
instrucciones del apartado A.2). Verá que después de la traducción de la instrucción SWI el ensamblador
ha puesto un «pool de literales» de dos palabras que contienen los punteros, a continuación 80 bytes
con contenido inicial 0x00 (si se ejecuta el programa paso a paso se puede ver cómo se van rellenando
esos bytes), y finalmente los códigos ASCII de la cadena original.
54 Capítulo 4. Programacion de BRM
/**************************************************
* *
* Copia una cadena de caracteres (máximo: 80) *
* *
**************************************************/
.text
.equ NUL, 0 @ código ASCII de NUL
.global _start
_start:
00000000 E59F0014 ldr r0,=original @ R0 y R1: punteros a
00000004 E59F1014 ldr r1,=copia @ las cadenas
bucle:
00000008 E4D02001 ldrb r2,[r0],#1
0000000C E4C12001 strb r2,[r1],#1
00000010 E3520000 cmp r2,#NUL
00000014 1AFFFFFB bne bucle
00000018 EF000011 swi 0x11
.data
copia:
00000000 00000000 .skip 80 @ reserva 80 bytes
00000000
00000000
00000000
00000000
original:
00000050 45737461 .asciz "Esta es una cadena cualquiera"
20657320
756E6120
63616465
6E612063
.end
Una instrucción de bifurcación, cuando la condición se cumple, retrasa la ejecución en dos ciclos
de reloj (apartado 3.2). Cualquier otra instrucción, si la condición no se cumple avanza en la cadena
aunque no se ejecute, pero no la para, y retrasa un ciclo de reloj. Es interesante, por tanto, prescindir de
bifurcaciones cuando sea posible, especialmente si están en un bucle que se ejecuta millones de veces.
Un ejemplo sencillo: poner en R1 el valor absoluto del contenido de R0. Con una bifurcación y la
instrucción RSB (tabla 3.2) puede hacerse así:
mov r1,r0
cmp r1,#0
bge sigue @ bifurca si (R1)>=0
rsb r1,r1,#0 @ Si (R1)< 0, 0-(R1)→ R1
sigue: ...
Un ejemplo un poco más elaborado es el del algoritmo de Euclides para calcular el máximo común
divisor de dos enteros. En pseudocódigo, llamando x e y a los números, una de sus versiones es:
En BRM también funciona ese programa, pero este otro es más eficiente:
Observe que la condición para la bifurcación BNE depende de CMP, ya que las dos instrucciones
intermedias no afectan a los indicadores.
4.6. Subprogramas
La instrucción BL (apartado 3.6), que, además de bifurcar, guarda la dirección de retorno en el
registro de enlace (LR = R14) permite llamar a un subprograma (apartado 2.7), del que se vuelve con
«MOV PC,LR». Así, el programa MCD puede convertirse en un subprograma sin más que añadirle esa
instrucción al final, y utilizarlo como muestra el programa 4.6, que incluye dos llamadas para probarlo.
Con subprogramas se pueden implementar operaciones comunes que no están previstas en el reper-
torio de instrucciones. ¿Ha pensado usted en cómo resolver el problema de la suma de bytes cuando
alguno de ellos es negativo (programa 4.4)? El problema se produce porque, por ejemplo, la repre-
sentación de −10 en un byte con complemento a 2 es 0xF6. Y al cargarlo con la instrucción LDRB en
un registro, el contenido de ese registro resulta ser 0x000000F6, que al interpretarlo como entero con
signo, es +246. Entre las instrucciones de ARM de las que hemos prescindido en BRM hay una, LDRSB
(por «signed byte»), que extiende hacia la izquierda el bit de signo del byte. Con ella se puede cargar
en el registro la representación correcta en 32 bits: 0xFFFFFFF6.
Decíamos que el problema se puede arreglar con dos sencillas instrucciones. ¿De verdad que ha
pensado en ello y no ha encontrado la respuesta? Pues es muy sencillo: desplazando 0x000000F6
56 Capítulo 4. Programacion de BRM
/***************************************************
* *
* Prueba del subprograma del algoritmo de Euclides *
* *
***************************************************/
.text
.global _start
_start:
00000000 E3A00009 ldr r0,=9
00000004 E3A010FF ldr r1,=255
00000008 EB000005 bl MCD @ llamada a MCD
0000000C E1A08000 mov r8,r0 @ MCD(9,255) = 3 → R8
00000010 E3A00031 ldr r0,=49
00000014 E59F101C ldr r1,=854
00000018 EB000001 bl MCD @ llamada a MCD
0000001C E1A09000 mov r9,r0 @ MCD(49,854) = 7 → R9
00000020 EF000011 swi 0x11
/* Subprograma MCD */
MCD:
00000024 E1500001 cmp r0,r1
00000028 C0400001 subgt r0,r0,r1
0000002C B0411000 sublt r1,r1,r0
00000030 1AFFFFFB bne MCD
00000034 E1A0F00E mov pc,lr
00000038 00000356 .end
Basta modificar el programa 4.4 sustituyendo la instrucción «ldrb r3,[r0],#1» por «bl LDRSB»
e incluyendo estas instrucciones al final (antes de «.data») para ver que si ponemos cualquier número
negativo como componente del vector el programa hace la suma correctamente.
En los dos ejemplos, MCD y LDRSB, el programa invocador le pasa parámetros de entrada al
subprograma (programa invocado) y éste devuelve parámetros de salida al primero.
4.6 Subprogramas 57
En MCD, los parámeros de entrada son los números de los que se quiere calcular su máximo común
divisor, cuyos valores se pasan por los registros R0 y R1. Y el parámetro de salida es el resultado, cuyo
valor se devuelve en R0 (y también en R1).
El parámetro de entrada a LDRSB es el byte original, y el de salida, el byte con el signo extendido.
El valor de este último se devuelve en R3, pero observe que el parámetro de entrada no es el valor
del byte, sino su dirección. En efecto, R0 no contiene el valor del byte, sino la dirección en la que se
encuentra. Se dice que es un paso de referencia.
El paso de referencia es a veces imprescindible. Por ejemplo, cuando el parámetro a pasar es una
cadena de caracteres: en un registro solo podemos pasar cuatro bytes; una cadena de longitud 52 ya nos
ocuparía los trece registros disponibles.
Un sencillo ejercicio que puede usted hacer y probar en el simulador es escribir un subprograma
para copiar cadenas de una zona a otra de la memoria (programa 4.5) y un programa que lo llame varias
veces pasándole cada vez como parámetros de entrada la dirección de la cadena y la dirección de una
zona reservada para la copia.
El programa 4.7 es otra versión del subprograma MCD y de las llamadas en la que los parámetros
y el resultado se pasan por la pila. Observe que ahora el subprograma puede utilizar cualquier pareja
de registros, pero los salva y luego los recupera por si el programa invocador los está usando para otra
cosa (aquí no es el caso, pero así el subprograma es válido para cualquier invocador, que solo tiene que
tener en cuenta que debe introducir en la pila los parámetros y recuperar el resultado de la misma).
/****************************************************
* Versión del subprograma del algoritmo de Euclides *
* con paso de parámetros por la pila *
* *
****************************************************/
.text
.global _start
/* Programa que llama al subprograma MCD */
_start:
00000000 E3A00009 ldr r0,=9
00000004 E3A010FF ldr r1,=255
00000008 E40D0004 str r0,[sp],#-4 @ pone parámetros en la pila
0000000C E40D1004 str r1,[sp],#-4 @ (PUSH)
00000010 EB000009 bl MCD @ llamada a MCD
00000014 E5BD8004 ldr r8,[sp,#4]!@ MCD(9,255) = 3 → R8 (POP R8)
00000018 E28DD004 add sp,sp,#4 @ para dejar la pila como estaba
0000001C E3A00031 ldr r0,=49
00000020 E59F1044 ldr r1,=854
00000024 E40D0004 str r0,[sp],#-4 @ pone parámetros en la pila
00000028 E40D1004 str r1,[sp],#-4 @ (PUSH)
0000002C EB000002 bl MCD @ llamada a MCD
00000030 E5BD9004 ldr r9,[sp,#4]!@ MCD(49,854) = 7 → R9 (POP R9)
00000034 E28DD004 add sp,sp,#4 @ para dejar la pila como estaba
00000038 EF000011 swi 0x11
/* Subprograma MCD */
0000003C E40D0004 MCD: str r3,[sp],#-4 @ PUSH R3 y R4 (salva
00000040 E40D1004 str r4,[sp],#-4 @ los registros que utiliza)
00000044 E59D000C ldr r3,[sp,#12] @ coje el segundo parámetro
00000048 E59D1010 ldr r4,[sp,#16] @ coje el primer parámetro
bucle:
0000004C E1500001 cmp r3,r4
00000050 C0400001 subgt r3,r3,r4
00000054 B0411000 sublt r4,r4,r3
00000058 1AFFFFFB bne bucle
0000005C E58D100C str r4,[sp,#12] @ sustituye el segundo
@ pa¯ametro de entrada por el resultado
00000060 E5BD1004 ldr r4,[sp,#4]! @ POP R4 y R3
00000064 E5BD0004 ldr r3,[sp,#4]! @ (recupera los registros)
00000068 E1A0F00E mov pc,lr
0000006C 00000356 .end
Anidamiento
Ya decíamos en el apartado 2.7 que si solo se usa la instrucción BL para salvar la dirección de retorno
en el registro LR, el subprograma no puede llamar a otro subprograma, porque se sobreescribiría el
contenido de LR y no podría volver al programa que le ha llamado a él. Comentábamos allí que algunos
procesadores tienen instrucciones CALL (para llamar a un subprograma) y RET (para retornar de él). La
primera hace un push del contador de programa, y la segunda, un pop. De este modo, en las llamadas
sucesivas de un subprograma a otro los distintos valores del contador de programa (direcciones de
retorno) se van apilando con las CALL y «desempilando» con las RET.
BRM no tiene estas instrucciones, pero, como antes, se puede conseguir la misma funcionalidad con
STR y LDR. Si un subprograma llama a otro, al empezar tendrá una instrucción «str lr,[sp],#-4»
para salvar en la pila el contenido del registro LR, y, al terminar, «ldr lr,[sp],#4!» para recuperarlo
inmediatamente antes de «mov pc,lr».
Este ejemplo va a ser un poco más largo que los anteriores, pero es conveniente, para que le queden
bien claras las ideas sobre la pila, que lo analice usted y que siga detenidamente su ejecución en el
simulador. Se trata de un subprograma que calcula el máximo común divisor de tres enteros basándose
en que MCD(x, y, z) = MCD(x, MCD(y, z)).
Como no cabe en una sola página, se ha partido en dos (programa 4.8 y programa 4.9) el listado que
genera el ensamblador GNU para el código fuente. Este código fuente contiene un programa principal
que llama al subprograma MCD3 que a su vez llama dos veces a MCD.
.text
.global _start
_start:
00000000 E3A0DA06 ldr sp,=0x6000 @ inicializa el puntero de pila
00000004 E3A0000A ldr r0,=10
00000008 E3A010FF ldr r1,=255
0000000C E3A02E19 ldr r2,=400
00000010 E40D0004 str r0,[sp],#-4 @ mete los tres parámetros
00000014 E40D1004 str r1,[sp],#-4 @ en la pila (PUSH)
00000018 E40D2004 str r2,[sp],#-4
0000001C EB00000B bl MCD3 @ llama a MCD3
00000020 E5BD8004 ldr r8,[sp,#4]! @ POP R8: recoje el resultado
@ MCD(10,255,400) = 5 → R8
00000024 E28DD008 add sp,sp,#8 @ para dejar la pila como estaba
00000028 E3A00009 ldr r0,=9
0000002C E3A01051 ldr r1,=81
00000030 E3A0209C ldr r2,=156
00000034 E40D0004 str r0,[sp],#-4 @ mete otros tres parámetros
00000038 E40D1004 str r1,[sp],#-4 @ en la pila (PUSH)
0000003C E40D2004 str r2,[sp],#-4
00000040 EB000002 bl MCD3 @ llama a MCD3
00000044 E5BD9004 ldr r9,[sp,#4]! @ POP R9: recoje el resultado
@ MCD(9,81,156) = 3 → R9
00000048 E28DD008 add sp,sp,#8 @ para dejar la pila como estaba
0000004C EF000011 swi 0x11
/* Subprograma MCD3 */
00000050 E40DE004 MCD3: str lr,[sp],#-4 @ PUSH LR
00000054 E40D1004 str r1,[sp],#-4 @ PUSH R1, R2 y R3
00000058 E40D2004 str r2,[sp],#-4 @ (salva los registros
0000005C E40D3004 str r3,[sp],#-4 @ que utiliza)
00000060 E59D3014 ldr r3,[sp,#20] @ coje el tercer parámetro
00000064 E59D2018 ldr r2,[sp,#24] @ coje el segundo parámetro
00000068 E59D101C ldr r1,[sp,#28] @ coje el primer parámetro
0000006C E40D2004 str r2,[sp],#-4 @ pone R2 y R3 en la pila
00000070 E40D3004 str r3,[sp],#-4
00000074 EB00000C bl MCD
00000078 E5BD2004 ldr r2,[sp,#4]! @ POP R2: MCD(R2,R3) → R2
0000007C E28DD004 add sp,sp,#4 @ pone la pila como estaba
00000080 E40D1004 str r1,[sp],#-4 @ pone R1 y R2 en la pila
00000084 E40D2004 str r2,[sp],#-4
00000088 EB000007 bl MCD
0000008C E5BD1004 ldr r1,[sp,#4]! @ POP R1: MCD(R1,R2) → R1
00000090 E28DD004 add sp,sp,#4 @ pone la pila como estaba
00000094 E58D1014 str r1,[sp,#20] @ sustituye el tercer
@ parámetro de entrada por el resultado
00000098 E5BD3004 ldr r3,[sp,#4]! @ POP R3, R2 y R1
0000009C E5BD2004 ldr r2,[sp,#4]! @ (recupera los registros
000000A0 E5BD1004 ldr r1,[sp,#4]! @ salvados)
000000A4 E5BDE004 ldr lr,[sp,#4]! @ POP LR
000000A8 E1A0F00E mov pc,lr @ vuelve con el resultado en
@ MP[(SP)+4] y los parámetros segundo
@ y primero en MP[(SP)+8] y MP[(SP)+12]
/* Subprograma MCD */
/* Como no llama a nadie, no es necesario salvar LR */
000000AC E40D3004 MCD: str r3,[sp],#-4 @ PUSH R3 y R4
000000B0 E40D4004 str r4,[sp],#-4 @ (salva los registros)
000000B4 E59D300C ldr r3,[sp,#12] @ coje el segundo parámetro
000000B8 E59D4010 ldr r4,[sp,#16] @ coje el primer parámetro
bucle:
000000BC E1530004 cmp r3,r4
000000C0 C0433004 subgt r3,r3,r4
000000C4 B0444003 sublt r4,r4,r3
000000C8 1AFFFFFB bne bucle
000000CC E58D400C str r4,[sp,#12] @ sustituye el segundo
@ parámetro de entrada por el resultado
000000D0 E5BD4004 ldr r4,[sp,#4]! @ POP R4 y R3
000000D4 E5BD3004 ldr r3,[sp,#4]! @ (recupera los registros)
000000D8 E1A0F00E mov pc,lr @ vuelve con el resultado
@ en MP[(SP)+4] y el primer
@ parámetro en MP[(SP)+8]
.end
Es muy instructivo seguir paso a paso la ejecución, viendo cómo evoluciona la pila (en la ventana
derecha del simulador, activándola, si es necesario, con «View > Stack»). Por ejemplo, tras ejecutar
la primera vez la instrucción que en el listado aparece en la dirección 0x00B0 (y que en el simulador
estará cargada en 0x10B0), es decir, tras la primera llamada del programa a MCD3 y primera llamada
de éste a MCD, verá estos contenidos:
Conforme avanza la ejecución el puntero de pila, SP, va «subiendo» o «bajando» a medida que se
introducen o se extraen elementos de la pila. Cuando se introduce algo (push) se sobrescribe lo que
pudiese haber en la dirección apuntada por SP, pero si un elemento se extrae (pop) no se borra. De
manera que a la finalización del programa SP queda como estaba inicialmente, apuntando a 0x6000, y
la zona que había sido utilizada para la pila conserva los últimos contenidos.
Ahora bien, a estas alturas, y si tiene usted el deseable espíritu crítico, se estará preguntando si
merece la pena complicar el código de esa manera solamente para pasar los parámetros por la pila.
Además, para que al final resulte un ejecutable menos eficiente. Y tiene usted toda la razón. Se trataba
solamente de un ejercicio académico, para comprender bien el mecanismo de la pila. En la práctica,
si los parámetros no son muchos, se pasan por registros, acordando un convenio. De hecho, hay uno
llamado APCS (ARM Procedure Call Standard):
• Si hay menos de cinco parámetros, el primero se pasa por R0, el segundo por R1, el tercero por R2
y el cuarto por R3.
• Si hay más, a partir del quinto se pasan por la pila.
• El parámetro de salida se pasa por R0. Generalmente, los subprogramas implementan funciones,
que, o no devuelven nada (valor de retorno «void»), o devuelven un solo valor.
• El subprograma puede alterar los contenidos de R0 a R3 y de R12 (que se usa como «borrador»),
pero debe reponer los de todos los otros registros.
62 Capítulo 4. Programacion de BRM
4.7. Módulos
Si ya en el último ejemplo, con un problema bastante sencillo, resulta un código fuente que empieza
a resultar incómodo por su longitud, imagínese una aplicación real, con miles y miles de líneas de
código.
Cualquier programa que no sea trivial se descompone en módulos, no solo por evitar un código
fuente excesivamente largo, también porque los módulos pueden ser reutilizables por otros programas.
Cada módulo se ensambla (o, en general, se traduce) independientemente de los otros, y como resultado
se obtiene un código binario y unas tablas de símbolos para enlazar posteriormente, con un montador,
todos los módulos.
Para que el ensamblador entienda que ciertos símbolos están definidos en otros módulos se utiliza
una directiva: «extern». Y los símbolos del módulo que pueden ser necesarios para otros se exportan
con la directiva «global». En el simulador ARMSim# se pueden cargar varios módulos: seleccionando
«File > Open Multiple» se abre un diálogo para irlos añadiendo. Si no hay errores (como símbolos
declarados «extern» que no aparecen como «global» en otro módulo), el simulador se encarga de
hacer el montaje y cargar el ejecutable en la memoria simulada a partir de la dirección 0x1000.
Se puede comprobar adaptando el último ejemplo. Solo hay que escribir, en diferentes ficheros,
los tres módulos «pruebaMCD3» (programa 4.8), «MCD3» y «MCD», añadiendo las oportunas extern y
global. Le invitamos a hacerlo como ejercicio.
Veamos mejor otro ejemplo que exije un cierto esfuerzo de análisis por su parte. Se trata de imple-
mentar un algoritmo recursivo para el cálculo del factorial de un número natural, N. Hay varias maneras
de calcular el factorial. Una de ellas se basa en la siguiente definición de la función «factorial»:
Si N es igual a 1, el factorial de N es 1;
si no, el factorial de N es N multiplicado por el factorial de N − 1
Si no alcanza usted a apreciar la belleza de esta definición y a sentir un cierto prurito por investigar
cómo puede materializarse en un programa que calcule el factorial de un número, debería preguntarse
si su vocación es realmente la ingeniería.
Se trata de una definición recursiva, lo que quiere decir que recurre a sí misma. La palabra no está
en el diccionario de la R.A.E. La más próxima es «recurrente»: «4. adj. Mat. Dicho de un proceso: Que
se repite.». Pero no es lo mismo, porque el proceso no se repite exactamente igual una y otra vez, lo
que daría lugar a una definición circular. Gráficamente, la recursión no es un círculo, sino una espiral
que, recorrida en sentido inverso, termina por cerrarse en un punto (en este caso, cuando N = 1).
El programa 4.10 contiene una llamada para calcular el factorial de 4 (un número pequeño para ver
la evolución de la pila durante la ejecución). La función está implementada en el módulo que muestra
el programa 4.11. Como necesita multiplicar y BRM no tiene instrucción para hacerlo3 , hemos añadido
otro módulo (programa 4.12) con un algoritmo trivial para multiplicar4 .
La función se llama a sí misma con la instrucción que en el código generado por el ensamblador
está en la dirección 0x0018, y en cada llamada va introduciendo y decrementando el parámetro de
entrada y guardando la dirección de retorno (siempre la misma: 0x101C) y el parámetro decrementado.
Si el parámetro de entrada era N, se hacen N − 1 llamadas, y no se retorna hasta la última.
3
ARM sí la tiene. Puede probar a sustituir la llamada a mult por la instrucción mul r2,r1,r2 y funcionará igual en el
simulador.
4
En la práctica se utiliza un algoritmo un poco más complejo pero mucho más eficiente, que se basa en sumas y desplaza-
mientos sobre la representación binaria: el algoritmo de Booth.
4.7 Módulos 63
La figura 4.1 muestra los estados sucesivos de la pila para N = 4, suponiendo que el módulo con
la función (programa 4.11) se ha cargado a partir de la dirección 0x1000 y que el montador ha puesto
el módulo que contiene el programa con la llamada (programa 4.10) a continuación de éste, es decir,
a partir de la dirección siguiente a 0x1038, 0x103C. La instrucción bl fact de esa llamada, a la que
correspondía la dirección 0x000C a la salida del ensamblador quedará, pues, cargada en la dirección
0x103C + 0x000C = 0x1048. La dirección siguiente, 0x104C, es la de retorno, que, tras la llamada del
programa, ha quedado guardada en la pila por encima del parámetro previamente introducido.
SP
0x4FE4 0x101C
SP
0x4FE8 1
0x4FEC 0x101C 0x4FEC 0x101C
SP 0x4FF0 2 0x4FF0 2
SP 0x4FF4 0x101C 0x4FF4 0x101C 0x4FF4 0x101C
0x4FF8 3 0x4FF8 3 0x4FF8 3
0x4FFC 0x104C 0x4FFC 0x104C 0x4FFC 0x104C 0x4FFC 0x104C
0x5000 4 0x5000 4 0x5000 4 0x5000 4
(a) tras la llamada (b) tras la primera (c) tras la segunda (d) tras la tercera
del programa llamada a sí misma llamada a sí misma llamada a sí misma
Tras la tercera llamada, al comprobar que el último parámetro metido es igual a 1, tiene lugar el
primer retorno pasando por cierre (es la única vez que se retorna por ahí), donde se inicializa R0
con 1 y se vuelve a la dirección 0x101C. A partir de ahí, en los sucesivos retornos, se van recuperando
los valores de N (sumándole 1) y actualizando R0 para que al final quede con el valor de N! Es en esta
segunda fase (la de los retornos) en la que realmente se aplica la definición recursiva.
Conseguir que el simulador ARMSim# monte los módulos en el orden supuesto (primero la función
factorial, luego el programa con la llamada y finalmente el de la multiplicación), y así poder comprobar
los datos de la explicación anterior, es muy fácil: basta añadirlos en ese orden en la ventana de diálogo
que se abre con «Load Multiple». Por cierto, se puede comprobar también que el registro PC se ini-
cializa con el valor 0x103C, que es donde queda la primera instrucción a ejecutar (la que en el código
fuente tiene la etiqueta «_start»).
Decíamos en el apartado 3.7 que para programar las comunicaciones entre el procesador y los puer-
tos de los periféricos es necesario asignar direcciones a éstos mediante circuitos externos al procesador.
Veamos un ejemplo.
Espera activa
Imaginemos dos periféricos de caracteres conectados a un procesador BRM a través de los buses A
y D: un teclado simplificado y un display que presenta caracteres uno tras otro. Cada uno de ellos tiene
un controlador con dos puertos (registros de un byte): un puerto de estado y un puerto de datos, que
tienen asignadas estas direcciones y estas funciones:
4.8 Comunicaciones con los periféricos e interrupciones 65
Supongamos que BRM tiene un reloj de 1 GHz. De las instrucciones que hay dentro del bucle
«esperatecl», LDRB y ANDS se ejecutan en un ciclo de reloj, pero BEQ, cuando la condición se cumple
(es decir, mientras no se haya pulsado una tecla y, por tanto, se vuelva al bucle) demora tres ciclos
(apartado 3.2). En total, cada paso por el bucle se ejecuta en un tiempo tB = 5/109 segundos. Por
otra parte, imaginemos al campeón o campeona mundial de mecanografía tecleando con una velocidad
de 750 pulsaciones por minuto5 . El intervalo de tiempo desde que pulsa una tecla hasta que pulsa la
siguiente es t M = 60/750 segundos. La relación entre uno y otro es el número de veces que se ha
5
http://www.youtube.com/watch?v=M9lpqG9ZvGY
66 Capítulo 4. Programacion de BRM
ejecutado el bucle durante t M : t M /tB = 16 × 106 . Por tanto, entre tecla y tecla el procesador ejecuta
3 × 16 × 106 instrucciones que no hacen ningún trabajo «útil». Podría aprovecharse ese tiempo para
ejecutar un programa que, por ejemplo, amenizase la tarea del mecanógrafo reproduciendo un MP3 con
la séptima de Mahler o una canción de Melendi, a su gusto.
bus A
IRQ
controlador controlador
del teclado del display
UCP 0x2001 IT PR 0x2003 IT PR
0x2000 0x2002
bus D
El procesador y los controladores están conectados como indica la figura 4.2. El símbolo sobre los
bits PR e IT de cada controlador representa un circuito que realiza la operación lógica NAND. Es decir,
cuando ambos bits son «1» (y solo en ese caso) la salida de ese circuito es «0», lo que activa a la línea
IRQ, que es la entrada de interrupciones externas al procesador.6 Inicialmente, el hardware, antes de
ejecutarse el programa de arranque en ROM (figura 3.12), inhibe las interrupciones de los controladores
(IT = 0) y el permiso general de interrupciones (I = 0 en el CPSR, figura 3.2)
Por lo que respecta al software, veamos esquemáticamente lo que debe hacer, sin entrar ya en todos
los detalles de la codificación en ensamblador (que no sería difícil, si se comprende bien lo que hay que
hacer, pero alargaría excesivamente la explicación).
En primer lugar, hay unas operaciones iniciales que se realizan en modo supervisor. Para el ejemplo
anterior (ir haciendo eco de los caracteres que se van tecleando) no es necesario que el display, que se
supone más rápido que el mecanógrafo, interrumpa: basta atender a las interrupciones del teclado. Por
tanto, la operación inicial sería poner «1» en el bit IT del puerto de estado del teclado y en el bit I
del CPSR, después de haber cargado la rutina de servicio del teclado (la única) a partir de la dirección
6
El motivo de que sea NAND y no AND y de que la línea se active con «0» y no con «1» lo comprenderá usted al estudiar
electrónica digital. La línea IRQ es una «línea de colector abierto».
4.8 Comunicaciones con los periféricos e interrupciones 67
0xB00 (suponiendo el mapa de memoria de la figura 3.12). Esta rutina de servicio consistiría simple-
mente en leer (LDRB) del puerto de datos del teclado y escribir (STRB) en el del display, sin ninguna
espera. Y terminaría con la instrucción SUBS PC,LR,#4, como explicamos en el apartado 3.7. Recuer-
de que al ejecutarse LDRB sobre el puerto de datos del teclado se pone a cero el bit PR, y no se vuelve a
poner a uno (y, por tanto, generar una nueva interrupción) hasta que no se vuelve a pulsar una tecla. De
este modo, para cada pulsación de una tecla solo se realizan las operaciones que automáticamente hace
el procesador para atender a la interrupción y se ejecutan tres instrucciones, quedando todo el tiempo
restante libre para ejecutar otros programas.
1. Para cada periférico, una zona de memoria (buffer) con una capacidad predefinida. Por ejemplo,
80 bytes. La idea es que conforme se teclean caracteres se van introduciendo en la zona del
teclado hasta que el carácter es «ret» (ASCII 0x0D) que señala un «fin de línea», o hasta que se
llena con los 80 caracteres (suponiendo que son ASCII o ISO). Igualmente, para escribir en el
display se rellenará previamente la zona con 80 caracteres (o menos, siendo el último «ret»).
2. Para cada periférico, una rutina de servicio:
• La del teclado debe contener una variable que se inicializa con cero y sirve de índice para que
los caracteres se vayan introduciendo en bytes sucesivos de la zona. Cada vez que se ejecuta
esta rutina se incrementa la variable, y cuando llega a 80, o cuando el carácter es «ret», la
rutina inhibe las interrupciones del teclado (poniendo IT = 0). Desde el programa que la usa,
y que procesa los caracteres de la zona, se volverá a inicializar poniendo a cero la variable y
haciendo IT = 1 cuando se pretenda leer otra línea.
• Análogamente, la rutina de servicio del display tendrá una variable que sirva de índice. El
programa que la usa, cada vez que tenga que escribir una línea, rellenará la zona de datos con
los caracteres, inicializará la variable y permitirá interrupciones. Cuando el periférico termina
de escribir un carácter el controlador pone a uno el bit PR, provocando una interrupción, y
cuando se han escrito 80, o cuando el último ha sido «ret» la rutina inhibe las interrupciones
del periférico.
Las zonas de memoria de cada periférico pueden estar integradas, cada una en una sección de
datos, en las mismas rutinas de servicio.
3. La identificación de la causa de interrupción. En efecto, las peticiones de los dos periféricos
llegan ambas al procesador por la línea IRQ, y la primera instrucción que se ejecuta al atender
a una es la de dirección 0xB00, de acuerdo con el mapa de memoria que estamos suponiendo
(figura 3.12). Para saber si la causa ha sido el teclado o el display, a partir de esa dirección
pondríamos unas instrucciones que leyeran uno de los puertos de estado, por ejemplo, el del
teclado y comprobasen si los bits IT y PR tienen ambos el valor «1». Si es así, se bifurca a las
operaciones de la rutina de servicio del teclado, y si no, a la del display.
68 Capítulo 4. Programacion de BRM
Consulta de interrupciones
En los ejemplos anteriores solo hay dos periféricos, pero en general puede haber muchos. Identificar
cuál ha sido el causante de la interrupción implicaría una secuencia de instrucciones de comprobación
de los bits PR e IT. Ésta sería una consulta por software. Normalmente no se hace así (salvo si es un
sistema especializado con pocas causas de interrupción).
Como decíamos al final del apartado 2.8, los mecanismos de interrupciones suelen combinar téc-
nicas de hardware y de software. De hecho, en el mecanismo que hemos visto el hardware, tanto el
del procesador como el de los controladores de periféricos, se ocupa de ciertas cosas, y el resto se
hace mediante software. Añadiendo más componentes de hardware se puede hacer más eficiente el
procesamiento de las interrupciones.
Un esquema muy utilizado consiste en disponer de un controlador de interrupciones que permite
prescindir de las instrucciones de indentificación de la causa, haciendo una consulta por hardware. El
controlador tiene varias entradas, una para cada una de las líneas de interrupción de los periféricos, y
una salida que se conecta a la línea IRQ. Cuando un periférico solicita interrupción el controlador pone
en un puerto un número identificativo y activa la línea IRQ. El programa que empieza en la dirección
correspondiente a IRQ (en nuestro mapa de memoria, la dirección 0xB00) solo tiene que leer ese puerto
para inmediatamente bifurcar a la rutina de servicio que corresponda.
Periféricos de bloques
Imagínese, mirando la figura 4.3, que el display es mucho más rápido, digamos mil veces: 30.000
caracteres por segundo. El tiempo entre interrupciones se reduce de 33 ms a 33 µs. Suponiendo que el
procesador ejecuta 600 MIPS (millones de instrucciones por segundo), el número de instrucciones que
pueden ejecutarse en ese intervalo se reduce de 20 millones a veintemil. Está claro que cuanto mayor
sea la tasa de transferencia del periférico menor será la proporción que hay entre las zonas blancas y las
sombreadas de la figura. Si se trata de un periférico rápido, como un disco, con una tasa de 300 MB/s,
el tiempo entre interrupciones se reduciría a 3,3 ns, y... ¡el número de instrucciones a 2! No hay ni
tiempo para ejecutar una rutina de servicio.
4.9 Software del sistema y software de aplicaciones 69
Por esta razón, para periféricos rápidos, o periféricos de bloques, se sigue la técnica de acceso
directo a memoria, o DMA (apartado 2.8). Para realizar una transferencia, el programa inicializa al
controlador de DMA con los datos necesarios para que el controlador, y no la CPU, se ocupe de esa
transferencia. Si se trata de un disco, estos datos son:
• Dirección de la zona de memoria (buffer) que contiene los bytes o en la que se van a recibir.
Esta inicialización se realiza mediante instrucciones STRB sobre direcciones que corresponden a
puertos del controlador. Una vez hecha, el controlador accede periódicamente a la memoria a través de
los buses A y D, «robando» ciclos al procesador (para eso está la señal de control wait, figura 3.1) y se
encarga de transferir los bytes directamente con la memoria, sin pasar por la CPU.
Cuando se han transferido todos los bytes del bloque, y solo en ese momento, el controlador de
DMA genera una interrupción. La rutina de servicio comprueba si se ha producido algún error leyendo
en un registro de estado del controlador.
En el apartado anterior hablábamos de «aplicaciones diversas» que utilizan los recursos a través
del sistema de interrupciones y de los controladores de DMA. Estas aplicaciones son programas que se
diseñan para resolver problemas generales (edición de textos, reproducción y conversión de formatos
de audio y vídeo, navegadores. . . ) o específicos (juegos, nóminas, cálculo de trayectorias. . . ). Si el
programador de una de esas aplicaciones tuviese que trabajar como a primera vista se deduce del
apartado anterior, es decir, inicializando y manejando el sistema de interrupciones, programando los
controladores, etc., el desarrollo sería una tarea inmensa. Como hemos visto en el Tema 1, para eso,
entre otras cosas, están los servicios que ofrece el sistema operativo a través de abstracciones: el sistema
de ficheros, el de entrada/salida, etc.
El software de aplicaciones, pues, hace uso de esos servicios, cuya implementación es «software del
sistema». Esto ya lo sabíamos del Tema 1. Lo que quizás pueda usted entender mejor en este momento,
en el que hemos descendido al nivel de máquina convencional, es el mecanismo básico en en el que
se basa esa implementación. Y este mecanismo se apoya en una sola instrucción, la de interrupción de
programa, SWI (en otros procesadores se llama SVC, o TRAP, o INT, pero esencialmente tiene la misma
función). Con esta instrucción los programas de aplicación hacen llamadas al sistema.
La instrucción SWI tiene que ir acompañada de datos que identifiquen la operación a realizar. En
el simulador ARMSim# se indica mediante un número incluido en la misma instrucción (que tiene el
formato de la figura 3.11), como hemos visto en los ejemplos. En las implementaciones de los sistemas
operativos es más común que ese número y otros datos se pongan en registros, siguiendo un convenio
preestablecido, antes de la instrucción SWI. La rutina de servicio (cuya primera instrucción estará en
la dirección 0x08, según la tabla 3.4) identificará la operación leyendo estos registros y llamará al
componente necesario del sistema para realizarla.
70 Capítulo 4. Programacion de BRM
Hola mundo
En un primer curso de programación es tradicional empezar con el «Hola mundo»: un programa que
escribe por la pantalla (la «salida estándar») esa cadena. El programa 4.13 lo hace en el ensamblador
de BRM.
.text
.global _start
_start:
00000000 E3A00001 mov r0, #1 @ R0=1: salida estándar
00000004 E59F1014 ldr r1, =cadena @ R1: dirección de la cadena
00000008 E3A0200E mov r2, #14 @ R2: longitud de la cadena
0000000C E3A07004 mov r7, #4 @ R7=4: número de "write"
00000010 EF000000 swi 0x00000000
00000014 E3A00000 mov r0, #0 @ R0=0: código de retorno normal
00000018 E3A07001 mov r7, #1 @ R7=1: número de "exit"
0000001C EF000000 swi 0x00000000
.data
cadena:
00000000 C2A1486F .asciz "½Hola mundo!\n"
6C61206D
756E646F
210A00
.end
En un lenguaje de alto nivel, la llamada para escribir en un fichero es una función de nombre
«write» que tiene como parámetros un número entero (el «descriptor del fichero», que en el caso de
la salida estándar es 1), un puntero a la zona de memoria que contiene los datos a escribir y el número
de bytes que ocupan éstos. En ensamblador, estos parámetros se pasan por registros: R0, R1 y R2, res-
pectivamente. El número que identifica a write es 4, y se pasa por R7. Esto explica las cuatro primeras
instrucciones que, junto con SWI, forman la llamada al sistema. Cuando el programa se ejecute, al
llegar a SWI se produce una interrupción. La rutina de servicio de SWI (que ya forma parte del sistema
operativo), al ver el contenido de R7 llama al sistema de gestión de ficheros, que a su vez interpreta los
contenidos de R0 a R2 y llama al gestor del periférico, que se ocupa de iniciar la transferencia mediante
interrupciones de la pantalla y el sistema operativo vuelve al programa interrumpido.
Después el programa hace otra llamada de retorno «normal» (sin error, parámetro que se pasa
por R0), y el número de llamada (que en alto nivel se llama «exit»), 1, se introduce en R7 antes de
ejecutarse la instrucción SWI. Ahora, la rutina de servicio llama al sistema de gestión de memoria, que
libera el espacio ocupado por el programa y hace activo a otro proceso.
4.10 Dos programas para Android 71
Saludo
Como muestran esos dos ejemplos, es fácil hacer uso de las llamadas «read» y «write» y que
el sistema operativo se encargue de los detalles de bajo nivel para leer del teclado y escribir en la
pantalla. Los programas de Fibonacci, del máximo común divisor o del factorial (que en los ejemplos
prescindían de la entrada de teclado por limitaciones de ARMSim#, vea la tabla A.1) se pueden adaptar
para que realicen los cálculos con datos del teclado.
Ahora bien, esas llamadas sirven para leer o escribir cadenas de caracteres. Si, por ejemplo, adap-
tamos el programa 4.2 para que diga «Dame un número y te digo si es de Fibonacci» y luego lea, y
si escribimos «233» lo que se almacena en memoria es la cadena de caracteres «233». Por tanto, hace
falta un subprograma que convierta esas cadenas a la representación en coma fija en 32 bits. Y si se
trata de devolver un resultado numérico por la pantalla es necesario otro subprograma para la opera-
ción inversa. Le dejamos como ejercicio su programación, que conceptualmente es sencilla (recuerde
el ejercicio planteado en clase, transparencia 36 del Tema 2), pero laboriosa (especialmente si, como
debe ser, se incluye la detección de errores: caracteres no numéricos, etc.)
«¿Puedo comprobar que esto funciona en mi teléfono o tableta con Android?» Vamos a ver que no
es difícil, pero antes hagamos unas observaciones:
• Como ejercicio es interesante comprobarlo, pero si está usted pensando en aprender a desarrollar
aplicaciones es recomendable (por no decir imprescindible) utilizar entornos de desarrollo para len-
guajes de alto nivel, como el que estudiará en la asignatura «Análisis de diseño de software».
• El procedimiento se ha probado en dos dispositivos Samsung: Galaxy S con Android 2.2 y Ga-
laxy Tab 10.1 con Android 3.2, y debería funcionar con cualquier otro dispositivo basado en ARM
y cualquier otra versión de Android.
• La grabación del programa ejecutable en el dispositivo requiere adquirir previamente privilegios de
administrador (en la jerga, «rootearlo») y ejecutar algunos comandos como tal. Como esto puede
conllevar algún riesgo (en el peor de los casos, y también en la jerga, «brickearlo»: convertirlo en un
ladrillo), ni el autor de este documento ni la organización en la que trabaja se hacen responsables de
eventuales daños en su dispositivo.
• Necesitará:
◦ un ordenador personal con un ensamblador cruzado, un montador y un emulador como qemu
(aunque éste último no es imprescindible), y leer las indicaciones del Apéndice (apartado A.3)
sobre su uso;
◦ el dispositivo con Android modificado para tener permisos de administrador y con una aplicación
de terminal (por ejemplo, «Terminal emulator»);
◦ una conexión por cable USB (o por alguna aplicación wifi) para transferir el programa ejecutable
del ordenador al dispositivo.
72 Capítulo 4. Programacion de BRM
.text
.global _start
_start:
00000000 E3A00001 mov r0, #1 @ R0 = 1: salida estándar
00000004 E59F1064 ldr r1, =cad1
00000008 E3A02014 mov r2, #20 @ En UTF-8 son 20 bytes
0000000C E3A07004 mov r7, #4 @ R7 = 4: número de "write"
00000010 EF000000 swi 0x00000000
00000014 E3A00000 mov r0, #0 @ R0 = 0: entrada estándar
00000018 E59F1054 ldr r1, =nombre
0000001C E3A02019 mov r2, #25
00000020 E3A07003 mov r7, #3 @ R7 = 3: número de "read"
00000024 EF000000 swi 0x00000000
00000028 E3A00001 mov r0, #1 @ R0 = 1: salida estándar
0000002C E59F1044 ldr r1, =cad2
00000030 E3A02006 mov r2, #6
00000034 E3A07004 mov r7, #4 @ R7 = 4: número de "write"
00000038 EF000000 swi 0x00000000
0000003C E3A00001 mov r0, #1
00000040 E59F102C ldr r1, =nombre
00000044 E3A02019 mov r2,#25
00000048 E3A07004 mov r7, #4
0000004C EF000000 swi 0x00000000
00000050 E3A00001 mov r0, #1
00000054 E59F1020 ldr r1, =cad3
00000058 E3A02002 mov r2,#2
0000005C E3A07004 mov r7, #4
00000060 EF000000 swi 0x00000000
00000064 E3A00000 mov r0, #0 @ R0 = 0: código de retorno normal
00000068 E3A07001 mov r7, #1 @ R7 = 1: número de "exit"
0000006C EF000000 swi 0x00000000
.data
00000000 C2BF43C3 cad1: .asciz "¾Cómo te llamas?\n>"
B36D6F20
7465206C
6C616D61
733F0A3E
00000015 00000000 nombre: .skip 25
00000000
00000000
00000000
00000000
0000002E 486F6C61 cad2: .asciz "Hola, "
2C2000
00000035 0A00 cad3: .asciz "\n"
.end
Figura 4.4 Órdenes para copiar el programa ejecutable en un directorio con permisos de ejecución.
En el apartado 1.2 introducíamos las ideas básicas de los lenguajes simbólicos de bajo y alto nivel.
Los programas fuente escritos en estos lenguajes necesitan ser traducidos al lenguaje de máquina del
procesador antes de poderse ejecutar. El traductor de un lenguaje de bajo nivel, o lenguaje ensamblador,
se llama ensamblador, y el de uno de alto nivel, compilador.
Algunos lenguajes de alto nivel están diseñados para que los programas se ejecuten mediante un
proceso de interpretación, no de traducción. Funcionalmente, la diferencia entre un traductor y un
intérprete es la misma que hay entre las profesiones que tienen los mismos nombres. El traductor recibe
como entrada todo el programa fuente, trabaja con él, y tras el proceso genera un código objeto binario
que se guarda en un fichero que puede luego cargarse en la memoria y ejecutarse. El intérprete analiza
también el programa fuente, pero no genera ningún fichero, sino las órdenes oportunas (instrucciones de
máquina o llamadas al sistema operativo) para que se vayan ejecutando las sentencias (apartado 1.2) del
programa fuente. Abusando un poco de la lengua, se habla de «lenguajes compilados» y de «lenguajes
interpretados» (realmente, lo que se compila o interpreta no es el lenguaje, sino los programas escritos
en ese lenguaje).
En este capítulo estudiaremos los principios de los procesos de traducción, montaje e interpretación
de una manera general (independiente del lenguaje). En la asignatura «Programación» estudiará usted
lenguajes compilados, por lo que no entraremos aquí en ellos (salvo por algún ejemplo para ilustrar el
proceso de compilación). Sí veremos las ideas básicas de algunos lenguajes interpretados que tienen
una importancia especial en las aplicaciones telemáticas: lenguajes de marcas y lenguajes de script, e
introduciremos mediante ejemplos tres de ellos: HTML, XML y JavaScript.
5.1. Ensambladores
Para generar el código objeto, un ensamblador sigue, esencialmente, los mismos pasos que tendría-
mos que hacer para traducir manualmente el código fuente. Tomemos como ejemplo el programa 4.2:
Después de saltar las primeras líneas de comentarios y de guardar memoria de que cuando aparez-
ca el símbolo «Num» tenemos que reemplazarlo por 233 = 0xE9, encontramos la primera instrucción,
mov r2,#0. El código de operación nemónico nos dice que es una instrucción del tipo «movimiento»,
75
76 Capítulo 5. Procesadores software y lenguajes interpretados
cuyo formato es el de la figura 3.5. Como no tiene condición (es MOV, no MOVEQ, ni MOVNE, etc.), los
bits de condición, según la tabla 3.1, son 1110 = 0xE. El operando es inmediato, por lo que el bit 25 es
I = 1, y el código de operación de MOV (tabla 3.2) es 1101. El bit 20 es S = 0 (sería S = 1 si el código
nemónico fuese MOVS). Ya tenemos los doce bits más significativos: 1110-00-1-1101-0 = 0xE3A. Los
cuatro bits siguientes, Rn, son indiferentes para MOV, y ponemos cuatro ceros. Los siguientes indican el
registro destino, Rd = R2 = 0010 y finalmente, como el operando inmediato es 0 (menor que 256), se
pone directamente en los doce bits del campo Op2. Llegamos así a la misma traducción que muestra el
listado del programa 4.2: 0xE3A02000.
De esta manera mecánica y rutinaria seguiríamos hasta la instrucción beq si que, a diferencia
de las anteriores, no podemos traducir aisladamente. En efecto, su formato es el de la figura 3.10, y,
como la condición es «eq» y L = 0, podemos decir que los ocho bits más significativos son 0x0A, pero
luego tenemos que calcular una distancia que depende del valor que tenga el símbolo «si». Para ello,
debemos avanzar en la lectura del código fuente hasta averiguar que su valor (su dirección) es 0x2C =
44. Como la dirección de la instrucción que estamos traduciendo es 0x18 = 24, habremos de poner en
el campo «Dist» un número tal que DE = 44 = 24 + 8 + 4×(Dist), es decir, (Dist) = 3, resultando así la
traducción completa de la instrucción: 0x0A000003.
En lugar de este proceso, que implica «avanzar» a veces en la lec-
Símbolo Valor tura para averiguar el valor correspondiente a un símbolo y luego «re-
Num 0xE9 troceder» para completar la traducción de una instrucción, podemos
bucle 0x10 hacer la traducción en dos pasos: en el primero nos limitamos a leer
si 0x2C el programa fuente e ir anotando los valores que corresponden a los
no 0x30 símbolos; terminada esta primera lectura habremos completado una
Tabla 5.1. Tabla de símbolos tabla de símbolos internos. En el ejemplo que estamos considerando
para el programa 4.2. resulta la tabla 5.1
En el segundo paso recomenzamos la lectura y vamos traducien-
do, consultando cuando es necesario la tabla.
Pues bien, los programas ensambladores también pueden ser de un paso o (más frecuentemente) de
dos pasos, y su ejecución sigue, esencialmente, este modelo procesal. Hemos obviado varias operacio-
nes que puede usted imaginar fácilmente: las comprobaciones de errores sintácticos en el primer paso,
como códigos de operación que no existen, instrucciones mal formadas, etc.
Por otra parte, es importante tener una idea clara de la secuencia de actividades que deben seguirse
para ejecutar un programa escrito en lenguaje ensamblador. La figura 5.1 ilustra estas actividades,
aunque le falta algo. El símbolo en forma de cilindro representa un disco. En este disco tendremos
disponible, entre otros muchos ficheros, el programa ensamblador en binario («ejecutable»).
1. Se carga el programa ensamblador mediante la ejecución de otro programa, el cargador, que tiene
que estar previamente en la MP1 .
2. Al ejecutarse el ensamblador, las instrucciones que forman nuestro programa fuente son los datos
para el ensamblador. Éste, a partir de esos datos, produce como resultado un programa escrito en
lenguaje de máquina (un «código objeto»), que, normalmente, se guarda en un fichero del disco.
3. Se carga el código objeto en la MP. El ensamblador ya no es necesario, por lo que, como indica la
figura, se puede hacer uso de la zona que éste ocupaba.
4. Se ejecuta el código objeto.
1
Si el cargador está en la MP, tiene que haber sido cargado. Recuerde del Tema 1 que este círculo vicioso se rompe gracias
al cargador inicial (bootstrap loader).
5.2 El proceso de montaje 77
programa fuente
MP
MP
ensamblador
ensamblador
ensamblador
ensamblador
MP
MP
código
datos
objeto código
ensamblador objeto
resultados
código objeto
cargador
¿Qué es lo que falta? Ya lo hemos avanzado en el apartado 4.4: el programa fuente normalmen-
te contiene símbolos que se importan de otros módulos, por lo que el código objeto que genera el
ensamblador no es aún ejecutable. Falta un proceso intermedio entre los pasos 2 y 3.
nombre del símbolo y la dirección de la instrucción cuya traducción no ha podido completar. Lo mismo
ocurre en el programa 4.11 con la instrucción bl mult. En este otro programa, fact está declarado
como símbolo a exportar («.global»), y el ensamblador lo pone en la tabla de símbolos de acceso
con su valor (0x0). Será el montador el que finalmente ajuste las distancias en las instrucciones de cada
módulo teniendo en cuenta las tablas de símbolos de acceso de los demás módulos y el orden en que
se han montado todos. Si, por ejemplo, ha puesto primero el programa 4.11 (a partir de la dirección 0)
y a continuación el 4.10 (a partir de 0x38 + 4 = 60), la instrucción bl fact quedará en la dirección
0x0C + 60 = 72, y su distancia deberá ser Dist = −72 ÷ 4 + 8 = −26 =-0x1A, o, expresada en 24
bits con signo, 0xFFFFEC. La codificación de la instrucción después del montaje será: 0xEBFFFFEC,
como puede usted comprobar si carga en ARMSim# los tres módulos en el orden indicado.
Por si analizando los detalles ha per-
código objeto dido usted la idea general, la figura 5.2
módulo tabla de símbolos la resume. Los tres módulos con los pro-
módulo objeto: externos
fuente Ensamblador
tabla de símbolos
gramas fuente, que suponemos guarda-
factorial.o
factorial.s de acceso dos en ficheros de nombres factorial.s,
diccionario de
reubicación llamada-fact.s y mult.s, se ensamblan
independientemente, dando como resultado
módulos
objeto módulos objeto que se guardan en los fiche-
factorial.o módulo
de carga:
código objeto
ros a los que hemos llamado factorial.o,
llamada−fact.o
Montador
factorial
diccionario de
reubicación llamada-fact.o y mult.o. El montador,
mult.o
con estos tres módulos, resuelve las referen-
cias incompletas y genera un fichero ejecu-
MP table («módulo de carga») con el nombre
módulo que le digamos, por ejemplo, factorial.
de carga
factorial
código
Pero no hemos mencionado en este
Cargador
constante de reubicador ejecutable ejemplo a los «diccionarios de reubica-
reubicación
ción». El montador genera un código que
luego el cargador pondrá en la memoria a
Figura 5.2. Procesos para el ensamblaje, el montaje partir de alguna dirección libre (la que le di-
y la ejecución. ga el sistema de gestión de la memoria, Te-
ma 1). Esta dirección de carga es descono-
cida para el montador. De hecho, si el programa se carga varias veces, será distinta cada vez. Lo que
ocurre en este ejemplo es que no hay nada que «reubicar»: el código generado por el montador funciona
independientemente de la dirección a partir de la cual se carga, y los diccionarios de reubicación están
vacíos.
Consideremos el programa 4.3. El ensamblador crea un «pool de literales» inmediatamente después
del código, en las direcciones 0x0000001C y 0x00000020, cuyos contenidos son las direcciones de
los símbolos palabra1 y palabra2. Estos símbolos están definidos en la sección «.data», que es
independiente de la sección «.text», y sus direcciones (relativas al comienzo de la sección) son 0 y 4,
como se puede ver en el listado del programa 4.3. Naturalmente, esos valores se tendrán que modificar
para que sean las direcciones donde finalmente queden los valores 0xAAAAAAAA y 0xBBBBBBBB. Pero
el ensamblador no puede saberlo, por lo que pone sus direcciones relativas, 0x0 y 0x4 y acompaña
el código objeto con la lista de dos direcciones, 0x0000001C y 0x00000020, cuyo contenido habrá
que modificar. Esta lista es lo que se llama «diccionario de reubicación». Si el montador coloca la
sección de datos inmediatamente después de la de código, reubica los contenidos de las direcciones del
diccionario, sumándoles la dirección de comienzo de la sección de datos, 0x24. El código que genera
5.3 Compiladores e intérpretes 79
Compiladores
A partir de este código fuente, basado en construcciones que son generales e independientes de
los registros y las instrucciones de ningún procesador (variables, como «x» e «y», sentencias como
«while» e «if»), un compilador genera un código objeto para un procesador concreto. El proceso es,
por supuesto, bastante más complejo que el de un ensamblador. A grandes rasgos, consta de cuatro
fases realizadas por los componentes que muestra la figura 5.3.
código objeto
Compilador
tabla de símbolos
módulo externos
módulo Analizador objeto:
fuente Analizador Generador
léxico sintáctico de código Optimizador tabla de símbolos
factorial.c (parser) factorial.o de acceso
diccionario de
reubicación
Análisis léxico
El análisis léxico consiste en la exploración de los caracteres del código fuente para ir identificando
secuencias que tienen un significado y se llaman «tokens»: «while» es un token, y «x» es otro. El
80 Capítulo 5. Procesadores software y lenguajes interpretados
analizador léxico los clasifica y se los va entregando al siguiente componente: «while» es una palabra
clave del lenguaje, mientras que «x» es un símbolo definido en este programa. En este proceso, el
analizador va descartando los comentarios (como los que aparecen entre «/*» y «*/»), de modo que
en la siguiente fase ya se trabaja a un nivel superior al de los caracteres individuales.
El análisis sintáctico se suele llamar (sobre todo, por brevedad) parsing, y el programa que lo
realiza, parser. Su tarea es analizar sentencias completas, comprobar su corrección y generar estructu-
ras de datos que permitirán descomponer cada sentencia en instrucciones de máquina. Como indica la
figura, el parser trabaja interactivamente con el analizador léxico: cuando recibe un token, por ejemplo,
«while», la sintaxis del lenguaje dice que tiene que ir seguido de una expresión entre paréntesis, por
lo que le pide al analizador léxico que le entregue el siguiente token: si es un paréntesis todo va bien, y
le pide el siguiente, etc. Cuando llega al final de la sentencia, señalado por el token «;», el parser en-
trega al generador de código un árbol sintáctico que contiene los detalles a partir de los cuales pueden
obtenerse las instrucciones de máquina necesarias para implementar la sentencia.
Para que el parser funcione correctamente es necesario que esté basado en una definición de la
sintaxis del lenguaje mediante estrictas reglas gramaticales. Un metalenguaje para expresar tales reglas
es la notación BNF (por «Backus–Naur Form»). Por ejemplo, tres de las reglas BNF que definen la
sintaxis del lenguaje C son:
La primera dice que una sentencia puede ser una sentencia «for», o una «while», o... La segunda,
que una sentencia «while» está formada por la palabra clave «while» seguida del símbolo «(», seguido
de una condición, etc.
BNF es un metalenguaje porque es un lenguaje para definir la gramática de los lenguajes de pro-
gramación. Las reglas se pueden expresar gŕáficamente con diagramas sintácticos, como los de la
figura 5.4, que corresponden a las tres reglas anteriores.
sentencia
sentencia−for
sentencia−while
sentencia−if
sentencia−while
while ( condición ) sentencia
sentencia−if
if ( condición ) sentencia else sentencia
Si el programa fuente está construido respetando estas reglas, el parser termina construyendo el
árbol sintáctico que expresa los detalles de las operaciones a realizar. La figura 5.5 presenta el corres-
pondiente a la función MCD. Esta representación gráfica es para «consumo humano». Realmente, se
implementa como una estructura de datos (las estructuras de datos se estudian en la asignatura «Pro-
gramación»).
int x
int y
while return
condición cuerpo
comparar x
op: !=
x y if
comparar
op: > asignar asignar
operación operación
x y x binaria y binaria
op: − op: −
x y y x
Generación de código
La generación de código es ya una operación dependiente de la arquitectura, puesto que se trata
de generar instrucciones de máquina específicas.
Recuerde (apartado 4.6) que en los compiladores se suele seguir el convenio de pasar los paráme-
tros de las funciones por la pila. En el ejemplo de la función MCD, el generador, al ver que hay dos
parámetros de entrada que son de tipo entero, les asigna dos registros, por ejemplo, r0 y r1, y genera
las instrucciones de máquina adecuadas para extraer los valores de la pila e introducirlos en los regis-
tros. A continuación ve «while» con su condición, que es «comparar utilizando el operando !=»; si
está generando código para ARM produce cmp r0,r1 y beq <dirección> (el valor de <dirección>
lo pondrá en un segundo paso), y así sucesivamente, va generando instrucciones conforme explora el
árbol sintáctico.
Optimización
En la última fase el compilador trata de conseguir un código más eficiente. Por ejemplo, si com-
pilamos la función MCD (factorial.c) para la arquitectura ARM sin optimización, se genera un
82 Capítulo 5. Procesadores software y lenguajes interpretados
código que contiene instrucciones b, beq y blt, como la primera de las versiones en ensamblador que
habíamos visto en el apartado 4.5. Pidiéndole al compilador que optimice y que entrege el resultado en
lenguaje ensamblador2 (sin generar el código binario) se obtiene este resultado:
.L7: cmp r0, r1
rsbgt r0, r1, r0
rsble r1, r0, r1
cmp r1, r0
bne .L7
(«.L7» es una etiqueta que se ha «inventado» el compilador).
No utiliza las mismas instrucciones, pero puede usted comprobar que es equivalente al que había-
mos escrito en el apartado 4.5. Ha optimizado bastante bien, pero no del todo (le sobra una instrucción).
Intérpretes
La diferencia básica entre un compilador y un intérprete es que éste no genera ningún tipo de
código: simplemente, va ejecutando el programa fuente a medida que lo va leyendo y analizando.
Según cómo sea el lenguaje del programa fuente, hay distintos tipos de intérpretes:
• Un procesador hardware es un intérprete del lenguaje de máquina.
• La implementación del lenguaje Java (que se estudia en la asignatura «Programación») combina una
fase de compilación y otra de interpretación. En la primera, el programa fuente se traduce al lenguaje
de máquina de un procesador llamado «máquina virtual Java» (JVM). Es «virtual» porque normal-
mente no se implementa en hardware. El programa traducido a ese lenguaje (llamado «bytecodes»)
se ejecuta mediante un programa intérprete.
• Hay lenguajes de alto nivel, por ejemplo, JavaScript o PHP, diseñados para ser interpretados me-
diante un programa intérprete. Son los «lenguajes interpretados».
En el último caso, el intérprete realiza también las tareas de análisis léxico y sintáctico, pero a
medida que va analizando las sentencias las va ejecutando. El proceso está dirigido por el bloque
«ejecutor» (figura 5.6): así como el parser le va pidiendo tokens al analizador léxico, el ejecutor le va
pidiendo sentencias al parser.
Intérprete
programa Analizador
fuente Analizador
léxico sintáctico Ejecutor
un_script.js (parser)
HTML
HTML (Hypertext Markup Language) recibe el nombre de su característica principal: un «hiper-
texto» es un documento de texto acompañado de anotaciones, o marcas, que indican propiedades para
la presentación (tamaño, color, etc.), o remiten a otras partes del documento o a otros documentos (hi-
perenlaces, o simplemente, «enlaces», para abreviar). En la red formada por los enlaces no solo hay
documentos de texto: también se enlazan ficheros con imágenes, sonidos y vídeos, de modo que el
concepto de «hipertexto» se generaliza a «hipermedia».
Desde 1995 se han sucedido varias versiones de HTML. Nos vamos a referir a la última, HTML5,
que aún está en desarrollo, pero las últimas versiones de los navegadores aceptan la mayoría de sus
nuevos elementos.
La aplicación principal de HTML es la composición de páginas web. Los datos para la presentación
de una página web están en uno o varios ficheros del servidor y se transmiten mediante el protocolo
HTTP (Hypertext Transfer Protocol) o HTTPS (HTTP Secure). Cuando el cliente (el navegador) los
recibe, el intérprete que tiene incorporado los analiza y genera las órdenes al sistema gráfico para que
haga la presentación. El «esqueleto» de un página web es así:
<HTML>
<HEAD>
<!-- Esto es un comentario. Dentro de "HEAD" se incluyen el título
del documento y marcas "META" que describen el contenido
(codificación de caracteres, palabras clave, autor, etc.)
-->
</HEAD>
<BODY>
<!-- Cuerpo del documento: marcas y texto
que el navegador presenta en pantalla -->
</BODY>
</HTML>
Unas cadenas de caracteres corresponden a marcado y otras a contenido. El marcado y el contenido
se diferencian por unas reglas sintácticas muy sencillas: todas las cadenas de marcado empiezan con
el carácter «<» y terminan con «>» (o, como veremos luego, pueden empezar con «&» y terminar con
«;»).
84 Capítulo 5. Procesadores software y lenguajes interpretados
Los espacios en blanco y los cambios de línea entre cadenas de marcado solo sirven para hacer más
legible el fichero fuente: para el navegador sería exactamente igual esto otro:
<HTML><HEAD>...</HEAD><BODY>...</BODY></HTML>
Las cadenas que empiezan con «<» y terminan con «>» son etiquetas. Las hay de apertura, como
<BODY>, de cierre, como </BODY> y sin contenido, como <BR>, que provoca un salto de línea, o como
<!-- ... -->, que solo sirve para poner comentarios en el documento fuente, comentarios que el
intérprete ignora.
Se llaman elementos los componentes del documento que o bien empiezan con una etiqueta de
apertura y terminan con la de cierre o bien consisten solo en una etiqueta sin contenido (como <BR>).
En el «esqueleto» anterior, el elemento <HTML> es el elemento raíz, y tiene como contenido todo
el documento. Este contenido incluye dos elementos («hijos» de <HTML>): la cabecera (<HEAD>) y
el cuerpo (<BODY>). Estas etiquetas deben ir por parejas, señalando el principio y el fin (</HTML>,
</HEAD>, </BODY>) de cada sección, y deben estar convenientemente anidadas: no se puede poner
<BODY> antes de «cerrar» <HEAD>, por ejemplo, pero dentro tanto de <HEAD> como de <BODY> van otros
elementos («hijos») con sus etiquetas de apertura y cierre (o con solo etiqueta, si no tienen contenido).
Hemos utilizado mayúsculas para mejor identificar las marcas, pero el lenguaje es insensible a la
caja, y con minúsculas el documento resulta más legible, como puede apreciarse en este ejemplo, que
incluye algunos de los elementos definidos en HTML5:
<!DOCTYPE html>
<html lang="es-ES">
<head>
<meta charset="UTF-8">
<title>Introducción a HTML5</title>
</head>
<body>
<h1 style="text-align:center;">Algunos elementos de HTML5</h1>
<p>El elemento <kbd><p></kbd> contiene párrafos de texto
y otros elementos.En cualquier punto se puede forzar
un salto de línea<br> con el elemento vacío <kbd><br></kbd>
</p>
<h2>Titular de segundo nivel</h2>
<h3>Titular de tercer nivel</h3>
<p>Y así hasta h6</p>
<h2>Listas</h2>
<h3>Una lista:</h3>
<ul>
<li> uno,</li>
<li> dos y</li>
<li> tres</li>
</ul>
<h3>Una lista numerada:</h3>
<ol>
<li> uno,</li>
<li> dos y</li>
<li> tres</li>
</ol>
<h2>Tipografía y colores</h2>
<ul>
5.4 Lenguajes de marcas 85
Al recibir esta secuencia de caracteres, el navegador los interpreta y los presenta como se reproduce
en la figura 5.7, o como puede usted ver en la dirección
http://www.lab.dit.upm.es/ftel/demos-tema3/intro-html5.html.
Si observa con atención la correspondencia entre las declaraciones del código fuente y el resultado
entenderá la mayor parte de los convenios sintácticos para los elementos que se han incluido (que son
solo unos pocos: en el borrador actual de la especificación de HTML5 hay más de cien). Algunos
detalles a destacar son:
• La primera línea, <!DOCTYPE html>, no es necesaria para el navegador, pero sí para que el validador
al que se enlaza al final acepte el documento.
• Las etiquetas pueden incluir valores de ciertos atributos del contenido. Por ejemplo, en la segunda
línea: se especifica un atributo, «lang», para todo el documento, cuyo valor es «es-ES» (español
de España). Esta información tampoco es necesaria a efectos de presentación, pero el alcance del
86 Capítulo 5. Procesadores software y lenguajes interpretados
lenguaje va más allá de la mera presentación de páginas web estáticas. En otra aplicación se podría
codificar un mensaje para un agente que, con este dato, podría saber cómo interpretar el contenido
y cómo responder.
• En la cabecera, aparte del título, hay un elemento vacío «<meta>» que informa al intérprete de la
codificación de caracteres. Este dato sí es importante para el navegador: si éste está configurado para
interpretar por defecto los caracteres como ISO Latin 1 o cualquier otro código, el dato es necesario
para que los caracteres no ASCII se presenten adecuadamente.
• En algunas marcas puede usted ver que hay atributos de estilo («style="..."») que especifican
detalles sobre la forma de la presentación. Siguen la sintaxis de otro lenguaje, CSS (Cascading Style
Sheets). Aquí se han incluido en el mismo documento HTML5, pero en general es mejor incluirlos
en un documento aparte.
• Observe el elemento <kbd><p></kbd>: la etiqueta «<kbd>» indica que el texto contenido
5.4 Lenguajes de marcas 87
debe presentarse con tipografía mecanográfica (keyboard), y este contenido debe verse como «<p>».
Ahora bien, los símbolos «<» y «>» tienen un significado especial en el lenguaje y no se pueden es-
cribir directamente. Para que el navegador entienda que tiene que presentar «<» y no interpretarlo
como la apertura de un nueva marca se utiliza una referencia a entidad de carácter (o, simplemen-
te, «entidad»): «<» se refiere al carácter «<» («lt» es por «less than», menor que). Otras entidades
para caracteres reservados son «>» (>: greater than), «&» (&: ampersand) y «"» (":
quotation).
• En la parte final se presentan una imagen y un vídeo acompañados cada uno de una leyenda, y para
que salgan en dos columnas se ha recurrido al elemento <table> (este elemento es para presentar
datos en forma tabular; para presentaciones de otros elementos, como aquí, se aconseja hacerlo con
CSS, pero así también funciona y es menos complicado). Hijos de <table>son <tr> (table row:
fila), <th> (table header, cabecera) y <td> (table data).
• El elemento «<video>» es nuevo en HTML5. Si el navegador no está preparado, presenta el texto
alternativo que se incluye en el contenido. Dentro de él hay dos elementos «<source>» que hacen
referencia al mismo vídeo en tres formatos diferentes, para que pueda reproducirse en distintos
navegadores (Tema 2, final del apartado 5.4).
XML
• La línea 1 dice que el elemento raíz, asignaturas2010, debe tener como hijos uno o más (eso
significa «+») elementos asignatura.
• La línea 2, que cada elemento asignatura debe contener un elemento nombre, uno creditos, uno
semestre, uno tipo y uno o ninguno (eso significa «?») guia.
• Las líneas 3 y 4, que el contenido de esos elementos es «PCDATA» (Parsed Character Data): texto
que no contiene marcas.
• Según las líneas 5, 6 y 8, los elementos semestre, tipo y url son vacíos.
• La línea 7 dice que guia (si existe, de acuerdo con la línea 2) debe tener uno o más elementos hijos
url.
Después están las especificaciones de valores de atributos:
• La línea 9 dice que el elemento asignatura tiene que llevar obligatoriamente («#REQUIRED») un
atributo de nombre codigo con valor CDATA (Character Data) (texto que no se analiza).
• Sin embargo, el atributo acronimo (línea 10) es opcional (#IMPLIED).
• La línea 11 dice que el elemento semestre tiene necesariamente el atributo S cuyo valor puede ser
"1" o "2"
• Similar es la línea 12: el atributo T de semestre debe tener uno de los tres valores indicados.
• Finalmente, según la línea 13, el atributo href de url, con valor CDATA, es obligatorio.
Si analiza usted el documento XML verá que respeta estas reglas. Y eso es lo que hace un analizador
(parser) de XML: validar un documento XML conforme a un DTD (o a un esquema) y entregar un
árbol sintáctico a la aplicación.
Estas definiciones de lenguaje se incluyen en un documento aparte (según indica la segunda línea
del documento XML estarían en el fichero asignaturas.dtd del mismo directorio del documento,
pero podrían estar en otra URL), o bien en el prólogo del mismo documento XML de la siguiente
forma:
Resumiendo, un documento XML está bien formado si su sintaxis respeta las reglas generales del
metalenguaje XML. Y es un documento válido si, además, respeta las reglas del lenguaje concreto
definido por una DTD, o por un esquema XSD (no entramos ya en explicar XSD, que es una forma
más moderna de definir el lenguaje).
Después de validar un documento, el intérprete ha de ejecutarlo (figura 5.6). Esto ya depende de la
aplicación. En el ejemplo anterior, el mismo documento XML podría servir para generar una página
web, para generar un pdf, para generar un mensaje de correo, etc. Para esto hay procesadores que, ba-
sándose en reglas que se definen en XSLT (Extensible Stylesheet Language Transformations), obtienen
los resultados deseados.
La flexibilidad y la universalidad de XML lo han convertido en la lengua franca de Internet. Cada
vez más lenguajes para aplicaciones diversas se basan en XML. De uno de ellos vimos un ejemplo en el
apartado 5.4 del Tema 2: SVG, para la representación de imágenes vectoriales. La DTD de SVG (bas-
tante más compleja que la anterior) está publicada en http://www.w3.org/TR/SVG/svgdtd.html.
90 Capítulo 5. Procesadores software y lenguajes interpretados
JavaScript
No hay que confundir JavaScript (que, para ser más precisos, deberíamos llamar «ECMAScript»,
nombre del estándar) con Java, un lenguaje compilado que estudirá usted en la asignatura «Programa-
ción».
El uso principal de JavaScript es la inclusión de funciones en las páginas web para conseguir efectos
dinámicos e interactivos añadidos a los efectos estáticos de HTML. Por eso, se habla de «DHTML»
(Dynamic HTML). Efectos como mostrar una alerta cuando el cursor pasa por una zona, validar valores
de un formulario antes de enviarlo al servidor, o cambiar una imagen por otra cuando el cursor pasa por
ella.
Se pueden insertar fragmentos de JavaScript en el mismo documento HTML. Por ejemplo:
<html>
<head>
<meta charset="UTF-8">
<title>Introducción a JavaScript (1)</title>
</head>
<body>
<h1>Fechas y datos del documento</h1>
<script type="text/javascript">
<!--
document.write("Hora local: ");
document.write(Date());
document.write("<br>URL: "+document.location+"<br>");
document.write("Modificado el "+document.lastModified);
// -->
</script>
</body>
</html>
Además de onClick hay otros atributos cuyo valor indica la función que se «dispara» al producirse
el evento: onMouseOver, onMouseOut, onKeyPress, etc. Unido a la facilidad de escribir funciones en
general (no como las utilizadas en el ejemplo, que son llamadas a funciones ya predefinidas) permite
diseñar páginas con efectos muy variados.
Como último ejemplo, ya que conocemos la función del máximo común divisor en C (apartado 5.3),
y en JavaScript es prácticamente idéntica, veamos cómo se puede integrar en una página HTML:
<html>
<head>
<meta charset="UTF-8">
<title>Introducción a JavaScript (2)</title>
<script type="text/javascript">
function MCD(x,y) // Calcula el máximo común divisor
while (x != y) { // de x e y mediante el
if (x > y) x = x - y; // algoritmo de Euclides
else y = y - x;
}
return x; // Devuelve el resultado en x
}
function numero(elemento) { //Traduce los caracteres a número entero
return (parseInt(elemento.value));
}
function Calcular(x,y) { //Llama a MCD y escribe el resultado
document.getElementById("resultado").innerHTML=
"El máximo común divisor de "+x+" y "+y+" es "+MCD(x,y);
}
</script>
</head>
<body>
<form>
Escribe un número:
<input type="text" value="" size="10" onChange="x = numero(this)">
<br>Escribe otro:
<input type="text" value="" size="10" onChange="y = numero(this)">
<br><input type="button" value="Calcular MCD" onClick="Calcular(x,y)">
</form>
<p id="resultado"></p>
</body>
</html>
En el script de la cabecera hay ahora tres funciones. Normalmente, en lugar de insertar el código
JavaScript (que puede contener muchas funciones) en la página HTML se incluye en un fichero aparte
con la extensión .js, y en la cabecera se enlaza con
<script type="text/javascript" src="fichero.js"></script>.
Pero veamos antes algunos elementos HTML del cuerpo que no habían aparecido en los ejemplos
anteriores:
• <form> se utiliza normalmente para rellenar y enviar datos al servidor; aquí solo para recoger dos
datos y llamar a un función JavaScript. En sí mismo, no es un elemento «visible»: sirve para contener
otros elementos.
5.5 Lenguajes de script 93
• <input> es para recoger datos del usuario. Puede adoptar distintas formas dependiendo del valor
del atributo type. En los dos primeros casos del ejemplo, "text" presenta un cuadro para que se
introduzca un texto con un máximo de 10 caracteres.
• El valor del atributo onChange se ejecuta cuando se cambia el contenido del texto: se asigna a la
variable x el resultado de la función numero(this). «this» es el propio elemento <input>.
• En la siguiente línea, el tipo es button. Al pulsarlo se ejecuta Calcular(x,y).
Las funciones de la cabecera son:
• MCD(x,y) es la misma que ya hemos comentado tanto en ensamblador (apartado 4.5) como en C
(apartado 5.3) para calcular el máximo común divisor mediante el algoritmo de Euclides.
• numero(elemento) recibe el elemento (los dos <input> que la llaman con this) y devuelve el
número que se ha escrito. Para ello, hace uso de la función incorporada en el lenguaje parseInt. En
efecto, con elemento.value lo que se obtiene es la cadena de caracteres. parseInt convierte esa
cadena en un número entero. Para hacer las cosas bien se debería comprobar previamente que to-
dos los caracteres introducidos son numéricos (hay un comentario sobre esto en el código fuente de
http://www.lab.dit.upm.es/ftel/demos-tema3/intro-JS3.html). Si se introducen carac-
teres no numéricos el resultado es imprevisible. Puede usted comprobarlo, bajo su responsabilidad:
es posible que el navegador, o todo el sistema, quede bloqueado por falta de memoria o uso excesivo
de la UCP4 .
• Calcular(x,y) simplemente le pasa a MCD los valores numéricos obtenidos y luego sustituye la
cadena vacía que habíamos puesto (<p id="resultado"></p>) por el resultado.
AJAX
Hay muchas más posibilidades para conseguir efectos dinámicos con JavaScript. Esta introducción
debe ser suficiente para que pueda usted profundizar en su estudio mediante la consulta de las fuentes
que se citan en la bibliografía.
Para terminar, y sin entrar ya en detalles de implementación, seguramente le interesará saber cómo
funcionan, por ejemplo, las «sugerencias» de Google. Habrá usted observado que a medida que se
escriben caracteres en el cuadro de búsqueda van apareciendo, debajo, posibles continuaciones. La
implementación de interfaces de usuario «dialogantes» como ésta se hace con «AJAX» (Asynchronous
JavaScript and XML). Se trata de una combinación de técnicas de JavaScript y de transmisión de datos
mediante los protocolos de la web con la que se consigue una interacción dinámica entre las acciones
del usuario y las respuestas del servidor.
La figura 5.8 resume el principio. Ante un evento en el cliente, es decir, el navegador (como pulsar
una tecla) se crea, mediante JavaScript, un objeto que se llama HttpRequest. En la asignatura «Progra-
mación» entenderá usted qué es eso de un «objeto». Digamos, simplificadamente, que es un conjunto
estructurado de datos junto con unas funciones asociadas (abrir, enviar, etc.). El objeto se envía al ser-
vidor, que, tras analizarlo, elabora la respuesta adecuada y la remite al cliente. A diferencia de una
interacción web «clásica», al recibir la respuesta el cliente no abre una página nueva: ejecuta funcio-
nes de JavaScript para modificar lo necesario en la página actual. Los datos se transmiten codificados
en XML o en una alternativa más ligera como JSON (JavaScript Object Notation). Y en el caso del
buscador de Google esta secuencia se produce cada vez que se pulsa una tecla.
4
De aquí se extrae una lección interesante: no se fíe de cualquier página que utilice JavaScript, póngale a su navegador
una extensión para bloquearlo y admita únicamente las páginas conocidas.
94 Capítulo 5. Procesadores software y lenguajes interpretados
1. Se produce
un evento
2. Genera un
XMLHttpRequest
3. Envía el 4. Procesa el
XMLHttpRequest XMLHttpRequest
5. Elabora una
Internet respuesta
8. Actualiza
la página
A.1. Instalación
Instalación en Windows
Se puede instalar en cualquier versión a partir de Windows 98. Requiere tener previamente insta-
lado el «framework» .NET, versión 3.0 o posterior, que se puede obtener libremente del sitio web de
Microsoft.
En la página de descargas de ARMSim# se encuentra un enlace para descargar el programa insta-
lador (y también hay un enlace para descargar .NET de Microsoft). La versión actual (mayo 2010) es
ARMSim1.91Installer.msi. Al ejecutarlo se crea un subdirectorio llamado «University of Victoria»
dentro de «Archivos de Programa».
Instalación en UN*X
«Mono» es un proyecto de código abierto que implementa .NET. Permite ejecutar el programa
ARMSim# en un sistema operativo de tipo UN*X: Linux, *BSD, Mac OS X, etc. Muchas distribucio-
nes (por ejemplo, Debian y Ubuntu) contienen ya todo el entorno de Mono. Si no se dispone de él, se
puede descargar de aquí: http://www.go-mono.com/mono-downloads/download.html
Estas son las instrucciones para instalar el simulador:
95
96 Apéndice A. El simulador ARMSim# y otras herramientas
ARMPluginInterfaces.dll
ARMSim.exe
ARMSim.exe.config
ARMSim.Plugins.EmbestBoardPlugin.dll
ARMSim.Plugins.UIControls.dll
ARMSimWindowManager.dll
DockingWindows.dll
DotNetMagic2005.dll
StaticWindows.dll
pase a ser:
<add key="DockingWindowStyle" value="StaticWindows"></add>
5. Con el editor, crear un fichero de nombre ARMSim.sh (por ejemplo) con este contenido:
#!/bin/sh
mono $HOME/dotnet/ARMSim.exe
6. Hacerlo ejecutable (chmod +x ARMSim.sh) y colocarlo en un directorio que esté en el PATH (por
ejemplo, en $HOME/bin). Opcionalmente, copiarlo en el escritorio.
A.2. Uso
La interfaz gráfica es bastante intuitiva y amigable, y sus posibilidades se van descubriendo con
el uso y la experimentación. Para cargar un programa, File > Load. Puede ser un programa fuente
(con la extensión «.s») o un programa objeto («.o») procedente, por ejemplo, del ensamblador GNU
(apartado A.3). Se muestran en varias ventanas las vistas que se hayan habilitado en «View». Todas,
salvo la central (la del código) se pueden separar y colocar en otro lugar («docking windows»).
(Nota para usuarios de UN*X: con las versiones actuales de Mono no funcionan las «docking
windows»; hay que inhabilitar esta característica haciendo las ventanas estáticas, como se ha indicado
en el paso 4 de las instrucciones de instalación).
A.2 Uso 97
Vistas
Las vistas muestran la salida del simulador y el contenido de los registros y la memoria. Se pueden
seleccionar desde el menú «View».
• Code View (en el centro): Instrucciones del programa en hexadecimal y en ensamblador. Siempre
visible, no puede cerrarse. Por defecto, el programa se carga a partir de la dirección 0x00001000,
pero puede cambiarse en File > Preferences > Main Memory.
• Registers View (a la izquierda): Contenido de los dieciséis registros y del CPSR.
• Output View - Console (abajo): Mensajes de error.
• Output View - Stdin/Stdout/Stderr (abajo): texto enviado a la salida estándar.
• Memory View (abajo): Contenido de la memoria principal. Por defecto, está deshabilitada; para verla
hay que seleccionarla en el menú «View». Se presenta solo un fragmento de unos cientos de bytes,
a partir de una dirección que se indica en el cuadro de texto de la izquierda. En la tabla resultante, la
primera columna son direcciones (en hexadecimal) y en cada línea aparecen los contenidos (también
en hexadecimal) de las direcciones sucesivas. Se pueden ver los contenidos de bytes, de medias
palabras y de palabras (seleccionando, a la derecha de la ventana, «8 bits», «16 bits» o «32 bits»,
respectivamente). En la presentación por bytes se muestra también su interpretación como códigos
ASCII.
• Stack View (a la derecha): Contenido de la pila del sistema. La palabra que está en la cima aparece
resaltada. El puntero de pila se inicializa con el valor 0x00005400. El simulador reserva 32 KiB para
la pila, pero también puede cambiarse en File > Preferences > Main Memory.
• Watch View (abajo): valores de las variables que ha añadido el usuario a una lista para vigilar durante
la ejecución
• Cache Views (abajo): Contenido de la cache L1.
• Board Controls View (abajo): interfaces de usuario de plugins (si no se cargó ninguno al empezar,
no está visible).
Debajo del menú principal hay una barra con seis botones:
• Step Into: hace que el simulador ejecute la instrucción resaltada y resalte la siguiente a ejecutar. Si
es una llamada a subprograma (bl o bx), la siguiente será la primera del subprograma.
• Step Over: hace que el simulador ejecute la instrucción resaltada en un subprograma y resalte la
siguiente a ejecutar. Si es una llamada a subprograma (bl o bx), se ejecuta hasta el retorno del
subprograma.
• Stop: detiene la ejecución.
• Continue (o Run): ejecuta el programa hasta encontrar un «breakpoint», una instrucción swi 0x11
(fin de ejecución) o un error de ejecución.
• Restart: ejecuta el programa desde el principio.
• Reload: carga un fichero con una nueva versión del programa y lo ejecuta.
98 Apéndice A. El simulador ARMSim# y otras herramientas
Para poner un breakpoint en una instrucción, mover el cursor hasta ella y hacer doble click. La
ejecución se detendrá justo antes de ejecutarla. Para seguir hasta el siguiente breakpoint, botón «run».
Para quitar el breakpoint, de nuevo doble click.
Pero lo más práctico es hacer uso de una «cadena de herramientas» (toolchain): un conjunto de
procesadores software que normalmente se aplican uno tras otro: ensamblador o compilador, montador,
emulador, depurador, simulador, etc. Las hay basadas en el ensamblador GNU y en el ensamblador
propio de ARM.
Herramientas GNU
• El proyecto de código abierto GNUARM («GNU ARM toolchain for Cygwin, Linux and MacOS»,
http://www.gnuarm.com) no se ha actualizado desde 2006, pero aún tiene miembros activos. Los
programas pueden ejecutarse en UN*X y en Windows (Cygwin, http://www.cygwin.com/, es un
entorno para tener la funcionalidad de Linux sobre Windows).
• Hay varios proyectos para incluir la cadena GNU en distribuciones de Linux. Por ejemplo, en Ubun-
tu (https://wiki.ubuntu.com/EmbeddedUbuntu) y en Debian (http://www.emdebian.org/).
Esta última es la que se ha usado para generar los listados de este Tema.
• Actualmente, el software más interesante no es totalmente libre, pero sí gratuito. Es el desarrollado
por la empresa «CodeSourcery», adquirida en 2011 por «Mentor Graphics», que le ha dado el nom-
bre «Sourcery CodeBench». Tiene varias versiones comerciales con distintas opciones de entornos
de desarrollo y soporte. La «lite edition» de la cadena GNU solamente contiene las herramientas de
línea de órdenes, pero su descarga es gratuita. Está disponible (previo registro) para Linux y para
Windows en:
http://www.mentor.com/embedded-software/sourcery-tools/
sourcery-codebench/editions/lite-edition/
La documentación que se obtiene con la descarga es muy clara y completa.
Los procesadores de la cadena GNU se ejecutan desde la línea de órdenes. Todos tienen muchas
opciones, y todos tienen página de manual. Resumimos los principales, dando los nombres básicos,
que habrá que sustituir dependiendo de la cadena que se esté utilizando. Por ejemplo, as es el en-
samblador, y para ver todas las opciones consultaremos man as. Pero «as» a secas ejecuta el en-
samblador «nativo»: si estamos en un ordenador con arquitectura x86, el ensamblador para esa arqui-
tectura. El ensamblador cruzado para ARM tiene un prefijo que depende de la versión de la cadena
GNU que se esté utilizando. Para la de Debian es arm-linux-gnueabi-as, y para la «Sourcery»,
arm-none-linux-gnueabi-as. Y lo mismo para los otros procesadores (gcc, ld, etc.).
• as es el ensamblador. Si el programa fuente está en el fichero fich.s, con la orden as -o fich.o
<otras opciones> fich.s se genera el código objeto y se guarda en fich.o (sin la opción -o,
se guarda en a.out).
Algunas otras opciones:
-EB genera código con convenio extremista mayor (por defecto es menor). Útil si solo se quiere un
listado que sea más legible, como se explica en el último párrafo del apartado 4.1.
-al genera un listado del programa fuente y el resultado, como los del capítulo 4.
-as genera un listado con la tabla de símbolos del programa fuente. Se puede combinar con el
anterior: -als
• gcc es el compilador del lenguaje C. Normalmente realiza todos los procesos de compilación, en-
samblaje y montaje. Si los programas fuente están en los ficheros fich1.c, fich2.c, etc., la orden
gcc -o fich <otras opciones> fich1.c fich2.c...
genera el código objeto y lo guarda en fich.
100 Apéndice A. El simulador ARMSim# y otras herramientas
Android SDK
El SKD (Software Development Kit) de Android es una colección de herramientas con las que
trabajará usted en la asignatura «Análisis y diseño de software». Se puede descargar libremente de
http://developer.android.com/sdk/, y lo incluimos aquí porque en el apartado 4.10 hemos he-
cho referencia a uno de los componentes, el adb (Android Debug Bridge), con el que se pueden instalar
y depurar aplicaciones en un dispositivo conectado al ordenador mediante USB.
Herramientas ARM
Keil es una de las empresas del grupo ARM, dedicada al desarrollo de herramientas de software.
Todas están basadas en un ensamblador propio, que es algo distinto al de GNU que hemos visto aquí.
El «MDK-ARM» (Microcontroller Development Kit) es un entorno de desarrollo completo para
varias versiones de la arquitectura ARM que solamente funciona en Windows. La versión de evaluación
se puede descargar gratuitamente (aunque exige registrarse) de http://www.keil.com/demo/.
Bibliografía
101