Table of Contents

2. Module de kernel

Cel mai facil și convenabil mod de a face dezvoltare la nivelul nucleului este prin module de kernel. Modulele de kernel reprezintă fișiere obiect care pot aduce în mod dinamic (la rulare) funcționalități nucleului.

Subiecte abordate

Resurse utile

Exerciții

Arhiva de suport pentru exerciții se găsește aici. Descărcați arhiva și apoi decomprimați-o folosind comanda

unzip cap-02-tasks.zip

În prealabil, trebuie să refaceți scripturile din codul sursă al nucleului. Pentru aceasta accesați directorul linux-kernel-dev/linux-3.13 și rulați comanda

make scripts

Afișarea de informații despre module

Orice sistem de operare (nu doar Linux) folosește module pentru extinderea în mod dinamic a funcționalităților sistemului de operare. De multe ori acestea sunt forma în care sunt implementatea driverele de dispozitive (device drivers). Înainte de opera modulele este util să putem afla informații despre module.

Sunt două operații frecvente de aflare de informații și de investigare a modulelor de kernel:

  1. Afișarea modulelor curente
  2. Afișarea de informații despre module

Pentru a afișa modulele curente în nucleul Linux folosim comanda

lsmod

Această comandă o putem folosi și pe mașina virtuală VirtualBox și pe mașina virtuală QEMU. O altă formă de afișare a acelorași informații este comanda

cat /proc/modules

Pentru a afișa informații despre un modul în parte se folosește comanda

sudo modinfo <nume-modul>

unde <nume-modul> este numele modulului. Se pot afișa informații și despre modulele încărcate și cele care nu au fost încărcate.

Modulele se găsesc de regulă în directorul /lib/modules/<kernel-version>. Comanda modinfo poate fi folosită doar dacă modulele se găsesc în acel director, motiv pentru care comanda va funcționa doar pe mașina virtuală VirtualBox.

Alte informații, dinamice, despre modulele existente în acel moment în kernel (fie compilate direct, built in, fie module încărcabile/descărcabile) se găsesc în /sys/module/.

Aflați informații despre un modul de kernel din mașina virtuală VirtualBox și despre un modul de kernel din mașina virtuală QEMU folosind intrările din /sys/module/.

Încărcarea și descărcarea unor module de sistem

Multe dintre modulele folosite de nucleu sunt module “de sistem”, adică sunt deja compilate și instalate. De obicei acestea rezită în directorul /lib/modules/<kernel-version>/.

Aceste module pot fi descărcate încărcate la nevoie. Întrucât aceste module se găsesc doar pe mașina virtuală VirtualBox, doar acolo putem să operăm cu acestea.

Pentru a descărca un modul de sistem de pe mașina virtuală VirtualBox, să spunem modulul lp, folosim una dintre comenzile:

sudo modprobe -r lp
sudo rmmod lp

Dacă se vor afișa din nou modulele din kernel, se va vedea că modulul lp lipsește.

Pentru a reinsera modulul de kernel lp avem, din nou, două opțiuni

sudo modprobe lp
sudo insmod /lib/modules/3.16.0-4-586/kernel/drivers/char/lp.ko

Comanda insmod primește calea completă către modulul de kernel, un fișier cu extensia .ko. Calea completă o putem afla cu ajutorul comenzii modinfo.

Comanda modprobe poate fi folosită doar pe module de sistem, cele localizate în directorul /lib/modules/<kernel-version/. Comenzile insmod și rmmod pot fi folosite pentru orice fel de module, inclusiv module “custom”, motiv pentru care le vom folosi preponderent de acum încolo.

Copiați modulul lp.ko în mașina virtuală QEMU, în fsimg/root/modules și apoi încercați să-l încărcați folosind comanda

insmod lp.ko

De ce nu puteți să încărcați modulul în kernel?

Încărcarea și descărcarea unui modul custom

În directorul cap-02-bin/ din arhiva de laborator găsiți modulul de kernel hello.ko. Vrem să încărcăm și să descărcăm acest modul în mașina virtuală QEMU.

Copiați modulul pe mașina virtuală QEMU și porniți mașina virtuală.

Folosiți insmod și rmmod pentru a încărca și descărca modulul din kernel. Observați mesajele afișate în momentul încărcării și descărcării modulului din kernel.

Compilarea unui modul de kernel

După cum am precizat în capitolul trecut, mașina virtuală QEMU este minalistă și folosită doar pentru a testa (rapid) funcționalitate. Compilarea modulelor de kernel pe care le vom folosi în cadrul mașinii virtuale o vom face pe mașina virtuală VirtualBox.

