Estratto del documento

Lezione 10: Ciclo di sviluppo del software

Facciamo una ricapitolazione veloce su quello che è il ciclo di sviluppo del software. Questo è il classico ciclo di sviluppo di un software dove partiamo dall’editing per poi passare al caricamento in memoria, nel mezzo possiamo trovare tutta una serie di fasi importanti che ci interessano.

Fasi del ciclo di sviluppo

La fase di preprocessing ad esempio fa un controllo sintattico del codice e poi vengono convertite le pseudoistruzioni. In questa fase quindi troviamo tutta una serie di azioni che non sono più legate alla compilazione che ci porta poi alla generazione del file oggetto, ma ci sono tutta una serie di azioni che sono riportate nel riquadro sopra. Il compilatore una volta esploso il file sorgente ci dà un file intermedio che dato in pasto al compilatore ci dà il file oggetto. Il file oggetto è la traduzione del codice in linguaggio tradotto dal compilatore in modo da passare da un linguaggio di alto livello ad un linguaggio di basso livello come è ad esempio il linguaggio macchina.

Nel software di solito si hanno più file oggetto che compongono il file eseguibile che è quello binario scritto in assembly che dovrà poi essere interpretato dalla CPU tramite delle istruzioni che si trovano all’interno dell’instruction set architecture. Questa serie di file oggetto viene linkata e viene poi ad essere caricata dal loader in memoria RAM. Nella fase finale del modello sopra abbiamo il passaggio dal programma al processo. Il processo come abbiamo visto subirà tutta una serie di operazioni importanti da parte del SO.

I file oggetto possono essere compilati per diverse architetture ad esempio a 32 o 64 bit. Ad esempio, in Linux avremo un file oggetto che si troverà in un formato diverso rispetto a Windows che prende il nome di ELF. I file oggetto hanno tra di loro una certa coesione, figurano in un unico eseguibile che linka questi file oggetto ed il loader eseguirà questo file.

Per rilocabili si intendono degli indirizzi relativi che una volta assegnato lo spazio di indirizzamento del programma vengono in qualche modo addizionati in relazione ad un offset. L’allocazione di un programma in RAM avviene attraverso la rilocazione. Con una rilocazione statica il programma verrà ad essere sempre caricato allo stesso indirizzo di memoria. Questo crea uno svantaggio in quanto quando il processo deve essere spostato nella memoria secondaria per fare ad esempio spazio ad altri processi, quando il processo dovrà rientrare in memoria, dovendo essere ricaricato nella stessa area di memoria, questo darà problemi.

Moduli e interfacce

Il modulo è un software che ha un'interfaccia ed un corpo, dove l’interfaccia specifica il cosa ed il corpo specifica il come. Quindi anche nella programmazione di sistema abbiamo una divisione che porta l’interfaccia ad essere posizionata nel file header mentre il corpo che viene implementato nel file .c o .cpp sarà il compito del main di andare ad unire queste due parti di programma. Questo meccanismo di separare l’interfaccia dal corpo è una buona pratica in quanto quando si deve andare a modificare l’interfaccia non bisognerà andare a modificare anche il codice del corpo e non bisognerà ad esempio andare a ricompilare il modulo utente.

I file oggetto a valle della compilazione devono essere collegati dal linker per creare un unico eseguibile. Questo flusso di lavoro sarà quello che ad esempio andremo ad utilizzare quando andremo a creare un Makefile. Nel Makefile potremmo andare a specificare in un unico file come devono essere compilati i diversi file sorgente all’interno della nostra applicazione.

Linkaggio e simboli

