Laborator 2 - Module de kernel și debugging

Obiectivele laboratorului

  • crearea unor module simple
  • descrierea procesului de compilare a surselor unui modul
  • prezentarea modului în care un modul poate fi adăugat în kernel
  • prezentarea metodelor de depanare a modulelor kernel

Cuvinte cheie

  • built-in, loadable
  • make, kbuild
  • insmod, rmmod
  • printk, dyndbg
  • objdump, addr2line, netconsole
  • KDB, Kprobes, Jprobes, Kretprobes

Materiale ajutătoare

Noțiuni generale

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.

Module kernel în Linux

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.

Un exemplu de modul kernel

Î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”.

hello_lin.c
#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 modulelor kernel

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:

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:

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

Pentru laborator și în temele de casă vom configura diferit variabila internă KDIR, în conformitate cu specificațiile mașinii virtuale:

KDIR = /usr/src/linux-so2
[...]

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:

Kbuild
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:

  • se vor compila sursele modul-a.c, modul-b.c, rezultând fișierele obiect modul-a.o și modul-b.o
  • modul-a.o și modul-b.o vor fi apoi legate în supermodul.o
  • din supermodul.o se va crea modulul supermodul.ko

Sufixul țintelor din Kbuild determină modul în care sunt folosite, astfel:

  • m (module) reprezintă o țintă pentru module de kernel încărcabile
  • y (yes) reprezintă o țintă pentru fișiere obiect ce vor fi compilate ca apoi să fie linkate în cadrul unui modul ($(nume_modul)-y) sau în cadrul kernel-ului (obj-y)
  • orice alt sufix de țintă va fi ignorat de Kbuild și nu va fi compilat

Aceste sufixe sunt folosite pentru a configura ușor kernel-ul prin rularea comenzii 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.

Încărcarea/descărcarea unui modul de kernel

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.

Kernel debugging

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.

Exemplu de kernel oops

Fie următorul modul de kernel care conține un bug pentru generarea unui oops:

debug_lin.c
/*
  * 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 ]---

objdump

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.

addr2line

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

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

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:

  • portul, adresa IP și numele interfeței sursă ale stației pe care se face debug
  • portul, adresa MAC și adresa IP a mașinii către care vor fi trimise mesajele de debug

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.

Logging

Printk Debugging

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 = 0
  • KERN_ALERT - n = 1
  • KERN_CRIT - n = 2
  • KERN_ERR - n = 3
  • KERN_WARNING - n = 4
  • KERN_NOTICE - n = 5
  • KERN_INFO - n = 6
  • KERN_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.

Pentru a afișa mesajele trimise cu 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:

  • se curăță fișierul debug de informațiile anterioare (sau, eventual, se face un backup):
echo "New debug session" > /var/log/debug
  • se rulează testul/testele
  • dacă nu apare o eroare critică, care să cauzeze un kernel panic, se consultă output-ul
  • dacă apare o eroare critică, iar mașina nu mai raspunde decat la restart, atunci se repornește sistemul, și se consultă /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ă.
  • Observație: __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.

Dynamic debugging

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

Fișierul /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:

  • func - doar mesajele de debug din funcțiile care au același nume cu cel definit în filtru.
    echo 'func svc_tcp_accept +p' > /debug/dynamic_debug/control
  • file - numele fișierului(elor) pentru care dorim să afișăm mesajele de debug. Poate fi doar numele sursei, dar și calea absolută sau calea relativă la kernel-tree.
    file svcsock.c
    file kernel/freezer.c
    file /usr/src/packages/BUILD/sgi-enhancednfs-1.4/default/net/sunrpc/svcsock.c
  • module - numele modulului.
 module sunrpc 
  • format - doar mesajele al căror format de afișare conține șirul specificat.
 format "nfsd: SETATTR" 
  • line - linia sau liniile pentru care dorim să activăm apelurile de debug.
 # 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 =.

  • p activează apelurile pr_debug().
  • f include numele funcției în mesajul printat.
  • l include numărul liniei în mesajul printat.
  • m include numele modulului în mesajul printat.
  • t include ID-ul threadului dacă nu e apelat din context întrerupere.
  • _ niciun flag nu este setat.

KDB: Kernel debugger

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:

  • tastatură non-usb + consolă vga text
  • serial port console
  • USB EHCI debug port

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

  • lsmod, ps, kill, dmesg, env, bt (backtrace)
  • dump trace logs
  • utilizare hardware breakpoints sau modificare memorie

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

Tracing

Kprobes

Kprobes permite instrumentarea dinamică a oricărei funcții din kernel pentru a colecta informații de debug într-un mod cât mai puțin invaziv. Cu kprobe, putem seta breakpoints la aproape orice adresă din kernel și configura handlere ce vor fi invocate în aceste puncte. Pot fi folosite trei tipuri de probe: Kprobes, Jprobes și Kretprobes (sau probe de return).

  1. O Kprobe poate fi inserată la orice adresă (instrucțiune) din kernel și putem specifica handlere ce vor fi apelate înainte sau după execuția instrucțiunii.
  2. Jprobe va fi adăugată la entry-pointul unei funcții, utilă pentru a monitoriza argumentele funcțiilor.
  3. O probă de return (Kretprobe) va fi apelată la întoarcerea dintr-o funcție, unde putem analiza rezultatul întors de aceasta.

De cele mai multe ori se va realiza instrumentarea cu kprobe prin intermediul unui modul. Funcția module_init va instala una sau mai multe probe care vor fi dezactivate apoi în module_exit. Înregistrarea unei probe (e.x. register_kprobe()) presupune specificarea adresei la care aceasta va fi inserată și a unui handler ce va fi apelat când se ajunge la adresa setată. Pentru mai multe detalii despre modul de funcționare Kprobe, Jprobe sau Kretprobe, expandați secțiunea de mai jos.

Afișați detalii implementare Kprobe

Ascundeți detalii implementare Kprobe

Kprobe

  1. La înregistrarea unei probe, Kprobes face o copie a instrucțiunii de la adresa specificată (instrucțiunea pe care dorim să o instrumentăm) și înlocuiește primii bytes cu o instrucțiune de breakpoint (e.g. int3).
  2. Când procesorul ajunge la instrucțiunea de breakpoint, se execută un trap și controlul revine la Kprobes (printr-un mecanism de tipul notifier_call_chain). În acest punct, Kprobes va executa pre_handler-ul asociat probei.
  3. După pre-handler, Kprobes execută instrucțiunea instrumentată, urmată post_handler-ul asociat.
  4. Execuția continuă cu următoarea instrucțiune după cea instrumentată.

Jprobe

JProbe este implementat prin inserarea unei kprobe la entry-pointul funcției. Folosește un mecanism de mirroring pentru a permite accesul la argumentele funcției instrumentate. Se așteaptă ca handler-ul înregistrat:

  • să aibă aceeași semnătură (lista de argumente și return type) ca funcția instrumentată.
  • să se termine mereu cu apelarea funcției jprobe_return().

Când se ajunge la un breakpoint, Kprobes face o copie a registrelor și a stivei, setând apoi instruction pointerul către handlerul înregistrat. Astfel, handlerul va fi apelat în aceeași stare (registre și stivă) ca funcția instrumentată. La ieșirea din handler, jprobe_return() restaurează conținutul inițial al stivei și al registrelor și continuă execuția cu funcția instrumentată.

Kretprobe

La înregistrarea unei probe de return, Kprobes instalează un kprobe la intrarea în funcția instrumentată. Acest kprobe are rolul de a salva o copie a adresei de return și de a înlocui adresa de return cu adresa unei porțiuni aleatoare de cod - o “trambulină” (e.x. o instrucțiune nop). Astfel, funcția instrumentată se va întoarce în codul din “trambulină”.

La inițializare, Kprobes setează un breakpoint la adresa acestei “trambuline”. La acest breakpoint se va apela handlerul asociat kretprobe, si se va reseta IP-ul la adresa de return salvată anterior (adresa de return inițială a funcției instrumentate).


Înregistrare Kprobe

Pentru înregistrarea unei Kprobe se va folosi o structură de tipul struct kprobe. Aici putem seta addr - adresa la care se va insera proba, pre_handler/post_handler, metode ce vor fi apelate înainte/după executarea instrucțiunii de la addr și fault_handler ce va fi apelat dacă apare un fault în handlerele pre/post sau la execuția instrucțiunii instrumentate. Adresa la care vrem să inserăm proba se poate specifica fie direct, fie folosind funcția kallsyms_lookup_name ce returnează adresa unui simbol primit ca parameru.

struct kprobe {
     /* location of the probe point */
     kprobe_opcode_t *addr;
     /* Called before addr is executed. */
     kprobe_pre_handler_t pre_handler;
     /* Called after addr is executed, unless... */
     kprobe_post_handler_t post_handler;
     /* Called if executing addr or handlers causes a fault (eg. page fault). */
     kprobe_fault_handler_t fault_handler;
     ...
}