Pentru a compila un modul de kernel acesta trebuie linkat la versiunea de nucleu corespunzătoare nucleului în care va fi inserat. De aceea, întotdeauna vom referi în procesul de compilare directorul cu sursele nucleului, în cazul de față /home/training/linux-kernel-dev/linux-3.13/.

În subdirectorul hello/ din directorul cap-02-skel/ avem codul sursă (hello.c) și fișierele de compilare Makefile și Kbuild pentru compilarea modului. În mașina virtuală VirtualBox folosim, în directorul hello/, comanda

make

pentru a compila modulul de kernel. În urma compilării rezultă fișierul hello.ko pe care îl vom copia în mașina virtuală QEMU și apoi pornim mașina virtuală și încărcăm și descărcăm modulul de kernel.

Aceștia sunt pașii uzuali pentru dezvoltarea și testarea modulului de kernel. Editarea/implementarea și compilarea au loc pe mașina virtuală VirtualBox, în timp ce testarea sa (încărcare și descărcare) au loc pe mașina virtuală QEMU.

Afișare mesaj și la descărcare

Actualizați modulul hello.c pentru a afișa un mesaj și la descărcarea din kernel, la operația rmmod.

Folosiți funcția printk astfel încât mesajele să fie afișate atât în bufferul kernel-ului (adică să fie afișate cu dmesg) cât și la consolă.

Folosiți nivelul corespunzător de logging la printk, inferior celui afișat în fișierul /proc/sys/kernel/printk. Nivelurile de logging se găsesc în codul sursă al nucleului, în fișierul include/linux/kern_levels.h.

Afișare PID

Actualizați modulul hello.c pentru a afișa PID-ul procesului curent atât la încărcarea modulului de kernel cât și la descărcarea acestuia.

Pointer-ul la procesul curent este dat de macro-ul current, găsit în include/linux/sched.h. Macro-ul este un pointer al structurii struct task_struct definită, de asemenea, în include/linux/sched.h.

Pentru a include conținutul fișierului header include/linux/sched.h folosiți construcția

#include <linux/sched.h>

La fel veți proceda și în cazul altor fișiere de tip header în care sunt definite structuri, tipuri de date sau macro-uri pe care le folosiți în modulul vostru.

Afișare comandă

Actualizați modulul hello.c pentru a afișa PID-ul și executabilul/comanda aferentă procesului curent, atât la încârcarea modulului în kernel cât și la descărcarea acestuia.

Comanda/executabilul aferent unui proces este dat de câmpul comm al structurii struct task_struct aferente.

Afișare informații despre procesul părinte

Actualizați modulul hello.c pentru a afișa PID-ul și executabilul/comanda aferentă procesului părinte al procesului curent, atât la încârcarea modulului în kernel cât și la descărcarea acestuia.