Alla sinistra nell’immagine sopra abbiamo il modulo oggetto che può essere composto da più moduli, in questi moduli abbiamo che può capitare di dover fare riferimento a funzioni implementate all’interno di altri file oggetto e quindi questi simboli altro non sono che riferimento a delle funzioni. Ad esempio, il main quando utilizziamo la printf può fare riferimento ad un simbolo che non è presente all’interno della nostra applicazione in quanto la printf viene ad essere implementata all’interno della libreria C che viene fornita come libreria dinamica all’interno dei sistemi Linux. Ci sono quindi diversi simboli che si dicono non risolti all’interno dei nostri moduli oggetti. Quando questi moduli oggetti devono essere caricati all’interno di un unico grande modulo caricabile che prende il nome di Load Module, abbiamo che questi riferimenti simbolici devono essere risolti e quindi vengono in questa fase di linkaggio ad essere tradotti con dei salti incondizionati verso aree di memoria che sono state a loro volta tradotte durante questa fase di linkaggio.

In questo caso, ad esempio, possiamo osservare che la chiamata CALL B (Modulo A) viene ad essere fatta attraverso una Jump ad L, dove L altro non è che l’indirizzo di inizio del modulo B. In questo modo abbiamo che tutta questa serie di file oggetto viene ad essere linkata in questo modo tra di loro. L’indirizzo relativo della serie di moduli costituisce quell’indirizzo che non corrisponderà a quello nel quale questi moduli verranno ad essere portati in memoria fisica, questa traduzione ulteriore verrà ad essere fatta dal caricatore. Possiamo avere un linking dinamico sia in fase di caricamento oppure a runtime.

Caricamento: vantaggi e svantaggi

  • Assoluto: ci sono degli indirizzi fissi ed assoluti e quindi il programma verrà ad essere collocato sempre allo stesso indirizzo di memoria. In questo caso il programma risiederà sempre nello stesso spazio di memoria per tutta la sua vita.
  • Rilocabile: ci sono degli indirizzi che non dipendono dagli effettivi indirizzi di memoria fisica, in questo caso il programma risiederà in un indirizzo di memoria che dipenderà da quando andiamo a caricare il programma all’interno della memoria.

In Linux abbiamo un approccio che è loading dinamico con linking dinamico.

Formato ELF

Nei sistemi basati su UNIX abbiamo questo formato ELF che sta per Executable and Linkable Format, questo formato è uno standard per file eseguibili, per i file oggetto, librerie condivise e Core Dumps che sono delle istantanee di memoria che vengono ad essere generate a valle di un kernel panic. Questo formato ELF supporta diverse architetture del processore e permette di eseguire e quindi istruire il Loader su come caricare in memoria principale un file eseguibile.

Il file ELF si compone di un header che compone la prima parte del file e specifica se stiamo utilizzando indirizzi a 32 o 64 bit, se stiamo utilizzando una codifica Little endian o Big endian, qual è il SO target. Nel Program Header invece ci viene detto come creare l’immagine del processo e quindi come andare a rilocare il file eseguibile in memoria principale. Nei vari segmenti o sezioni abbiamo che ci sono delle aree che riguardano le aree delle istruzioni e quindi ad esempio la tabella dei simboli, l’area dati statici, l’area dati globale ecc. Il file oggetto che viene formattato attraverso questo standard non è propriamente fatto in codice binario ma possiamo osservare dalla classificazione fatta sopra quanta roba ci sarà al suo interno.

Il comando objdump -x scritto sopra serve per mostrare questo file ELF e quindi tutte le sezioni viste sopra.

Make e Makefile

Make è un programma che utilizzeremo sempre e deve essere utilizzato per fare una compilazione separata dei programmi. Di solito un programma risulta essere composto da più file sorgenti e librerie e risulta quindi conveniente scrivere un unico file in una volta che permette una volta digitato make di compilare automaticamente tutti i file oggetto e linkare i file oggetto in un unico eseguibile attraverso un unico comando. Si parla di regole del Makefile.

N.B il comando Makefile è case sensitive e per tale ragione deve essere scritto proprio con l’iniziale maiuscola “M”. Se si va a modificare il nome quando si digita make uscirà l’errore che non è stato possibile trovare il Makefile, se si vuole quindi modificare il nome si dovrà poi andare a richiamarlo con il comando “make -f altronome”.

