Você está na página 1de 46

Teoría de Fundamentos de Computadores

Tema 4:
Fundamentos de Lenguaje Máquina

Profesor responsable:
Santiago Romaní (santiago.romani@urv.cat)
Departamento de Ingeniería Informática y Matemáticas
Escuela Técnica Superior de Ingeniería
Universitat Rovira i Virgili
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

Índice de contenido
Sección 1 :Introducción al lenguaje máquina ARM................................................4
1.1 El sistema programable................................................................................................... 4
1.2 El procesador................................................................................................................... 5
1.3 La memoria......................................................................................................................6
1.4 Codificación de datos e instrucciones............................................................................. 7
1.5 La secuencia de ejecución............................................................................................... 8
1.6 Cálculo del tiempo de ejecución de un programa........................................................... 9
1.7 Los registros.................................................................................................................. 10
1.8 El operando inmediato...................................................................................................11
1.9 El operando registro desplazado....................................................................................12
1.10 Instrucciones aritméticas............................................................................................. 13
1.11 Instrucciones lógicas................................................................................................... 14
1.12 Instrucciones de salto.................................................................................................. 15
1.13 Instrucciones de acceso a memoria............................................................................. 16
1.14 Instrucciones de acceso a la pila..................................................................................17

Sección 2 :Introducción al lenguaje ensamblador GAS........................................ 18


2.1 Definición de Lenguaje Ensamblador........................................................................... 18
2.2 Comentarios...................................................................................................................19
2.3 Secciones de memoria................................................................................................... 20
2.4 Definición de valores.....................................................................................................21
2.5 Variables globales..........................................................................................................22
2.6 Alineación de memoria..................................................................................................23
2.7 Etiquetas de salto...........................................................................................................24
2.8 Carga de direcciones de memoria..................................................................................25
2.9 Definición de rutinas..................................................................................................... 26

Sección 3 :Programación en bajo nivel...................................................................27


3.1 Traducción de un programa simple............................................................................... 27
3.2 Instrucciones secuenciales.............................................................................................28
3.3 Estructura condicional 'if-else'...................................................................................... 29
3.4 Condiciones múltiples................................................................................................... 30
3.5 Estructura repetitiva 'for'............................................................................................... 31
3.6 Estructuras repetitivas 'while' y 'do-while'.................................................................... 32
3.7 Acceso a vectores.......................................................................................................... 33
3.8 Almacenamiento de matrices.........................................................................................34
3.9 Acceso bidimensional a matrices.................................................................................. 35
3.10 Acceso unidimensional a matrices.............................................................................. 36

Sección 4 :Programación con rutinas..................................................................... 37


4.1 Llamada a una función simple.......................................................................................37
4.2 Implementación de una función simple.........................................................................38
4.3 Paso de parámetros por referencia.................................................................................39
4.4 Paso de vectores por parámetro.....................................................................................40
4.5 Paso de matrices por parámetro.....................................................................................41

2
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

4.6 Paso de matrices como vectores.................................................................................... 42


4.7 Invocación de rutina pasando un matriz como vector................................................... 43
4.8 Llamadas anidadas........................................................................................................ 44
4.9 Llamadas recursivas...................................................................................................... 45
4.10 Evolución de las llamadas recursivas.......................................................................... 46

3
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

Sección 1 : Introducción al lenguaje máquina ARM


1.1 El sistema programable
Cualquier sistema electrónico programable, ya sea un ordenador (también denominado
computadora), un dispositivo personal (teléfono móvil, PDA, GPS, tarjeta inteligente, etc.) o un
control de un mecanismo (vehículo, electrodoméstico, maquinaria industrial, etc.), está constituido
básicamente por elementos de procesamiento (Procesador), elementos de almacenamiento de
información (Memoria) y elementos que se encargan de la transferencia de información con el
exterior (Entrada/Salida).
El siguiente esquema muestra la conexión lógica entre los tres tipos de elementos a través del
denominado Bus del sistema, que consiste en una serie conexiones por cable y otros componentes
electrónicos que permiten la comunicación entre dichos elementos.

Sistema programable

Entrada/ exterior
Procesador Memoria
Salida

Bus
direcciones
datos
control

El bus del sistema está formado por varios (sub)buses:


• bus de direcciones: selecciona la posición de la memoria o del espacio de entrada/salida
que el procesador quiere leer o escribir,
• bus de datos: transfiere el contenido desde (lectura) o hacia (escritura) esa posición
• bus de control: permite indicar, entre otras cosas, el sentido de la transferencia.
Este tipo de sistemas lo denominaremos genéricamente como un computador. Para estudiar los
fundamentos de la programación de un computador a bajo nivel, o sea, en Lenguaje Máquina, nos
basaremos en el modelo de los procesadores ARM (versión 4), puesto que es uno de los más
utilizados actualmente para dispositivos personales y de control (dispositivos “embedded” o
empotrados).

4
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

1.2 El procesador
El procesador es la unidad electrónica (habitualmente contenida en un chip) que se encarga de leer y
procesar las instrucciones y los datos de un programa, que están almacenadas en la memoria del
computador.
El siguiente esquema muestra un modelo muy simplificado de los elementos que componen un
procesador y su relación con los buses del sistema, ignorando el bloque de entradas y salidas:

Procesador Memoria
direcciones dirección contenido
Cálculo de
000...000000 00000000
direcciones 000...000001 00111000
Registros 000...000010 00001010
000...000011 00000111
temporales 000...000100 11110100
000...000101 10010101
000...000110 00000011
000...000111 00000000
Registros datos
Unidad 000...001000 11111111
Aritmético de L.M. 000...001001 11110000
000...001010 11110000
Lógica 000...001011 00010111
000...001100 00011100
000...001101 01101001
… …
… …
control … …
Unidad de Control … …
111...111111 00000000

Los componentes del procesador indicados tienen las siguientes funciones:


• Registros de L.M.: son los registros que el programador de lenguaje máquina tiene a su
disposición para manipular los datos y las direcciones del programa,
• Registros temporales: son registros que contienen información temporal para completar la
acción de las instrucciones que se están procesando en ese momento,
• Unidad Aritmético-Lógica (ALU): es la parte que se encarga de realizar las operaciones
aritméticas (suma, resta, multiplicación, etc.) y lógicas (AND, OR, desplazamientos, etc.)
con los datos contenidos en los registros,
• Cálculo de direcciones: se encarga de efectuar algunas operaciones aritméticas y lógicas
para obtener las direcciones en memoria de las instrucciones y los datos del programa,
• Unidad de Control: es la parte que gestiona el funcionamiento del resto de componentes
del procesador.
Todos estos componentes se implementan con circuitos electrónicos digitales básicos (puertas
lógicas), bloques combinacionales (sumadores, multiplexores, etc.) y bloques secuenciales
(biestables, registros, contadores, etc.); además, la Unidad de Control consiste en una máquina de
estados finita (autómata) que interpreta las instrucciones, genera las señales de control para el resto
de bloques y gestiona el secuenciamiento del programa (orden de las instrucciones).

5
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

1.3 La memoria
En la memoria están almacenadas todas las instrucciones y los datos de los programas a ejecutar,
organizadas como un vector de 2n posiciones de m bits cada una, donde n indica el número de bits
de la dirección.
Habitualmente, m=8, o sea que la unidad mínima de información que se puede leer o escribir en la
memoria es un byte, es decir, un paquete de 8 bits.
En los procesadores ARM, n=32, o sea que se pueden generar más de 4 mil millones de direcciones
diferentes, es decir, 4 Gigabytes.
Además, los procesadores ARM pueden procesar datos de 8, 16 o 32 bits. Esto significa que es
posible acceder a 1, 2 o 4 posiciones consecutivas de memoria con una sola transferencia. Cuando
se accede a 2 o 4 posiciones a la vez, es preciso conocer el orden de los bytes. Existen dos
posibilidades:
• Little Endian: los bytes están ordenados de menor a mayor peso,
• Big Endian: los bytes están ordenados de mayor a menor peso.
Los siguientes esquemas muestran ejemplos de ordenación de varias combinaciones de bytes; dado
que la lectura de secuencias largas de bits es muy confusa para las personas, habitualmente se
utiliza la notación hexadecimal, ya que cada dígito representa una agrupación de 4 bits. El prefijo
'0x' significa que el número que viene a continuación está expresado en base 16. Además, si se trata
de paquetes de 32 bits, los 8 dígitos hexadecimales se separan en dos grupos de 4 con un espacio en
blanco, también para facilitar su lectura:

dirección bytes
: : : Ordenación Little Endian
Byte = 0xC3
0x0000 001F 0x00 Dirección = 0x0000 00A0 Half Word = 0x81C3
0x0000 00A0 0xC3 Word = 0xF410 81C3
0x0000 00A1 0x81
0x0000 00A2 0x10
0x0000 00A3 0xF4
0x0000 00A4 0x92 Ordenación Big Endian
0x0000 00A5 0x5B Byte = 0xC3
0x0000 00A6 0xEA Dirección = 0x0000 00A0 Half Word = 0xC381
: : : Word = 0xC381 10F4
: : :

En el ejemplo anterior se introduce la nomenclatura que utiliza ARM para denominar los paquetes
de 8 bits (byte), 16 bits (half word) y 32 bits (word). Hay que observar que el acceso a la misma
dirección de memoria (0x0000 00A0) implica acceder a 1, 2 o 4 posiciones consecutivas, según el
tamaño de los paquetes. Además, el valor del paquete depende del tipo de ordenación, excepto en el
caso del byte.
Los procesadores ARM se pueden configurar para trabajar con los dos tipos de ordenación, pero el
tipo que se escoja no se puede cambiar mientras se está ejecutando el programa. Como convenio, en
este manual vamos a utilizar siempre la ordenación Little Endian.

6
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

1.4 Codificación de datos e instrucciones


Dentro de un computador, cualquier información (valores, textos, imágenes, sonido, gráficas, etc.)
se codifica con números, habitualmente enteros o naturales, pero también con valores reales (coma
flotante). En el procesador y en la memoria, estos números se almacenan e interpretan con lógica
digital binaria, es decir, como ceros y unos.
Hay que tener en cuenta que el tamaño del paquete de bits determina los rangos máximos de valores
que pueden abarcar dichos paquetes. También hay que codificar valores negativos en binario,
habitualmente con la notación de Complemento a 2. Los siguientes ejemplos muestran la
codificación en binario y en hexadecimal de tres números enteros:
Números Tamaño Representación en binario(Ca2) Hexadecimal
18 byte 00010010 0x12
13.456 half word 0011010010010000 0x3490
-2 word 1111111111111111 1111111111111110 0xFFFF FFFE

Las instrucciones de los programas también se codifican en binario, siguiendo un patrón establecido
por los diseñadores del procesador. En el caso del ARM, todas las instrucciones son de 32 bits. El
siguiente ejemplo muestra los códigos binarios de la instrucción “ add r0, r1, r2”, cuya acción es
sumar los registros de L.M. r1 y r2, y almacenar el resultado en el registro r0:
cnd TA IF TB S Rn Rd Rs ST SF Rm
1110 00 0 0100 0 0001 0000 0000 0 00 0 0010 = 0xE081 0002

De los diversos campos que componen la instrucción, vamos a fijarnos en los más significativos
para generar una instrucción del tipo “ add Rd, Rn, Rm”:
• TA: código de operación A → “00” para instrucciones aritméticas o lógicas,
• TB: código de operación B → “0100” para la suma,
• Rn: primer registro fuente → “0001” para r1,
• Rd: registro destino → “0000” para r0,
• Rm: segundo registro fuente → “0010” para r2.

