Laborator 2: Exerciții

Pentru desfășurarea laboratorului pornim de la arhiva de sarcini a laboratorului. Descărcăm și decomprimăm arhiva în directorul so2/ din directorul home al utilizatorului student de pe sistemul de bază (stația asgard):

student@asgard:~$ cd so2/
student@asgard:~/so2$ wget http://elf.cs.pub.ro/so2/res/laboratoare/lab02-tasks.zip
student@asgard:~/so2$ unzip lab02-tasks.zip
student@asgard:~/so2$ tree lab02-tasks

În cadrul directorului lab02-tasks/ se găsesc resursele necesare pentru dezvoltarea exercițiilor de mai jos: fișiere schelet de cod sursă, fișiere Makefile și Kbuild, scripturi și programe de test.

Vom dezvolta exercițiile pe sistemul de bază (stația asgard) și apoi le vom testa pe mașina virtuală QEMU. După editarea și compilarea unui modul de kernel îl vom copia în directorul dedicat pentru mașina virtuală QEMU folosind o comandă de forma

student@asgard:~/so2$ cp /path/to/module.ko ~/so2/qemu-so2/fsimg/root/modules/

unde /path/to/module.ko este calea către fișierul obiect aferent modulului de kernel. Apoi vom porni, din directorul ~/so2/qemu-so2/, mașina virtuală QEMU folosind comanda

student@asgard:~/so2/qemu-so2$ make

După pornirea mașinii virtuale QEMU vom putea folosi comenzi în fereastra QEMU pentru a încărca și descărca modulul de kernel:

# insmod root/modules/module-name.ko
# rmmod root/modules/module-name

unde module-name este numele modulului de kernel.

Pentru dezvoltarea laboratorului, este recomandat să folosim trei terminale sau, mai bine, trei tab-uri de terminal. Pentru a deschide un nou tab de terminal folosim combinația de taste Ctrl+Shift+t. Cele trei tab-uri de terminal îndeplinesc următoarele roluri:

  1. În primul tab de terminal dezvoltăm modulul de kernel: editare, compilare, copiere în directorul dedicat pentru mașina virtuală QEMU. Lucrăm în directorul aferent rezultat în urma decomprimării arhivei de sarcini a laboratorului.
  2. În al doilea tab de terminal pornim mașina virtuală QEMU și apoi testăm modulul de kernel: încărcare/descărcare modul, rulare teste. Lucrăm în directorul aferent mașinii virtuale: ~/so2/qemu-so2/.
  3. În al treilea tab de terminal pornim minicom sau un server UDP care să primească mesajele de netconsole. Nu contează în ce director ne aflăm. Folosim comanda
    student@asgard:~$ nc -l -p 6000 -u

Citiți cu atenție toate precizările unui exercițiu înainte de a începe rezolvarea acestuia.

Linux

0. [0.5p] Intro

Term search: Folosiți cscope sau LXR pentru a localiza în sursele nucleului Linux locul de definiție al următoarelor simboluri:

  • macro-urile module_init și module_exit. Ce realizează cele două macro-uri? Ce reprezintă init_module respectiv cleanup_module?
  • variabila ignore_loglevel. La ce este folosită această variabilă?

Pentru cscope parcurgeți secțiunea cscope din primul laborator.

Dacă aveți probleme în folosirea cscope este posibil să nu fie bine obținuți indecși de căutare de simboluri. Pentru a regenera fișierele de indexare folosiți comanda de mai jos în codul sursă al nucleului:

make ARCH=x86 cscope

Atunci când folosiți cscope, va trebui să vă aflați în directorul so2/linux-4.9.11/ din directorul home al utilizatorului student.

Atunci când căutați o structură folosind cscope în Vim folosiți doar numele structurii. Adică pentru a căuta definiția structurii struct module folosiți comanda

vim -t module

sau, în Vim, construcția

:cs f g module

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 :cscope find g <symbol_name> (unde <symbol_name> este numele simbolului.

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.

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ă). Este locul în care este definit simbolul de tip structură căutat.

La fel, dacă ați găsit mai multe match-uri și dacă ai deschis o subfereastră cu toate match-urile (folosind :copen) și dacă sunteți în căutarea locului de definiție a unui macro, este indicat să căutați în subfereastră (folosind /slash) după șirul define (șirul care indică locul de definiție a unui macro).

1. [1p] Module

