ELEMANIA
Z80 - Lo stack e la chiamata
Lo stack nella chiamata a sottoprogrammi

Nella lezione precedente abbiamo visto che l'istruzione CALL deve salvare da qualche parte l'indirizzo di ritorno dal sottoprogramma in modo che tale indirizzo possa essere usato dall'istruzione RET. L'area in cui avviene il salvataggio e il recupero dell'indirizzo è lo stack.

In pratica l'istruzione CALL esegue un push automatico del contenuto del program counter (il registro/contatore che contiene sempre l'indirizzo della successiva istruzione da eseguire) sullo stack. Viceversa la RET esegue una pop dalla cima dello stack al program counter:

Tale meccanismo di salvataggio e di recupero è assolutamente automatico e trasparente per il programmatore, il quale deve solo occuparsi di allocare un'area di memoria per lo stack (inizializzando opportunamente il valore dello stack pointer).

L'utilità di questo meccanismo di salvataggio e ripristino risulta evidente se si considera il caso, tutt'altro che infrequente, di un sottoprogramma che al proprio interno ne chiama un altro. Consideriamo l'esempio seguente:

  LD SP,0000h  ; inizializza il fondo dello stack a 0000h
  LD B, 2 ; carica in B il numero 2
  LD C, 5  ; carica in C il numero 5
  CALL MEDIA ; chiama il sottoprogramma di nome MEDIA
  LD B, D  ; carica D in B
  LD C, 7  ; carica in C il numero 7
  CALL MEDIA ; chiama il sottoprogramma di nome MEDIA
  LD E, D   ; carica D in E
  ..... ; il programma prosegue con altre istruzioni
DIVIDI:   SRA A ; divide A per due (scalandolo a destra di uno)
  RET ; ritorna al punto di chiamata (fine del sottoprogramma)
     
MEDIA:  LD A,B ; carica B in A (inizio del sottoprogramma)
  ADD A,C   ; somma A e C e mette il risultato in A
  CALL DIVIDI  ; chiama il sottoprogramma DIVIDI
  LD D, A   ; sposta il risultato della divisione in D
  RET ; ritorna al punto di chiamata (fine del sottoprogramma)
           

In questo esempio il programma principale chiama il sottoprogramma MEDIA, il quale a sua volta chiama DIVIDI. Affinché il meccanismo delle chiamate e dei ritorni funzioni correttamente occorre che la RET di DIVIDI torni a MEDIA, il quale a sua volta deve tornare al programma principale:

E qui entra in gioco lo stack. Infatti l'indirizzo dell'istruzione LD B,D nel programma principale viene salvato sullo stack al momento di CALL MEDIA. Quando, all'interno di MEDIA, viene chiamata DIVIDI, viene salvato sullo stack l'indirizzo dell'istruzione LD D,A (istruzione del sottoprogramma MEDIA). Quindi la RET di DIVIDI preleva questo indirizzo dallo stack, mentre la seconda RET, quella di MEDIA, preleva dallo stack il primo indirizzo salvato:

Si osservi che, ogni volta che si esegue una CALL, viene salvato un nuovo indirizzo di ritorno sulla cima dello stack, con conseguente crescita dello stack stesso. Non essendo previsto nello Z80 nessun automatismo di controllo del valore assunto dallo stack pointer, se il numero di chiamate annidate (cioè di sottoprogrammi che chiamano altri sottoprogrammi) è troppo elevato, il rischio è che lo stack possa straripare (stack overflow) andando a sovrascrivere altre aree di memoria.

Per evitare problemi, è bene sovradimensionare la zona di memoria destinata allo stack, cercando di stimare le proprie necessità in termini di annidamenti ed uso normale delle PUSH e POP. Microprocessori più recenti e più evoluti possiedono dei meccanismi di protezione contro questo tipo di overflow della memoria.

Passaggio di parametri e valore di ritorno

Per poter eseguire i propri calcoli una subroutine deve ricevere un certo numero di valori dal chiamante (parametri); in modo analogo essa deve fornire un risultato (valore di ritorno) al chiamante. Mediante parametri e valore di ritorno una subroutine è in sostanza in grado di comunicare col chiamante. Un esempio tratto dal linguaggio C è il seguente:

risultato = somma(num1, num2);

La funzione somma riceve i valori di num1 e di num2 dal chiamante, esegue la somma e restituisce un valore di ritorno che viene salvato in risultato.

In assembly non esiste nessun meccanismo automatico per realizzare questo passaggio: è compito del programmatore implementarlo. Un modo semplice è quello di utilizzare dei registri del µP per inviare e ricevere valori dalla subroutine. Questa soluzione (usata in tutti i nostri precedenti esempi) è molto facile da realizzare ma presenta alcuni inconvenienti:

Il primo problema può essere facilmente risolto usando locazioni di memoria, invece di registri, per salvare i dati da passare da e verso la subroutine. Il secondo problema è invece più grave, in particolare nel caso di programmi complessi, con molte subroutine che si chiamano reciprocamente.

Una soluzione più elegante è quella di usare lo stack anche per il passaggio dei parametri e per la ricezione del valore di ritorno dalla subroutine. A questo proposito sono due le tecniche e le convenzioni maggiormente usate:

Entrambe le tecniche presentano pro e contro che non possiamo discutere approfonditamente in questa sede. Come esempio di chiamata di subroutine secondo la convenzione C osserviamo lo schema seguente:

              PUSH DE    ; questa istruzione serve solo per lasciare uno spazio in cima allo stack
                            ; in tale spazio la funzione salverà il suo valore di ritorno
                            ; al posto di DE è possibile scrivere qualsiasi cosa

             PUSH BC    ; salva i parametri BC sullo stack

             CALL subroutine    ; chiama la subroutine
 
             POP BC    ; ripulisce lo stack dai parametri
                           ; (al posto di BC si può usare qualsiasi coppia di registri)

             POP DE    ; preleva il valore di ritorno dallo stack

 

precedente - successiva

Sito realizzato in base al template offerto da

http://www.graphixmania.it