Sólo con estos campos se pueden definir hasta 16 instrucciones aritméticas o lógicas, y podemos
especificar hasta 16 registros distintos como operandos, puesto que disponemos de 4 bits para cada
uno de los campos correspondientes.
Como resultado de la codificación, 0xE081 0002 es el Código Máquina de “add r0, r1, r2”, que
es la representación simbólica de la instrucción. Esta representación simbólica se llama Lenguaje
Ensamblador, y nos permite expresar el lenguaje máquina de una forma más comprensible que el
código máquina puro. Además, existen programas que traducen automáticamente el lenguaje
ensamblador a código máquina, que se denominan Ensambladores.
Hay que señalar que la mayoría de procesadores ARM también son capaces de interpretar un
“dialecto” de lenguaje máquina que se denomina THUMB, y que utiliza sólo 16 bits por
instrucción, lo que permite ahorrar espacio de memoria para codificar el programa, a costa de
limitar algunas de las opciones que permite la codificación ARM. Por ejemplo, sólo se pueden
utilizar 8 de los 16 registros del procesador, porque para cada operando de tipo registro sólo se
reservan 3 bits para su codificación. En este manual utilizaremos siempre la codificación ARM,
aunque no estudiaremos todas las instrucciones y operandos posibles, ya que no se pretende dar un
curso exhaustivo sino una visión introductoria y ágil de la programación en ARM.

7
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

1.5 La secuencia de ejecución


Cuando un computador ejecuta un programa, habitualmente sigue un orden secuencial, es decir,
después de ejecutar una instrucción, por defecto, pasa a ejecutar la siguiente instrucción del
programa. A este comportamiento se le denomina Secuenciamiento Implícito.
Evidentemente, en algún punto del programa será necesario romper este orden, bien para ejecutar
algunas instrucciones si se cumple cierta condición (estructura condicional), bien para repetir un
trozo de programa cierto número de veces (estructura repetitiva). Las instrucciones de salto
condicionales o incondicionales permiten especificar la dirección de memoria del punto del
programa dónde se requiere continuar la ejecución. Esto se denomina Secuenciamiento Explícito.
Para ejemplificar estos comportamientos, se sugiere el siguiente programa de ejemplo:

dirección instrucciones
0x0000 mov r0, #0
0x0004 mov r1, #2
0x0008 add r0, r1
0x000C cmp r0, #6
0x0010 bne 0x0008
0x0014 b 0x0014
0x0018 :
: :

Se trata de un par de inicializaciones (mov) de los registros r0 y r1, una suma (add) de r1 sobre r0,
una comparación (cmp) de r0 con el valor 6, un salto condicional (bne) si r0 diferente de 6, y un
salto incondicional (b) hacia 0x0014, lo que supone un bucle infinito. La secuencia de ejecución de
este programa será la siguiente:

Contador de programa Instrucción Resultado Secuenciamiento

PC: 0x0000 mov r0, #0 R0: 0x0000 implícito

PC: 0x0004 mov r1, #2 R1: 0x0002 implícito

PC: 0x0008 add r0, r1 R0: 0x0002 implícito

PC: 0x000C cmp r0, #6 No iguales implícito

PC: 0x0010 bne 0x0008 Saltar explícito

PC: 0x0008 add r0, r1 R0: 0x0004 implícito


PC: 0x000C cmp r0, #6 No iguales implícito
PC: 0x0010 bne 0x0008 Saltar explícito

PC: 0x0008 add r0, r1 R0: 0x0006 implícito

PC: 0x000C cmp r0, #6 Iguales implícito

PC: 0x0010 bne 0x0008 No saltar implícito

PC: 0x0014 b 0x0014 Saltar explícito

PC: 0x0014 b 0x0014 Saltar explícito

PC: 0x0014 b 0x0014 Saltar explícito

etc.

8
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

1.6 Cálculo del tiempo de ejecución de un programa


Cualquier programa tiene un tiempo de ejecución asociado, que vendrá determinado básicamente
por tres factores:
• Número total de instrucciones ejecutadas
• Ciclos máquina asociados a cada tipo de instrucción
• Tiempo por ciclo máquina
El número total de instrucciones ejecutadas no se refiere al número total de instrucciones del
programa, puesto que las estructuras condicionales y repetitivas provocan que algunas instrucciones
no se ejecuten o se ejecuten más de una vez.
En el programa de ejemplo del apartado anterior podemos contar 12 instrucciones ejecutadas hasta
el punto donde el procesador se queda “enganchado” en el bucle infinito, que es lo que podríamos
considerar el final del programa.
Observar que el número total de instrucciones del programa es 6, y que el número de instrucciones
ejecutadas depende de los valores que maneja el programa. Por ejemplo, si la instrucción de
comparación fuese 'cmp r0, #8', el número de instrucciones ejecutadas seria 15 (una vuelta más del
bucle → 3 instrucciones más a ejecutar).
Sin embargo, para saber el tiempo de ejecución hay que distinguir los tipos de instrucciones
involucradas, puesto que cada tipo tiene su número de ciclos. A groso modo, podemos establecer las
siguientes equivalencias:
• instrucción aritmética o lógica → 1 ciclo
• instrucción de salto efectivo → 3 ciclos
• instrucción de salto no efectivo → 4 ciclos
Por lo tanto, para el programa del ejemplo anterior habría que realizar los siguientes cálculos:
✔ 8 instrucciones aritméticas o lógicas (mov, add y cmp) → 8 ciclos
✔ 3 instrucciones de salto efectivo (bne, b) → 9 ciclos
✔ 1 instrucción de salto no efectivo (bne) → 4 ciclos
Total, 8+9+4 = 21 ciclos máquina. Si disponemos de un sistema ARM con frecuencia de reloj del
procesador de 1 Ghz (1000 millones de ciclos máquina por segundo), el tiempo de cada ciclo
máquina es la inversa de la frecuencia, o sea, 10 elevado a -9 segundos, o dicho de otro modo, 1
nanosegundo (la milésima parte de un microsegundo).
Por lo tanto, el programa anterior tarda 21 nanosegundos en ejecutarse. Aunque es un tiempo
inimaginablemente corto para nosotros, hay que tener en cuenta que los programas tienen que
manejar miles de millones de datos para obtener los resultados que se espera de ellos, lo cual
conlleva tiempos de ejecución perceptibles para las personas (del orden de segundos).
Por dicha razón, siempre hay que buscar la forma de optimizar los programas, ya que una
instrucción menos dentro de un bucle puede ser determinante para que el programa pueda responder
en un tiempo aceptable o no, sobretodo cuando se trata de aplicaciones empotradas en dispositivos
móviles o de control.

9
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

1.7 Los registros


En apartados anteriores hemos utilizado registros de lenguaje máquina para introducir el
comportamiento de determinadas instrucciones. También hemos comentado que la instrucciones en
formato ARM reservan 4 bits para especificar los operandos de tipo registro, lo que implica un total
de 16 registros posibles.
La nomenclatura ARM de los registros de lenguaje máquina es, en principio, muy clara: existen 16
registros de 32 bits cada uno, cuyos identificadores van del r0 al r15.
Sin embargo, algunos de estos registros tienen asignadas funciones especiales, por lo que
habitualmente se sustituye su identificador genérico por otro más específico:
• r13 = sp (stack pointer); es el puntero de pila (ver apartado 1.14),
• r14 = lr (link register); es el registro de enlace, que sirve para almacenar la dirección
de retorno de las rutinas (ver sección 4),
• r15 = pc (program counter); es el puntero de programa, que indica la dirección en
memoria de la siguiente instrucción a ejecutar.
Existen además otros convenios que especifican un uso particular del resto de los registros, pero de
momento vamos a considerar que los registros del r0 al r12 contienen simplemente datos (o
direcciones) del programa.

32 bits

r0: dato
r1: dato
r2: dato
r3: dato
r4: dato
r5: dato
r6: dato
r7: dato
r8: dato
r9: dato
r10: dato
r11: dato
r12: dato
r13: sp

r14: lr

r15: pc

10
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

1.8 El operando inmediato


En el apartado 1.5 también hemos introducido los operandos inmediatos, en instrucciones como
“mov r1, #2”. En esta instrucción, el operando destino es un registro (r1), y el operando fuente es
un valor inmediato o explícito (2), que por convenio del lenguaje ensamblador se precede con el
signo '#'.
Un operando inmediato de ARM se codifica en un espacio de 12 bits con la siguiente estructura:
• IR (4 bits): se multiplica por 2 para obtener el número de rotaciones a la derecha de IN,
• IN (8 bits): indica el valor base, que se desplazará a la derecha IR*2 bits.
Esta estructura se ha ideado para permitir codificar valores de hasta 32 bits con sólo 12 bits, ya que
los restantes 20 bits se necesitan para codificar el resto de componentes de la instrucción ARM. Sin
embargo, con 12 bits sólo se pueden representar 4096 números diferentes, lo cual es muy inferior a
los más de 4 mil millones de combinaciones que podemos generar con 32 bits reales.
De todos modos, los diseñadores del procesador ARM consideraron que con este método se pueden
representar los subconjuntos de valores que son más habituales. Los siguientes ejemplos muestran
diversas combinaciones para estos subconjuntos:

IR IN valor
0000 00000010 = 0x0000 0002
0100 00111011 = 0x3B00 0000
1001 00111011 = 0x000E C000

El primer ejemplo corresponde a un valor de 8 bits no desplazado, es decir, podemos representar


cualquier valor de 8 bits en 32 bits (24 bits altos a cero).
El segundo ejemplo muestra un patrón de 8 bits (0x3B) desplazado 8 bits a la derecha; hay que
tener en cuenta que los bits que “salen” por el extremo derecho se trasladan al bit de más peso del
número (“entran” por la izquierda). De este modo se pueden conseguir valores muy grandes, aunque
sólo se puede fijar el estado de 8 bits consecutivos.
El tercer ejemplo corresponde al mismo patrón de bits desplazado 18 bits a la derecha; como el
número de desplazamientos no es múltiplo de 4, los dígitos hexadecimales diferentes de cero han
cambiado respecto al ejemplo anterior, pero el patrón de bits no cambia.
En general, existen algunos números que se pueden codificar y otros no. Por ejemplo, podemos
codificar 512 pero no 511. Si intentamos introducir una instrucción con un operando inmediato que
no se pueda codificar, el programa ensamblador nos advertirá de la situación con un mensaje de
error. En este caso, necesitaremos construir el número con un par de instrucciones, por ejemplo,
cargar 512 en r0 y restarle 1:

mov r0, #512


sub r0, #1

Existe otro método para especificar un número inmediato cualquiera de 32 bits, que se explicará en
el apartado 2.9.

11
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

1.9 El operando registro desplazado


En los dos apartados anteriores hemos visto dos tipos de opernados que se pueden utilizar como
segundo operando fuente de una instrucción: registro e inmediato. Existe otra posibilidad que es el
registro desplazado, que consiste en especificar un registro seguido de un código de desplazamiento
y un valor que indique el número de bits a desplazar.
Los tipos básicos de desplazamiento son los siguientes:
• lsl (logical shift left): desplazamiento lógico a la izquierda; equivale a multiplicar
por 2 elevado al número de bits a desplazar,
• lsr (logical shift right): desplazamiento lógica a la derecha; equivale a dividir por 2
elevado al número de bits a desplazar,
• asr (aritmetic shift right): desplazamiento aritmético a la derecha; es igual que el
anterior, pero extiende el bit de signo, o sea que permite dividir número enteros,
• ror (rotate right): rotación a la derecha; desplaza un determinado número de bits a la
derecha pero coloca el bit de menos peso sobre el de más peso, efectuando un
desplazamiento circular del patrón de 32 bits.
Además, el número de bits a desplazar puede ser un valor inmediato de 5 bits (rango de 0 a 31) o un
registro cualquiera (de r0 a r15). La siguiente tabla muestra algunos ejemplos del segundo operando
fuente como registro desplazado, suponiendo inicialmente que r0 = 1:
Instrucción Ejecución Resultado Resultado decimal
mov r1, r0, lsl #4 r1 = r0 << 4 r1 = 0x0000 0010 16
add r2, r0, r1, ror #5 r2 = r0 + r1 ror 5 r2 = 0x8000 0001 -2147483647
mov r3, r2, asr r1 r3 = r2 / 2^r1 r3 = 0xFFFF 8000 -32768
mov r4, r2, lsr r0 r4 = r2 >> r0 r4 = 0x4000 0000 1073741824

• en la primera instrucción, el operando fuente es r0 desplazado a la izquierda 4 bits, lo que


