Lezione 32: Introduzione alle chiamate di sistema in Linux
Le chiamate di sistema sono quel componente che ci fa da interfaccia tra i processi che eseguono in userspace che vogliono interagire con il SO e che vogliono invocare delle funzionalità del kernel. In realtà tutto questo sottocomponente è parte del kernel stesso e questa intermediazione è necessaria per evitare problemi legati alla sicurezza. Oggi si è molto più sensibili riguardo alla sicurezza, si pensi ad esempio alle problematiche legate alla Cyber Security, e quindi è necessaria questa intermediazione.
La tabella delle chiamate di sistema
Se vediamo la tabella delle chiamate di sistema, abbiamo molte più chiamate indipendentemente dalla versione del SO che abbiamo installato, ma di base un kernel offre più di 300 chiamate sullo standard IEEE Posix. I meccanismi di comunicazione che abbiamo all'interno del kernel si basano su apposite chiamate di sistema, ma spesso in realtà vengono ad essere usati speciali file virtuali all'interno delle directory /dev e /proc tramite l'uso delle system call read e write. Generalmente, le chiamate di sistema sono uno dei meccanismi principali all'interno del kernel per la comunicazione, basti pensare ad esempio ai device driver e quindi tutto quel software che opera con read e write su questa directory che è quella dei device.
La GNU Lib C e i wrapper
In Linux di default viene ad essere installata la GNU Lib C, che viene fornita anche come libreria dinamica e permette anche di ottenere dei cosiddetti wrapper verso le chiamate di sistema. Nella figura sopra possiamo osservare che c'è una chiamata di sistema che è implementata in kernel space e quindi all'interno del codice sorgente del kernel; questa write verrà ad essere invocata tramite questo meccanismo delle system call. Questo meccanismo delle chiamate di sistema viene incapsulato di fatto dalla libreria C che viene poi ad essere linkata all'applicazione utente.
Se proviamo ad esempio a scrivere un'applicazione che invoca la printf, quando andiamo a fare la compilazione, l'applicazione viene ad essere linkata automaticamente alla libreria dinamica glib C e quando andiamo ad invocare la printf non faremo altro che invocare la printf nella libreria C, che poi andrà ad invocare a sua volta il wrapper per la chiamata di sistema che si chiama con lo stesso nome della chiamata di sistema implementata a livello kernel. Nell'immagine sopra è quindi rappresentata la serie di chiamate per la printf, per la open, per la write, per la read ecc. Quindi chi vuole implementare non si deve solo preoccupare di sviluppare questa parte del kernel ma si deve preoccupare anche di sviluppare la parte client side, se questa la vogliamo vedere come server side e quindi come dal livello del kernel al livello dell'utente; deve quindi svilupparsi almeno un wrapper per la chiamata di sistema.
Parametri e convenzioni delle chiamate di sistema
Queste chiamate di sistema sono delle semplici funzioni. Abbiamo quindi dei parametri di ingresso, dei parametri di uscita, e abbiamo un valore di ritorno che tipicamente è negativo in caso di failure della chiamata di sistema e nullo nel caso corretto. Questi sopra rappresentati sono i parametri che vengono ad essere coinvolti con una chiamata di sistema. Abbiamo poi gli effetti che sono scaturiti dopo l'invocazione della chiamata di sistema.
Un esempio di chiamata di sistema è quello sopra. In Linux abbiamo una convenzione da seguire un po' come abbiamo visto per lo sviluppo delle librerie dinamiche e statiche, e possiamo osservare come il valore di ritorno deve essere di tipo long, il nome della chiamata di funzione deve essere preceduto dal prefisso sys_nomefunzione. In particolare in questa chiamata, che è la getpid, viene ad essere ritornato il tred_group_id (tgid), e questo non ci sorprende in quanto la getpid ritorna il pid del processo che sta eseguendo e quindi il tgid serve perché tutti i thread di un dato processo hanno tutti come pid del processo padre lo stesso pid.
In questo caso ci viene dato il tgid in quanto, come detto nelle scorse lezioni, abbiamo che in Linux quando facciamo il main di un programma e andiamo a creare un processo in realtà viene sempre ad essere associato un thread master a quel processo in quanto l'unità fondamentale schedulabile di Linux è il task che in realtà vede come se fosse un thread e quindi non ci meraviglia che ci sia anche questo campo. Current viene ad essere indicato semplicemente come variabile globale che viene mantenuta all'interno del kernel di Linux per prendere il riferimento al Process Control Block che sta eseguendo all'interno della CPU, questa è una facility che ci è offerta dal kernel di Linux per prendere subito il riferimento al processo che sta eseguendo correntemente.
Nella firma della funzione possiamo osservare poi la presenza di asmlinkage, che è un tag che viene ad essere utilizzato nello sviluppo. Quando vedremo come si sviluppano le chiamate di sistema, abbiamo che tutte le chiamate di sistema devono avere questo asmlinkage, che è una define che viene ad essere messa come tag per questo tipo di funzioni ed indica al compilatore che stiamo sviluppando una chiamata di sistema, e quindi che deve trattare gli argomenti che vengono passati a questa chiamata di funzione. Nel caso in esame non abbiamo argomenti ma è presente un void, ma tipicamente ci sono sempre degli argomenti che vengono ad essere salvati. Il primo argomento che di solito viene ad essere salvato all'atto della system call è il numero di system call, che è il cosiddetto system call number; abbiamo bisogno di questo numero in quanto una chiamata di sistema è un'interruzione sincrona e le interruzioni in un'architettura basata a computer vengono ad essere servite attraverso il vettore delle interruzioni ed il relativo handler che abbiamo chiamato Interrupt Service Routine.
Passaggio alla Kernel Space
In particolare abbiamo che ci si accorge che bisogna passare in Kernel Space attraverso una specifica istruzione Assembly che in particolare nelle architetture Intel si chiama Int e la particolare Interrupt Service Routine è indicata dal numero esadecimale 0x80. In questo caso non stiamo invocando un handler di un Interrupt ma stiamo invocando un handler di tutte le system call all'interno del kernel che stanno eseguendo, questo handler deve però capire a che system call deve riferirsi e quindi quando in questo esempio andiamo ad invocare getpid questo dovrà avere un certo numero che dovrà prima o poi permettere di invocare la funzione sopra rappresentata. È necessario quindi andare a passare il numero di system call che è il primo parametro che viene ad essere generalmente pushato sul primo registro general-purpose delle CPU Intel, che è quello EAX nelle architetture a 32 bit.
Quindi oltre a dover utilizzare questo valore del numero di system call, ovviamente dobbiamo dire al compilatore che si deve aspettare che tutti i parametri che vengono ad essere passati per la chiamata di sistema devono essere fatti sui registri della CPU e non sullo stack, e questo in particolare deve essere poi in qualche modo comunicato al compilatore. Questo ci istruisce anche che il primo parametro da consumare non è il primo argomento che è quello che viene ad essere passato all'interno della lista degli argomenti delle system call ma è proprio il numero della system call che deve essere consumato nel primo registro della serie di registri general-purpose delle architetture x86 o anche x64. Questo è quindi quello che ci comunica in qualche modo il tag tgid.
Invocazione delle chiamate di sistema
Quando invochiamo una chiamata di sistema, l'applicazione colloca un codice numerico che è proprio il system call number su un registro, che in particolare è quello EAX all'interno delle architetture x86; sarà sempre diverso a seconda della system call che stiamo invocando e quindi che sia read, write, open, getpid ecc. Automaticamente verrà ad essere invocata questa istruzione speciale INT 0x80 per invocare il system call handler, che è come se fosse un dispositivo fisico registrato sulla linea di interrupt 0x80, questa è la convenzione che viene ad essere utilizzata nelle architetture Intel.
Una volta invocato il system call handler, andrà a cercare il system call number che è stato salvato nel registro EAX insieme a tutti i parametri di ingresso della chiamata di sistema, questi altri parametri sono salvati sempre attraverso la convenzione che ci impone asmlinkage in degli altri registri.
Nel sviluppo delle chiamate di sistema, si è cercato di fare delle funzioni con una precisa firma e non con un numero elevatissimo dei parametri di ingresso. Quello che viene quindi fatto per convenzione all'interno del kernel di Linux è avere massimo 4 parametri collocabili sullo stack + il numero della system call che viene mosso sempre tramite la move su questo registro. Si è preferito non avere troppi parametri di ingresso per una system call perché generalmente quando si testa una funzione per capire se è stata progettata in modo corretto, si procede con il fornire degli input di prova e nel testing in teoria si procede con il provare tutte le possibili combinazioni dei parametri di ingresso, ed ovviamente al crescere dei parametri di ingresso cresce anche il numero di input da dare alla funzione. Quindi avere delle chiamate di sistema che possono impattare negativamente sull'esecuzione del Kernel e quindi sulle reliability del kernel è quindi chiaro che si cerca di avere anche nei sistemi più critici una disponibilità altissima del cuore del SO del kernel è importantissimo per non provocare danni non riparabili. Questa è la ragione per la quale gli sviluppatori hanno deciso di avere una firma compatta con un numero di parametri ridotti.
È possibile andare a fare una system call con più di 4 parametri, ma il 4° parametro sarà un riferimento ad una struttura che conterrà più parametri, ma questo andrà a rischio e pericolo dello sviluppatore; generalmente all'interno di una chiamata di sistema non abbiamo più di 4 o 5 parametri. Ad un certo punto avremo che il system call handler andrà a cercare nella system call table il riferimento alla chiamata di sistema dato il numero di chiamata di sistema e poi invocherà la system call. Qui già siamo in kernel space e siamo arrivati a questa invocazione del system call handler attraverso la chiamata INT 0x80. Questa istruzione viene ad essere automaticamente invocata quando il wrapper della chiamata di sistema che abbiamo sviluppato chiamerà una certa istruzione che in Linux viene ad essere fornita attraverso una libreria C che si chiama system_call e quindi syscall avrà tra i suoi parametri anche il numero della chiamata di sistema che vogliamo andare ad invocare.
I parametri di ingresso sono prelevati dai registri individuati nella slide sotto e sono poi collocati sullo stack. Questa è una convenzione che ci viene offerta da asmlinkage, quindi il compilatore sa esattamente dove sono posizionati il numero della chiamata di sistema e i parametri di ingresso. Quando la chiamata di sistema termina in modo corretto o non corretto, il valore di ritorno viene sempre rimesso per convenzione nel registro EAX, dove abbiamo messo il numero di quella data chiamata di sistema.
Questa convenzione prende il nome di ABI (Application Binary Interface) ed è fissata tra gli sviluppatori del kernel e gli sviluppatori delle applicazioni per garantire anche una backword compatibility. Detto questo, abbiamo che il concetto che deve rimanere è che dall'applicazione si passa al wrapper che andrà per i processori Intel attraverso INT 0x80, per altre tipologie di processori si utilizzerà un'altra convenzione, andrà ad invocare il system call handler che è il gestore di tutte le system call e che sa dove andare a mettere mano e che quindi come se fosse un vettore delle interruzioni conosce il riferimento della tabella delle system call, sa dove si trova esattamente in memoria questa tabella ed indicizzando questa tabella tramite il numero della system call va ad invocare la giusta system call.
Sviluppo e compilazione delle system call
Una system call si sviluppa come sopra. Siccome dobbiamo andare a cambiare la tabella delle system call, andando a mettere almeno il riferimento e quindi come si chiama la system call che stiamo andando a sviluppare e il relativo numero, ovviamente quando andiamo ad aggiungere una nuova system call al kernel sarà necessario andare a fare una nuova ricompilazione del kernel. Il sorgente della system call lo dobbiamo andare a piazzare all'interno del sorgente del kernel e quindi può essere messo al di sotto della cartella /kernel. Dobbiamo poi andare a creare una funzione all'interno di questo file sorgente con una certa firma con asmlinkage, valore di ritorno long e il prefisso sys_ e i vari parametri; si deve poi procedere con il modificare il Makefile, aggiungendo la riga specificata sopra al passo 3 e quindi aggiungendo la riga obj-y. Questo sta ad indicare che il file oggetto specificato deve essere linkato al binario totale del kernel di Linux e questo ci viene detto dalla y; questo ci ricorda molto la is che si vedeva all'interno del file di configurazione di Linux (.config) dove si vedeva una serie di righe chiave-valore ed avevamo l'opzione del kernel uguale a y oppure m, dove m stava ad indicare che era modulo del kernel, in questo caso con la y stiamo dicendo che questa funzionalità è built-in all'interno del kernel e può essere solo così in quanto una system call non può essere caricata a runtime come modulo del kernel. Una volta fatto questo, bisogna dire al kernel che stiamo aggiungendo una nuova system call modificando il file che rappresenta la tabella delle system call all'interno del kernel e poi si procede con il ricompilare il kernel.
La tabella delle system call è il file rappresentato nella figura sopra nel rettangolo in evidenza, il path di questo file cambia con il variare delle versioni del Kernel di Linux. Generalmente per trovare questo file si può procedere con il ricercarlo e vedere dove sta memorizzato. Questo file è tutta una serie di righe con un certo formato, il formato in generale è così strutturato: la prima colonna è il numero della chiamata di sistema, abbiamo poi la convenzione da specificare se abbiamo x32 o x64, viene specificato poi il nome della chiamata di sistema e poi l'entry point che è proprio il nome della funzione; possiamo osservare che viene ad essere applicata la convenzione poco sopra introdotta, possiamo infatti ad esempio osservare che abbiamo il nome come la read, abbiamo poi il riferimento che si va ad utilizzare nel kernel che è il nome effettivo e che per il caso in esame è sys_read.
La chiamata di sistema va collocata al primo numero di system call utile, in questa versione del kernel sul quale stiamo basando l'esempio, il primo numero utile per la system call era 548 ma questo dipende dalla versione del kernel. Aggiungendo questa riga possiamo andare a sviluppare la nostra system call. Per la nostra system call dobbiamo andare a sviluppare due parti che sono la parte Wrapper -> lato utente, quando abbiamo una nuova system call la libreria C non sa quale wrapper invocare e quindi per questo motivo dobbiamo andare a sviluppare questo wrapper e la parte Kernel Side.
Per poter accedere ed invocare una system call e quindi per poter invocare l'handler tramite l'istruzione INT 0x80, la libreria C ci fornisce la chiamata sotto indicata (syscall()) che è una chiamata a funzione per una generica system call. Quello che dovremmo andare a fare lato utente per il wrapper è scrivere la stessa firma della chiamata di sistema che stiamo implementando lato kernel, nel caso in esempio stiamo implementando il wrapper della chiamata di sistema open, all'interno di questo wrapper andiamo ad invocare questa funzione che è questa syscall generica che viene ad essere tradotta con un'istruzione INT 0x80. Questa chiamata syscall ha come parametri tutti i parametri che sono stabiliti per quella chiamata di sistema, in particolare nel caso in esame abbiamo un riferimento ad un path del file, i flag e la modalità di accesso a questo file e quindi i permessi, possiamo poi osservare che come primo argomento abbiamo il numero della system call che in questo caso appartiene al valore 2. Vengono quindi scatenate tutta una serie di istruzioni che porteranno a chiamare il system call handler, il system call handler otterrà questo numero che è 2, che sarà messo sul registro EAX, e quindi saprà esattamente dove andare a trovare il numero di system call ed in base alla tabella che abbiamo visto prima andrà ad invocare la funzione sys_read che è l'entry point della system call che si sta andando a chiamare.
L'immagine sopra mostra un esempio di system call che vedremo a breve; sopra viene mostrato il lato kernel in questo caso abbiamo che la system call è molto semplice, ci accorgiamo che stiamo dal lato kernel in quanto se osserviamo il codice possiamo osservare che abbiamo la presenza di asmlinkage, la printk e abbiamo il prefisso sys_. Questo è quindi proprio l'entry point che stiamo andando a sviluppare. Questa chiamata di sistema non fa niente altro che prendere in ingresso il PID del processo, avere un buffer da utilizzare per fare delle stampe e poi abbiamo la sua dimensione. Dato il PID, abbiamo che la system call va a trovare il Process Control Block tramite la funzione find_task_by_vpid, che è una funzione di utilità all'interno del kernel che restituisce il riferimento al PCB dato un pid e ci permette di andare a prendere il nome del processo. Abbiamo un campo all'interno della struttura task struct che abbiamo visto la scorsa volta permetteva di astrarre il PCB di un processo all'interno del kernel di Linux; preso questo nome del processo è possibile tornare ad user space andando a copiare questo nome nel buffer, questo buffer verrà poi usato lato user space. La funzione copy_to_user viene ad essere utilizzata per evitare tutta una serie di problemi legati all'uso delle stringhe che come abbiamo visto sono molto comuni.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
Scarica il documento per vederlo tutto.
-
Sistemi Operativi - Appunti Teoria Seconda Parte
-
Appunti Sistemi operativi - parte 3
-
Appunti Sistemi operativi - parte 5
-
Appunti Sistemi operativi - parte 6