Registro de Activación
El Registro de Activación o Stack Frame se suele usar para almacenar tanto parámetros como variables de ámbito local a una función. Esto es interesante porque evita colisiones de nombres de variable, entre otras cosas, así como mayor aislamiento del código.
Por ejemplo, definimos la función en lenguaje C:
int funcion(int a) { int x; // Variable local x. x = 12 + a; return x; }
No importa que no conozcas el lenguaje C. Lo que interesa aquí es que la variable x es local, y sólo permanece dentro del cuerpo de la función. Las instrucciones que, dentro de la función, trabajen con la variable x siempre lo harán con la variable local y no interferirán con otras variables llamadas x definidas en otras partes del programa.
El BASIC del ZX no tenía esta característica (este proyecto persigue poder definir variables de ámbito local dentro de una función). Todas las variables eran de ámbito global, de manera que, si el programa era muy largo, el programador podría cometer el error de utilizar una variable en un sitio del programa que ya estuviera siendo usado en otro y sobreescribirla accidentalmente.
Implementación del Registro de Activación
Tradicionalmente se suele usar un registro con indexado para implementar Stack Frames. Una forma de implementar un registro de activación en una función en Z80 sería la siguiente:
:FuncionX ; Funcion(a, b, c) push ix ; Guarda el registro de activación de la función "llamadora" ld ix, -NN ; Tamaño en bytes de las variables locales en esta función add ix, sp ; "ld ix, sp" -- disminuye SP en NN. Esto equivale a varios PUSH a la vez ... ; código de la función pop ix ; Comienzo del código de retorno ex (sp), hl ; HL es la dirección de retorno ld sp, ix ; Pone el stack a como estaba antes de meter los parámetros jp (hl) ; "Ret" (implementado como salto, el valor ya se sacó antes de la pila)
De manera que el código que llamase a X, que requiere 3 parámetros haría algo como:
; Secuencia para llamar a FuncionX(<param1>, <param2>, <param3>) ld hl, <param3> push hl ld hl, <param2> push hl ld hl, <param1> push hl call FuncionX ; Llama a FuncionX(<param1>, <param2>, <param3>)
Los parámetros se pasan en orden inverso al que se definen en la función. Al retornar de FuncionX, el valor de SP sería el del Registro de Activación (guardado en el registro IX) que tenía antes de llamarla. Esto es importante, porque si hacemos lo siguiente, NO FUNCIONARÁ:
; En este punto, SP vale IX push bc ; Ahora SP vale IX - 2; ¡Ojo con hacer push antes de llamar a una función con "stack frame"! ; Secuencia para llamar a FuncionX(<param1>, <param2>, <param3>) ld hl, <param1> ... ... push hl call FuncionX ; Hemos retornado. Ahora SP vale IX, qué ha pasado con BC?? pop bc ; La pila está inconsistente. El valor de BC está 2 posiciones más abajo
Por lo general no se suele hacer push antes de llamar a una función con Registro de Activación, sino que se utiliza una posición de memoria local (para ello se usan las variables locales, por ejemplo). Otra forma de arreglarlo (más enrevesada) es guardar los registros de activación antes de hacer el push y el pop del ejemplo anterior.
Otra opción que si conserva el valor de SP antes de la llamada y que evita este problema es poner una secuencia de llamada más compleja:
:FuncionX ; Funcion(a, b, c) push ix ; Guarda el anterior registro de activación ld ix, 0 add ix, sp ; Crea el nuevo apuntando a la pila actual ld hl, -_NN add hl, sp ld sp, hl ; Reserva NN bytes para variables locales ... ... ld sp, ix ; Secuencia de retorno. Sacamos las variables locales de la pila pop ix ; Recuperamos el antiguo registro de activación pop de ; Dirección de retorno ld hl, _PP add hl, sp ld sp, hl ; Sacamos de la pila los parámetros de la llamada ex de, hl ; DE contiene la dirección de retorno jp (hl) ; "ret" (140 Ciclos de Reloj, 25 bytes)
Parámetros y Variables Locales
Hemos visto que una forma de pasar parámetros es mediante una secuencia de instrucciones push en la pila. Al entrar en una función, se guarda el registro de activación y se establece el nuevo (IX) al valor de SP.
FuncionX: push ix ld ix, 0 add ix, sp ; IX <-- SP
En este punto, la pila habrá quedado así (los parámetros son de 16 bits):
: ... : +---------+---------+ | <param2> | IX+6 +---------+---------+ | <param1> | IX+4 +---------+---------+ | dir. de retorno | IX+2 <-- Dirección de RETorno de la Subrutina +---------+---------+ SP | Antiguo valor IX | IX+0 <-- Valor actual del Registro de Activación +---------+---------+
De manera que podemos acceder a los parámetros mediante la indirección (IX + n). Por ejemplo acceder al valor de param1 sería:
ld e, (IX + 4) ld d, (IX + 5)
Esta secuencia carga en DE el valor de param1. La única pega es que en el Z80, las instrucciones con IX son más largas (llevan un byte extra) y más lentas en general. En el foro se ha sugerido usar HL en vez de IX para esto. HL no permite indirección, pero puede ser más eficiente si se cargan los parámetros, con una secuencia como:
ld hl, 2 add hl, sp ; HL <-- SP + 2 ld e, (hl) inc hl ld d, (hl) inc hl ; DE = entero
Pero nos inhabilitaría el registro HL.