Il target e le dipendenze sono una lista dei file, una volta definito il target: dipendenze prima di mettere il comando di sistema non bisogna dimenticarsi di inserire il carattere TAB. Quando le dipendenze non sono presenti devono essere risolte e quindi ci sarà un altro target che dovrà essere risolto con altre dipendenze.

Nell’esempio abbiamo che il target è rappresentato da “file.o”, questo target dipende da file.c e file.h ed il comando che andiamo ad eseguire è una compilazione infatti con il comando “gcc -c file.c” viene ad essere richiamata la compilazione del file.c. Possiamo osservare che la dipendenza del file.o sopra è una dipendenza da file.c e file.h che sono due file che di solito già esistono essendo rispettivamente il file sorgente ed il file header e quindi non devono essere nuovamente risolti. Se questi file non ci fossero stati avremmo dovuto avere un’altra regola nel Makefile che avrebbe dovuto generare quei file per quel target.

Nella slide in basso abbiamo quella che rappresenta la regola generale che è quella che consente una volta generati tutti i file oggetto di andarli a linkare; il linker viene ad essere richiamato attraverso l’opzione -o.

Le regole il comando make le va ad analizzare in modo ordinato e quindi se ad esempio mettiamo come prima regola start il make cercherà di risolvere questa regola, se mettiamo alla fine main.o questa regola sarà messa alla fine. Possiamo osservare dall’esempio sopra come start dipenda da lib.o e main.o e quindi make non riesce ad avviare il comando e quindi passa l’esecuzione del comando alle altre due regole. In questo caso abbiamo che ci sta sia per lib.o che per main.o dei file sorgente e quindi il make eseguirà prima questi comandi. Una volta che sono stati generati i file lib.o e main.o allora in modo ricorsivo make torna verso l’alto per poter generare questo file start. Con make ovviamente possiamo invocare singole regole attraverso il comando make nome_regola.

Regola clean nel Makefile

Generalmente nel make andiamo a creare una regola senza dipendenze che prende il nome di clean, questa regola serve per fare pulizia dei file ed artefatti che vengono ad essere creati dopo la compilazione. Possiamo osservare che clean non ha dipendenze e i comandi che devono essere eseguiti sono:

  • Rm -f *.o: dove rm sta per remove, l’opzione -f indica di effettuare la rimozione in modo forzato, *.o sta ad indicare di prendere tutti i file che hanno suffisso .o.
  • Rm -f nomeEseguibile: rimuove in modo forzato il target.

Per capire bene il funzionamento vediamone un esempio. Possiamo osservare che andando ad eseguire il Makefile abbiamo le stesse regole viste nella slide sopra, tutte queste regole saranno utilizzate per generare start. Possiamo poi osservare la presenza della regola all (all: start) che è una regola base e sta ad indicare il genera tutto, dove il tutto diventa l’eseguibile start.

Nel clean possiamo osservare come vengano cancellati prima tutti i file .o e poi start, con rm -f *~ serve per eliminare in modo forzato dei file che vengono creati di backup e con questo comando vengono rimossi. Se andiamo ora ad eseguire il make a video vengono ad essere mostrati i comandi che sono stati specificati nella regola. Possiamo osservare come vengono invocati per primi i comandi che si possono risolvere, infatti non vengono risolti quei comandi dove ci sono delle dipendenze non risolte.

Se andiamo a fare ls -l possiamo osservare come è stato generato il file eseguibile start e ce ne accorgiamo sia perché è presente il permesso -x sia perché è evidenziato in verde.

Errori comuni nel Makefile

Vediamo ora che cosa accade se nella shell invece di usare il carattere TAB mettiamo degli spazi. Quello che possiamo osservare è che la shell che è evoluta non evidenzia il comando in prima linea in rosso come invece accade per le linee successive. Supponiamo di ignorare questo primo alert che ci viene offerto da questa shell ed immaginiamo di compilare, quello che accade se andiamo a fare make è che ci viene dato il seguente messaggio: il numero 4 sta ad indicare la riga del Makefile dove si è verificato l’errore e questa linea ci dice anche il tipo di errore che in questo caso è la mancanza del TAB.

