Stack frames - WinDizio

Transcript

Stack frames - WinDizio
http://www.windizio.altervista.org/appunti/
File distribuito con licenza Creative Commons BY­NC­SA 2.5 IT
Copyright © 2007 ­ Michele Tartara
Parte I
Il backend
Figura 1: La struttura di un compilatore
Il backend è la parte di un compilatore che si occupa di ottimizzare il programma che si sta compilando (mentre è sotto forma
di linguaggio intermedio) e successivamente di tradurlo in codice macchina per una specica architettura.
1 Linguaggi intermedi
Esistono diversi tipi di linguaggi intermedi:
• istruzioni a 3 indirizzi (quadruples )
• Polish notation
• Abstract Syntax Tree (AST)1
• Macchina virtuale a pila (ne sono esempi i bytecode Java e .Net)
1.1 Caratteristiche
Il linguaggio intermedio è neutrale rispetto all'architettura, per ragioni di portabilità e essibilità. Per questo, le variabili
non hanno indirizzo di memoria, ma solo una rappresentazione simbolica.
Gli operatori sono simili a quelli del linguaggio sorgente (verranno precisati in seguito, perchè possono dipendere dalla specica
ALU).
Vengono già distinte le operazioni in base al tipo (interi, virgola mobile, ecc). In tal modo il backend le può mappare
più facilmente sulle istruzioni macchina.
2 Caratteristiche hardware che inuenzano il backend
• Memory hierarchy:
registri
dynamic memory allocation
memoria
garbage collection
cache
∗ 1◦ livello
∗ 2◦ livello
In presenza di molti registri migliorano le prestazioni, ma si complica il backend che deve determinarne l'assegnamento e
la gestione.
• Quali istruzioni macchina abbiamo?
RISC (Reduced Instruction Set Computer)
CISC (Complex Instruction Set Computer)
1 Il linguaggio intermedio usato nel libro Modern Compiler Implementation in Java di A. W. Appel
1
http://www.windizio.altervista.org/appunti/
File distribuito con licenza Creative Commons BY­NC­SA 2.5 IT
Copyright © 2007 ­ Michele Tartara
I processori RISC sono molto più diusi. L'architettura Pentium è intermedia: un RISC abbondante.
• Dove mettere il codice? Code layout
• Pipelined CPU
La pipeline della CPU serve a far eseguire contemporaneamente più istruzioni in fasi diverse. È utile per istruzioni
consecutive. Fallisce in presenza di salti. Il BE deve schedulare le istruzioni in modo da ridurre gli stalli del pipeline.
• Instruction level parallelism
Very Long Instruction Word (VLIW)
Superscalar
Il compilatore deve decidere quali istruzioni possono esser eseguite contemporaneamente tramite analisi dataow
delle dipendenze.
• Predicative execution
Vengono eseguite istruzioni che non si sa se serviranno (ad es: entrambi i rami di un'if) se sono disponibili parti
inutilizzate di processore. Spreco energia ma aumento le prestazioni.
• Mutithreading
Presenza di più Program Counter. Permette di eseguire in parallelo più programmi purchè non interferiscano nell'uso
di risorse
• Multi-processor (o multi-core)
La parallelizzazione è attualmente eseguita principalmente dal programmatore. Si studiano metodi per automatizzarla.
Parte II
Record di attivazione
Se una funzione presenta delle variabili locali o dei parametri, è necessario allocare dello spazio in memoria per queste
variabili e deallocarlo al termine della funzione stessa. Ogni funzione può essere richiamata più volte ricorsivamente, quindi
anche lo spazio per le variabili locali deve essere, in tal caso, allocato più volte.
Siccome una funzione ha termine solo quando tutte le funzioni da lei chiamate hanno già avuto termine, si osserva che le
chiamate a funzione hanno un comportamento di tipo LIFO. Ciò signica che possiamo usare una pila per conservare questi
dati.
In realtà, in presenza di un linguaggio che supporti sia le funzioni annidate (nested functions ) sia l'assegnazione di funzioni
alle variabili (function-valued variables ), potrebbe essere necessario mantenere alcune variabili locali dopo il termine della
funzione, quindi non si può usare una struttura a stack.
Ciò avviene in linguaggi come ML o Scheme (che qui non verranno trattati oltre). La presenza di una sola delle due
caratteristiche (Pascal, Java, C) non comporta questo problema.
3 Stack frames
La struttura dati che viene utilizzata si comporta, in generale, come una pila, con accesso solo all'elemento superiore e istruzioni
di push e pop.
Abbiamo tuttavia alcune esigenze speciche della compilazione:
• le variabili locali sono inserite sulla pila a gruppi (all'ingresso di una funzione)
• sono eliminate dalla pila a gruppi (all'uscita dalla funzione)
• non sempre le variabili inserite sulla pila sono inizializzate
• vogliamo poter accedere anche a valori che si trovano anche all'interno della pila stessa
2
http://www.windizio.altervista.org/appunti/
File distribuito con licenza Creative Commons BY­NC­SA 2.5 IT
Copyright © 2007 ­ Michele Tartara
Figura 2: Uno stack frame con i relativi campi
le variabili locali
i parametri di ingresso della funzione
variabili non locali, o globali
Per avere queste funzionalità, usiamo come pila (crescente verso il basso, ogni volta che si alloca spazio per l'esecuzione di una
nuova funzione) un grande array, con gli indirizzi decrescenti verso il basso. Memorizziamo inoltre due puntatori:
stack pointer (SP) indica la cima della pila. Tutto ciò che sta al di là di esso (verso indirizzi più bassi) è inutile (garbage ).
Tutto le posizioni di memoria precedenti sono allocate.
frame pointer (FP) indica l'inizio dell'area dello stack dedicata alla funzione corrente.
Al momento di una chiamata di funzione, FP assume il valore del vecchio SP.
Se il frame ha dimensione ssa, FP è un valore virtuale, ottenuto da SP+<dimensione stack frame>.
FP←SP
SP←SP+<dimensione stack frame>
Soprattutto, invece, nel caso in cui la dimensione non è ssa, FP è utile perchè le posizioni relative ad esso possono essere
determinate tramite un oset in modo immediato. Non si può fare lo stesso con SP perchè il suo valore è noto solo molto
più tardi, quando tutti i campi sono stati deniti.
Tutto ciò che è compreso tra frame pointer e stack pointer è il record di attivazione (o stack frame, o activation record ) della
funzione corrente.
L'ordine dei dati contenuti nello stack frame non è necessariamente sso, ma per ragioni di compatibilità è generalmente
denito dal manuale di riferimento dell'architettura.
Il primo stack frame (quello con indirizzi più alti, cioè sul fondo della pila), è relativo al main del programma. Al suo interno
(o vicino a lui) sono presenti le varibili globali.
P
P
Le dimensioni di uno stack frame sono dim. variabili dichiarate (nella funzione, e nei blocchi contenuti)+ dim. parametri+
altri dati.
3.1 Descrizione dei campi
Incoming arguments Parametri d'ingresso della funzione. Strettamente parlando, fanno parte del frame precedente.
Variabili locali spazio dedicato alle variabili locali dichiarate all'interno della funzione.
3
http://www.windizio.altervista.org/appunti/
File distribuito con licenza Creative Commons BY­NC­SA 2.5 IT
Copyright © 2007 ­ Michele Tartara
Return address indirizzo (nell'instruction segment ) della prima istruzione che dovrà essere eseguita al termine dell'esecuzione
di questa funzione.
Se il chiamante ha le istruzioni:
a-1: istr.
a: call F
a+1: istr.
allora return address dello stack frame di F contiene l'indirizzo a + 1.
L'indirizzo è inserito dall'istruzione call, alla creazione dello stack frame.
Nelle architetture moderne, spesso è salvato in un registro. In tal modo, solo le procedure non foglia dovranno scriverlo in
memoria.
Temporaries variabili temporanee usate dal compilatore per memorizzare sottoespressioni già calcolate ma non ancora utilizzate.
Saved registers Area utilizzata per salvare il contenuto dei registri che dovrà essere utilizzato in seguito quando è necessario
sovrascriverli.
Outgoing arguments Spazio riservato alla scrittura dei parametri di ingresso delle funzioni chiamate dalla funzione corrente.
Static link Viene usato nei linguaggi con struttura a blocchi, per far sì che si possa accedere alle variabili non locali
ma visibili. Punta alla base del frame dell'attivazione corrente (avvenuta più di recente) della funzione che racchiude
staticamente (nel codice sorgente) la funzione attuale f.
Alternativamente, esistono altri modi per rendere accessibili le variabili non locali:
• Lambda lifting : le variabili non locali accessibili da una funzione vengono automaticamente incluse tra i parametri
della funzione stessa.
• Si mantiene un array globale che contiene, a ogni posizione i, il puntatore al frame della più recente funzione eseguita
che abbia livello di annidamento statico i.
Dynamic link puntatore al Frame Pointer della funzione chiamante. Viene utilizzato al termine di una funzione, quando
il suo stack frame deve essere deallocato. Viene anche utilizzato per determinare la visilibilità delle funzioni in linguaggi
con scope dinamico.
4 Assegnazione dei registri
Le architetture moderne contengono molti registri (almeno 32) quindi, per ragioni di prestazioni, alcune variabili e valori intermedi
non vengono scritte in memoria, ma vengono lasciate nei registri.
Se è necessario sovrascrivere il contenuto di un registro e riutilizzarlo in seguito, la politica di salvataggio è denita dalle
convenzioni dell'architettura, che dividono i registri in:
caller-saved Il chiamante li deve salvare prima di eseguire una chiamata a funzione, se ritiene di aver ancora bisogno di ciò che
contengono in seguito. Generalmente vengono usati per memorizzare i dati non più utili dopo una chiamata. In tal
modo si risparmiano una store e una load.
callee-saved si ha la certezza che (a cura della funzione chiamata) i valori contenuti in questi registri, se sovrascritti, verrano
ripristinati al termine della funzione, mantenendo il valore che avevano al momento della chiamata. Utili per i valori
che dovranno essere disponibili successivamente alla chiamata.
Generalmente si può determinare tramite l'analisi di usso quali a quali registri assegnare una determinata variabile, a seconda
se dovrà ancora essere usata o meno.
4
http://www.windizio.altervista.org/appunti/
File distribuito con licenza Creative Commons BY­NC­SA 2.5 IT
Copyright © 2007 ­ Michele Tartara
4.1 Registri per il passaggio di parametri
Molte funzioni hanno al massimo da 4 a 6 parametri. Perciò un tal numero di registri viene riservato per il passaggio
di parametri.
In alcuni linguaggi, i parametri possono essere acceduti per indirizzo, e devono essere adiacenti. I registri non hanno indirizzo.
Perciò si riserva comunque spazio sullo stack, ed è la funzione chiamata a riempirlo, solo se eettivamente necessario. In
tal modo, comunque, si risparmia il tempo necessario al caricamento e scaricamento dalla memoria.
Se una funzione ne chiama un'altra, i suoi parametri possono dover sovrascrivere quelli della chiamante, contenuti nei registri,
salvandone quindi il contenuto precedente nello stack. Si ha comunque un risparmio di tempo rispetto all'uso della sola memoria
perchè:
• Alcune procedure, le procedure foglia (leaf procedure ), non ne chiamano altre. Quindi i loro parametri possono stare nei
registri senza che debbano essere salvati in memoria.
• Alcuni compilatori ottimizzanti utilizzano la interprocedural register allocation
• Se una procedura non ha più bisogno di usare i propri parametri quando chiama una sottoprocedura, la sottoprocedura può sovrascrivere i registri senza salvarli (e lo può scoprire tramite analisi statica del codice).
• Alcune architetture hanno register windows, che permettono di allocare un insieme di registri senza traco in memoria.
4.2 Salvataggio in memoria
Ogni volta che è possibile, i registri vengono sfruttatti per conservare le variabili. I valori vengono scritti in memoria per
una delle seguenti ragioni:
• La variabile sfugge (escapes ):
La variabile verrà passata per riferimento, quindi ha bisogno di un indirizzo in memoria
La variabile è acceduta da una funzione innestata in quella corrente
• Il valore è troppo grande per un singolo registro
• Il registro che contiene la variabile deve essere riutilizzato perchè ha una funzione specica (es: passaggio parametri)
• Ci sono troppe variabili per usare solo i registri.
5 Chiamate di funzioni
function f()
{
[istr]
g();
[istr]
}
function g()
{
[istr]
}
Caller Funzione chiamante: f()
Callee Funzione chiamata: g()
5.1 Nested procedures
Si parla di procedure annidate (nested procedures) quando è possibile denire una funzione all'interno dell'altra.
Dato un insieme di procedure innestate, è possibile denire una albero di annidamento statico (Static Nesting Tree ),
che determina, a livello di codice sorgente, quale procedura contiene al suo interno quale. In linguaggi con scope statico, è
possibile determinare le variabili visibili da una determinata funzione risalendo da essa alla radice dell'albero SNT.
5
http://www.windizio.altervista.org/appunti/
File distribuito con licenza Creative Commons BY­NC­SA 2.5 IT
Copyright © 2007 ­ Michele Tartara
Algorithm 1 Dichiarazione di funzioni annidate
main()
{
var a;
function f()
{
var b;
function g()
{
var c;
}
[istr]
}
function k()
{
[istr]
}
g();
[istr]
}
Figura 3: Lo Static Nesting Tree relativo all'algoritmo 1
6