equivale a multiplicar por 16 (2 elevado a 4),
• en la segunda instrucción (add), el primer primer operando fuente es de tipo registro y el
segundo operando fuente es r1 rotado a la derecha 5 bits, lo que provoca que el único bit a 1
del registro r1 se copie en el bit de más peso de r2 (en Ca2, resulta un valor negativo),
• en la tercera instrucción, el operando fuente es r2 desplazado 16 bits a la derecha, lo que
equivale a dividir r2 por 65.636 (2 elevado a 16); hay que observar que el resultado de la
división conserva el signo negativo en Ca2, porque se ha replicado el bit de signo en el
desplazamiento (parte alta a 0xFFFF),
• en la cuarta instrucción, el operando fuente es r2 desplazado 1 bit a la derecha, ya que r0=1;
esta vez, sin embargo, no se ha replicado el bit de signo, por consiguiente el resultado pasa a
ser positivo, lo cual no es admisible para operaciones con enteros.
Por último hay que señalar que el lenguaje ensamblador permiten especificar instrucciones de dos
operandos fuente utilizando un único operando fuente. En realidad, lo que se codifica como primer
operando fuente es el propio operando destino:
Ejemplo: add r2, r0 equivale a add r2, r2, r0 /* r2 += r0 */

12
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

1.10 Instrucciones aritméticas


