AOS - 20 - LINUX MODULES I
Lecture Info
Data:
Sito corso: link
Slides: AOS - 5 LINUX MODULES
Progresso unità: 1/2
Argomenti:
Linux Modules Requirements
Modules API for kernel <= 2.4
Modules API for kernel >= 2.6
Module Parameters
Loading/Unloading of Kernel Modules
Building a Kernel Object
Management of usage_count
Kernel Exported Symbols
Dynamic Symbol Querying with kprobing
Introduzione: In questa lezione abbiamo iniziato a vedere in modo più dettagliato la tecnologia dei moduli offerta dal kernel.
1 Linux Module Basics
Un modulo nel contesto del kernel linux è un componente software che può essere aggiunto al kernel in modo dinamico, quando quest'ultimo sta già girando. Il vantaggio nell'utilizzo dei moduli è che eventuali cambiamenti a del codice aggiunto tramite dei moduli non necessitano la compilazione dell'intero kernel, ma solamente dei rispettivi moduli. Questo fatto rende la tecnologia dei moduli molto flessibile. Viene ad esempio utilizzata per testare e debuggare il software che viene incluso nella compilazione.
In genere i moduli vengono accoppiati all'esistenza dei device drivers. Un device driver è un oggetto all'interno del kernel che implementa delle funzioni basiche per l'I/O.
1.1 Requirements
Per poter montare un modulo all'interno del kernel necessitiamo delle seguenti cose:
Della memoria RAM in cui caricare il modulo.
L'indirizzo logico della memoria in cui viene inserito il modulo. Questo ci serve per risolvere i riferimenti interni al modulo stesso.
La posizione nella memoria logica delle kernel facilities utilizzate dal modulo, come ad esempio la funzione printk().
Durante il carimento del modulo quindi tutti i riferimenti, sia quelli interni al modulo e sia quelli esterni che fanno riferimento ad oggetti già presenti nel kernel devono essere risolti.
1.2 Who is Responsible?
Lo specifico software che si occupa di caricare i moduli varia a seconda della versione del kernel. In particolare c'è stata una rottuta nel kernel linux a partire dalla versione 2.6.
Prima della 2.6, la maggior parte del lavoro veniva fatto a livello di applicazione:
Moduli rappresentati da objectfiles (formato .o ELF) , compilati con
gcc
.Comandi della shell utilizzati per preservare memoria, risolvere gli indirizzi dei simboli, e caricare il modulo nella RAM.
Dopo il kernel 2.6, la maggior parte del lavoro viene fatta internamente al kernel:
Moduli rappresentati da kernel-objectfiles ELF (.ko). Il kernel object contiene più metadati e viene utilizzato dal kernel per fare ciò che prima veniva fatto da applicazioni user level.
Comandi della shell utilizzati per chiamare system call offerte dal kernel per fare memory allocation, address resolving, e module loading.
2 Kernel Modules APIs
A seconda della versione del kernel (up to 2.4 | from 2.6) abbiamo diverse API.
2.1 Module Struct
La struttura del modulo è definita come segue
struct module { unsigned long size_of_struct; struct module *next; const char *name; unsigned long size; long usecount; // .... int (*init) (void); void (*cleanup) (void); // .... };
2.2 API for kernel <= 2.4
Tutte le funzioni menzionate si trovano in <linux/module.h>
.
create_module(): Mette da parte il buffer logico del kernel per caricare il modulo e associa il nome del modulo al buffer riservato. Questa system call può essere utilizzata solo da
root
(superuser). In caso di fallimento ritorna -1, altrimenti ritorna l'indirizzo in cui il modulo verrà caricato.caddr_t create_module(const char *name, size_t size);
init_module(): Carica l'immagine del modulo nel kernel buffer, chiama la setup function del modulo e aggiorna i metadati associati al modulo.
int init_module(const char *name, struct module *image);
delete_module(): Chiama la shutdown function del modulo e rilascia il buffer logico associato al modulo. Se fallisce ritorna -1 e setta
errno
, altrimenti ritorna 0.int delete_module(const char *name);
2.3 API for kernel >= 2.6
A partire dal kernel 2.6
si è tolta la create_module()
, e il lavoro
svolto dalla funzione viene invece fatto tutto da
init_module()
. Otteniamo quindi la seguente API
init_module(): Mette da parte il buffer logico del kernel per caricare il modulo; associa il nome del modulo al buffer riservato; carica l'immagine del modulo nel kernel buffer; chiama la setup function del modulo; aggiorna i metadati associati al modulo.
Il parametro
module_image
deve puntare ad un buffer contenente l'immagine binaria del modulo da caricare, che dovrebbe essere un kernel object (.ko
).int init_module(void *module_image, unsigned long len, const char *param_values);
delete_module(): Chiama la shutdown function del modulo e rilascia il buffer logico associato al modulo.
int finit_module(int fd, const char *param_values, int flags);
2.4 Common Parts
Le due APIs hanno i seguenti aspetti in comune:
In entrambe le API un modulo possono contenere due funzioni particolare: la funzione di startup e la funzione di shutdown. Queste funzioni indicano rispettivamente le azioni da eseguire durante lo start-up e durante l'unloading del modulo. Mentre la funzione di shutdown è opzionale, quella di start-up è necessaria per poter caricare il modulo.
// Funzione di start-up. Viene seguita durante il caricamento del // modulo nell'immagine del kernel, e deve sempre esistere. int init_module(void); // Funzione di shutdown. void cleanup_module(void);
Tra i meta-dati che vengono gestiti per ogni modulo troviamo anche lo
usage-count
, oreference-count
, il cui compito è quello di tener conto del numero di threads che hanno bisogno del modulo o che hanno ancora qualche riferimento da utilizzare presente nel modulo in questione. Se lo usage-count associato ad modulo non è 0, allora il modulo è lockato e non può essere tolto dalla memoria. Per poter smontare in modo forzato un modulo con un usage_count > 0 dobbiamo configurare il kernel in modo appropriato.In entrambe le tecnologie siamo in grado di passare dei parametri ai moduli. Questi parametri non vengono passati come parametri funzionali, ma vengono passati come valori iniziali a variabili globali presenti nel codice sorgente del modulo. Queste variabili globali, una volta definite, devono essere esplicitamente marchate come "parametri di modulo".
2.5 Module Parameters
Nel file include/linux/module.h
o include/linux/moduleparm.h
sono
presenti le macro per definire che una data variabile globale è un
parametro del modulo. Tale macro dipende dalla versione del kernel
utilizzata.
Per i kernel <= 2.4 abbiamo
MODULE_PARM(variable, type)
Per i kernel >= 2.6 invece
module_param(variable, type, perm)
In entrambi i casi il type
del parametro può essere uno dei
seguenti: int, long, string, byte. A seguire un esempio
dimostrativo
int MAX_MESSAGE_SIZE = 4096; module_param(MAX_MESSAGE_SIZE, int, 0660);
2.5.1 Pseudo-files Interface
L'utilizzo degli pseduo-files (discussi nella sezione virtual-file-system del corso) ci permette di esporre ed eventualmente modificare il contenuto di una variable definita come parametro del modulo. Queste modifiche possono anche avvenire dopo che il modulo è stato caricato, permettendo di ottenere una comunicazione più flessibile.
Gli pseudo-file funzionano nel seguente modo: quando proviamo ad accedere in lettura o scrittura ad uno pseudo-file si attiva uno specifico driver del kernel in grado sia di generare dinamicamente delle informazioni che poi noi possiamo leggere che di ricevere le informazioni che noi gli stiamo scrivendo per modificare lo stato interno del kernel.
Gli pseudo-files relativi ai parameteri dei moduli sono presenti nella seguente path
/sys/modules/<module_name>/parameters
2.5.2 Array as Module Parameter
Se vogliamo utilizzare un array di valori con un singolo parameter_name possiamo utilizzare la funzione module_param_array(), che riceve in input quattro argomenti:
il nome del parametro;
Il tipo base degli elementi dell'array;
L'indirizzo della variabile che specifica il size dell'array;
Le permissioni per l'accesso del parametro tramite lo pseduo file system.
Come mostra il seguente esempio
module_param_array(myintarray, int, &size, 0)
2.6 Loading/Unloading Modules
Per quanto riguarda il caricamento/scaricamento di un modulo, il kernel ci offre i seguenti comandi:
insmod
: Ci permette di caricare il modulo specificato all'interno del kernel. Se poi vogliamo inizializzare il valore di alcuni parametri del modulo durante il caricamento, possiamo passare i parametri utilizzando la sintassi 'variable=value'. Nelle versioni del kernel <= 2.4, il comando
insmod
andava anche a rilocare i simboli contenuti nel modulo con i giusti indirizzi logici. Per fare questo utilizzava la symtab esposta dal kernel nello pseudo-file/proc/kallsyms
. Per motivi di sicurezza nelle successive versioni del kernel si è scelto di non esporre più il file/proc/kallsysms
, e dunque questa funzionalità è stata relegata ad una componente interna del kernel.rmmod
: Ci permette di rimuovere un modulo precedentemente caricato. modprobe
: Ci permette di montare un modulo andandolo a cercare in determinare directory. Di default cerca nella seguente directory /lib/modules/$(uname -r)
dove con
$(...)
intendiamo la sintassi per effettuare la command substitution offerta da bash.
2.7 Building A Kernel Object (.ko)
Per ottenere il file .ko si utilizza il comando modpost
. Il
funzionamento è il seguente:
Si scrive il codice sorgente del modulo (
.c
), e lo si compila per ottenere il file object (.o
).Si utilizza il comando
modpost
che genera un ulteriore file sorgente C (.mod
) che contiene dei meta-dati sul file originario.Il file
.mod
viene poi compilato e linkato con il file.o
per formare il kernel object (.ko
).
Per portare un esempio pratico, consideriamo il seguente modulo
( main.c
)
#define MODNAME "AOS-20" #include <linux/kernel.h> #include <linux/module.h> int init_module(void) { printk("%s - Module Loaded!\n", MODNAME); return 0; } void cleanup_module(void) { printk("%s - Module Unloaded!\n", MODNAME); return; }
Per compilare il kernel object di questo modulo e montarlo nel
kernel possiamo utilizzare il seguente makefile ( Makefile
)
obj-m = main.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
con i seguenti comandi
make sudo insmod main.ko # inserts module sudo rmmod main.ko # removes module sudo dmesg | grep 'AOS' # checks if module was loaded
2.8 Module Headings
Per specificare che vogliamo programmare lato kernel, o che stiamo in un modulo, possiamo utilizzare le seguenti define
#define __KERNEL__ #define MODULE
Notiamo che questo ci permette di utilizzare delle particolari facility nelle librerie standard che vengono appositamente offerte per la programmazione kernel.
2.9 Management of usage_count
Abbiamo visto che il kernel associa ad ogni modulo un contantore,
chiamato usage_count
, che conta il numero di thread che ancora
necessitano di quel particolare modulo. Per incrementare o
decrementare lo usage_count
del modulo corrente, esistono le
seguenti macro, definite nel file include/linux/module.h
MOD_INC_USE_COUNT; MOD_DEC_USE_COUNT; MOD_IN_USE;
Per motivi di debug potrebbe essere conveniente ridefinire le macro
MOD_INC_USE_COUNT e MOD_DEC_USE_COUNT come no-ops in modo da
evitare scenari in cui il modulo fallisce la sua esecuzione e non
può più essere rimosso. Lo pseudo-file /proc/modules
offre
informazioni sui moduli correntemente montati, tra cui lo
usage_count
e la memoria riservata per il modulo.
Molto spesso però il semplice utilizzo di queste macro non basta in quanto sono queste macro che lavorano sullo usage_count del modulo che le sta utilizzando. Sono però possibili delle situazioni in cui il modulo viene smontato subito dopo che il thread in esecuzione ha utilizzato la macro MOD_DEC_USE_COUNT ma poco prima che il thread finisce la sua esecuzione e ritorna il controllo ad altri flussi. La possibilità di lockare/unlockare moduli solo quando entriamo o usciamo dagli stessi non ci permette quindi di risolvere eventuali problemi di concorrenza.
Dalla versione 2.6 sono state offerte le seguenti APIs più
articolate per gestire lo usage_count
. In particolare troviamo,
find_module(): Finds module to lock/unlock. This allows us to target external modules. If we want to refer to ourselves, we can use the macro
THIS_MODULE
.struct module* find_module(const char *name);
try_module_get(): increment usage_count of a general module, not necessarily the module that is being executed.
try_module_get(struct module* module);
module_put(): decrement usage_count of a general module, not necessarily the module that is being executed.
module_put(struct module* module);
3 Kernel Exported Symbols
L'esportazione dei simboli deve essere garantita per poter caricare i moduli in modo dinamico. Un simbolo esportato, che può essere una variabile o il nome di una funzione, viene reso disponibile a tutti i moduli che vengono caricati. Se un modulo prova ad utilizzare un simbolo del kernel che però non è stato esportato il loading del modulo fallisce.
Per poter esportare un simbolo viene utilizzata la macro
EXPORT_SYMBOL(symbol) definita in include/linux/module.h
.
All'interno del kernel esiste poi una tabella in cui sono inseriti tutti i simboli che sono stati esportati durante la compilazione del kernel. Dato poi che ogni modulo caricato dinamicamente può esportare simboli a sua volta, ad ogni modulo è associata una tabella di simboli esportati.
In genrale tutti i simboli che sono esportati dal kernel (e dai
moduli) sono accessibili tramite lo pseudo-file
/proc/kallsyms
. Questo file contiene una linea per ogni simbolo
esportato. Il formato utilizzato è il seguente:
Kernel-memory-address | symbol-type | symbol-name
La lista dei simboli contenuti nel file, tipicamente, è un sovrainsieme della lista dei simboli che sono effettivamente utilizzabili per montare i nuovi moduli. Nelle ultime versioni del kernel infatti la lista vera è mantenuta all'interno del kernel ed è visibile nel file
/lib/modules/<kernel version>/build/Module.symvers
Il file /proc/kallsyms
viene comunque utilizzato per ispezionare il
tipo del simbol all'interno del kernel.
Infine, il kernel può essere parametrizzato durante la compilazione
per potere esportare varie tipologie di simboli. In particolare se
settiamo CONFIG_KALLSYMS = y
, allora i nomi delle funzioni vengono
esportati, mentre se CONFIG_KALLSYMS_ALL = y
, e anche quella di
prima è attiva, allora anche i nomi delle variabili vengono
esportate.
4 Dynamic Symbol Querying
Il kernel probing offre un meccasnimo di "self-patching" per il kernel che ci permette di modificare in modo dinamico il funzionamento di codice del kernel precedentemente caricato. Questa tecnica prende il nome di instrumentalizzazione del codice e consiste nel creare un preambolo o una coda ad un blocco di codice.
L'idea quindi è che un modulo contenente un preambolo o una coda può essere caricato all'interno del kernel in modo tale da modificare il comportamento del kernel ogni volta che una specifica funzione viene chiamata lato kernel. Un possibile utilizzo di questa meccanismo è per il debugging.
Le funzioni self_patchabili possono essere modificate solo perché sono scritte in un modo molto particolare.
L'API esposta è la seguente
__kprobes_register_kprobe(): Registra una kernel probe in cui posso avere sia un preambolo sia. Le informazioni importanti sono la funzione target, la funzione preambolo e la funzione coda
int __kprobes_register_kprobe(struct kprobe *p);
__kprobes__unregister_kprobe_top(): Rimuovo l'utilizzo di una kprobe.
static int __kprobes__unregister_kprobe_top(struct kprobe *p);
__kprobes_register_kretprobe(): Registro solamente una coda sul return della funzione.
int __kprobes_register_kretprobe(struct kprobe *p);
Andiamo a vedere un esempio di utilizzo di questo meccanismo. In questo caso lo utilizziamo solamente per ottenere l'indirizzo logico di una funzione del kernel.
// get kernel probe to acces flush_tb_all() memset(&kb, 0, sizeof(kb)); kp.symbol_name = "flush_tlb_all"; if (!register_kprobe(&kp)) { flush_tlb_all_lookup = (void *) kp.addr; unregister_kprobe(&kp); }
Per permettere l'utilizzo di questa API il kernel deve essere compilato con la seguente configurazione
CONFIG_KPROBES=y CONFIG_KALLSYMS=y CONFIG_KALLSYMS_ALL=y
5 PARAMETRIC-MESSAGE-EXCHANGE-SERVICE
In questo running example abbiamo un servizio di message logging analogo a quello visto nella precedente lezione. Questa volta però il size del buffer è un parametro esposto tramite il sistema degli pseudo-file.
#define DEFAULT_MEX 128 // variable that defines the amount of space I can actually use to // post messages. // // it is a module parameter that can be configured at run time via the // sys file system. static int buff_size = DEFAULT_MEX; module_param(buff_size, int, 0660); char kernel_buff[4096]; size_t valid = 0; asmlinkage sys_log_message(char *mex, size_t size) { // post a message in the buffer kernel_buff } asmlinkage sys_get_message(char *mex, size_t size) { // retrieve the last message posted in the buffer kernel_buff } // Code to add system calls...
Se vogliamo modificare il valore della variabile buff_size possiamo modificare il valore del seguente pseudo-file da root nel seguente modo
sudo echo 4096 > /sys/module/parametric-message-exchange-service/parameters/buff_size