Urmăriți în cadrul structurii struct task_struct care este câmpul cu ajutorul căruia se determină procesul părinte (tot un pointer la o structură de tipul struct task_struct.

Exemplu de hook: firewall minimal

În directorul hook/ din arhiva de suport capitolului se găsește un exemplu de folosire a framework-ului netfilter din cadrul nucleului Linux. Este framework-ul folosit și de utilitarul iptables.

Parcurgeți codul sursă, observați ce se întâmplă și apoi obțineți modulul și testați-l în mașina virtuală QEMU.

Pentru testare folosiți ping din mașina virtuală VirtualBox către mașina virtuală QEMU

ping 172.20.0.2

Adresa 172.20.0.2 este adresa interfeței eth0 a mașinii virtuale QEMU.

Urmăriți numerele de secvență ale mesajelor ICMP în output-ul comenzii ping, adică partea cu icmp_seq.

Și apoi pe mașina virtuală QEMU încărcați modulul hook.ko. Observați acum care sunt numerele de secvență ale mesajelor ICMP. Observați că acum un pachet din două nu este afișat, pentru că este filtrat de modul.

Descărcați modulul hook.ko și observați că acum numerele de secvență revin la numere consecutive, nemaifiind filtrate de modul.

Modul din surse multiple

Uneori dorim să separăm funcționalitatea unui modul în fișiere multiple pentru a nu încărca tot codul sursă într-un singur fișier. În acea situația avem nevoie de o actualizare a modului în care sunt constituite fișierele Kbuild și Makefile.

În directorul multi/ din directorul cap-02-skel/ din arhiva capitolului există un exemplu (academic) de modul de kernel din surse multiple. Urmăriți conținutul acestora și compilați fișierele folosind comanda

make

Urmăriți fișierele intermediare și fișierul modul final.

Copiați fișierul modul final în mașina virtuală QEMU, porniți mașina virtuală QEMU și încărcați și descărcați modululul din kernel.

Modul nou din surse multiple

Creați un modul care să aibă două fișiere cod sursă. Într-un fișier cod sursă sunt implementate funcțiile de inițializare și ieșire ale modulului. În celălalt fișier implementați o funcție care face dump în hexacimal la cel mult 4096 de octeți de la o adresă dată. Adresa dată trebuie să fie adresă de kernel space (>= 0xc0000000). Funcția este apelată din primul modul în cadrul funcției de inițializare a acestuia.

Compilați modulul, copiați-l în mașina virtuală QEMU și testați-l.

Actualizare modul de hook

Actualizați modulul de hook prin adăugarea unei noi structuri de operații și a unei funcții care să respingă pachetele de tipul echo reply care pleacă de la stația locală, o dată la 3 pachete.

Pentru pachetele care pleacă folosiți ca hooknum valoarea NF_INET_LOCAL_OUT. Este vorba de câmpul hooknum din cadrul structura icmp_nf_ops.

Pachetele de tipul echo reply au câmpul icmp_type al structurii icmp_hdr egal cu valoarea ICMP_ECHOREPLY.

Puteți găsi definițiile de tipuri de câmpuri cu ajutorul cscope cu o comandă de genul

vi -t ICMP_ECHO

Informații cscope

Cscope este un program pentru parcurgerea eficientă a surselor C. Pentru a-l folosi, trebuie generată o bază de date cscope din sursele existente. Într-un tree Linux, este suficientă folosirea make ARCH=x86 cscope. Precizarea arhitecturii prin variabila ARCH este opțională, dar recomandată; altfel, unele funcții dependente de arhitectură vor apărea de mai multe ori în baza de date.

Cscope poate fi folosit și stand-alone, dar este mult mai util în combinație cu un editor. Pentru a folosi cscope cu Vim, este necesar să instalați ambele pachete și să adăugați următoarele linii în fișierul .vimrc (mașina din laborator are deja configurările făcute):

if has("cscope")
        " Look for a 'cscope.out' file starting from the current directory,
        " going up to the root directory.
        let s:dirs = split(getcwd(), "/")
        while s:dirs != []
                let s:path = "/" . join(s:dirs, "/")
                if (filereadable(s:path . "/cscope.out"))
                        execute "cs add " . s:path . "/cscope.out " . s:path . " -v"
                        break
                endif
                let s:dirs = s:dirs[:-2]
        endwhile
 
        set csto=0	" Use cscope first, then ctags
        set cst		" Only search cscope
        set csverb	" Make cs verbose
 
        nmap <C-\>s :cs find s <C-R>=expand("<cword>")<CR><CR>
        nmap <C-\>g :cs find g <C-R>=expand("<cword>")<CR><CR>
        nmap <C-\>c :cs find c <C-R>=expand("<cword>")<CR><CR>
        nmap <C-\>t :cs find t <C-R>=expand("<cword>")<CR><CR>
        nmap <C-\>e :cs find e <C-R>=expand("<cword>")<CR><CR>
        nmap <C-\>f :cs find f <C-R>=expand("<cfile>")<CR><CR>
        nmap <C-\>i :cs find i ^<C-R>=expand("<cfile>")<CR>$<CR>
        nmap <C-\>d :cs find d <C-R>=expand("<cword>")<CR><CR>
 
        " Open a quickfix window for the following queries.
        set cscopequickfix=s-,c-,d-,i-,t-,e-,g-
endif

Script-ul caută un fișier numit cscope.out în directorul curent, sau în directoarele părinte ale acestuia. Dacă Vim găsește acest fișier, puteți folosi combinația Ctrl+] sau Ctrl+\ g (combinația control-\, urmată de tasta g) pentru a sări direct la definiția cuvântului de sub cursor (funcție, variabilă, structură etc.). Similar, puteți folosi Ctrl+\ s pentru a merge la locurile unde este folosit cuvântul de sub cursor.

Puteți lua un fișier .vimrc cscope-enabled (and other goodies) de la https://github.com/ddvlad/cfg/blob/master/_vimrc. Următoarele indicații se bazează pe acest fișier, dar au listate și comenzile de bază vim care obțin același efect.

Dacă există mai multe rezultate (de obicei există) vă puteți deplasa între ele folosind F6 și F5 (:cnext și :cprev) sau deschizând o subfereastră nouă cu rezultatele, folosind :copen. Ca să închideți subfereastra folosiți comanda :cclose.