Pentru înregistrarea/deînregistrarea unei probe se vor apela funcțiile register_kprobe, respectiv unregister_kprobe. Mai jos puteți urmări un exemplu în care este inițializată o probă de tipul Kprobe.

static struct kprobe kp;
 
/* kprobe pre_handler: called just before the probed instruction is executed */
int handler_pre(struct kprobe *p, struct pt_regs *regs);
 
/* kprobe post_handler: called after the probed instruction is executed */
void handler_post(struct kprobe *p, struct pt_regs *regs, unsigned long flags);
 
/* fault_handler: this is called if an exception is generated for any
 * instruction within the pre- or post-handler, or when Kprobes
 * single-steps the probed instruction.
 */
int handler_fault(struct kprobe *p, struct pt_regs *regs, int trapnr);
 
int init_module(void)
{
         kp.pre_handler = handler_pre;
         kp.post_handler = handler_post;
         kp.fault_handler = handler_fault;
         kp.addr = (kprobe_opcode_t*) probe_addr;
 
         if ((ret = register_kprobe(&kp) < 0)) {
                 printk("register_kprobe failed, returned %d\n", ret);
                 return -1;
         }
         printk("kprobe registered\n");
         return 0;
}

Înregistrare Jprobe

Pentru Jprobe, vom folosi o structură de tipul struct jprobe, al cărei câmp entry determină adresa handlerului ce va fi apelat la intrarea în funcția instrumentată. Pentru înregistrare/deînregistrare se vor folosi register_jprobe/unregister_jprobe. În exemplul de mai jos se folosește un modul pentru a monitoriza apelurile funcției do_execveat_common.

  • Semnătura handlerului trebuie să fie aceeași cu semnătura funcției instrumentate.
  • Nu uitați să apelați jprobe_return() la ieșirea din handler.

kprobes.c
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/kprobes.h>
#include <linux/kallsyms.h>
 
MODULE_DESCRIPTION("Probes module");
MODULE_AUTHOR("So2rul Esforever");
MODULE_LICENSE("GPL");
 
/*
 * Pre-entry point for do_execve.
 */
static int my_do_execveat_common(int fd, struct filename * filename,
		char __user *__user *argv,
		char __user *__user *envp)
{
	pr_info("%s(%s) [%s] \n", __func__, filename->name, current->comm);
	/* Always end with a call to jprobe_return(). */
	jprobe_return();
	/*NOTREACHED*/
	return 0;
}
 
static struct jprobe my_jprobe = {
	.entry = (kprobe_opcode_t *) my_do_execveat_common
};
 
static int my_probe_init(void)
{
	int ret;
 
	my_jprobe.kp.addr =
		(kprobe_opcode_t *) kallsyms_lookup_name("do_execveat_common");
	if (my_jprobe.kp.addr == NULL) {
		pr_err("Couldn't find %s to plant jprobe\n", "do_execveat_common");
		return -1;
	}
 
	ret = register_jprobe(&my_jprobe);
	if (ret < 0) {
		pr_err("register_jprobe failed, returned %d\n", ret);
		return -1;
	}
	pr_info("Planted jprobe at %p, handler addr %p\n",
			my_jprobe.kp.addr, my_jprobe.entry);
 
	return 0;
}
 
static void my_probe_exit(void)
{
	unregister_jprobe(&my_jprobe);
	pr_info("jprobe unregistered\n");
}
 
module_init(my_probe_init);
module_exit(my_probe_exit);

Înregistrare Kretprobe

Kretprobes se vor insera într-un mod similar, folosind o structură de tipul struct kretprobe și funcțiile register_kretprobe, unregister_kretprobe.

static int ret_handler(struct kretprobe_instance *ri, struct pt_regs *regs);
 
static struct kretprobe my_kretprobe = {
         .handler = ret_handler,
};

Resurse utile

Linux

1) Pentru mai multe informații despre printk consultați laboratorul 3
2) Nivelul implicit setat pe consolă se poate configura din /proc/sys/kernel/printk; spre exemplu, comanda echo 8 > /proc/sys/kernel/printk va face ca toate mesajele din kernel să fie afișate la consolă
4) Pentru a verifica / modifica ce fișiere sunt folosite pentru logarea erorilor fiecărui nivel (loglevel), inspectați fișierul de configurare pentru syslogd, /etc/syslog.conf
so2/laboratoare/lab02.txt · Last modified: 2017/03/05 19:21 by octavian.purdila
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0