Pentru a putea lucra cu modulele de kernel, vom realiza următorii pași, descriși și mai sus în pagină:

  1. Vom compila modulul de kernel. Adică vom rula, în directorul în care se găsesc sursele și fișierele de tip Makefile și Kbuild aferente modulului, comanda:
    make
  2. Vom copia modulul în directorul din care va fi generat sistemul de fișiere al mașinii virtuale. Adică vom folosi comanda cp <nume-modul>.ko ~/so2/qemu-so2/fsimg/root/modules/.
  3. Vom porni mașina virtuală. Adică vom rula, din cadrul directorului aferent mașinii virtuale (adică ~/so2/qemu-so2/) comanda
    make

Realizați pașii de mai sus pentru modulul din subdirectorul 1-2-test-mod/ din arhiva laboratorului. Urmăriți fișierele din subdirector. Apoi urmați pașii de mai sus pentru a porni mașina virtuală cu fișierul modul hello_mod.ko în sistemul de fișiere al mașinii virtuale. În cadrul mașinii virtuale, fișierul modul se găsește în directorul /root/modules/.

Parcurgeți secțiunea Compilarea modulelor de kernel din laborator.

Apoi urmați pașii uzuali în foosirea unui modul de kernel:

  1. Încărcați modulul în kernel.
  2. Listați modulele din kernel și verificați existența modulului curent.
  3. Descărcați modulul din kernel.
  4. Vizualizați mesajele afișate de modul la încărcarea, respectiv descărcarea din nucleu folosind comanda dmesg.

Parcurgeți secțiunea Încărcarea/descarcarea unui modul de kernel din laborator.

La descărcarea unui modul din kernel poate fi precizat doar numele modulului (fără extensie).

Este indicat să facem curat în locul în care am lucrat. Pentru aceasta, pe mașina fizică, în subdirectorul 1-2-test-mod/ în care ați compilat sursele modulului, pentru a curăța directorul rulați comanda

make clean

2. [1p] Printk

Urmăriți consola mașinii virtuale. De ce mesajele nu au fost afișate direct la consola mașinii virtuale?

Inspectați fișierul cod sursă. Modificați fișierul cod sursă astfel încât mesajele să fie afișate direct la consolă.

Citiți secțiunea Printk Debugging din laborator și urmăriți precizările referitoare la folosirea funcției printk.

Pentru aceasta va trebui să editați opțiunile de bootare din fișierul /home/student/so2/qemu-so2/Makefile, și să adăugați opțiunea de boot ignore_loglevel pe linia care începe cu append root...

Compilați modulul. Încărcați și apoi descărcați modulul din kernel. Mesajele sunt afișate la consola mașinii virtuale. Dacă ați deschis pseudo-terminalul indicat de qemu (pentru char device redirected to /dev/pts/19 (label virtiocon0) folosiți minicom -D /dev/pts/19) atunci veti observa mesajele și acolo.

3. [1p] Error

Intrați în directorul 3-error-mod/. Obțineți modulul de kernel asociat. De ce au apărut erori de compilare? Hint: Cu ce diferă acest modul de modulul precedent?

Modificați modulul pentru a rezolva cauza apariției acelor erori. Compilați, încărcați și descărcați modulul.

4. [1p] Sub-Module

Intrați în directorul 4-multi-mod/. Inspectați fișierele sursă C: mod1.c și mod2.c. Exemplul este unul academic (modulul 2 conține doar definiția unei funcții folosite de modulul 1).

Creați un fișier Kbuild care să conducă la crearea fișierului-modul multi_mod.ko pornind de la cele două fișiere sursă C. Hint: Citiți secțiunea Compilarea modulelor de kernel din laborator.

Compilați, încărcați și descărcați modulul. Mesajele sunt afișate corespunzător la consolă.

5. [1.5p] Kernel Oops

Intrați în directorul 5-oops-mod/ și inspectați fișierul sursă C. Observați unde va apărea problema. Adăugați în fișierul Kbuild opțiunea -g pentru compilare.

Parcurgeți secțiunea Compilarea modulelor de kernel din laborator.

Compilați modulul asociat și încărcați-l în kernel. Identificați adresa de memorie la care a apărut oops-ul.

Parcurgeți secțiunea Exemplu de kernel oops din laborator.

Pentru a identifica adresa, urmăriți mesajul de oops și extrageți valoarea registrului de tip pointer de cod/instrucțiuni (EIP).

Determinați ce instrucțiune a dus la apariția oops-ului.

Folosiți informația din /proc/modules din mașina virtuală.

Folosiți, pe mașina fizică, objdump și/sau addr2line. objdump are nevoie de suport de debugging la compilare!

Parcurgeți secțiunile objdump și addr2line din laborator.

Descărcați modulul din kernel. Observați că operația nu funcționează pentru că au rămas referințe de la modulul de kernel în cadrul nucleului din momentul apariției oops-ului; până la eliberarea acelor referințe (lucru cvasi-imposibil în cazul unui oops) modulul nu poate fi descărcat.

6. [1p] Parametri Module

Reporniti masina virtuală. Intrați în directorul 6-cmd-mod/ și inspectați fișierul sursă C cmd_mod.c. Compilați modulul asociat și încărcați modulul în kernel pentru a vedea mesajul. Descărcați apoi modulul din kernel.

Dacă nu ați pornit, porniți și netconsole pentru a captura mesajul. La nevoie încărcați și descărcați din nou modulul. Hint: Urmăriți indicațiile legate de netconsole de la începutul secțiunii.

Fără a modifica sursele, încărcați modulul în kernel, astfel încât mesajul afișat să fie Early bird gets tired. Hints: Variabila str poate fi modificată ca parametru transmis modulului. Accesati acest link.

7. [1.5p] Proc Info

Intrați în directorul 7-list-proc/. Creați un modul care să afișeze informații despre procesul curent. Numele modulului trebuie sa fie list_proc.ko.

Afișați process ID-ul (PID-ul procesului) și numele executabilului. Informațiile vor fi afișate atât la încărcarea cât și la descărcarea modulului.

Nu începeți de la zero. Copiați fișierele Makefile și Kbuild și fișierele sursă C din unul din directoarele anterioare și modificați-le corespnzător.

În kernel-ul Linux un proces este descris de structura struct task_struct.

Folositi LXR sau cscope pentru a afla conținutul unei structuri din nucleu (în cazul de față struct task_struct).

Pentru a găsi câmpul structurii ce conține numele executabilului aferent, căutați șirul executable.

Pointer-ul la structura procesului ce rulează la un moment dat în kernel este dat de variabila current (de tipul struct task_struct *).

Pentru a folosi variabila current va trebui să includeți header-ul în care este definită structura struct task_struct, adică linux/sched.h.

Compilați și încărcați modulul obținut. Descărcați modului de kernel.

Repetați apoi operația de încărcare/descărcare. Observați că diferă PID-urile proceselor afișate. Acest lucru se întâmplă pentru că la încărcarea modulului se creează un proces pornind de la executabilul /sbin/insmod iar la descărcarea modulului se creează un proces pornind de la executabilul /sbin/rmmod. Procesele vor fi diferite.

8. [2.5p] Kprobes

[0.5p] Intrați în directorul 8-kprobes/ din arhiva de resurse a laboratorului. Urmăriți fișierul sursă kprobes.c. Modulul folosește jprobes pentru a urmări apelul do_execve_common, aferent apelului de sistem exec.

Compilați și încărcați în mașina virtuală modulul de kernel rezultat (kprobes.c). Urmăriți mesajele de debug și comparați cu procesele existente în sistem.

Este chiar exmplul din laborator din secțiunea Kprobes.

[2p] Creați un modul care analizează valoarea de retur a funcției _do_fork. La întoarcerea din funcție, afișați valoarea de retur, numele și pid-ul procesului părinte și pid-ul procesului curent.

Revedeți secțiunea Kretprobes.

Puteți urmări și exemplul de cod din sursele nucleului din samples/kprobes/kretprobe_example.c.

Urmăriți comentariile marcate cu TODO din cadrul fișierului cod sursă kprobes.c.

Pentru a afișa adresa de retur în handler-ul de kretprobe (adică în funcțion my_ret_handler) folosiți funcția regs_return_value.

Pentru a obține structura de tip struct task_struct * aferentă procesului părinte al procesului curent folosiți construcția current->parent.

Procesul interceptat de kretprobe este shell-ul. În mașina virtuală procesul său părinte este procesul init/busybox (cu PID-ul 1). Valoarea întoarsă de apelul _do_fork afișată în cadrul handler-ului kretprobe este PID-ul procesului copil proaspăt creat, așa cum se întâmplă și în user space în cazul apelului fork pentru procesul părinte.

Puteți folosi comanda pstree -p pentru a afișa ierarhia de procese a sistemului și pentru a verifica astfel, informațiile afișate de handler-ul de kretprobe în modulul de kernel. Puteți, deasemenea, confirma acest lucru verificând pid-ul procesului din proba anterioară ( do_execveat_common).

Extra: Pentru acasă

1. [1KP] KDB

Intrați în directorul 9-kdb/. Activați KDB peste serială și intrați în modul KDB folosind SysRq.

Conectațivă la pseudo-terminalul conectat la virtiocon0 folosind minicom, configurați KDB pentru a folosi portul serial hvc0 (echo hvc0 > /sys/module/kgdboc/parameters/kgdboc) și activați-l folosind SysRq (Ctrl+O g). Analizați starea curentă a sistemului (help pentru a vedea comenzile KDB disponibile). Continuați execuția kernelului folosind comand go.

Inserați modulul hello_kdb. Modulul va simula un bug la scrierea în fișierul /proc/hello_kdb_bug. Pentru a simula un bug folosiți comanda de mai jos:

echo 1 > /proc/hello_kdb_bug 

În urma rulării comenzii de mai sus la fiecare bug/panic nucleul se oprește și intră în modul debug.

Analizați stacktrace-ul și determinați codul care a generat bugul. Cum putem afla din KDB adresa la care a fost încărcat modulul?

În paralel, folosiți GDB într-o nouă fereastră pentru a vizualiza codul pornind de la informațiile din KDB. Hint: Încărcați fișierul de simboluri. Folosiți info line.

La scrierea în fișierul /proc/hello_kdb_break, modulul va incrementa variabila kdb_write_address. Intrați în KDB și setați un breakpoint pentru fiecare acces de scriere al variabilei kdb_write_address. Reveniți în kernel pentru a declanșa o scriere folosind:

 echo 1 > /proc/hello_kdb_break 

2. [1KP] PS Module

Modificați modulul creat la exercițiul Proc Info pentru a afișa informații despre toate procesele din sistem la încărcarea modulului, nu doar procesul curent. Comparați apoi rezultatul obținut cu ieșirea comenzii ps.

Procesele din sistem sunt structurate într-o listă circulară.

Macrourile de tipul for_each_... (ca de exemplu for_each_process) sunt utile în situațiile în care se dorește parcurgerea elementelor dintr-o listă.

Pentru înțelegerea modului de folosire a unei funcții sau a unui macro folosiți LXR sau Vim și cscope și căutați scenarii de utilizare.

3. [1KP] Memory Info

Creați un modul de kernel care să afișeze zonele de memorie virtuală ale procesului curent; pentru fiecare zonă de memorie va afișa adresa de start și adresa de sfârșit.

Porniți de la un modul existent.

Investigați structura struct task_struct, structura struct mm_struct și structura struct vm_area_struct. O zonă de memorie este indicată de o structură de tipul

Să includeți header-ele în care sunt definite structurile necesare.

4. [2KP] Dynamic Debugging

Intrați în directorul 10-dyndbg/ și compilați modulul dyndbg.ko.

Familiarizați-vă cu sistemul de fișiere debugfs montat în /debug și analizați conținutul fișierului /debug/dynamic_debug/control. Inserați modulul dyndbg.ko și observați noul conținut al fișierului dynamic_debug/control.

Ce apare în plus? Rulați

 grep dyndbg /debug/dynamic_debug/control 

Configurați dyndbg astfel încât, la descărcarea modulului să fie afișate doar mesajele marcate ca “Important” din funcția my_debug_func. Exercițiul va filtra doar apelurile pr_debug - printk fiind afișate mereu.

Specificați două moduri prin care puteți realiza filtrarea. Hint: Citiți secțiunea Dynamic debugging și analizați opțiunile dyndbg (ex. line, format).

Realizați filtrarea și revizualizați fișierul dynamic_debug/control. Ce s-a schimbat? Cum vă dați seama ce apeluri sunt activate? Hint: Dyndbg flags. Descărcați apoi modulul și vizualizați mesajele.

5. [1KP] Dyndbg la inițializare

După cum ați observat, apelurile pr_debug pot fi activate/filtrate doar după inserarea modulului. În unele situații ar fi util să putem vizualiza și mesajele de la inițializarea modulului. Acest lucru poate fi realizat cu ajutorul unui parametru implicit (fake) numit dyndbg ce poate fi transmis ca argument la inițializarea modulului. Prin acest parametru se pot adăuga/șterge flaguri dyndbg. Hint Recitiți ultima parte a secțiunii Dynamic debugging și observați flagurile disponibile (ex: +/- p).

Citiți secțiunea Debug Messages at Module Initialization Time și inserați modulul astfel încât mesajele din my_debug_func (apelată în dyndbg_init) să fie afișate și la inițializare. Warning: în mașina din laborator va trebui să folosiți insmod în loc de modprobe.

Fără a descărca modulul, dezactivați apelurile pr_debug. Hint: puteți șterge flagurile setate. Descărcați modulul.

Soluții

so2/laboratoare/lab02/exercitii.txt · Last modified: 2017/03/05 19:23 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