Pentru a vă întoarce la locația precedentă, folosiți Ctrl+o (litera o, nu cifra zero). Comanda poate fi invocată de mai multe ori și funcționează chiar dacă cscope a schimbat fișierul pe care îl editați.

Pentru a merge la definiția unui simbol direct când porniți vim, folosiți vim -t task_struct. Sau, dacă ați deschis Vim și vreți ulterior să căutați un simbol după nume, puteți folosi comanda :cs find g <symbol_name> (unde <symbol_name> este numele simbolului.

Dacă ați găsit mai multe match-uri și dacă ați deschis o subfereastră cu toate match-urile (folosind :copen) și dacă sunteți în căutarea unui simbol de tip structură, este indicat să căutați în subfereastră (folosind /slash) caracterul { (acoladă deschisă).

Un sumar al comenzilor cscope îl puteți obține folosind :cs help.

Pentru mai multe informații, folosiți help-ul integrat al Vim: :h cscope sau :h copen.

Dacă sunteți utilizatori emacs, wiki-ul emacs conține informații relevante pentru configurarea cscope.

Pentru o interfață mai simplă, Kscope este un frontend pentru cscope care foloseşte QT. Este lightweight, foarte rapid și foarte ușor de folosit. Permite căutare folosind expresii regulate, grafuri de apel etc. Kscope nu mai este, în momentul de fața, menținut. Există şi un port al versiunii 1.6 pentru Qt4 şi KDE 4 care păstrează integrarea editorului Kate şi este mai uşor de folosit decât ultima versiune prezentă pe SourceForge.

Dacă nu există deja un fișier cscope.out generat sau dacă s-a stricat, îl puteți genera folosind

make ARCH=x86 cscope

cscope spelunking

Folosiți direct Vim și comenzile cscope pentru parcurgerea codului sursă cu indicațiile de mai jos.

Determinați fișierul în care sunt definite următoarele tipuri de date:

Pentru o structură se caută doar numele ei. Spre exemplu, în cazul struct task_struct se caută șirul task_struct.

De obicei veți obține mai multe match-uri caz în care:

  1. Listați toate match-urile folosind, în Vim, comanda :copen. Vă apare o fereastră secundară cu toate match-urile.
  2. Căutați match-ul potrivit (în care este definită structura) căutând după acoladă deschisă ({), un caracter sigur pe linia de definire a structurii. Pentru căutarea acoladei deschise folosiți, în Vim, construcția /{.
  3. Pe linia aferentă apăsați Enter ca să vă ajungă editorul în codul sursă unde e definită variabila.
  4. Închideți fereastra secundară folosind coamanda :cclose.

Determinați fișierul în care sunt declarate următoarele variabile globale la nivelul nucleului:

Pentru aceasta folosiți în Vim o comandă de forma :cs f g <symbol> (unde construcția <symbol> reprezintă numele simbolului căutat).

Determinați fișierul în care sunt declarate următoarele funcții:

Pentru aceasta folosiți în Vim o comandă de forma :cs f g <symbol> (unde construcția <symbol> reprezintă numele simbolului căutat).

Parcurgeți secvența de structuri:

  1. struct task_struct
  2. struct mm_struct
  3. struct vm_area_struct
  4. struct vm_operations_struct

Adică parcurgeți din aproape în aproape structurile: accesați o structură și apoi găsiți câmpuri cu tipul de date al următoarei structuri, accesați-o pe aceasta etc. Rețineți în ce fișiere sunt definite; o să vă fie utile la alte laboratoare.

Pentru a căuta un simbol în Vim (cu suport cscope) atunci când sunteți plasați cu cursorul pe acesta, folosiți construcția Ctrl+].

Pentru a reveni în match-ul anterior (înante de căutare/salt) folosiți construcția Ctrl+o. Pentru a avansa în căutare (pentru a reveni la match-urile de dinainte de Ctrl+o) folosiți construcția Ctrl+i.

La fel ca mai sus, parcurgeți secvența de apeluri de funcții:

  1. bio_alloc
  2. bio_alloc_bioset
  3. bvec_alloc
  4. kmem_cache_alloc
  5. slab_alloc

Aveți în vedere indicațiile din secțiunea Informații cscope de mai sus.

[BONUS] Afișarea unui șir primit ca parametru pentru modul

Actualizați scriptul hello.c astfel încât să primească un parametru message care să fie inițializat la un șir. Acel șir este afișat folosind printk la inserarea modulului.

Indicații sunt în directorul cap-02-doc/ din arhiva capitolului și la link-urile din secțiunea de resurse: