proyectos:zxbcompiler:zxbc_stack_frame

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.

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)

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.

  • proyectos/zxbcompiler/zxbc_stack_frame.txt
  • Última modificación: 24-01-2009 20:52
  • por sromero