Las instrucciones aritméticas son las que realizan cálculos orientados al valor natural o entero de los
operandos. Las más básicas son las siguientes:
Instrucción Función Ejemplo
add Rd, Rn, Op2 Rd = Rn + Op2 add r0, r1, #2
sub Rd, Rn, Op2 Rd = Rn – Op2 sub r2, r1, r0, lsr r3
mul Rd, Rm, Rs Rd = Rm * Rs mul r4, r2, r0
mull RdLo, RdHi, Rm, Rs RdHiLo = Rm * Rs mull r4, r5, r2, r0
cmp Rn, Op2 (Rn – Op2) Flags cmp r4, r1, lsl #2
La representación simbólica de los operandos significa lo siguiente:
• Rd: registro destino,
• RdLo, RdHi: dos registros destino, para resultados de 64 bits (Lo = 32 bits bajos, Hi = 32
bits altos),
• Rn, Rs, Rm: registros fuente,
• Op2: segundo operando fuente; puede ser de tipo registro, inmediato o registro desplazado.
Las instrucciones de suma (add) y resta (sub) operan indistintamente con valores naturales o enteros
en Ca2.
La instrucción de multiplicación (mul), sin embargo, sólo admite valores naturales. Hay que tener
en cuenta que el resultado puede exceder fácilmente los 32 bits, puesto que estamos multiplicando
dos registros fuente de 32 bits cada uno. Por esta razón se introduce la instrucción de
multiplicación larga (mull), que permite generar el resultado completo de 64 bits y guardarlo en dos
registros cualquiera.
La instrucción de comparación (cmp) simplemente efectúa una resta, pero no guarda el resultado en
ningún registro, sino que actualiza los Flags (banderas) del Registro Actual de Estado del
Procesador (CPSR: Current Processor Status Register). Este registro contiene los flags en diversos
bits, los más significativos de los cuales son:
• Z (Zero): indica si el resultado ha sido igual a cero,
• C (Carry): indica si la operación ha provocado acarreo (exceso de números naturales),
• V (oVerflow): indica si la operación ha provocado desbordamiento (de números enteros),
• N (Negative): indica si el resultado ha sido negativo (bit de más peso del resultado).
De hecho, cualquier operación aritmética puede alterar los flags, pero en este manual vamos a
considerar que sólo la instrucción de comparación los modifica.
Otras operaciones útiles podrían ser las de cambio de signo (negación) y división. Los diseñadores
del ARM consideraron que no eran necesarias, puesto que se pueden realizar por otros medios. La
negación se consigue restando a cero el valor a negar (sub r1, #0, r0 → r1 = -r0). Para la división
hay que utilizar una rutina que necesitará de diversas instrucciones de lenguaje máquina básicas, lo
que repercute enormemente en el coste de ejecución respecto a un divisor implementado por
hardware. Sin embargo, las divisiones son bastante poco frecuentes, así que es dudoso que esto
pueda afectar sensiblemente al rendimiento general del programa, y el espacio físico del procesador
para implementar el divisor se puede destinar a otros usos.

13
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

1.11 Instrucciones lógicas


Las instrucciones lógicas son las que realizan cálculos orientados al valor de los bits de los
operandos. Las más básicas son las siguientes:
Instrucción Función Ejemplo
mov Rd, Op2 Rd = Op2 mov r0, #2
mvn Rd, Op2 Rd = not Op2 mvn r2, r1, lsr #3
and Rd, Rn, Op2 Rd = Rn and Op2 and r3, r3, r2, ror r0
orr Rd, Rn, Op2 Rd = Rn or Op2 orr r4, r3, r2
eor Rd, Rn, Op2 Rd = Rn xor Op2 eor r4, r3, r2
tst Rn, Op2 (Rn and Op2) Flag Z tst r3, r2
La representación simbólica de los operandos Rd, Rn y Op2 es la misma para todas las
instrucciones (véase apartado anterior).
La instrucción de mover (mov) sirve para copiar un operando fuente de 32 bits a un registro destino,
o sea que también se podría incluir en el apartado de instrucciones aritméticas. La instrucción de
negar y mover (mvn), sin embargo, es básicamente una instrucción lógica, ya que hace referencia a
una negación bit a bit, es decir, cada uno de les 32 bits que está a 1 en el operando fuente pasa a 0, y
cada bit que está a 0 pasa a 1.
Las operaciones lógicas y (and), o (orr) y o exclusiva (eor) también se realizan bit a bit entre dos
operandos fuente. Por ejemplo, una y bit a bit implica realizar 32 ands, una para cada bit del
resultado, tomando como valores de entrada los dos bits con la misma posición en los operandos
fuente. Ejemplo:
0001001000100010 0011010010010000 (Rn)
and 1111000000001111 1101100010001110 (Op2)

0001000000000010 0001000010000000 (Rd)

La instrucción de test de bits iguales (tst) permite comprobar si existen al menos una coincidencia
de dos bits a 1 en la misma posición de los operandos fuente. Se realiza con una y lógica, pero no se
guarda el resultado en ningún registro destino, de forma análoga a la instrucción de comparación
aritmética (cmp). El flag Z se actualizará según el resultado obtenido, de forma que tomará el valor
1 si no existe ninguna coincidencia de bits a 1 (resultado de 32 bits igual a cero), y tomará el valor 0
si existe al menos una coincidencia de bits a 1 (resultado de 32 bits diferente de cero). En el ejemplo
anterior de la y bit a bit, el resultado de 32 bits es diferente de cero, porque hay 4 coincidencias de
pares de bits a 1, por lo tanto el flag Z tomaría el valor 0. Este tipo de comprobaciones sirven para
verificar si determinados bits de un operando fuente están a 1, utilizando el otro operando fuente
como máscara, es decir, como patrón de los bits que queremos verificar.
El resto de instrucciones lógicas también son capaces de modificar el flag de Z de acuerdo con el
resultado, pero para simplificar no vamos a utilizar esta posibilidad en este manual.
Otras operaciones lógicas de interés son los desplazamientos lógicos (y aritméticos) de los bits, pero
en ARM no existen instrucciones específicas para estas operaciones puesto que se pueden conseguir
fácilmente con la instrucción de mover y el modo de registro desplazado como segundo operando
fuente. Por consiguiente, se pueden realizar los 4 desplazamientos básicos lsl, lsr, asr y ror que
se han introducido en el apartado 1.9.

14
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

1.12 Instrucciones de salto


Como instrucciones de salto podemos distinguir dos grupos, según el salto sea condicional o
incondicional.
Las instrucciones de salto condicional son las que analizan el estado de los flags para determinar si
se realiza el salto (secuenciamiento explícito) o se sigue con la siguiente instrucción
(secuenciamiento implícito). Habitualmente se utilizan después de una comparación de números
naturales o enteros (cmp), o después de un test de bits (tst). La siguiente tabla estructura las
instrucciones condicionales básicas según el tipo de comparación previo:
Después de cmp A, B Instrucción Expresión (Branch if) Función
(A == B) nat., ent. beq Dir. EQual Si Z==1, PC = Dir.
(A != B) nat., ent. bne Dir. Not Equal Si Z==0, PC = Dir.
(A > B) naturales bhi Dir. HIgher Si C==1 & Z==0, PC = Dir.
(A < B) naturales blo Dir. LOwer Si C==0, PC = Dir.
(A >= B) naturales bhs Dir. Higher or Same Si C==1, PC = Dir.
(A <= B) naturales bls Dir. Lower or Same Si C==0 | Z==1, PC = Dir.
(A > B) enteros bgt Dir. Great Than Si N==V & Z==0, PC = Dir.
(A < B) enteros blt Dir. Less Than Si N!=V, PC = Dir.
(A >= B) enteros bge Dir. Great or Equal Si N==V, PC = Dir.
(A <= B) enteros ble Dir. Less or Equal Si N!=V | Z==1, PC = Dir.
Después de tst A, B Instrucción Expresión (Branch if) Función
(A and B) = 0 beq Dir. EQual Si Z==1, PC = Dir.
(A and B) > 0 bne Dir. Not Equal Si Z==0, PC = Dir.
El operando Dir. es la dirección de una instrucción del programa. En la sección 2 se explica como
expresar estas direcciones.
Para los saltos condicionales después de una comparación de dos números hay que observar que,
salvo las comprobaciones de igual y no igual, existen instrucciones diferentes para valores
naturales y para valores enteros, dado que los flags a evaluar son distintos como resultado de la
resta de los dos valores.
Para los saltos condicionales después de un test de bits, se utilizan las mismas instrucciones que
para las comprobaciones de números iguales y no iguales, dado que el flag a evaluar es el Z (cero).
Las instrucciones básicas de salto incondicional son las siguientes:
Instrucción Expresión Función Ejemplo
b Dir. Branch PC = Dir. b 0x2A00F74
bl Dir. Branch with Link LR = PC; PC = Dir. bl 0x1B32
La instrucción de salto básica (b) no realiza ninguna comprobación para efectuar el salto. La
instrucción de salto con referencia (bl) tampoco realiza ninguna comprobación, pero guarda el valor
actual del PC (dirección de la siguiente instrucción) en el registro LR (Link register) para permitir el
retorno al contexto del programa donde se realiza el salto. Esta funcionalidad es fundamental para la
implementación de las rutinas (ver sección 4).

15
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

1.13 Instrucciones de acceso a memoria


Todas las instrucciones anteriores operan con valores inmediatos o contenidos en un registro
(desplazado o no), y las que generan un resultado lo guardan en otro registro. Pero hay que tener en
cuenta que las variables de los programas están ubicadas en memoria. Esto significa que antes de
poder procesar el valor de una variable, habrá que leerlo de memoria y cargarlo en un registro.
Además, si hay que actualizar el valor de la variable, habrá que escribir el contenido del registro
resultado en memoria.
Otro factor a tener en cuenta es el tipo de datos de la variable que se está transfiriendo,
distinguiendo entre word, half word y byte, y si se trata de un valor entero o natural.
Finalmente, hay que conocer también los distintos Modos de Direccionamiento disponibles, que
determinan cómo se calcula la dirección final de la posición de memoria a acceder.
La siguiente tabla muestra un conjunto básico de las instrucciones ARM de acceso a memoria:
Instrucción Función Ejemplo
ldr Rd, [Md1] Rd = Mem32[Md1] ldr r0, [r3]
str Rd, [Md1] Mem32[Md1] = Rd str r2, [r0, r1, lsr #3]
ldrb Rd, [Md1] Rd = Up32(Mem8[Md1]) ldrb r0, [r1, r2, ror #10]
ldsb Rd, [Md2] Rd = Ext32(Mem8[Md2]) ldsb r4, [r5, r3]
strb Rd, [Md1] Mem8[Md1] = Low8(Rd) strb r5, [r1, r2, lsl #3]
ldrh Rd, [Md2] Rd = Up32(Mem16[Md2]) ldrh r6, [r1, #22]
ldsh Rd, [Md2] Rd = Ext32(Mem16[Md2]) ldsh r7, [r4, r2]
strh Rd, [Md2] Mem16[Md2] = Low16(Rd) strh r8, [r0]
Las instrucciones básicas ldr y str transfieren 32 bits, sean números naturales o enteros.
Las instrucciones de leer bytes distinguen entre naturales (ldrb) y enteros (ldsb). En los dos casos
hay que convertir el valor de 8 bits de la posición de memoria a los 32 bits del registro Rd. En el
primer caso se llenan los 24 bits altos a 0 (función Up32). En el segundo caso se extiende el bit de
signo del valor de 8 bits a los 24 bits altos (función Ext32). La instrucción de escribir byte (strb) no
distingue naturales de enteros, puesto que se transfieren sólo los 8 bits bajos (función Low8).
Las instrucciones de 16 bits (ldrh, ldsh y strh) funcionan de manera análoga a las de bytes, pero
atendiendo a los 16 bits bajos del registro Rd.
Los identificadores Md1 y Md2 indican que modos de direccionamiento se pueden utilizar para
cada tipo de instrucción. La siguiente tabla muestra cuatro modos básicos, de los cuales Md1 los
admite todos, pero Md2 sólo admite los tres primeros:
Modo de direccionamiento Formato Ejemplo Dirección
Registro base [Rb] [r3] r3
Registro base + valor [Rb, #inm] [r1, #22] r1+22
inmediato
Registro base + Registro [Rb, Ri] [r0, r1] r0+r1
índice
Registro Base + Registro [Rb, Ri, desp #inm] [r1, r2, ror #10] r1+(r2 ror 10)
índice desplazado

16
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

1.14 Instrucciones de acceso a la pila


La pila es una estructura de datos almacenada en memoria que permite salvar y restaurar
rápidamente el contenido de una serie de registros de lenguaje máquina. Sólo necesita dos
instrucciones para su manejo:
Instrucción Función Ejemplo
push {reg_list(n)} sp = sp – n*4; push {r0-r3, r6, lr}
Mem32[sp + i*4] = reg_list(i);
pop {reg_list(n)} reg_list(i) = Mem32[sp + i*4]; pop {r4-r8, pc}
sp = sp + n*4
El operando {reg_list(n)} simboliza una lista de registros, separados por comas y también por
rangos de registros. Por ejemplo, {r0-r3, r6, lr} se refiere a los registros r0, r1, r2, r3, r6 y r14. El
símbolo n indica el número de registros de la lista (6 en el ejemplo).
El índice i que aparece en la función representa la posición de cada registro dentro de la lista. En el
ejemplo, i=0 → r0, i=1 → r1, i=2 → r2, i=3 → r3, i=4 → r6, i=5 → r14 (lr). Nótese que NO
representa el índice del registro (i=4 no implica r4). Además, los registros de la lista seguirán
siempre su orden en el banco de registros, independientemente del orden en que se definan en la
lista. Por ejemplo, {r6, lr, r0-r3} generaría la misma lista con los mismos índices que {r0-r3, r6, lr}.
La instrucción de apilar (push) crea espacio en la pila para todos los registros de la lista restando
n*4 (cada registro ocupa 4 bytes) al puntero actual de la pila (sp o r13), y después graba la lista
ordenada de registros en memoria.
La instrucción de “desapilar” (pop) recupera de la memoria el contenido de la lista ordenada de
registros, y después libera el espacio de pila asociado sumando n*4 al puntero actual de pila.
De este modo, es fácil mantener un espacio de información temporal donde guardar información
“encima” de la información existente, y recuperar la información dejando la pila tal y como estaba.
El siguiente esquema muestra la evolución de la pila y los registros al efectuar el push y luego el
pop del ejemplo:

Dirección Contenido
r0: 0x00000002 : : r0: 0x00000002
r1: 0xFFCE0467 Zona r1: 0xFFCE0467
0x000007D0 0x00000000
r2: 0x0345FBA2 libre r2: 0x0345FBA2
0x000007D4 0x00012340
r3: 0x12A3338F r3: 0x12A3338F
0x000007D8 0x001A5D00
r4: 0x00000000 r4: 0x00000002
0x000007DC 0x00000002
r5: 0x00000001 r5: 0xFFCE0467
push 0x000007E0 0xFFCE0467
r6: 0xFFFFFFFE r6: 0x0345FBA2
0x000007E4 0x0345FBA2 Último
r7: 0xC300012D r7: 0x12A3338F
0x000007E8 0x12A3338F push
r8: 0x2850AA1F r8: 0xFFFFFFFE
0x000007EC 0xFFFFFFFE pop
r9: 0x00000000 r9: 0x00000000
0x000007F0 0x000084AC
r10: 0x00000000 r10: 0x00000000
0x000007F4 0x00000000 Contenido
r11: 0x00000000 r11: 0x00000000
0x000007F8 0x00112FFC
r12: 0xE40013F2 Anterior r12: 0xE40013F2
r13: 0x000007F4 0x000007FC 0xB3000000 r13: 0x000007F4
r14: 0x000084AC 0x00000800 0x0A1039FC r14: 0x000084AC
r15: 0x000086D0 : : r15: 0x000084AC
(r13: 0x000007DC)

17
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

Sección 2 : Introducción al lenguaje ensamblador GAS


2.1 Definición de Lenguaje Ensamblador
El Lenguaje Ensamblador está constituido por un conjunto de palabras clave y funcionalidades que
facilitan la programación en lenguaje máquina. El siguiente texto es un ejemplo de programa escrito
en lenguaje ensamblador:
@;=== programa de ejemplo en lenguaje ensamblador para ARM ===

.data @;directiva declaración sección de datos


var1: .word -127 @;directivas definición de variables
var2: .byte 1

.text @;directiva declaración sección de código


.align 2 @;directiva para alinear palabras
.arm @;directiva definición código ARM
.global _start
_start: @;etiqueta global de inicio programa
mov r0, #0
mov r1, #2
.Lbucle: @;etiqueta local de inicio de bucle
add r0, r1
cmp r0, #10
bne .Lbucle @;salto al inicio del bucle
ldr r4, =var1 @;carga de direcciones de variables
ldr r0, [r4] @;acceso a las variables
mov r2, #0x3b
ldr r3, =var2
strb r2, [r3, r1, lsl #3]
.Lstop: b .Lstop @;bucle infinito

.end @;fin del fichero

El texto anterior contiene nemotecnias de las instrucciones y registros de lenguaje máquina (en
azul), directivas (en naranja), símbolos (en negro) y valores (en rosa) que se traducen
sistemáticamente por el Programa Ensamblador en un fichero binario con los códigos máquina
correspondientes:
Direcciones Símbolos Código Máquina Lenguaje Máquina
0x8000 _start: 0xE3A0 0000 mov r0, #0
0x8004 0xE3A0 1002 mov r1, #2
0x8008 .Lbucle: 0xE080 0001 add r0, r0, r1
0x800C 0xE350 000A cmp r0, #10
0x8010 0x1AFF FFFC bne 0x8008 @;bne .Lbucle
0x8014 0xE59F 4010 ldr r4, [pc, #16] @;ldr r4, =var1
0x8018 0xE594 0000 ldr r0, [r4]
0x801C 0xE3A0 203B mov r2, #59
0x8020 0xE59F 3008 ldr r3, [pc, #8] @;ldr r3, =var2
0x8024 0xE7C3 2181 strb r2, [r3, r1, lsl #3]
0x8028 .Lstop: 0xEAFF FFFE b 0x8028 @;b .Lstop
0x802C 0x0001 0034 (dirección var1)
0x8030 0x0001 0038 (dirección var2)

0x10034 var1: 0xFFFF FFF81 @; -127 (32 bits)


0x10038 var2: 0x01 @; 1 (8 bits)

18
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

2.2 Comentarios
Los comentarios son texto que se añade al código fuente para ayudar a entender el funcionamiento
del programa. Este texto es ignorado por el programa ensamblador.
Para el ensamblador GAS (Gnu ASsembler), un comentario se prefija con el carácter '@'. El
comentario es todo el texto siguiente, hasta el final de la linea. Por ejemplo:
@;=== programa de ejemplo en lenguaje ensamblador para ARM ===
es un comentario que ocupa toda la linea, mientras que en este otro ejemplo:
ldr r0, [r4] @;acceso a las variables
el comentario empieza detrás de una instrucción de lenguaje máquina.

19
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

2.3 Secciones de memoria


Tanto los datos como las instrucciones se tienen que ubicar en unas zonas concretas de la memoria
del computador. Las directivas '.data' y '.text' permiten especificar las secciones de memoria donde
se ubicarán los códigos máquina correspondientes a las líneas de texto que siguen a la directiva:
.data @;directiva declaración sección de datos

.text @;directiva declaración sección de código


Otra sección importante es '.bss', que es una sección de datos que sirve para definir variables de
programa sin inicializar, al contrario de la sección '.data' dónde las variables ya están inicializadas.
Por ejemplo:
.bss @;sección de variables no inicializadas
suma: .space 4

incluye la declaración de una variable de nombre 'suma' y reserva 4 bytes de espacio para almacenar
el contenido de la variable con la directiva '.space n'.
Respecto al ejemplo del apartado 2.1, hay que observar que el programa ensamblador ha ubicado la
sección '.data' en la dirección de memoria 0x10034 y la sección '.text' en la dirección de memoria
0x8000.
El manejo automático de las direcciones físicas de memoria constituye en herramienta muy útil para
la programación en lenguaje máquina, puesto que libera al programador de la tarea de especificar
constantemente dichas posiciones de memoria.

20
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

2.4 Definición de valores


El ensamblador GAS utiliza la misma convención que el lenguaje de programación C para
especificar constantes numéricas, más una notación adicional para números binarios:
Base Notación Ejemplo Valor decimal
10 Número decimal, positivo o negativo -127 -127
8 Prefijo '0' + número octal 021375 8957
16 Prefijo '0x' + número hexadecimal 0xFFFF FFFE -2
2 Prefijo '0b' + número binario 0b110100100 420
Además, también se permite especificar caracteres simples entre comillas simples (p.ej. 'c'), o
cadenas de caracteres entre comillas dobles (p.ej. “Esto es una frase.”); en los dos casos, el
ensamblador convierte los caracteres en sus códigos correspondientes de la tabla ASCII.
Otro detalle a tener en cuenta es la obligatoriedad de incluir el símbolo '#' delante de los operandos
inmediatos en las instrucciones de lenguaje máquina, por ejemplo:
cmp r0, #10

21
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

2.5 Variables globales


Las variables globales de un programa escrito en lenguaje ensamblador se tienen que definir dentro
de secciones del tipo '.data' o '.bss'. Ya se ha comentado que el primer tipo de sección está destinado
a las variables con un valor inicial conocido mientras que el segundo tipo está destinado a las
variables sin valor inicial.
Para definir valores de variables se pueden utilizar las directivas '.word', '.hword' y '.byte', que
especifican un tamaño de 32, 16 u 8 bits, respectivamente, pero no indican el tipo, como Entero,
Natural, Boleano, Carácter, etc. En lenguaje ensamblador, el tipo de una variable lo determina el
uso que las instrucciones del programa hacen de esa variable.
Las siguientes líneas muestran un ejemplo de definición de variables globales:
.data
x: .word 0xCAFEBABE
n_array: .word 10, 25, -3, 18, 32000
short1: .hword 0xF001
.byte 0x00, -1, 230, 'h'

Estas línias definen un fichero binario como el siguiente:


Direcciones Símbolos Código Máquina
0x10034 x: 0xCAFE BABE @;valor hexadecimal(32 bits)
0x10038 n_array: 0x0000 000A @;n_array[0] = 10 (32 bits)
0x1003C 0x0000 0019 @;n_array[1] = 25
0x10040 0xFFFF FFFD @;n_array[2] = -3
0x10044 0x0000 0012 @;n_array[3] = 18
0x10048 0x0000 7D00 @;n_array[4] = 32000
0x1004C short1: 0xF001 @;valor hexadecimal (16 bits)
0x1004E 0x00 @;vector de bytes
0x1004F 0xFF @;-1
0x10050 0xE6 @;230
0x10051 0x68 @;'h'

En este ejemplo se observa el uso de etiquetas para identificar el nombre de las variables, como 'x',
'n_array' y 'short1'. Estas etiquetas, que tienen que ir seguidas del símbolo ':', se corresponden con
las direcciones de memoria que utiliza el ensamblador para ubicar los valores de las variables. De
este modo, 'x' → 0x10034, 'n_array' → 0x10038 y 'short1' → 0x1004E.
Después de una etiqueta hay que especificar una directiva de tamaño ('.word', '.hword' o '.byte') y a
continuación uno o varios valores separados por comas. Si hay más de un valor se entiende que la
variable es un vector, puesto que apunta al principio de una lista de valores, como es el caso de
'n_array'.
Además, se pueden especificar valores sin necesidad de utilizar etiqueta, como es el caso de la
última definición de 4 bytes.

22
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

2.6 Alineación de memoria


La directiva '.align n' sirve para indicar al programa ensamblador que los siguientes códigos
máquina que se generen deben ir en posiciones de memoria múltiples de 2 elevado a n. Es
importante porque el procesador sólo puede acceder a valores de 16 bits o 32 bits si están
correctamente alineados en memoria.
Las siguientes líneas muestran un ejemplo del uso de la directiva de alineación:
.data
x1: .byte 0xCA
.align 1
y: .hword 0xFE01
x2: .byte 0xBA
.align 2
z: .word 0xBE8000

Estas línias definen un fichero binario como el siguiente:


Direcciones Símbolos Código Máquina
0x10034 x1: 0xCA @;un byte (8 bits)
0x10035 0x00 @;desplazamiento de .align 1
0x10036 y: 0xFE01 @;un halfword (16 bits)
0x10038 x2: 0xBA @;otro byte
0x10039 0x0000 00 @;desplazamiento de .align 2
0x1003C z: 0x00BE 8000 @;un word (32 bits)

La primera directiva '.align 1' ha introducido un desplazamiento de un byte para asegurar que la
dirección de la variable 'y' sea múltiple de 2, mientras que la directiva '.align 2' ha introducido un
desplazamiento de tres bytes para asegurar que la dirección de la variable 'z' sea múltiple de 4.
Hay que observar que el número de bytes de desplazamiento que añade una directiva '.align n'
depende de la dirección actual del proceso de ensamblado. Por ejemplo, una directiva '.align 2'
puede añadir 1, 2, 3 bytes o ningún byte, si la dirección siguiente ya está alineada correctamente.
En cualquier caso, siempre hay que utilizar directivas '.align 1' delante de '.hword' o '.align 2'
delante de '.word' o de instrucciones de lenguaje máquina, cuando pueda pasar que la dirección de
esas variables o instrucciones no sea múltiple del correspondiente número de bytes, lo cual es
posible si antes se ha ensamblado algún valor con una multiplicidad inferior.

23
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

2.7 Etiquetas de salto


El siguiente texto es un extracto del ejemplo del programa introducido en el primer apartado de esta
sección:
.text @;directiva declaración sección de código
.align 2 @;directiva para alinear palabras
.arm @;directiva definición código ARM
.global _start
_start: @;etiqueta global de inicio programa
mov r0, #0
mov r1, #2
.Lbucle: @;etiqueta local de inicio de bucle
add r0, r1
cmp r0, #10
bne .Lbucle @;salto al inicio del bucle
etc...

Después de indicar la sección de instrucciones con la directiva '.text', se introduce una directiva
'.align 2' para asegurar que el ensamblado empieza en una posición múltiple de 4, más otra directiva
'.arm' para indicar el tipo de codificación que se requiere (instrucciones de 32 bits).
A continuación se especifica una directiva '.global' seguida del símbolo '_start' para indicar que este
símbolo se puede referenciar desde un módulo externo al fichero que lo contiene. En el ejemplo se
trata de la Rutina de Inicio de cualquier programa ARM, que por defecto tiene que empezar por la
etiqueta de salto '_start'.
La siguiente etiqueta de salto '.Lbucle', sin embargo, es local al fichero que contiene el texto del
programa ensamblador. Por convención, se aconseja anteceder las etiquetas locales con el prefijo
'.L', aunque no es estrictamente necesario. En cualquier caso, todas las etiquetas de salto tienen que
terminar con el carácter ':', de igual modo que se utilizaba para definir variables.
El ensamblado del extracto anterior produce el siguiente contenido de memoria:
Direcciones Símbolos Código Máquina Lenguaje Máquina
0x8000 _start: 0xE3A0 0000 mov r0, #0
0x8004 0xE3A0 1002 mov r1, #2
0x8008 .Lbucle: 0xE080 0001 add r0, r0, r1
0x800C 0xE350 000A cmp r0, #10
0x8010 0x1AFF FFFC bne 0x8008 @;bne .Lbucle

Del listado anterior se puede deducir que el ensamblador ha asignado la dirección 0x8000 a la
etiqueta '_start' y la dirección 0x8008 a la etiqueta '.Lbucle'. Todavía más importante es observar
que el ensamblador ha sustituido el símbolo '.Lbucle' de la última instrucción 'bne .Lbucle' por su
correspondiente dirección de salto 'bne 0x8008'.
De este modo, el programador de lenguaje ensamblador puede utilizar etiquetas simbólicas en
instrucciones de salto o en instrucciones de acceso a variables, es decir, sin necesidad de conocer
exactamente la posición de memoria que se asigna a cada etiqueta.
Nota: en esta sección se visualizan las direcciones y contenidos de memoria como resultado del
proceso de ensamblado por razones didácticas; en el ciclo normal de desarrollo de programas en
lenguaje ensamblador no es necesario conocer dicha información.

24
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

2.8 Carga de direcciones de memoria


Para acceder a las variables del programa es necesario cargar primero su dirección de memoria en
un registro, por ejemplo:
ldr r4, =var1 @;carga de direcciones de variables
ldr r0, [r4] @;acceso a las variables

En la primera instrucción 'ldr r4, =var1' la dirección de memoria de la variable 'var1' se carga sobre
el registro r4. Nótese que una dirección de memoria en ARM es de 32 bits, por lo tanto, no se puede
utilizar una instrucción como 'mov r4, dir.', dónde la dirección sea un valor inmediato.
Por esta razón se utiliza la instrucción 'ldr r4, =var1', dónde '=var1' indica al ensamblador que
utilice el modo de direccionamiento Relativo a PC, que consiste en guardar la dirección de 'var1' en
un lugar próximo a la instrucción 'ldr', para poder especificar la posición dónde se guarda la
dirección como el valor actual del PC (puntero de programa) más un desplazamiento. Esto se puede
observar en el contenido de la memoria:
Direcciones Símbolos Código Máquina Lenguaje Máquina
0x8014 0xE59F 4010 ldr r4, [pc, #16] @;ldr r4, =var1
0x8018 0xE594 0000 ldr r0, [r4]
0x801C 0xE3A0 203B mov r2, #59
0x8020 0xE59F 3008 ldr r3, [pc, #8] @;ldr r3, =var2
0x8024 0xE7C3 2181 strb r2, [r3, r1, lsl #3]
0x8028 .Lstop: 0xEAFF FFFE b 0x8028 @;b .Lstop
0x802C 0x0001 0034 (dirección var1)
0x8030 0x0001 0038 (dirección var2)

0x10034 var1: 0xFFFF FFF81 @; -127 (32 bits)


0x10038 var2: 0x01 @; 1 (8 bits)

La instrucción en lenguaje ensamblador 'ldr r4, =var1' se ha convertido en la instrucción en lenguaje


máquina 'ldr r4, [pc, #16]', lo que significa que el registro r4 se cargará con el contenido de la
posición de memoria [pc+16]. El contenido del registro pc en ese momento es 0x801C, porque el pc
se incrementa dos veces antes de entrar en la fase de ejecución de la instrucción, para cargar y
decodificar las siguientes instrucciones del programa. Si sumamos 0x10 (16) a 0x801C se obtiene
0x802C, que contiene 0x0001 0034, es decir, la dirección de 'var1'. El programa ensamblador es
quien ha colocado automáticamente la dirección de 'var1' después de la última instrucción de salto.
Como resultado de la ejecución de 'ldr r4, =var1', el registro r4 tomará el valor 0x0001 0034. La
siguiente instrucción 'ldr r0, [r4]' cargará en el registro r0 el contenido de la posición de memoria
0x0001 0034, es decir, el contenido de la variable 'var1', que es 0xFFFF FF81.

25
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

2.9 Definición de rutinas


En el apartado 2.7 ya se ha comentado que la rutina de inicio de cualquier programa ARM se tiene
que etiquetar con el símbolo '_start:', que además se tiene que declarar como global.
Para definir cualquier otra rutina solamente hace falta declarar una etiqueta de salto sobre la primera
instrucción de la rutina. Desde otras partes del programa se podrá invocar la rutina con la
instrucción en lenguaje ensamblador 'bl etiqueta'. Por ejemplo, supongamos que existe una función
de nombre 'func1'; para invocarla se puede utilizar la siguiente instrucción en lenguaje ensamblador:
bl func1

Esquemáticamente, la definición de la rutina podría ser:


.global func1
func1: @;etiqueta global de inicio rutina
push {r0-r2, lr} @;primera instrucción de la rutina

@;instrucciones contenidas en la rutina

pop {r0-r2, pc} @;última instrucción de la rutina

En este ejemplo, la primera instrucción salva el contenido de los registros r0, r1, r2 y lr (r14) en la
pila. Se supone que los registros r0-r2 son de trabajo para las instrucciones contenidas en la rutina,
por eso hay que salvar su valor antes de entrar en la rutina y hay que restaurar su valor de la pila
antes de retornar de la rutina. El contenido del registro lr, sin embargo, no se restaura sobre sí
mismo, sino sobre el registro pc (r15), lo cual provoca el retorno a la siguiente instrucción después
de 'bl func1'.
Las rutinas pueden declararse globales o no, con la directiva '.global etiqueta', si se necesita que la
rutina se accesible (o no) desde otros ficheros del programa.
En la sección 4 se abordará más en profundidad el tema de las rutinas.

26
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

Sección 3 : Programación en bajo nivel


3.1 Traducción de un programa simple
Los Compiladores son programas informáticos que se encargan de traducir un código fuente escrito
en un determinado lenguaje de programación (C, C++, Java, etc.) en su equivalente en lenguaje
máquina. El resultado es una serie de códigos binarios que pueden ser ejecutado por el procesador
para el cual se ha compilado el programa. Por lo tanto, los compiladores resultan muy útiles porque
nos permiten programar en lenguajes de alto nivel.
Sin embargo, los compiladores no pueden realizar traducciones tan óptimas como los humanos. Es
por esta razón que en esta sección y la siguiente vamos a convertir programas escritos en lenguaje C
a su equivalente en lenguaje ensamblador, es decir, en bajo nivel. El primer programa en C que se
propone es el siguiente:

int max = 200; /* variable global inicializada */


int s; /* variable global no inicializada */

void main(void) /* programa principal */


{
int i; /* variable local */
s=0; /* inicializar sumatorio */
for (i=1; i<=max; i++) /* desde 1 hasta max */
s += i; /* acumular el valor del índice */
}

Una posible traducción en lenguaje ensamblador GAS para ARMv4 seria la siguiente:
@;=== Sumatorio: traducción de un programa simple en C ===

.data
max: .word 200 @;variables global inicializada
.bss
s: .space 4 @;variables global no inicializada

.text
.align 2
.arm
.global _start
_start: @;programa principal
ldr r2, =max @;r2 apunta a variable 'max'
ldr r3, [r2] @;r3 captura el contenido de 'max'
mov r0, #0 @;r0 acumulara temporalmente el valor de 's'
mov r1, #1 @;r1 sera la variable local 'i'
.Lfor: @;etiqueta local de inicio de bucle
add r0, r1 @;s += i;
add r1, #1 @;incrementa indice 'i'
cmp r1, r3 @;compara indice 'i' con valor máximo 'max'
bne .Lfor @;salto al inicio del bucle
ldr r4, =s @;r4 apunta a variable 's'
str r0, [r4] @;almacena el sumatorio en memoria
.Lstop: b .Lstop @;bucle infinito

.end @;fin del fichero

27
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

3.2 Instrucciones secuenciales


Las instrucciones secuenciales son las que realizan operaciones matemáticas o lógicas. Dicho de
otro modo, son todas las instrucciones que no son ni estructuras condicionales ni estructuras
repetitivas ni llamadas a funciones. El siguiente programa en C es un ejemplo formado por una
única instrucción secuencial:
int a = 2; /* variables globales inicializadas */
int b = 5;
int c; /* variable global no inicializada */

void main(void) /* programa principal */


{
c = a*a – 3*b + 7; /* instrucción secuencial */
}

La traducción a lenguaje ensamblador podría ser la siguiente:


@;=== Ejemplo de traducción de instrucción secuencial ===

.data
a: .word 2 @;variables globales inicializada
b: .word 5
.bss
c: .space 4 @;variables global no inicializada

.text
.align 2
.arm
.global _start
_start: @;programa principal
ldr r3, =a @;r3 apunta a variable 'a'
ldr r0, [r3] @;r0 captura el contenido de 'a'
mul r2, r0, r0 @;r2 = a*a
ldr r3, =b @;r3 apunta a variable 'b'
ldr r1, [r3] @;r1 captura el contenido de 'b'
add r1, r1, lsl #1 @;r1 = 3*b (b + 2*b)
sub r2, r1 @;r2 = a*a - 3*b
add r2, #7 @;r2 = a*a – 3*b + 7
ldr r3, =c @;r3 apunta a variable 'c'
str r2, [r3] @;almacena el resultado en memoria
.Lstop: b .Lstop @;bucle infinito

.end @;fin del fichero

Una instrucción secuencial en alto nivel puede generar varias instrucciones en lenguaje máquina,
debido principalmente a que las instrucciones en lenguaje máquina tienen sólo dos operandos
fuente, lo cual obliga a calcular resultados parciales. Además, hay que cargar el contenido de las
variables de entrada en registros y después guardar el resultado en memoria.

28
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

3.3 Estructura condicional 'if-else'


La estructura condicional 'if-else' permite ejecutar un trozo de programa u otro en función de una
condición. El siguiente fragmento de programa en C es un ejemplo:
:
:
if (c > 0)
{
d = c / 2;
c++;
}
else
d = c / 4;
:
:

La traducción a lenguaje ensamblador podría ser la siguiente:


@;=== Ejemplo de traducción de estructura condicional 'if-else' ===

.text
:
:
ldr r3, =c @;r3 contiene la dirección de c
ldr r0, [r3] @;r0 = c
cmp r0, #0 @;comprobar condición
ble .Lelse @;saltar si r0 <= 0 (caso del else)
mov r1, r0, asr #1 @;r1 = c/2
add r0, #1 @;r0 = c+1
str r0, [r3] @;actualiza variable c
b .Lcontinue @;saltar al final de la estructura if-else
.Lelse:
mov r1, r0, asr #2 @;r1 = c/4
.Lcontinue:
ldr r2, =d @;r2 contiene la dirección de d
str r1, [r2] @;almacena c/2 o c/4 en d
:
:
Hay que observar que la condición que se evalúa es justamente la contraria de la que se indica en
alto nivel, porque es muy recomendable mantener la misma ordenación del cuerpo de instrucciones
del 'if' y el cuerpo de instrucciones del 'else', por motivos de claridad del código fuente.
Por lo tanto, dado que en lenguaje máquina sólo disponemos de saltos condicionales, hay que saltar
el cuerpo del 'if' cuando NO se cumple la condición, hacia el cuerpo del 'else' (o al final del 'if', si no
existe 'else'). Además, cuando acaba el cuerpo del 'if', hay que saltar de manera incondicional (con
instrucción 'brach') al final del 'if', para no ejecutar el cuerpo del 'else'.
Puesto que la instrucciones 'ldr r2, =d' y 'str r1, [r2]' se tienen que ejecutar en el cuerpo del 'if' y
también en el cuerpo del 'else', se ha decidido extraerlas de los dos cuerpos y colocarlas al final una
sola vez. Esto es una optimización del código máquina, que traducido sin optimizar repetiría las
instrucciones en los dos cuerpos.

29
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

3.4 Condiciones múltiples


Las condiciones múltiples se presentan en las estructuras condicionales como combinaciones
lógicas de diversas condiciones, o bien con estructuras 'else if'. El siguiente fragmento de programa
en C es un ejemplo:
:
if ((c > 0) && (d == a-b))
d--;
else if (d != a+b)
d++;
:

La traducción a lenguaje ensamblador podría ser la siguiente:


@;=== Ejemplo de traducción de condiciones múltiples ===

.text
:
ldr r4, =a
ldr r0, [r4] @;r0 = a
ldr r4, =b
ldr r1, [r4] @;r1 = b
ldr r4, =c
ldr r2, [r4] @;r2 = c
ldr r4, =d
ldr r3, [r4] @;r3 = d
cmp r2, #0 @;comprobar primera condición
ble .Lelse @;salta si (c <= 0)
sub r5, r0, r1 @;r5 = a-b
cmp r3, r5 @;comprobar segunda condición
bne .Lelse @;salta si (d != a-b)
sub r3, #1 @;r3 = d-1
b .Lcontinue @;saltar al final de if-else if
.Lelse:
add r5, r0, r1 @;r5 = a+b
cmp r3, r5 @;comprobar condición segundo if
beq .Lcontinue @;salta si (d == a+b)
add r3, #1 @;r3 = d+1
.Lcontinue:
str r3, [r4] @;actualiza variable d
:

En el caso de dos condiciones conectadas con un operador lógico AND hay que saltar a
continuación del cuerpo del 'if' cuando alguna de las condiciones sea falsa. Si las condiciones están
conectadas con operadores OR hay que saltar dentro del cuerpo del 'if' cuando alguna de las
condiciones sea cierta.
En el caso de la estructura 'else if' se trata simplemente de colocar las instrucciones de
comprobación del segundo 'if' al principio del cuerpo del 'else'. Esta estructura se puede encadenar
repetidas veces, y permite implementar las estructuras de caso ('switch', en C).

30
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

3.5 Estructura repetitiva 'for'


Cuando hay que repetir una serie de instrucciones un cierto número de veces se utiliza una
estructura 'for', que gestiona un índice que toma un valor entero inicial y se incrementa (o
decrementa) hasta un valor final. El siguiente fragmento de programa en C es un ejemplo:
:
m = 1;
for (i = 2; i <= max; i++)
{
m *= i;
}
:

La traducción a lenguaje ensamblador podría ser la siguiente:


@;=== Ejemplo de traducción de estructura repetitiva for ===

.text
:
ldr r3, =max
ldr r1, [r3] @;r1 = max
mov r2, #1 @;r2 calcula m, se guardará al final
mov r0, #2 @;r0 es indice i, se inicializa a 2
cmp r0, r1 @;comprobar si hay que entrar en el bucle
bgt .Lfinfor @;salta el cuerpo del 'for' si i > max
.Lfor:
mul r2, r2, r0 @;cuerpo del 'for' (m *= i;)
add r0, #1 @;i++
cmp r0, r1 @;comprobar si hay que continuar el bucle
ble .Lfor @;repite el 'for' mientras i <= max
.Lfinfor:
ldr r3, =m
str r2, [r3] @;actualiza variable m
:

En lenguaje ensamblador hay que explicitar la instrucción de inicialización del índice (mov r0, #2),
el incremento del índice (add r0, #1) y la comprobación de la condición de repetición (cmp r0, r1;
ble .Lfor).
Además, si no se puede garantizar que el bucle tendrá al menos una iteración (una ejecución del
cuerpo del bucle), hay que comprobar al principio si se tiene que producir dicha iteración o por el
contrario hay que saltar todo el cuerpo del bucle. En este ejemplo dependería del valor contenido en
la variable max, ya que si éste es menor que 2 el cuerpo del bucle no se debe ejecutar nunca.
En bucles que se pueda asegurar que iteran al menos una vez no hace falta dicha verificación inicial,
por ejemplo, en un bucle como “for (i=0; i<10; i++)”.

31
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

3.6 Estructuras repetitivas 'while' y 'do-while'


Cuando hay que repetir una serie de instrucciones un número indeterminado de veces se utiliza una
estructura 'while' o 'do-while', dependiendo de si es necesario efectuar la comprobación antes o
después de la primera iteración. El siguiente fragmento de programa en C es un ejemplo:
:
i = 0;
while (frase[i] != 0)
{
i++;
}
:
La traducción a lenguaje ensamblador podría ser la siguiente:
@;=== Ejemplo de traducción de estructura repetitiva while ===
:
ldr r2, =frase @;r2 contiene la dirección base de frase
mov r0, #0 @;r0 es indice i, se inicializa a 0
.Lwhile:
ldrb r1, [r2, r0] @;r1 captura el contenido de frase[i]
cmp r1, #0 @;comprobar si hay que continuar el bucle
beq .Lfinwhile @;salta al final del bucle si frase[i] == 0
add r0, #1 @;cuerpo del bucle (i++;)
b .Lwhile @;repite el 'while' desde el principio
.Lfinwhile:
:

En el principio de un bucle 'while' hay que verificar la condición de fin, si no ha llegado el fin
ejecutar el cuerpo del bucle y finalmente hay que saltar a inicio del bucle. La siguiente versión
utiliza una estructura 'do-while':
:
i = 0;
do
{
i++;
} while (frase[i] != 0);
:
La traducción a lenguaje ensamblador podría ser la siguiente:
@;=== Ejemplo de traducción de estructura repetitiva do-while ===
:
ldr r2, =frase @;r2 contiene la dirección base de frase
mov r0, #0 @;r0 es indice i, se inicializa a 0
.Ldowhile:
add r0, #1 @;cuerpo del bucle (i++;)
ldrb r1, [r2, r0] @;r1 captura el contenido de frase[i]
cmp r1, #0 @;comprobar si hay que continuar el bucle
bne .Ldowhile @;salta al principio si frase[i] != 0
:

Aquí sólo se realiza un salto condicional, si se cumple la condición de continuación. Aunque esta
versión es más sencilla que la anterior, nunca analiza el contenido de frase[0], lo cual podría ser
perjudicial si el contenido de la primera posición de la frase puede ser cero.

32
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

3.7 Acceso a vectores


En el apartado anterior se ha mostrado un ejemplo de acceso a un vector de caracteres de nombre
'frase'. La instrucción de lenguaje máquina para realizar el acceso al vector era 'ldrb r1, [r2, r0]',
donde r1 era el registro destino, r2 era el registro que contenía la dirección base del vector y r0 era
el registro índice, es decir, el valor de 'i'. El modo de direccionamiento de la memoria '[r2, r0]' se
llama “registro base + registro índice”, tal como se explicó en el apartado 1.13.
En general, cualquier acceso a vector se basa en este cálculo:
dirección(vector[i]) = dirección_base(vector) + desplazamiento(i)

dónde la dirección base es la dirección del primer elemento del vector en memoria y el
desplazamiento consiste en multiplicar el índice por el número de bytes que ocupa cada elemento
del vector. En el ejemplo de la frase, 'i' se multiplica por 1, ya que cada posición del vector es un
byte. El siguiente ejemplo en C consiste en multiplicar todos los elementos de un vector de 10
words por un valor constante (k):
:
for (i = 0; i < 10; i++)
v[i] *= k;
:
La traducción a lenguaje ensamblador podría ser la siguiente:
@;=== Ejemplo de acceso a vector de words ===
:
ldr r4, =k
ldr r3, [r4] @;r3 = 'k'
ldr r0, =v @;r0 contiene la dirección base de 'v'
mov r2, #0 @;r2 es indice 'i', se inicializa a 0
.Lfor:
ldr r1, [r0, r2, lsl #2] @;r1 captura el contenido de 'v[i]'
mul r1, r1, r3 @;multiplica v[i]*k
str r1, [r0, r2, lsl #2] @;actualizar el contenido de 'v[i]'
add r2, #1 @;incremento de índice (i++;)
cmp r2, #10 @;comprobar si hay que continuar el bucle
bne .Lfor @;repite el 'for' desde el principio
:
El modo de direccionamiento '[r0, r2, lsl #2]' significa que r2 se multiplica por 4 y se suma a r0 que
contiene la dirección base del vector. La siguiente representación gráfica muestra un acceso al
vector de words con índice en r2:
0x00010040 0x001AFE40
@v[] 0x00010044 0x8F4209A0 :v[0]
0x00010048 0x048FFFE2 :v[1]
0x0001004C 0xFFCE0467 :v[2]
0x00010050 0x0345FBA2 :v[3]
r2*4 +
0x00010054 0x12A3338F :v[4]
0x00010058 0xFFFFFFFE :v[5]
0x0001005C 0x000084AC :v[6]
@v[r0] 0x00010060 0x002F4531 :v[7]
0x00010064 0x00000000 :v[8]
0x00010068 0x00112FFC :v[9]
0x0001006C 0xB3000000
0x00010070 0x2E089D12

33
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

3.8 Almacenamiento de matrices


Habitualmente las matrices se almacenan en la memoria por filas. El siguiente esquema muestra un
ejemplo de almacenamiento de una matriz de 3 filas por 4 columnas, dónde cada elemento es un
word:

En general, para calcular la posición de un elemento (i, j) de una matriz de MF filas por MC
columnas y con un tamaño T de cada elemento hay que utilizar la siguiente fórmula:
dir(i,j) = Base + (i*MC + j)*T

34
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

3.9 Acceso bidimensional a matrices


El siguiente extracto en C inicializa una matriz de NxN words a la identidad, es decir, todo a ceros
menos la diagonal, que contendrá unos:

#define N 10

int matriz[N][N];

void main(void)
{
for (i = 0; i < N; i++)
for (j = 0; j < N; j++)
if (i == j) matriz[i][j] = 1;
else matriz[i][j] = 0;
}

La traducción a lenguaje ensamblador podría ser la siguiente:


@;=== Ejemplo de acceso a matriz de words, con dos índices ===
N = 10 @;constante!
.bss
matriz: .space N*N*4 @;variables global no inicializada

.text
.align 2
.arm
.global _start
_start: @;programa principal
ldr r3, =matriz @;r3 contiene dirección base de matriz
mov r2, #N @;r2 = N
mov r6, #0 @;r6 = 0
mov r7, #1 @;r7 = 1
mov r0, r6 @;r0 es 'i'
.Lfori: @;primer bucle
mul r4, r0, r2 @;r4 = i*N
mov r1, r6 @;r1 es 'j'
.Lforj: @;segundo bucle
add r5, r4, r1 @;r5 = i*N + j
cmp r1, r0 @;verificar si estamos en diagonal
bne .Lelse @;en caso negativo saltar al else
str r7, [r3, r5, lsl #2] @;matriz[i][j] = 1;
b .Lfinif @;salta el cuerpo del else
.Lelse:
str r6, [r3, r5, lsl #2] @;matriz[i][j] = 0;
.Lfinif:
add r1, r7
cmp r1, r2 @;cierra bucle j
blo .Lforj

add r0, r7
cmp r0, r2 @;cierra bucle i
blo .Lfori

.Lstop: b .Lstop @;bucle infinito

.end @;fin del fichero

35
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

3.10 Acceso unidimensional a matrices


Otra forma de acceder al contenido de la matriz como si fuera un vector. En el caso del programa
para generar la matriz identidad podemos realizar un recorrido lineal para poner toda la matriz a
ceros, y luego recorrer la diagonal con “saltos” de N+1 posiciones, puesto que ello nos sitúa en la
siguiente fila más una columna, o sea, la siguiente posición de la diagonal:
#define N 10

int matriz[N*N];

void main(void)
{
for (i = 0; i < N*N; i++)
matriz[i] = 0;
i = 0;
for (j = 0; j < N; j++)
{
matriz[i] = 1;
i += N+1;
}
}

La traducción a ensamblador (sin multiplicaciones dentro de los bucles) podría ser la siguiente:
@;=== Ejemplo de acceso a matriz de words, con un índice ===
N = 10 @;constante!
.bss
matriz: .space N*N*4 @;variables global no inicializada

.text
.align 2
.arm
.global _start
_start: @;programa principal
ldr r3, =matriz @;r3 contiene dirección base de matriz
mov r2, #N @;r2 = N
mul r5, r2, r2 @;r5 = N*N
mov r6, #0 @;r6 = 0
mov r7, #1 @;r7 = 1
mov r0, r6 @;r0 es 'i'
.Lfori: @;primer bucle
str r6, [r3, r0, lsl #2] @;matriz[i] = 0;
add r0, r7
cmp r0, r5 @;cierra bucle i
blo .Lfori
mov r0, r6 @;r0 = 0
mov r1, r6 @;r1 es 'j'
.Lforj: @;segundo bucle
str r7, [r3, r0, lsl #2] @;matriz[i] = 1;
add r0, r2 @;i += N
add r0, r7 @;i += 1
add r1, r7
cmp r1, r2 @;cierra bucle j
blo .Lforj
.Lstop: b .Lstop @;bucle infinito

.end @;fin del fichero

36
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

Sección 4 : Programación con rutinas


4.1 Llamada a una función simple
Las funciones (o métodos o procedimientos) de los programas sirven para encapsular una
determinada serie de instrucciones para realizar una determinada acción, que se puede invocar
desde diferentes puntos de un mismo programa, o incluso desde otros programas si las funciones se
recogen en una librería (o clase o fichero objeto). Las ventajas de utilizar funciones para obtener
programas bien estructurados son múltiples, del mismo modo que programar sin funciones es
totalmente desaconsejable.
En lenguaje ensamblador las funciones se implementan como rutinas. En el apartado 2.9 ya se ha
comentado como definir una rutina y como invocarla. En esta sección vamos a profundizar en el
uso de las rutinas, partiendo de ejemplos escritos en lenguaje C. El primer ejemplo es el siguiente:

#define N 10

int fact;

void main(void)
{
fact = factorial(N);
}

El código anterior sólo muestra la llamada a una función que recibe un número entero como único
parámetro y devuelve el factorial de ese número. Por convenio, se establece que la rutina que vamos
a implementar para realizar dicha función recibe el parámetro por el registro r0 y devuelve el
resultado por el registro r1. La traducción a ensamblador podría ser la siguiente:
@;=== Ejemplo de programa que llama a la función (rutina) factorial ===

N = 10 @;constante!

.bss
fact: .space 4 @;variable resultado

.text
.align 2
.arm
.global _start
_start: @;programa principal
mov r0, #N @;r0 = N
bl factorial @;llamada a la función
ldr r2, =fact
str r1, [r2] @;guardar el resultado en 'fact'
.Lstop: b .Lstop @;bucle infinito

En el texto anterior, después de cargar el registro r0 con el parámetro, se realiza la llamada a la


rutina 'factorial' con la instrucción 'bl', y a continuación se guarda el resultado contenido en el
registro r1 dentro de las posiciones de memoria correspondientes a la variable global 'fact'.

37
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

4.2 Implementación de una función simple


A continuación se define el código C de la función para calcular el factorial:

int factorial(int n)
{
int i, f = 1;

if ((n < 0) || (n > 12)) /* verificación de rango de entrada */


return -1;

for (i = 2; i <= n; i++) /* cálculo normal */


f *= i;

return f;
}

La traducción a ensamblador podría ser la siguiente:


:
:
@;=== Función 'factorial' ===
@; Calcula el factorial de un número entero positivo, entre 0 y 12
@; Parámetros de entrada:
@; - r0: número del cual hay que obtener el factorial
@; Resultados de salida:
@; - r1: factorial del número, o -1 si parámetro fuera de rango

.global factorial
factorial: @;etiqueta de inicio de rutina
push {r2, lr} @;salvar estado
cmp r0, #0 @;verificación rango de entrada
blt .Labortar @;si n < 0, saltar a sección de abortar
cmp r0, #12 @;verificación rango de entrada
bgt .Labortar @;si n > 12, saltar a sección de abortar
b .Lnormal @;salta la sección de cálculo normal
.Labortar:
mov r1, #0
sub r1, #1 @;resultado r1 = -1
b .Lfinfactorial @;salta al final de la rutina
.Lnormal:
mov r1, #1 @;r1 es variable local 'f'
mov r2, #2 @;r2 es variable local 'i'
cmp r2, r0 @;comprobar si hay que entrar en el bucle
bgt .Lfinfactorial @;salta el cuerpo del 'for' si i > n
.Lfor:
mul r1, r1, r2 @;cuerpo del 'for' (f *= i;)
add r2, #1 @;i++
cmp r2, r0 @;comprobar si hay que continuar el bucle
ble .Lfor @;repite el 'for' mientras i <= n

.Lfinfactorial:
pop {r2, pc} @;restaura estado y retorna al invocador

.end @;fin del fichero

38
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

4.3 Paso de parámetros por referencia


En el ejemplo anterior el parámetro 'n' se pasaba por Valor, es decir, se copia el valor del parámetro
real (que era 10) dentro del registro que recibe el parámetro formal (que era r0). El resultado
también se devolvía por valor, dentro del registro r1.
La otra forma de pasar parámetros es por Referencia, que significa que se copia la dirección de
memoria del parámetro real dentro de un registro que es la referencia del parámetro formal. De este
modo, cualquier cambio que se efectúe dentro de la función sobre el parámetro formal quedará
reflejado en el parámetro real, puesto que se modifica directamente su contenido en memoria.
El siguiente ejemplo consiste en utilizar una función de swap para intercambiar el contenido de dos
variables:
int a = 5, b = 3;

void main(void) { swap(&a, &b); }

void swap(int *x, int *y)


{
int temp = *x; *x = *y; *y = temp;
}

La traducción a ensamblador podría ser la siguiente:


@;=== Ejemplo de paso de parámetros por referencia ===

.data
a: .word 5 @;variables globales
b: .word 3

.text
.align 2
.arm
.global _start
_start: @;programa principal
ldr r0, =a @;r0 es dirección de 'a'
ldr r1, =b @;r1 es dirección de 'b'
bl swap @;llamada a la función
.Lstop: b .Lstop @;bucle infinito

@;=== Función 'swap' ===


@; Intercambia el contenido de dos enteros
@; Parámetros de entrada:
@; - r0, r1: direcciones de los dos enteros
.global swap
swap: @;etiqueta de inicio de rutina
push {r2-r3, lr} @;salvar estado
ldr r2, [r0] @;r2 es 'temp' (captura contenido *x)
ldr r3, [r1] @;r3 captura contenido *y
str r3, [r0] @;*x = *y
str r2, [r1] @;*y = temp
pop {r2-r3, pc} @;restaura estado y retorna al invocador

.end @;fin del fichero

39
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

4.4 Paso de vectores por parámetro


Para pasar un vector por parámetro hay que utilizar siempre el método de copiar la dirección, puesto
que el vector puede ser muy largo y en la mayoría de los casos no hay suficientes registros para
copiar todas las posiciones del vector. Además, suele ser necesario pasar también la longitud (por
valor), ya que la rutina no tiene porqué saber este dato, que depende de la definición del vector
(independiente de la función).
El siguiente ejemplo invierte el orden de los elementos de un vector de half-words:
short v1[] = {39, -120, 1054, -309, 4000, -1, 82, 93};

void main(void) { reverse(v1, 8); }

void reverse(short vector[], int lon)


{
int i, temp;
for (i = 0; i < lon/2; i++)
swap(&vector[i], &vector[lon – i]);
}

La traducción a ensamblador podría ser la siguiente:


@;=== Ejemplo de paso de un vector por parámetro ===
.data
v1: .hword 39, -120, 1054, -309, 4000, -1, 82, 93

.text
.align 2
.arm
.global _start
_start: @;programa principal
ldr r0, =v1 @;r0 es dirección de 'vector'
mov r1, #8 @;r1 es longitud del vector
bl reverse @;llamada a la función
.Lstop: b .Lstop @;bucle infinito

@;=== Función 'reverse' ===


@; Invierte el orden de los elementos de un vector de half-words
@; Parámetros de entrada:
@; - r0: dirección inicial del vector
@; - r1: longitud del vector
reverse: @;etiqueta de inicio de rutina
push {r0-r3, lr} @;salvar estado
mov r2, r1, lsr #1 @;r2 = lon/2
add r1, r0, r1, lsl #1 @;r1 = dir.inicial + lon*2
sub r1, #2 @;r1 apunta a último elemento del vector
.Lfor:
bl swap @;swap(&vector[i], &vector[lon – i]);
add r0, #2 @;avanza puntero inicial
sub r1, #2 @;retrocede puntero final
cmp r0, r1
blo .Lfor @;repite mientras no se crucen los punteros
pop {r0-r3, pc} @;restaura estado y retorna al invocador
:
:

40
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

4.5 Paso de matrices por parámetro


La matrices también se pasan por referencia. Existe un problema adicional al paso de vectores: hay
que conocer el número de columnas de la matriz para poder calcular el desplazamiento de sus
elementos. Por lo tanto, se supone que la matriz tiene un número predeterminado de columnas,
aunque el número de filas puede ser libre, en cuyo caso se tendrá que pasar también por parámetro
(por valor).
La siguiente función calcula la suma de todas las filas de una matriz de 8 columnas de words,
añadiendo una fila más con los resultados de las sumas (se realiza una suma por cada columna):

void sumaFilas(int matriz[][8], int num_filas)


{
int i, j, suma;
for (j = 0; j < 8; j++) /* para cada columna */
{ suma = 0; /* reinicia acumulador */
for (i = 0; i < num_filas; i++)
{
suma += matriz[i][j]; /* acumula todas las filas */
}
matriz[num_filas][j] = suma; /* guarda resultado en fila */
} /* adicional */
}

La traducción a ensamblador podría ser la siguiente:


@;=== Función 'sumaFilas' ===
@; Suma todas las filas de una matriz de words de 8 columnas
@; Parámetros de entrada:
@; - r0: dirección inicial de la matriz
@; - r1: número inicial de filas
sumaFilas: @;etiqueta de inicio de rutina
push {r2-r6, lr} @;salvar estado

mov r3, #0 @;r3 es 'j'


.Lfor_j:
mov r4, #0 @;r4 es 'suma'
mov r2, #0 @;r2 es 'i'
.Lfor_i:
add r5, r3, r2, lsl #3 @;r5 = j + i*8 (8 es número de columnas)
ldr r6, [r0, r5, lsl #2] @;r6 = matriz[i][j]
add r4, r6 @;suma += matriz[i][j];

add r2, #1
cmp r2, r1
blo .Lfor_i @;repite tantas veces como filas haya

add r5, r3, r1, lsl #3 @;r5 = j + num_filas*8


str r4, [r0, r5, lsl #2] @;matriz[num_filas][j] = suma;

add r3, #1
cmp r3, #8
blo .Lfor_j @;repite para 8 columnas

pop {r2-r6, pc} @;restaura estado y retorna al invocador


:

41
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

4.6 Paso de matrices como vectores


Si se requiere programar una rutina que pueda trabajar con matrices de cualquier número de
columnas, hay que tratar a la matriz como si fuera un vector. La siguiente función suma las filas de
una matriz, sea cual sea su número de columnas:

void sumaFilas2(int matriz[], int num_filas, int num_columnas)


{
int i, j, suma;
for (j = 0; j < num_columnas; j++) /* para cada columna */
{ suma = 0; /* reinicia acumulador */
for (i = 0; i < num_filas; i++)
{
suma += matriz[i*num_columnas + j]; /* acumula filas */
}
matriz[num_filas*num_columnas + j] = suma; /* guarda resultado */
} /* en fila adicional */
}

La traducción a ensamblador podría ser la siguiente:


@;=== Función 'sumaFilas2' ===
@; Suma las filas de una matriz de words de un número indeterminado de columnas
@; Parámetros de entrada:
@; - r0: dirección inicial de la matriz
@; - r1: número inicial de filas
@; - r2: número de columnas
sumaFilas2: @;etiqueta de inicio de rutina
push {r3-r7, lr} @;salvar estado

mov r3, #0 @;r3 es 'j'


.Lfor_j:
mov r4, #0 @;r4 es 'suma'
mov r7, #0 @;r7 es 'i'
.Lfor_i:
mul r5, r7, r2 @;r5 = i*num_columnas
add r5, r3 @;r5 = i*num_columnas + j
ldr r6, [r0, r5, lsl #2] @;r6 = matriz[i*num_columnas + j]
add r4, r6 @;acumula contenido de matriz;

add r7, #1
cmp r7, r1
blo .Lfor_i @;repite tantas veces como filas haya

mul r5, r1, r2 @;r5 = num_filas*num_columnas


add r5, r3 @;r5 = num_filas*num_columnas + j
str r4, [r0, r5, lsl #2] @;almacena resultado en fila adicional

add r3, #1
cmp r3, r2
blo .Lfor_j @;repite el número de columnas indicado

pop {r3-r7, pc} @;restaura estado y retorna al invocador


:

42
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

4.7 Invocación de rutina pasando un matriz como vector


El siguiente programa en C invoca a la función desarrollada en el apartado anterior, usando dos
matrices de diferentes tamaños:
int mat4x6[] = {{50, -276, 325, 217, 201, 30},
{102, -8, 50, -276, 325, -12},
{-27, 541, 102, -8, 69, 35},
{23, 50, -276, 325, -276, 325},
{0, 0, 0, 0, 0, 0}};

int mat3x4[][] = {{9, -5, 8, -2},


{4, 6, 7, 0},
{-9, 7, -1, 0},
{0, 0, 0, 0}};

void main(void)
{
sumaFilas2((int *)mat4x6, 4, 6); /* hay que hacer un 'casting' */
sumaFilas2((int *)mat3x4, 3, 4); /* porque no coinciden los tipos */
}

La traducción a ensamblador podría ser la siguiente:


@;=== Ejemplo de paso de matrices como vectores ===

.data
mat4x6: .word 50, -276, 325, 217, 201, 30 @;declaración de matrices
.word 102, -8, 50, -276, 325, -12
.word -27, 541, 102, -8, 69, 35
.word 23, 50, -276, 325, -276, 325
.word 0, 0, 0, 0, 0, 0 @;fila adicional para resultados

mat3x4: .word 9, -5, 8, -2


.word 4, 6, 7, 0
.word -9, 7, -1, 0
.word 0, 0, 0, 0 @;fila adicional para resultados

.text
.align 2
.arm
.global _start
_start:
ldr r0, =mat4x6 @;r0 apunta al inicio de mat4x6
mov r1, #4 @;no es necesario hacer ningún 'casting'
mov r2, #6
bl sumaFilas2 @;primera llamada a la función

ldr r0, =mat3x4 @;r0 apunta al inicio de mat3x4


mov r1, #3 @;no es necesario hacer ningún 'casting'
mov r2, #4
bl sumaFilas2 @;segunda llamada a la función

.Lstop: b .Lstop @;bucle infinito

43
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

4.8 Llamadas anidadas


A continuación se define una función en C para calcular un número combinatorio (m sobre n)
llamando a la función para calcular el factorial definida en el apartado 4.2:
int combinatorio(int m, int n)
{
return factorial(m)/(factorial(m-n)*factorial(n));
}

La traducción a ensamblador podría ser la siguiente:


@;=== Función 'combinatorio' ===
@; Calcula el combinatorio de dos números m y n,
@; suponiendo que m >= 0, n >= 0, m >= n y m <= 12
@; Parámetros de entrada:
@; - r0: m
@; - r1: n
@; Resultados de salida:
@; - r2: m sobre n
combinatorio:
push {r0, r1, r3, r4, lr} @;salvar estado

mov r2, r1 @;guardar n en r2


bl factorial
mov r3, r1 @;r3 = m!

sub r0, r2
bl factorial
mov r4, r1 @;r4 = (m-n)!

mov r0, r2
bl factorial
mul r2, r4, r1 @;r2 = (m-n)!·n!

mov r0, r3
mov r1, r2
bl dividir @;llamar la función definida en problema 4.1
mov r2, r0 @;r2 = m! / (m-n)!·n!

pop {r0, r1, r3, r4, pc} @;restaura estado y retorna al invocador

El siguiente esquema muestra la evolución de la pila cuando se llama a la rutina 'factorial' desde
dentro de la rutina 'combinatorio':
:

Zona libre
SP2 R2 Llamada a rutina
LR (@ret2) 'factorial'
SP1 R0
R1
R3 Llamada a rutina
R4 'combinacional'
LR (@ret1)
Zona ocupada

44
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

4.9 Llamadas recursivas


Una llamada recursiva ocurre cuando una función se llama a sí misma. A continuación se propone
una versión recursiva de la función para calcular el número combinatorio (m sobre n):

int combinatorioR(int m, int n)


{
if ((m <= 1) || (n <= 0) || (n >= m))
return 1;

return combinatorioR(m-1, n) + combinatorioR(m-1, n-1);


}

La traducción a ensamblador podría ser la siguiente:


@;=== Función 'combinatorioR' ===
@; Calcula el combinatorio de dos números m y n,
@; suponiendo que m >= 0, n >= 0 y m >= n (m puede ser > 12)
@; Parámetros de entrada:
@; - r0: m
@; - r1: n
@; Resultados de salida:
@; - r2: m sobre n
combinatorioR:
push {r0, r1, r3, lr} @;salvar estado

cmp r0, #1 @;comprobación de condiciones triviales


ble .Ltrivial @;m <= 1
cmp r1, #0
ble .Ltrivial @;n <= 0
cmp r1, r0
bge .Ltrivial @;n >= m
b .Lrecursividad
.Ltrivial:
mov r2, #1 @;solución trivial
b .Lfincombi

.Lrecursividad:
sub r0, #1 @;r0 = m-1; r1 = n;
bl combinatorioR
mov r3, r2 @;r3 = (m-1 sobre n)

sub r1, #1 @;r0 = m-1; r1 = n-1


bl combinatorioR
add r2, r3 @;r2 = (m-1 sobre n-1) + (m-1 sobre n)

.Lfincombi:
pop {r0, r1, r3, pc} @;restaura estado y retorna al invocador

45
Teoría de Fundamentos de Computadores Tema 4: Fundamentos de Lenguaje Máquina

4.10 Evolución de las llamadas recursivas


El siguiente gráfico muestra la evolución de las llamadas recursivas realizadas por la rutina del
apartado anterior, suponiendo que se llame por primera vez con los parámetros m = 8, n = 3:
:

Zona libre
SP8 R0 (1)
R1 (1) Llamada
R3 Recursiva 7
LR (@ret2)
SP7 R0 (2)
R1 (1) Llamada
R3 Recursiva 6
LR (@ret2)
SP6 R0 (3)
R1 (1) Llamada
R3 Recursiva 5
LR (@ret2)
SP5 R0 (4)
R1 (1) Llamada
R3 Recursiva 4
LR (@ret2)
SP4 R0 (5)
R1 (2) Llamada
0: 1: 2: 3: R3 Recursiva 3
LR (@ret2)
0: 1
SP3 R0 (6)
1: 1 1 R1 (3) Llamada
R3 Recursiva 2
2: 1 2 1 LR (@ret2)
SP2 R0 (7)
3: 1 3 3 1 R1 (3) Llamada
4: 1 4 6 4 1 R3 Recursiva 1
LR (@ret2)
5: 1 5 10 10 5 1 SP1 R0 (8)
R1 (3) Llamada
6: 1 6 15 20 15 6 1 R3 Externa
LR (@ret1)
7: 1 7 21 35 35 21 7 1
SP0 Zona ocupada
8: 1 8 29 56 70 56 29 8 1
:
Como se puede observar, cada llamada recursiva supone un incremento del nivel de profundidad de
acceso al grafo de números que aparece en la izquierda, dónde el índice de las columnas representa
la n y el índice de las filas representa la m. En la pila se representan las llamadas correspondientes
al “camino” de nodos del grafo marcado por la linea en rojo.
El recorrido en profundidad supone que la pila va creciendo por cada nivel de profundidad que se
“desciende”, y va disminuyendo cuando se retorna a niveles más cercanos a la llamada inicial
(externa). És importante tener esto en cuenta porque hay que calcular el tamaño necesario de la pila
en función de la ocupación de esta por cada nivel de profundidad que se pueda descender.

46

Você também pode gostar