Capítulo 10
Programación de BRM
Ya hemos dicho en el apartado 1.6 que un lenguaje ensamblador, a diferencia de uno de alto nivel,
está ligado a la arquitectura (ISA) de un procesador. Pero no sólo 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 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 B.3 del apéndice B 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 B.3): un programa ensam-
blador que se ejecuta bajo un sistema operativo (Windows, Linux, Mac OS X...) 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. También
se pueden ensamblar directamente y ejecutar en un sistema con arquitectura ARM, como se explica en
el apartado 10.9. Debería usted no solamente 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.
10.1. Primer ejemplo
Construimos un programa en lenguaje ensamblador escribiendo secuencialmente instrucciones con
los convenios sintácticos que hemos ido avanzando en el capítulo anterior. Por ejemplo, para sumar los
números enteros 8 y 10 y dejar el resultado en R2, la secuencia puede ser:
mov r0,#8
mov r1,#10
add r2,r0,r1
Un conjunto de instrucciones en lenguaje ensamblador se llama programa fuente. Se traduce a
binario con un ensamblador, que da como resultado un programa objeto, o código objeto en un
fichero binario (apartado 7.4).
167
168 Capítulo 10. Programación de BRM
Pero a nuestro programa fuente le faltan al menos dos cosas:
• Suponga que el código objeto se carga a partir de la dirección 0x1000 de la memoria. Las tres
instrucciones ocuparán las direcciones 0x1000 a 0x100B inclusive. El procesador las ejecutará
en secuencia (no hay ninguna bifurcación), y, tras la tercera, irá a ejecutar lo que haya a partir
de la dirección 0x100C. Hay que decirle que no lo haga, ya que el programa ha terminado. Para
esto está la instrucción SWI (apartado 9.6). Normalmente, tendremos un sistema operativo o, al
menos, un programa de control, que se ocupa de las interrupciones de programa. En el caso del
simulador ARMSim#, swi 0x11 hace que ese programa de control interprete, al ver «0x11»,
que el programa ha terminado (tabla B.1). Por tanto, añadiremos esta instrucción.
• El programa fuente puede incluir informaciones u órdenes para el programa ensamblador que
no afectan al código resultante (no se traducen por ninguna instrucción de máquina). Se llaman
directivas. Una de ellas es «.end» (en el ensamblador GNU todas las directivas empiezan con
un punto). Así como la instrucción SWI provoca, en tiempo de ejecución, una interrupción para el
procesador (hardware), la directiva .end le dice al ensamblador (procesador software), en tiempo
de traducción, que no siga leyendo porque se ha acabado el programa fuente. Siempre debe ser
la última línea del programa fuente.
Iremos viendo más directivas. De momento vamos a introducir otra: «.equ» define un símbolo y le
da un valor; el ensamblador, siempre que vea ese símbolo, lo sustituye por su valor.
Por último, para terminar de acicalar nuestro ejemplo, podemos añadir comentarios en el programa
fuente, que el ensamblador simplemente ignorará. En nuestro ensamblador se pueden introducir de dos
maneras:
• Empezando con la pareja de símbolos «/*», todo lo que sigue es un comentario hasta que aparece
la pareja «*/».
• Con el símbolo «@», todo lo que aparece hasta el final de la línea es un comentario1 .
Para facilitar la legibilidad se pueden añadir espacios en blanco al comienzo de las líneas. Entre el
código nemónico (por ejemplo, «mov») y los operandos (por ejemplo, «r0,r1») debe haber al menos
un espacio en blanco. Los operandos se separan con una coma seguida o no de espacios en blanco.
De acuerdo con todo esto, nuestro programa fuente queda así:
/***********************************************
* *
* Primer ejemplo *
* *
***********************************************/
.equ cte1,10
.equ cte2,8
mov r0,#cte1 @ Carga valores en R0
mov r1,#cte2 @ y R1
add r2,r0,r1 @ y hace (R0) + (R1) → R2
swi 0x11
.end
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 un fichero binario), nos dé un listado (en el apartado B.3 se explica cómo hacerlo), obtenemos lo
que muestra el programa 10.1.
1
En el ensamblador GNU se puede usar, en lugar de «@», «#» (siempre que esté al principio de la línea), y en ARMSim#
se puede usar «;».
10.2 Etiquetas y bucles 169
/***********************************************
* *
* 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
Programa 10.1: Comentarios y directivas equ y end.
Vemos que nos devuelve lo mismo que le habíamos dado (el programa fuente), acompañado de
dos columnas que aparecen a la izquierda, con valores en hexadecimal. La primera columna son direc-
ciones de memoria, y la segunda, contenidos de palabras que resultan de haber traducido a binario las
instrucciones.
Si carga usted el mismo fichero en el simulador ARMSim# verá en la ventana central el mismo
resultado. Luego, puede simular la ejecución paso a paso poniendo puntos de parada (apartado B.2)
para ver cómo van cambiando los contenidos de los registros. La única diferencia que observará es que
la primera dirección no es 0x0, sino 0x1000. El motivo es que el simulador carga los programas a partir
de esa dirección2 .
Este listado y los siguientes se han obtenido con el ensamblador GNU (apartado B.3), y si prueba
usted a hacerlo notará que la segunda columna es diferente: los contenidos de memoria salen escritos «al
revés». Por ejemplo, en la primera instrucción no aparece «E3A0000A», sino «0A00A0E3». En efecto,
recuerde que el convenio es extremista menor, por lo que el byte menos significativo de la instrucción,
que es 0A, estará en la dirección 0, el siguiente, 00, en la 1, y así sucesivamente. Sin embargo, la otra
forma es más fácil de interpretar (para nosotros, no para el procesador), por lo que le hemos pasado
al ensamblador una opción para que escriba como extremista mayor. En el diseño de ARMSim# han
hecho igual: presentan las instrucciones como si el convenio fuese extremista mayor, pero internamente
se almacenan como extremista menor. Es fácil comprobarlo haciendo una vista de la memoria por bytes
(apartado B.2).
10.2. Etiquetas y bucles
En el apartado 8.7, al hablar de modos de direccionamiento, pusimos, en pseudocódigo, un ejemplo
de bucle con una instrucción de bifurcación condicionada, e, incluso, adelantamos algo de su codifi-
cación en ensamblador. Como ese ejemplo requiere tener unos datos guardados en memoria, y aún
no hemos visto la forma de introducir datos con el ensamblador, lo dejaremos para más adelante, y
codificaremos otro que maneja todos los datos en registros.
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)
2
Aunque esto se puede cambiar desde el menú: File > Preferences > Main Memory.
170 Capítulo 10. Programación de BRM
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 10.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
Programa 10.2: Ejemplo de bucle.
Fíjese en cuatro detalles importantes:
• Aparte del símbolo «Num», que se iguala con el valor 233, en el programa se definen tres sím-
bolos, que se llaman etiquetas. Para no confundirla con otro tipo de símbolo, la etiqueta, en
su definición, debe terminar con «:», y toma el valor de la dirección de la instrucción a la que
acompaña: «bucle» tiene el valor 0x10, «si» 0x2C, y «no» 0x30. De este modo, al escribir en
ensamblador nos podemos despreocupar de valores numéricos de direcciones de memoria.
• Debería usted comprobar cómo ha traducido el ensamblador las instrucciones de bifurcación:
◦ En la que está en la dirección 0x18 (bifurca a «si» si son iguales) ha puesto «3» en el campo
«Dist» (figura 9.10). Recuerde que la dirección efectiva se calcula (en tiempo de ejecución)
10.3 El pool de literales 171
como la suma de la dirección de la instrucción más 8 más 4×Dist. En este caso, como 0x18 =
24, resulta 24 + 8 + 4 × 3 = 34 = 0x2C, que es el valor del símbolo «si».
◦ En la que está en 0x1C (bifurca a «no» si mayor) ha puesto la misma distancia, 3. En efecto,
la «distancia» a «no» es igual que la anterior.
◦ En la que está en 0x28 (40 decimal) (bifurca a «bucle») ha puesto Dist = FFFFF8, que, al
interpretarse como número negativo en complemento a dos, representa «-8». DE = 40 + 8 +
4 × (−8) = 16 = 0x10, dirección de «bucle».
• 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 9.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 al tratar de ensamblar.
10.3. El pool de literales
Para aquellas constantes cuyos valores no pueden obtenerse con el esquema de la rotación de ocho
bits no hay más remedio que tener la constante en una palabra de la memoria y acceder a ella con una
instrucción LDR (apartado 9.5).
Aún no hemos visto cómo poner constantes en el código fuente, pero no importa, porque el servicial
ensamblador hace el trabajo por nosotros, y además nos libera de la aburrida tarea de calcular la dis-
tancia necesaria. Para ello, existe una pseudoinstrucción que se llama «LDR». Tiene el mismo nombre
que la instrucción, pero se usa de otro modo: para introducir en un registro una constante cualquiera,
por ejemplo, para poner 2.011 en R0, escribimos «ldr r0, =2011» y el ensamblador se ocupará de
ver si la puede generar mediante rotación. Si puede, la traduce por una instrucción mov. Y si no, la
traduce por una instrucción LDR que carga en R0, con direccionamiento relativo, la constante 2.011 que
él mismo introduce al final del código objeto.
Compruebe cómo funciona escribiendo un programa que contenga estas instrucciones (u otras si-
milares):
ldr r0, =2011 ldr r1, =-2011 ldr r2, =0xfff
ldr r3, =0xffff ldr r4, =4096 ldr r5, =0xffffff
ldr r6, =2011 ldr r7, =-2011 ldr r7, =0xfff
ldr r8, =0xffff
172 Capítulo 10. Programación de BRM
y cargándolo en el simulador. Obtendrá este resultado:
00001000 E59F0024 ldr r0, =2011
00001004 E59F1024 ldr r1, =-2011
00001008 E59F2024 ldr r2, =0xfff
0000100C E59F3024 ldr r3, =0xffff
00001010 E3A04A01 ldr r4, =4096
00001014 E3E054FF ldr r5, =0xffffff
00001018 E59F600C ldr r6, =2011
0000101C E59F700C ldr r7, =-2011
00001020 E59F700C ldr r7, =0xfff
00001024 E59F800C ldr r8, =0xffff
00001028 EF000011 swi 0x11
Veamos lo que ha hecho al ensamblar:
• La primera instrucción la ha traducido como 0xE59F0024. Mirando los formatos, esto corres-
ponde a ldr r0, [pc, #0x24] (figura 9.9). Cuando el procesador la ejecute PC tendrá el valor
0x1000 + 8, y la dirección efectiva será 0x1008 + 0x24 = 0x102C. Es decir, carga en R0 el con-
tenido de la dirección 0x102C. ¿Y qué hay en esa dirección? La ventana central del simulador
sólo muestra el código, pero los contenidos de la memoria se pueden ver como se indica en el
apartado B.2, y puede usted comprobar que el contenido de esa dirección es 0x7DB = 2011. ¡El
ensamblador ha creado la constante en la palabra de dirección inmediatamente siguiente al có-
digo, y ha sintetizado la instrucción LDR con direccionamiento relativo y la distancia adecuada
para acceder a la constante!
• Para las tres instrucciones siguientes ha hecho lo mismo, creando las constantes 0xFFFF825
(−2011 en complemento a 2), 0xFFF y 0xFFFF en las direcciones siguientes (0x1030, 0x1034 y
0x1038), y generando las instrucciones LDR oportunas.
• Para 4.096, sin embargo, ha hecho otra cosa, porque la constante la puede generar con «1» en el
campo «inmed_8» y «10» en el campo «rot» (figura 9.6). Ha traducido igual que si hubiésemos
escrito mov r4,#4096.
• La siguiente es interesante: E3E054FF, si la descodificamos de acuerdo con su formato, que es el
de la figura 9.5, resulta que la ha traducido como una instrucción MVN con «0xFF» en el campo
«inmed_8» y «4» en el campo «rot»: un operando inmediato que resulta de rotar 2 × 4 veces a
la derecha (o 32 − 8 = 24 veces a la izquierda) 0xFF, lo que da 0xFF000000. Es decir, la ha
traducido igual que si hubiésemos escrito mvn r5,#0xFF000000. La instrucción MVN hace la
operación NOT (tabla 9.2), y lo que se carga en R5 es NOT(0xFF000000) = 0x00FFFFFF, como
queríamos. Tampoco ha tenido aquí necesidad el ensamblador de generar una constante.
• Las cuatro últimas LDR son iguales a las cuatro primeras y si mira las distancias comprobará que
el ensamblador no genera más constantes, sino accesos a las que ya tiene.
En resumen, utilizando la pseudoinstrucción LDR podemos poner cualquier constante (entre −2−31
y 2 − 1) y el ensamblador se ocupa, si puede, de generar una MOV (o una MVN), y si no, de ver si ya
31
tiene la constante creada, de crearla si es necesario y de generar la instrucción LDR adecuada. Al final,
tras el código objeto, inserta el «pool de literales», que contiene todas las constantes generadas.
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 10.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.