Stack frames - WinDizio
Transcript
Stack frames - WinDizio
http://www.windizio.altervista.org/appunti/ File distribuito con licenza Creative Commons BYNCSA 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 BYNCSA 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 BYNCSA 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 BYNCSA 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 BYNCSA 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 BYNCSA 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