AOS - 20 - LINUX MODULES I


Lecture Info

  • Data: [2019-11-08 ven]

  • 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 , o reference-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:

  1. il nome del parametro;

  2. Il tipo base degli elementi dell'array;

  3. L'indirizzo della variabile che specifica il size dell'array;

  4. 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:

  1. Si scrive il codice sorgente del modulo ( .c ), e lo si compila per ottenere il file object ( .o ).

  2. Si utilizza il comando modpost che genera un ulteriore file sorgente C ( .mod ) che contiene dei meta-dati sul file originario.

  3. 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