Un kernel monolitic, deși mai rapid decât un microkernel, are dezavantajul lipsei de modularitate și extensibilitate. La kernel-ele monolitice moderne, acest lucru a fost rezolvat prin utilizarea de module de kernel. Un modul de kernel (sau modul de kernel încărcabil) este un fișier obiect care conține cod ce poate extinde funcționalitatea kernel-ului în timp real (este încărcat la nevoie); când un modul de kernel nu mai este necesar, acesta poate fi descărcat. Cea mai mare parte a driver-elor de dispozitiv (device drivers) sunt utilizate în forma de module de kernel.
Pentru dezvoltarea de device drivere în Linux, se recomandă descărcarea surselor nucleului, configurarea și compilarea acestora, iar apoi instalarea versiunii compilate pe mașina de test/dezvoltare.
În cele ce urmează, prezentăm un exemplu foarte simplu de modul kernel. La încărcarea în kernel, acesta va genera mesajul “Hi”. La descărcarea modulului din kernel, se va genera mesajul “Bye”.
#include <linux/kernel.h> #include <linux/init.h> #include <linux/module.h> MODULE_DESCRIPTION("My kernel module"); MODULE_AUTHOR("Me"); MODULE_LICENSE("GPL"); static int dummy_init(void) { printk( KERN_DEBUG "Hi\n" ); return 0; } static void dummy_exit(void) { printk( KERN_DEBUG "Bye\n" ); } module_init(dummy_init); module_exit(dummy_exit);
Mesajele generate nu vor fi afișate la consolă, ci vor fi salvate într-o zonă de memorie special rezervată pentru acest lucru, de unde vor fi extrase de către daemonul de loguri (syslog). Pentru a afișa mesajele de kernel, puteți să folosiți comanda dmesg sau să inspectați logurile:
# cat /var/log/syslog | tail -2 Feb 20 13:57:38 asgard kernel: Hi Feb 20 13:57:43 asgard kernel: Bye # dmesg | tail -2 Hi Bye
Compilarea unui modul kernel diferă de compilarea unui program obișnuit. În primul rând, trebuie folosite alte headere. De asemenea, modulul nu trebuie legat de biblioteci. Și, nu în ultimul rând, modulul trebuie compilat cu aceleași opțiuni ca și nucleul în care vom încărca modulul. Din aceste motive există o metodă standard de compilare (kbuild). Această metodă necesită folosirea a două fișiere: un fișier Makefile
și un fișier Kbuild
.
În continuare, este prezentat un exemplu de fișier Makefile
:
KDIR = /lib/modules/`uname -r`/build kbuild: make -C $(KDIR) M=`pwd` clean: make -C $(KDIR) M=`pwd` clean
și exemplul de fișier Kbuild
asociat, folosit la compilarea unui modul:
EXTRA_CFLAGS = -Wall -g obj-m = modul.o
După cum se observă, invocarea make pe fișierul Makefile din exemplul prezentat va duce la invocarea make în directorul cu sursele kernelului (/lib/modules/`uname -r`/build) și cu referință la directorul curent (M=`pwd`). Acest proces duce în cele din urmă la citirea fișierului Kbuild din directorul curent și la compilarea modulului conform instrucțiunilor din acest fișier.
KDIR
, în conformitate cu specificațiile mașinii virtuale:
KDIR = /home/student/so2/linux [...]
Un fișier Kbuild conține una sau mai multe directive pentru compilarea unui modul de kernel. Cel mai simplu exemplu de astfel de directivă este obj-m = modul.o. În urma acestei directive, va fi creat un modul de kernel modul.ko (ko - kernel object), plecând de la fișierul modul.o. modul.o va fi creat plecând de la modul.c sau modul.S. Toate aceste fișiere se găsesc în directorul în care se află și Kbuild.
Un exemplu de fișier Kbuild care folosește mai multe sub-module este prezentat mai jos:
EXTRA_CFLAGS = -Wall -g obj-m = supermodul.o supermodul-y = modul-a.o modul-b.o
Pentru exemplul de mai sus, pașii efectuați la compilare sunt:
Sufixul țintelor din Kbuild determină modul în care sunt folosite, astfel:
($(nume_modul)-y)
sau în cadrul kernel-ului (obj-y)
make menuconfig
sau direct prin editarea fișierului .config
. Acest fișier setează o serie de variabile ce sunt folosite pentru a stabili ce feature-uri sunt adăugate la kernel în momentul build-ului. Spre exemplu, în primul laborator, în momentul adăugării suportului pentru BTRFS cu ajutorul make menuconfig
, se adăuga în fișierul .config
linia: CONFIG_BTRFS_FS=y. Kbuild-ul pentru BTRFS conține linia obj-$(CONFIG_BTRFS_FS) := btrfs.o, ce devine obj-y := btrfs.o. Astfel, se va compila obiectul btrfs.o
și va fi linkat în cadrul kernel-ului. Înaintea setării variabilei linia devenea obj- := btrfs.o și deci era ignorată, iar kernel-ul era build-at fără suport BTRFS.
Pentru mai multe detalii, consultați fișierul makefiles.txt și fișierul modules.txt din cadrul surselor kernel-ului.
Pentru a încărca un modul în kernel, se folosește utilitarul insmod
. Acest utilitar primește ca parametru calea către fișierul .ko în care a fost compilat și link-editat modulul. Descărcarea modulului din kernel se face cu ajutorul comenzii rmmod
, care primește ca parametru numele modulului.
# insmod modul.ko # rmmod modul
La încărcarea modulului în kernel va fi executată rutina specificată ca parametru macroului module_init. Similar, la descărcarea modulului va fi executată rutina specificată ca parametru macroului module_exit.
Un exemplu complet de compilare și încărcare/descărcare modul este prezentat în continuare:
faust:~/lab-01/modul-lin# ls Kbuild Makefile modul.c faust:~/lab-01/modul-lin# make make -C /lib/modules/`uname -r`/build M=`pwd` make[1]: Entering directory `/usr/src/linux-2.6.28.4' LD /root/lab-01/modul-lin/built-in.o CC [M] /root/lab-01/modul-lin/modul.o Building modules, stage 2. MODPOST 1 modules CC /root/lab-01/modul-lin/modul.mod.o LD [M] /root/lab-01/modul-lin/modul.ko make[1]: Leaving directory `/usr/src/linux-2.6.28.4' faust:~/lab-01/modul-lin# ls built-in.o Kbuild Makefile modul.c Module.markers modules.order Module.symvers modul.ko modul.mod.c modul.mod.o modul.o faust:~/lab-01/modul-lin# insmod modul.ko faust:~/lab-01/modul-lin# dmesg | tail -1 Hi faust:~/lab-01/modul-lin# rmmod modul faust:~/lab-01/modul-lin# dmesg | tail -2 Hi Bye
Informații despre modulele încărcate în kernel se pot afla cu ajutorul comenzii lsmod
, prin inspectarea fișierului /proc/modules
sau a directorului /sys/module
.
Depanarea unui modul de kernel este mult mai complicată decât depanarea unui program obișnuit. În primul rând, o greșeală într-un modul kernel poate duce la blocarea întregului sistem. Depanarea este din această cauză mult încetinită. Pentru a evita secvențele de reboot, se recomandă instalarea unei mașini virtuale și utilizarea snapshot-urilor.
Atunci când un modul ce conține bug-uri este inserat în kernel, se va genera în cele din urmă un kernel oops. Un kernel oops reprezintă o operație invalidă detectată de nucleu și poate fi generată doar de către acesta. Pentru o versiune stabilă de kernel, aceasta înseamnă, aproape sigur, că modulul conține un bug. După apariția oops-ului, kernelul va continua să funcționeze.
Foarte importantă la apariția unui kernel oops este salvarea mesajului generat. După cum s-a precizat anterior, mesajele generate de kernel se salvează în loguri și pot fi afișate cu comanda dmesg
. Pentru a fi siguri că nu se pierde un kernel oops, se recomandă inserarea/testarea modulului kernel direct din consolă, sau verificarea periodică a mesajelor de kernel. De remarcat este faptul că un oops poate apărea din cauza unei erori de programare, dar și a unei erori hardware.
În cazul în care apare o eroare fatală, după care sistemul nu mai poate reveni la o stare stabilă, se generează un kernel panic.
Fie următorul modul de kernel care conține un bug pentru generarea unui oops:
/* * Oops generating kernel module */ #include <linux/kernel.h> #include <linux/module.h> #include <linux/init.h> MODULE_DESCRIPTION ("Oops"); MODULE_LICENSE ("GPL"); MODULE_AUTHOR ("PSO"); #define OP_READ 0 #define OP_WRITE 1 #define OP_OOPS OP_WRITE static int my_oops_init (void) { int *a; a = (int *) 0x00001234; #if OP_OOPS == OP_WRITE *a = 3; #elif OP_OOPS == OP_READ printk (KERN_ALERT "value = %d\n", *a); #else #error "Unknown op for oops!" #endif return 0; } static void my_oops_exit (void) { } module_init (my_oops_init); module_exit (my_oops_exit);
Inserarea acestui modul in kernel va genera un oops:
faust:~/lab-01/modul-oops# insmod oops.ko [...] faust:~/lab-01/modul-oops# dmesg | tail -32 BUG: unable to handle kernel paging request at 00001234 IP: [<c89d4005>] my_oops_init+0x5/0x20 [oops] *de = 00000000 Oops: 0002 [#1] PREEMPT DEBUG_PAGEALLOC last sysfs file: /sys/devices/virtual/net/lo/operstate Modules linked in: oops(+) netconsole ide_cd_mod pcnet32 crc32 cdrom [last unloaded: modul] Pid: 4157, comm: insmod Not tainted (2.6.28.4 #2) VMware Virtual Platform EIP: 0060:[<c89d4005>] EFLAGS: 00010246 CPU: 0 EIP is at my_oops_init+0x5/0x20 [oops] EAX: 00000000 EBX: fffffffc ECX: c89d4300 EDX: 00000001 ESI: c89d4000 EDI: 00000000 EBP: c5799e24 ESP: c5799e24 DS: 007b ES: 007b FS: 0000 GS: 0033 SS: 0068 Process insmod (pid: 4157, ti=c5799000 task=c665c780 task.ti=c5799000) Stack: c5799f8c c010102d c72b51d8 0000000c c5799e58 c01708e4 00000124 00000000 c89d4300 c5799e58 c724f448 00000001 c89d4300 c5799e60 c0170981 c5799f8c c014b698 00000000 00000000 c5799f78 c5799f20 00000500 c665cb00 c89d4300 Call Trace: [<c010102d>] ? _stext+0x2d/0x170 [<c01708e4>] ? __vunmap+0xa4/0xf0 [<c0170981>] ? vfree+0x21/0x30 [<c014b698>] ? load_module+0x19b8/0x1a40 [<c035e965>] ? __mutex_unlock_slowpath+0xd5/0x140 [<c0140da6>] ? trace_hardirqs_on_caller+0x106/0x150 [<c014b7aa>] ? sys_init_module+0x8a/0x1b0 [<c0140da6>] ? trace_hardirqs_on_caller+0x106/0x150 [<c0240a08>] ? trace_hardirqs_on_thunk+0xc/0x10 [<c0103407>] ? sysenter_do_call+0x12/0x43 Code: <c7> 05 34 12 00 00 03 00 00 00 5d c3 eb 0d 90 90 90 90 90 90 90 90 EIP: [<c89d4005>] my_oops_init+0x5/0x20 [oops] SS:ESP 0068:c5799e24 ---[ end trace 2981ce73ae801363 ]---
Deși relativ criptic, mesajul oferit de kernel la apariția unui oops oferă informații prețioase despre eroarea apărută. Prima linie:
BUG: unable to handle kernel paging request at 00001234 IP: [<c89d4005>] my_oops_init+0x5/0x20 [oops]
ne spune cauza și adresa instrucțiunii care a generat eroarea. În cazul de față este vorba de un acces invalid la memorie.
Linia următoare:
Oops: 0002 [#1] PREEMPT DEBUG_PAGEALLOC
ne spune că este vorba de primul oops (#1
). Acest lucru este important în contextul în care un oops poate duce la apariția altor oops-uri. De obicei, doar primul oops este relevant. Mai mult, codul oops-ului (0002
) oferă informații despre tipul erorii (în memory manager → fault.c):
*bit 0 == 0 means no page found, 1 means protection fault *bit 1 == 0 means read, 1 means write *bit 2 == 0 means kernel, 1 means user-mode
În cazul de față avem un acces de tip scriere care a generat oops-ul (bitul 1 este 1).
În continuare se afișează un dump al registrelor. Se decodifică valoarea EIP (instruction pointer) și se observă că bug-ul a apărut în cadrul funcției my_oops_init cu un offset de 5 octeți (EIP: [<c89d4005>] my_oops_init+0x5
). Mesajul prezintă și conținutul stivei și un backtrace al apelurilor de până atunci.
În cazul în care se generează un apel invalid de citire (#define OP_OOPS OP_READ
), mesajul va fi asemănător. Va diferi codul de oops, care acum ar avea valoarea 0000
:
faust:~/lab-01/modul-oops# dmesg | tail -33 BUG: unable to handle kernel paging request at 00001234 IP: [<c89c3016>] my_oops_init+0x6/0x20 [oops] *de = 00000000 Oops: 0000 [#1] PREEMPT DEBUG_PAGEALLOC last sysfs file: /sys/devices/virtual/net/lo/operstate Modules linked in: oops(+) netconsole pcnet32 crc32 ide_cd_mod cdrom Pid: 2754, comm: insmod Not tainted (2.6.28.4 #2) VMware Virtual Platform EIP: 0060:[<c89c3016>] EFLAGS: 00010292 CPU: 0 EIP is at my_oops_init+0x6/0x20 [oops] EAX: 00000000 EBX: fffffffc ECX: c89c3380 EDX: 00000001 ESI: c89c3010 EDI: 00000000 EBP: c57cbe24 ESP: c57cbe1c DS: 007b ES: 007b FS: 0000 GS: 0033 SS: 0068 Process insmod (pid: 2754, ti=c57cb000 task=c66ec780 task.ti=c57cb000) Stack: c57cbe34 00000282 c57cbf8c c010102d c57b9280 0000000c c57cbe58 c01708e4 00000124 00000000 c89c3380 c57cbe58 c5db1d38 00000001 c89c3380 c57cbe60 c0170981 c57cbf8c c014b698 00000000 00000000 c57cbf78 c57cbf20 00000580 Call Trace: [<c010102d>] ? _stext+0x2d/0x170 [<c01708e4>] ? __vunmap+0xa4/0xf0 [<c0170981>] ? vfree+0x21/0x30 [<c014b698>] ? load_module+0x19b8/0x1a40 [<c035d083>] ? printk+0x0/0x1a [<c035e965>] ? __mutex_unlock_slowpath+0xd5/0x140 [<c0140da6>] ? trace_hardirqs_on_caller+0x106/0x150 [<c014b7aa>] ? sys_init_module+0x8a/0x1b0 [<c0140da6>] ? trace_hardirqs_on_caller+0x106/0x150 [<c0240a08>] ? trace_hardirqs_on_thunk+0xc/0x10 [<c0103407>] ? sysenter_do_call+0x12/0x43 Code: <a1> 34 12 00 00 c7 04 24 54 30 9c c8 89 44 24 04 e8 58 a0 99 f7 31 EIP: [<c89c3016>] my_oops_init+0x6/0x20 [oops] SS:ESP 0068:c57cbe1c ---[ end trace 45eeb3d6ea8ff1ed ]---
Informații detaliate despre instrucțiunea care a generat oops-ul pot fi aflate folosind utilitarul objdump
de inspecție a unui cod obiect. Opțiunile utile de folosit sunt -d
pentru dezasamblarea codului și -S
pentru intercalarea codului C în cod în limbaj de asamblare. Pentru o decodificare eficientă avem însă nevoie de adresa unde a fost încarcat modulul de kernel. Aceasta poate fi regasită în /proc/modules
.
Prezentăm în continuare un exemplu de utilizare a objdump pe modulul de mai sus pentru a identifica instrucțiunea care a generat oops-ul:
faust:~/lab-01/modul-oops# cat /proc/modules oops 1280 1 - Loading 0xc89d4000 netconsole 8352 0 - Live 0xc89ad000 pcnet32 33412 0 - Live 0xc895a000 ide_cd_mod 34952 0 - Live 0xc8903000 crc32 4224 1 pcnet32, Live 0xc888a000 cdrom 34848 1 ide_cd_mod, Live 0xc886d000 faust:~/lab-01/modul-oops# objdump -dS --adjust-vma=0xc89d4000 oops.ko oops.ko: file format elf32-i386 Disassembly of section .text: c89d4000 <init_module>: #define OP_READ 0 #define OP_WRITE 1 #define OP_OOPS OP_WRITE static int my_oops_init (void) { c89d4000: 55 push %ebp #else #error "Unknown op for oops!" #endif return 0; } c89d4001: 31 c0 xor %eax,%eax #define OP_READ 0 #define OP_WRITE 1 #define OP_OOPS OP_WRITE static int my_oops_init (void) { c89d4003: 89 e5 mov %esp,%ebp int *a; a = (int *) 0x00001234; #if OP_OOPS == OP_WRITE *a = 3; c89d4005: c7 05 34 12 00 00 03 movl $0x3,0x1234 c89d400c: 00 00 00 #else #error "Unknown op for oops!" #endif return 0; } c89d400f: 5d pop %ebp c89d4010: c3 ret c89d4011: eb 0d jmp c89c3020 <cleanup_module> c89d4013: 90 nop c89d4014: 90 nop c89d4015: 90 nop c89d4016: 90 nop c89d4017: 90 nop c89d4018: 90 nop c89d4019: 90 nop c89d401a: 90 nop c89d401b: 90 nop c89d401c: 90 nop c89d401d: 90 nop c89d401e: 90 nop c89d401f: 90 nop c89d4020 <cleanup_module>: static void my_oops_exit (void) { c89d4020: 55 push %ebp c89d4021: 89 e5 mov %esp,%ebp } c89d4023: 5d pop %ebp c89d4024: c3 ret c89d4025: 90 nop c89d4026: 90 nop c89d4027: 90 nop
Se observă că instrucțiunea care a generat oops-ul (cea de la adresa c89d4005
identificată anterior) este:
c89d4005: c7 05 34 12 00 00 03 movl $0x3,0x1234
adică exact cum era de așteptat - stocarea valorii 3
la adresa 0x0001234
.
Fișierul /proc/modules
este folosit pentru a afla adresa unde este încărcat un modul de kernel. Opțiunea –adjust-vma
permite afișarea instrucțiunilor relativ la adresa 0xc89d4000
. Opțiunea -l
afișează numărul fiecărei linii din codul sursă intercalat cu codul în limbaj de asamblare.
O modalitate mai simplistă de a găsi codul care a generat un oops este de a folosi utilitarul addr2line:
faust:~/lab-01/modul-oops# addr2line -e oops.o 0x5 /root/lab-01/modul-oops/oops.c:23
unde 0x5
este valoarea contorului program (EIP = c89d4005
) la care s-a generat kernel oops, minus adresa de bază a modulului (0xc89c4000
), conform /proc/modules
.
Minicom (sau alte utilitare echivalente, e.g. picocom, screen) este un utilitar ce poate fi folosit pentru a ne connecta și interacționa cu un port serial. Portul serial este metoda de bază pentru a analiza mesajele de kernel sau a interacționa cu un sistem (embedded) în faza de dezvoltare. Sunt două modalități mai comune de conectare: * un port serial clasic caz în care device-ul pe care o să îl folosim este /dev/ttyS0 * un port USB serial (FDTI) caz în care device-ul pe care o sa îl folosim este /dev/ttyUSB)
În cazul mașinii virtuale folosite în laborator, device-ul pe care trebuie să îl folosim este afișat după ce mașina virtuala pornește:
char device redirected to /dev/pts/20 (label virtiocon0)
Utilizare minicom:
#pentru conectarea via COM1 și folosirea unei viteze de 115200 caractere pe secundă minicom -b 115200 -D /dev/ttyS0 #pentru conectare via port serial USB minicom -D /dev/ttyUSB0 #pentru conectarea la portul serial al mașinii virtuale minicom -D /dev/pts/20 # numărul variază, inspectați output-ul qemu
Netconsole este un utilitar care permite logarea mesajelor de debug din kernel prin intermediul rețelei. Acest lucru este folositor atunci când sistemul de logging pe disk nu funcționează, când nu sunt disponibile porturi seriale sau când terminalul nu răspunde la comenzi. Netconsole vine sub forma unui modul de kernel.
Pentru a funcționa, acesta are nevoie de următorii parametri:
Acești parametri pot fi configurați atunci când modulul este inserat în kernel, sau, chiar în timp ce modulul este inserat dacă acesta a fost compilat cu opțiunea CONFIG_NETCONSOLE_DYNAMIC.
Un exemplu de configurare în momentul inserării este următorul:
alice:~# modprobe netconsole netconsole=6666@192.168.191.130/eth0,6000@192.168.191.1/00:50:56:c0:00:08
Astfel, mesajele de debug de pe stația ce are adresa 192.168.191.130 vor fi trimise pe interfața eth0, având ca port sursă 6666. Mesajele vor fi trimise către 192.168.191.1, ce are adresa MAC 00:50:56:c0:00:08, pe portul 6000.
Mesajele pot fi ascultate pe stația destinație folosind netcat
:
bob:~# nc -l -p 6000 -u
Alternativ, stația destinație poate configura syslogd
pentru a intercepta aceste mesaje. Mai multe informații puteți găsi aici.
The two oldest and most useful debugging aids are Your brain and Printf
Pentru depanare, cel mai adesea se folosește un mijloc primitiv, dar destul de eficient: afișarea de mesaje (printk debugging). Deși se poate folosi și un debugger, în general acesta nu este foarte folositor: bug-urile simple (variabile neinițializate, probleme la gestiunea memoriei etc.) pot fi ușor localizate cu ajutorul mesajelor de control si mesajului de oops decodificat de către kernel.
La bugurile mai complexe, nici chiar un debugger nu ne poate ajuta prea mult dacă nu se înțelege foarte bine structura sistemului de operare. La depanarea unui modul de kernel intervin o mulțime de necunoscute în ecuație: contexte multiple (avem mai multe procese și threaduri ce rulează la un moment dat), întreruperi, memorie virtuală etc.
Pentru afișarea mesajelor din kernel către user space se poate folosi printk
1). Acesta este similar ca funcționalitate lui printf
; singura diferență constă în faptul că mesajul transmis se poate prefixa cu un șir de forma ”<n>”, unde n indică nivelul (loglevel-ul) erorii și are valori între 0 și 7. În loc de ”<n>”, nivelurile pot fi codificate și prin constante simbolice:
KERN_EMERG
- n = 0KERN_ALERT
- n = 1KERN_CRIT
- n = 2KERN_ERR
- n = 3KERN_WARNING
- n = 4KERN_NOTICE
- n = 5KERN_INFO
- n = 6KERN_DEBUG
- n = 7
Definițiile tuturor loglevel-urilor se găsesc în linux/kern_levels.h. Practic, aceste loglevel-uri sunt utilizate de sistem pentru a ruta mesajele trimise către diverse output-uri: consolă, fișiere log din /var/log
etc.
printk
în user space, trebuie ca nivelul folosit la apelul printk
să fie prioritar valorii variabilei console_loglevel 2). Adică nivelul de logging să fie mai mic strict decât nivelul variabilei console_loglevel. De exemplu, dacă variabila console_loglevel
are valoarea 5
(specifică valorii KERN_NOTICE
) doar mesajele cu loglevel-ul mai mic strict decât 5
(adică 0
, 1
, 2
, 3
și 4
, adică KERN_EMERG
, KERN_ALERT
, KERN_CRIT
, KERN_ERR
, KERN_WARNING
) vor fi afișate 3).
Mesajele redirectate la consolă pot fi utile pentru a vizualiza rapid efectul execuției codului inserat în kernel, însă ele nu mai sunt așa folositoare în cazul în care în kernel apare o eroare irecuperabilă, iar sistemul îngheață. În acest caz, trebuie consultate log-urile sistemului, deoarece în ele se păstrează informațiile între restart-uri ale sistemului. Acestea se găsesc în /var/log
4) și sunt fișiere text, populate cu informație de syslogd
și klogd
pe parcursul rulării kernelului. syslogd
și klogd
preiau la rândul lor informațiile din sistemul virtual de fișiere montat în /proc
. În principiu, cu syslogd
și klogd
pornite, toate mesajele venite de la kernel vor ajunge în /var/log/kern.log
.
O variantă mai simplă pentru etapa de debugging este folosirea fișierului /var/log/debug
. Acesta este populat numai cu mesajele printk
venite de la kernel cu loglevel-ul KERN_DEBUG
.
Având în vedere faptul că un kernel de producție (similar celui pe care probabil îl rulăm și noi :P) conține doar cod de release, modulul nostru este printre puținele care trimit mesaje prefixate cu KERN_DEBUG
. În acest fel, putem naviga ușor prin informațiile din /var/log/debug
, găsind mesajele corespunzatoare unei sesiuni de debug pentru modulul nostru.
Un exemplu de utilizare ar fi urmatorul:
echo "New debug session" > /var/log/debug
/var/log/debug
Formatul mesajelor trebuie, evident, să conțină toate informațiile de interes pentru a depista eroarea, însă inserarea în cod a “printk-urilor” care să ofere informații detaliate poate fi la fel de time-consuming ca și scrierea codului pentru rezolvarea problemei. De aceea se face de obicei un trade-off între completitudinea mesajelor de debugging afișate folosind printk și timpul necesar pentru inserarea acestor mesaje în text.
O variantă foarte simplă, puțin costisitoare din punctul de vedere al timpului necesar pentru inserarea printk-urilor, și care oferă posibilitatea analizării fluxului de instrucțiuni în cazul testelor, este cea a folosirii constantelor predefinite __FILE__
, __LINE__
și __func__
:
__FILE__
este înlocuită, la compilare, de către compilator, cu numele fișierului sursă în care se găsește la momentul respectiv.__LINE__
este înlocuită, la compilare, de către compilator, cu numărul liniei pe care se găsește instrucțiunea curentă în cadrul fișierului sursă curent.__func__
/ __FUNCTION__
este înlocuită, la compilare, de către compilator, cu numele funcției în care se găsește instrucțiunea curentă.__LINE__
și __FILE__
fac parte din specificațiile standardului ANSI C; __func__
face parte din specificațiile C99; __FUNCTION__
reprezintă o extensie GNU C și nu este portabilă; însă, având în vedere că scriem cod pentru kernelul de Linux, o putem folosi fără probleme.Se poate folosi în acest caz următoarea macrodefiniție:
#define PRINT_DEBUG \
printk(KERN_DEBUG "[%s]:FUNC:%s:LINE:%d\n", __FILE__ , __FUNCTION__ , __LINE__)
Apoi, în fiecare punct în care dorim să vedem dacă este “atins” în execuție, inserăm PRINT_DEBUG; aceasta este o modalitate simplă și rapidă, și poate da roade analizând cu atenție output-ul oferit.
Pentru a vedea mesajele afișate cu printk
, dar care nu apar la consolă, se folosește comanda dmesg.
Pentru a șterge toate mesajele anterioare dintr-un fișier de log, se rulează cat /dev/null > /var/log/debug
. Pentru a șterge mesajele afișate de comanda dmesg
, se folosește dmesg -c
.
Depanarea dinamică dyndbg permite activarea/dezactivarea în mod dinamic a mesajelor de debug. Spre deosebire de printk
, oferă opțiuni mai avansate de filtrare a mesajelor pe care dorim să le afișăm - foarte util în cazul unor module complexe sau pentru depanarea subsistemelor. Astfel, se reduce semnificativ cantitatea de mesaje afișate, rămânând doar cele relevante pentru contextul depanării. Pentru activarea dyndbg, kernelul trebuie compilat cu opțiunea CONFIG_DYNAMIC_DEBUG
. Odata configurat, apelurile pr_debug()
, dev_dbg()
și print_hex_dump_debug()
, print_hex_dump_bytes()
pot fi activate dinamic per callsite.
Pentru filtrarea mesajelor sau vizualizarea filtrelor existente se va folosi fișierul /sys/kernel/debug/dynamic_debug/control
din cadrul sistemului de fișiere debugfs
(unde /sys/kernel/debug
este calea la care a fost montat debugfs).
mount -t debugfs none /debug
Debugfs este un sistem de fișiere simplu, folosit ca interfață între kernel-space și user-space pentru a configura diferite opțiuni de debug. Orice utilitar de debug își poate crea și folosi propriile fișiere/directoare în debugfs.
De exemplu, pentru afișarea filtrelor existente în dyndbg se va folosi:
cat /debug/dynamic_debug/control
iar pentru activarea mesajului de debug de la linia 1603 din fișierul svcsock.c:
echo 'file svcsock.c line 1603 +p' > /debug/dynamic_debug/control
/debug/dynamic_debug/control
nu este un fișier obișnuit. Afișarea lui duce la afișarea setărilor dyndbg asupra filtrelor. Scrierea în el cu echo va duce la modificarea acestor setări (nu va realiza efectiv o scriere). Atenție la faptul că fișierul conține setări pentru mesajele de debug dyndbg. Nu se face logging în acest fișier.
Opțiuni dyndbg:
echo 'func svc_tcp_accept +p' > /debug/dynamic_debug/control
file svcsock.c file kernel/freezer.c file /usr/src/packages/BUILD/sgi-enhancednfs-1.4/default/net/sunrpc/svcsock.c
module sunrpc
format "nfsd: SETATTR"
# activează mesajele de debug între liniile 1603 și 1605 din fișierul svcsock.c echo 'file svcsock.c line 1603-1605 +p' > /sys/kernel/debug/dynamic_debug/control # activează mesajele de debug de la începutul fișierului până la linia 1605 echo 'file svcsock.c line -1605 +p' > /sys/kernel/debug/dynamic_debug/control
Pe lângă opțiunile de mai sus, pot fi specificate și o serie de flaguri ce pot fi adăugate, eliminate sau setate cu operatorii +,- sau =.
pr_debug()
.Kernel debugger-ul s-a dovedit a fi foarte util pentru a facilita procesul de dezvoltare și depanare. Unul dintre avantajele majore ale acestuia fiind posibilitatea de a face live debugging. Astfel, putem monitoriza în timp real accesele la memorie sau chiar modifica zone de memorie în cadrul procesului de debug. Acesta a fost integrat în mainline începând cu versiunea 2.6.26-rc1. KDB nu este un “source debugger”, dar pentru o analiză completă poate fi folosit în paralel cu gdb și fișiere de simboluri - vezi Depanare.
Pentru a folosi KDB, trebuie să aveți una din următoarele opțiuni:
În laborator vom folosi o serială legată la gazdă. Comanda de mai jos va activa KDB prin serială:
echo hvc0 > /sys/module/kgdboc/parameters/kgdboc
KDB este un “stop mode debugger”. Astfel, când suntem în debug mode, toate celelalte procese sunt oprite. Pentru a “forța” intrarea în KDB în timpul execuției se poate folosi următoarea comandă SysRq
echo g > /proc/sysrq-trigger
sau folosind secvența de taste Ctrl+O g
dintr-un terminal conectat (de exemplu folosind minicom) la portul serial.
KDB permite numeroase comenzi pentru a controla și defini în detaliu contextul sistemului depanat.
Pentru o descriere a tuturor comenzilor disponibile puteți apela help
din shell-ul KDB. În exemplul de mai jos, puteți observa un exemplu de utilizare KDB ce seteaza un hardware breakpoint pentru a monitoriza modificările variabilei mVar.
# trigger KDB echo g > /proc/sysrq-trigger # or if we are connected to the serial port issue Ctrl-O g # breakpoint la accesul de scriere al variabilei mVar kdb> bph mVar dataw # revenire din KDB kdb> go
echo 8 > /proc/sys/kernel/printk
va face ca toate mesajele din kernel să fie afișate la consolă /etc/syslog.conf