Per andare a chiamare ad esempio la regola clean, in questo caso per andare a pulire i file oggetto dove sono stati generati basterà andare a fare. In questo modo vengono ad essere rimossi i file oggetto ed il file eseguibile. Nel Makefile possiamo andare a mettere qualunque comando; ad esempio, se provassimo a modificare il Makefile in questo modo dovrebbe essere eseguita anche la echo. Nel Makefile possiamo mettere quello che vogliamo ma dobbiamo fare attenzione a ciò che viene fatto.

Utilizzo delle variabili nel Makefile

Vediamo ora l’utilizzo delle variabili nel Makefile andando ad aprire quello che è il Makefile 2 contenuto in esercitazioni/SO-ES3-Makefile_Librerie-GDB/1_makefile_esempio/Makefile 2. Una variabile all’interno del Makefile viene ad essere definita come OBJ=lib.o main.o e quindi nome_variabile=valore_variabile, questo deriva dal linguaggio bash che è una tipologia di linguaggio che viene ad essere fornito in tutte le distribuzioni Linux. Tale linguaggio è un linguaggio molto potente in quanto consente di eseguire all’interno della shell tutta una serie di comandi ma in modo più programmatico con un file dove sono presenti tutti i comandi che abbiamo scritto.

Il valore di una variabile all’interno del Makefile può essere preso attraverso la seguente nomenclatura $(nome_variabile). Per richiamare il Makefile 2 dovremmo usare il comando make -f Makefile2 in quanto il nome dei Makefile non è Makefile solo ma presenta anche il 2. Il make si accorge anche che i file oggetto che sono stati generati dal file eseguibile sono aggiornati. Quando andiamo ad eseguire il Makefile2 abbiamo che l’effetto è lo stesso dell’esecuzione del Makefile soltanto che in questa seconda versione abbiamo utilizzato delle variabili.

Se andiamo ad aprire Makefile2 possiamo osservare che con %.o indichiamo che tutti i file che hanno l’estensione .o dipendono in realtà tutti dallo stesso file.cpp. Possiamo poi osservare che i token $@, $^ sono relativi al target della regola, alla lista delle dipendenze. Abbiamo poi la prima dipendenza ecc. In questo modo con questi token abbiamo una regola più compatta ma meno decifrabile.

Librerie statiche e dinamiche

Vediamo ora lo sviluppo delle librerie statiche e dinamiche. Generalmente la libreria è un modulo software che viene ad essere distribuito con l’intestazione e con il corpo e quindi con la generazione del file oggetto per quella libreria. In Linux le librerie vengono fornite come librerie dinamiche. La libreria è fondamentale per l’utilizzo di funzionalità che sono già state implementate da qualche sviluppatore e quindi ci esonera dal doverle reimplementare.

I tipi di libreria dipendono dalle fasi in cui vengono collegate al modulo caricabile e da come vengono caricate in memoria centrale. Abbiamo quindi librerie:

  • Statiche: sono collegate all’eseguibile staticamente e quindi in fase di linkaggio.
  • Dinamiche: sono caricate e collegate a runtime.

Vedremo come generare una libreria statica o dinamica. Per quanto riguarda le librerie statiche di solito abbiamo una dimensione maggiore dell’applicazione perché colleghiamo tutto quanto ci serve staticamente prima dell’esecuzione al file eseguibile e quindi abbiamo ad esempio nel main che si va a generare tutta una serie di situazioni che sono risolte staticamente e che quindi sono attaccate materialmente al file eseguibile del main. Questo è svantaggioso in quanto richiede la ricompilazione nel caso in cui la libreria si dovesse aggiornare.

