====== 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 ===== * Nevoia de module de kernel, ce este un modul de kernel * Aflarea de informații despre module de kernel * Încărcarea și descărcarea unui modul de kernel * Compilarea unui modul de kernel * Conținutul unui modul de kernel: funcții de inițializare și de ieșire * Conectarea modului de kernel la nucleu; extinderea funcționalităților * Module de kernel din fișiere cod sursă multiple ===== Resurse utile ===== * [[http://www.tldp.org/LDP/lkmpg/2.6/html/x323.html|The Linux Kernel Module Programming Guide: Passing Command Line Arguments to a Module]] * [[https://lwn.net/Kernel/LDD3/|Linux Device Drivers, 3rd Edition]] (free) * [[https://lwn.net/images/pdf/LDD3/ch02.pdf|Linux Device Drivers, 3rd Edition, Chapter 2: Building and Running Modules]] (free, PDF) * [[http://lxr.free-electrons.com/|Linux Cross Reference (LXR) (Free Electrons)]] ===== Exerciții ===== Arhiva de suport pentru exerciții se găsește [[http://koala.cs.pub.ro/training/res/linux-kernel-dev/arc/cap-02-tasks.zip|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: - Afișarea modulelor curente - 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 unde '''' 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/''. 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//''. 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/ 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 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 ==== [[http://cscope.sourceforge.net/|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 s :cs find s =expand("") nmap g :cs find g =expand("") nmap c :cs find c =expand("") nmap t :cs find t =expand("") nmap e :cs find e =expand("") nmap f :cs find f =expand("") nmap i :cs find i ^=expand("")$ nmap d :cs find d =expand("") " 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 '' (unde '''' 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, [[http://www.emacswiki.org/emacs/CScopeAndEmacs|wiki-ul emacs]] conține informații relevante pentru configurarea cscope. Pentru o interfață mai simplă, [[http://sourceforge.net/projects/kscope/|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 [[https://opendesktop.org/content/show.php/Kscope4?content=156987|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: * ''struct task_struct'' * ''struct semaphore'' * ''struct list_head'' * ''spinlock_t'' * ''struct file_system_type'' 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: - Listați toate match-urile folosind, în Vim, comanda '':copen''. Vă apare o fereastră secundară cu toate match-urile. - 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 ''/{''. - Pe linia aferentă apăsați ''Enter'' ca să vă ajungă editorul în codul sursă unde e definită variabila. - Închideți fereastra secundară folosind coamanda '':cclose''. Determinați fișierul în care sunt declarate următoarele variabile globale la nivelul nucleului: * ''sys_call_table'' * ''file_systems'' * ''current'' * ''chrdevs'' Pentru aceasta folosiți în Vim o comandă de forma '':cs f g '' (unde construcția '''' reprezintă numele simbolului căutat). Determinați fișierul în care sunt declarate următoarele funcții: * ''copy_from_user'' * ''vmalloc'' * ''schedule_timeout'' * ''add_timer'' Pentru aceasta folosiți în Vim o comandă de forma '':cs f g '' (unde construcția '''' reprezintă numele simbolului căutat). Parcurgeți secvența de structuri: - ''struct task_struct'' - ''struct mm_struct'' - ''struct vm_area_struct'' - ''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: - ''bio_alloc'' - ''bio_alloc_bioset'' - ''bvec_alloc'' - ''kmem_cache_alloc'' - ''slab_alloc'' Aveți în vedere indicațiile din secțiunea [[#informatii-cscope|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 [[#resurse-utile|secțiunea de resurse]]: * [[http://www.tldp.org/LDP/lkmpg/2.6/html/x323.html|The Linux Kernel Module Programming Guide: Passing Command Line Arguments to a Module]] * [[https://lwn.net/images/pdf/LDD3/ch02.pdf|Linux Device Drivers, 3rd Edition, Chapter 2: Building and Running Modules]], secțiunea ''Module Parameters''