Un aspecto muy peculiar de la arquitectura ARM es que las llamadas a subrutinas se hacen mediante un sencillo añadido a la instrucción de salto B (Branch).
Sintaxis:
B{L} {<cond>} <dirección_destino>
Donde:
L setea en 1 el bit 24 (el bit L) en la instrucción. Con esta opción, la instrucción almacena una dirección de retorno en el registo de link LR (R14). Si no se pone el sufijo L, la instrucción simplemente salta sin almacenar ninguna dirección.
<cond> Es la condicion para que se ejecute la instrucción.
<dirección_destino> Especifica la dirección a donde hay que saltar. Puede ser una etiqueta, pero se codifica en 24 bits. La dirección de destino se calcula de la siguiente manera:
Si agregamos el sufijo L, la instrucción B es BL y se llama (Branch and Link), y se usa para llamar a una subrutina, mediante un salto a la subrutina y escribiendo en el registro LR (R14) la dirección de la siguiente instrucción.
main : mov r1, #1 mov r2, #2 bl subrut mov r4, #4 /* Siguiente instrucción */ ... subrut : mov r3, #3 bx lr
La instrucción BL se usa en combinación con la instruccion BX (Branch and Exchange).
Sintaxis:
BX{<cond>} <Rm>
Donde:
<cond> Es la condicion para que se ejecute la instrucción.
<Rm> Registro que contiene el valor de la dirección de destino del salto
En particular, en el retorno de una subrutina se usa Rm=LR. El retorno se logra copiando el registro LR (R14) al PC (Program Counter).
Continuemos analizando el ejemplo de subrutina anterior
main : mov r1, #1 mov r2, #2 bl subrut mov r4, #4 /* Siguiente instrucción */ ... subrut : mov r3, #3 bx lr
Si seguimos el flujo del programa primero cargamos r1 a 1, luego r2 a 2 y lo siguiente que hay es una llamada a la subrutina. En dicha llamada el procesador carga en lr la dirección de la siguiente instrucción “mov r4, #4” y salta a la etiqueta subrut. Se ejecuta el “mov r3, #3” de la subrutina y después “bx lr” que vendría a ser la instrucción de retorno. Es decir, salimos de la subrutina retomando el flujo del programa principal, ejecutando “mov r4, #4”.
La convención AAPCS nos servirá para trabajar con las subrutinas de manera estandarizada:
Podriamos seguir nuestras propias reglas, pero si queremos interactuar con las librerías del sistema, tanto para llamar a funciones como para crear nuestras propias funciones y que éstas sean invocadas desde un lenguaje de alto nivel, tenemos que seguir una serie de pautas, lo que denominamos AAPCS (Procedure Call Standard for the ARM Architecture).
Cuando programamos no es necesario seguir estas reglas. Es más, podemos escribir una función sin seguir la norma incluso si trabajamos bajo Linux, pero no es recomendable ya que no podríamos
Para poder reusar nuestras funciones en otros proyectos es necesario seguir estas reglas. Aunque cuando programamos en el emulador, ó en Bare Metal (Programas sin el sistema operativo como intermediario) podemos no seguir algunas reglas como la alineación del stack.
Lo mejor para entender estas reglas es con una serie de ejemplos:
En este primer ejemplo crearemos nuestras propias funciones con pasaje de parámetros o argumentos
/* Organizacion del Computador UNGS: Programa en ensamblador ARM: ejer03funsuma.s EJEMPLO SIMPLE DE LLAMADO A FUNCION/SUBRUTINA CON PASAJE DE PARAMETROS RESPETANDO LA CONVENCION AAPCS En este ejemplo la funcion queda arriba del main .fnstart - .fnend, esto se usa si la funcion es llamada desde C Link Register ó Registro de Enlace. Almacena la dirección de retorno cuando una instrucción BL ó BLX ejecuta una llamada a una rutina. */ .data /* Definicion de datos */ @ @ .text @ Defincion de codigo del programa @ ------------ Código de la función mifuncion: .fnstart add r0,#1 @ lo que hace la funcion mifuncion bx lr @ salimos de la funcion mifuncion .fnend @ ------------- Código del main .global main @ global, visible en todo el programa main: mov r0, #0xB @ R0 <-- 11 @ solo paso un parametro r0 bl mifuncion @ Llamamos a la funcion mov r1, #0xA @ R1 <-- 10 mov r2, #0x7 @ R2 <-- 7 @ mov r7, #1 // Salida al sistema swi 0 // Salida al sistema operativo
En el ejemplo anterior vimos un sencillo esquema que vale para un sólo nivel de subrutinas, es decir, dentro de subrut no podemos llamar a otra subrutina porque sobreescribimos el valor del registro lr. La solución para extender a cualquier número de niveles es almacenar el registro lr en pila con las instrucciones push y pop.
main : mov r1, #1 mov r2, #2 bl nivel1 @ No es necesario guardar lr mov r5, #5 /* Siguiente instrucción */ ... /* ---- Subrutinas ---- */ nivel1 : push {lr} @ como esta funcion llama a otra mov r3, #3 @ se guarda lr, pq sino se pierde bl nivel2 @ llama a la funcion anidada pop {lr} @ restauramos el lr de esta funcion bx lr /* ------------ */ nivel2 : mov r4, #4 @ no es necesario guardar lr pq bx lr @ es la ultima funcion
Vemos en el último nivel (nivel2) podemos ahorrarnos el tener que almacenar y recuperar lr en la pila.
Las instrucciones de salto en la arquitectura ARM abarcan una zona muy extensa, hasta 64 Mb (32 Mb hacia adelante y otros 32 Mb hacia atrás). Este rango esta determinado por los 24 bits para codificar el destino del salto. En caso de necesitar un salto mayor recurrimos a la misma solución de la carga de inmediatos del mov, solo que el registro a cargar es el pc.
ldr pc, =etiqueta