Nelle librerie dinamiche viene ad essere importato invece nel file eseguibile solo quello che realmente serve, in realtà indipendentemente che una libreria viene ad essere caricata dinamicamente, ogni volta che viene richiesta una certa funzionalità questa viene al volo caricata in memoria principale. Quindi abbiamo che nella realtà dei fatti questa libreria dinamica non risiede in memoria principale. Ad esempio, la libreria C viene ad essere fornita come libreria dinamica in quanto se ad esempio volessimo linkare la libreria C ad un semplice programmino che fa lo Hello World avremmo ogni volta delle applicazioni enormi. Vediamo a scopo didattico come è possibile creare una libreria statica.

Creazione di una libreria statica

Per creare una libreria statica si usa il comando ar. Nel Makefile bisogna allora avere una riga come sopra dove si va a creare un’unica libreria statica con estensione .a. La convenzione che si adotta è che il nome della libreria deve essere lib come suffisso e poi il nome della libreria vero e proprio (lib_nome_libreria), gli altri parametri sono tutti i file oggetto che vengono ad essere collegati staticamente alla libreria. Nel caso in esempio abbiamo che file1.o e file2.o vengono ad essere collegati a libnomelib.a. Vediamo l’esempio che è fornito nel repository GitHub. In questo caso abbiamo che oltre al Makefile ed il main.c viene ad essere fornita la libreria mat con il suo header e con il suo corpo. Possiamo osservare come nella libreria venga ad essere utilizzata la compilazione condizionale e nel corpo della libreria mat.c si ha l’implementazione.

Il Makefile è il seguente. Possiamo osservare che andiamo ad utilizzare la variabile CURR che è posta uguale al path corrente (${PWD}). La PWD è molto simile al comando pwd visto come comando in precedenza che ci dice...

Anteprima
Vedrai una selezione di 10 pagine su 153
Appunti Sistemi operativi - parte 3 Pag. 1 Appunti Sistemi operativi - parte 3 Pag. 2
Anteprima di 10 pagg. su 153.
Scarica il documento per vederlo tutto.
Appunti Sistemi operativi - parte 3 Pag. 6
Anteprima di 10 pagg. su 153.
Scarica il documento per vederlo tutto.
Appunti Sistemi operativi - parte 3 Pag. 11
Anteprima di 10 pagg. su 153.
Scarica il documento per vederlo tutto.
Appunti Sistemi operativi - parte 3 Pag. 16
Anteprima di 10 pagg. su 153.
Scarica il documento per vederlo tutto.
Appunti Sistemi operativi - parte 3 Pag. 21
Anteprima di 10 pagg. su 153.
Scarica il documento per vederlo tutto.
Appunti Sistemi operativi - parte 3 Pag. 26
Anteprima di 10 pagg. su 153.
Scarica il documento per vederlo tutto.
Appunti Sistemi operativi - parte 3 Pag. 31
Anteprima di 10 pagg. su 153.
Scarica il documento per vederlo tutto.
Appunti Sistemi operativi - parte 3 Pag. 36
Anteprima di 10 pagg. su 153.
Scarica il documento per vederlo tutto.
Appunti Sistemi operativi - parte 3 Pag. 41
1 su 153
D/illustrazione/soddisfatti o rimborsati
Acquista con carta o PayPal
Scarica i documenti tutte le volte che vuoi
Dettagli
SSD
Ingegneria industriale e dell'informazione ING-INF/05 Sistemi di elaborazione delle informazioni

I contenuti di questa pagina costituiscono rielaborazioni personali del Publisher Dadox94 di informazioni apprese con la frequenza delle lezioni di Sistemi operativi e studio autonomo di eventuali libri di riferimento in preparazione dell'esame finale o della tesi. Non devono intendersi come materiale ufficiale dell'università Università degli studi di Napoli Federico II o del prof Crotoneo Domenico.
Appunti correlati Invia appunti e guadagna

Domande e risposte

Hai bisogno di aiuto?
Chiedi alla community