Laboratorul 07. Kernel Build System

Kernel-ul reprezintă o parte a sistemului de operare responsabilă cu accesul la hardware și managementul dispozitivelor dintr-un sistem de calcul (ex: procesoul, memoria, dispozitivele de I/O). De asemenea, el are rolul de a simplifica accesul la diferitele dispozitive hardware, oferind o interfață generică pentru aplicații prin intermediul system-call-urilor. În spatele interfeței generice se află porțiuni din kernel, numite drivere, care implementeză comunicația cu dispozitivele hardware. Un alt rol al kernel-ului este de a izola aplicațiile între ele, atât pentru stabilitatea sistemului, cât și din considerente de securitate.

Architectura unui sistem de operare [By Bobbo (Own work) [CC-BY-SA-3.0 (http://creativecommons.org/licenses/by-sa/3.0) or GFDL (http://www.gnu.org/copyleft/fdl.html)], via Wikimedia Commons]
Architectura unui sistem de operare

Pentru a îndeplini toate aceste sarcini, codul kernel-ului rulează într-un mod special de lucru al procesorului, fapt care îi permite să execute o serie de instrucțiuni privilegiate. Acest mod privilegiat de lucru nu este accesibil aplicațiilor obișnuite. Spunem că aplicațiile rulează în user-space (modul neprivilegiat), iar kernel-ul rulează în kernel-space (modul privilegiat).

Linux

Linux este numele unui kernel creat de către Linus Torvalds, care stă la baza tuturor distribuțiilor GNU/Linux. Inițial, el a fost scris pentru procesorul Intel 80386 însă, datorită licenței permisibile, a cunoscut o dezvoltare extraordinară, în ziua de astăzi el rulând pe o gamă largă de dispozitive, de la ceasuri de mână până la super-calculatoare. Această versatilitate, cât și numărul mare de arhitecturi și de periferice suportate, îl face ideal ca bază pentru un sistem embedded.

Arhitecturile suportate de kernel-ul Linux se pot afla listând conținutul directorului arch din cadrul surselor.

Linux este un kernel cu o arhitectură monolitică, acest lucru însemnând că toate serviciile oferite de kernel rulează în același spațiu de adresă și cu aceleași privilegii. Linux permite însă și încărcarea dinamică de cod (în timpul execuției) în kernel prin intermediul modulelor. Astfel, putem avea disponibile o multime de drivere, însă cele care nu sunt folosite des nu vor fi încarcate și nu vor rula. Spre deosebire de aplicații însă, care rulează în modul neprivilegiat (user-space) și nu pot afecta funcționarea kernel-ului, un modul are acces la toată memoria kernel-ului și se execută în kernel-space (poate executa orice instrucțiune privilegiată). Un bug într-un modul sau un modul malițios poate compromite întregul sistem.

Dezvoltarea kernel-ului Linux se face în mod distribuit, folosind sistemul de versionare Git. Versiunea oficială a kernel-ului, denumită mainline sau vanilla este disponibilă în repository-ul lui Linus Torvalds, la adresa https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git.

Versiunea oficială este însă rar folosită într-un sistem embedded nemodificată. Este foarte comun ca fiecare sistem să folosească o versiune proprie a kernelului (numită un tree) bazată mai mult sau mai puțin pe versiunea oficială. Datorită licenței GPLv2 a kernelului, însă, orice producător care folosește o versiune modificată a kernelului este obligat să pună la dispoziție modificările aduse.

Aceste modificări sunt puse la dispoziție sub formă de patch-uri care trebuie aplicate unei anumite versiuni de kernel. O altă modalitate, care este folosită și de către fundația RaspberryPi, este de a publica un repository de Git cu versiunea modificată (un tree alternativ). Datorită modelului distribuit de dezvoltare suportat de Git, această a doua metodă are avantajul că permite dezvoltarea ușoară în paralel a celor două versiuni. Modificările făcute într-una pot fi portate și în cealaltă, iar Git va ține minte ce diferențe există în fiecare versiune. Cele două versiuni sunt de fapt două branch-uri de dezvoltare, care se întâmplă să fie găzduite pe servere diferite.

Kernel-ul folosit pe RaspberryPi, care include suportul pentru SoC-ul Broadcom BCM2835 folosit de acesta, se găsește la adresa https://github.com/raspberrypi/linux.git.

Linux kernel build system

Pentru compilare și generarea tuturor componentelor kernel-ului (ex: imaginea principală - vmlinux, module, firmware) Linux folosește un sistem de build dezvoltat o dată cu kernel-ul, bazat pe utilitarul make. Acest sistem de build însă nu seamănă cu clasicul config/make/make install, deși ambele sunt bazate pe utilitarul make.

Toți pașii de compilare sunt implementați ca target-uri pentru make. De exemplu, pentru configurare se poate folosi target-ul config. Acestă metodă de configurare însă nu este recomandată deoarece oferă o interfață foarte greoaie de configurare a kernel-ului.

Target-ul help oferă informații despre aproape toate operațiile suportate de către sistemul de build.

$ make help

Operațiile oferite sunt grupate în diferite categorii:

  • cleaning - conține target-uri pentru ștergerea fișierelor generate la compilare
    • spre deosebire de clean, mrproper șterge în plus toate fișierele generate, plus configurarea și diferite fișiere de backup
  • configuration - conține diferite target-uri pentru generarea unei configurări a kernelului; există target-uri care permit:
    • generarea unui fișier de configurare nou; ex: defconfig, allmodconfig, …
    • actualizarea unui fișier de configurare existent; ex: olddefconfig, localyesconfig, …
    • editarea unui fișier de configurare existent; ex: menuconfig, nconfig, …
  • generic - conține target-uri generice; există target-uri pentru:
    • compilare; ex: all - target-ul implicit care este executat la o invocare simplă a lui make, iar vmlinux și modules compilează imaginea kernel-ului și, respectiv, modulele selectate
    • instalare; ex: headers_install, module_install, …
    • informații; ex: kernelrelease, image_name, …
  • architecture dependent - conține target-uri dependente de architectură, care diferă în funcție de arhitectura selectată; există target-uri pentru;
    • generarea de imagini în diferite formate: compresate (zImage și bzImage), pentru U-Boot (uImage), …
    • generarea de configurări implicite pentru sisteme bazate pe arhitectura selectată; ex: *_defconfig

Arhitectura care va fi compilată este selectată de variabila de mediu ARCH.

$ ARCH=x86 make [<targets>]
sau
$ make ARCH=x86 [<targets>]

Dacă arhitectura nu este setată explicit, se folosește implicit arhitectura sistemului pe care se face compilarea.

În cele mai multe situații se dorește compilarea unui kernel pentru un sistem deja existent, fie pentru a adăuga sau elimina funcționalități sau pentru a actualiza versiunea de kernel folosită. În aceste cazuri folosirea target-urilor de generare a unei configurații noi, chiar și a celor care generează o configurație implicită pentru arhitectura noastră nu sunt neapărat utile. Este posibil ca kernel-ul existent să aibă deja o configurație personalizată care se dorește doar a fi actualizată/modificată folosind target-urile de editare.

Un kernel care rulează poate conține fișierul de configurare (de obicei în format comprimat gzip) din care a fost compilat, dacă această funcționalitatea a fost selectată la build. Acest fișier se regăsește în /proc/config.gz. Tot ce rămâne este să extragem acest fișier și să-l modificăm conform dorințelor.

Device Tree Structure

Device Tree-ul este o structura de date, la nivelul kernel-ului, care descrie componentele hardware prezente pe sistem, care nu pot fi descoperite automat de kernel. El este prezent pe sistemele cu arhitecturi ARM, dar si pe alte sisteme (nu pe cele care se bazeaza pe arhitectura x86). Fiecare intrare a device tree-ului descrie o componenta individuala.

Device Tree-ul este stocat in 2 fisiere: .dtb (device tree blob), in format binar, si .dts (device tree source), in format text. Ambele tipuri de fisiere se gasesc in arch/<arhitectura>/boot/dts, fiind generate de target-ul dtbs al comenzii make.

Testare

Pentru a testa un nou kernel acesta trebuie instalat pe target. Această procedură diferă de la sistem la sistem, iar pe RaspberryPi constă în copierea acestuia pe card-ul SD în partiția de boot sub numele de kernel8.img. În momentul dezvoltării și testării unui nou kernel, instalarea fiecărei versiuni a acestuia pe target reprezintă un bottleneck major.

O alternativă la instalarea kernel-ului pe target o reprezintă încărcarea acestuia prin rețea direct pe de host-ul folosit la dezvoltare, dacă există suport din partea bootloader-ului. Din păcate, bootloader-ul implicit de pe RaspberryPi nu are suport pentru a încărca o imagine de kernel de pe rețea. Un bootloader care oferă însă acestă facilitate este U-Boot [2], el folosind protocolul TFTP pentru a boota o imagine de kernel prin rețea.

Instalare pachete/programe din surse

De multe ori ne lovim de problema instalării unui pachet sau a unui program pe care îl găsim doar pe un repository public, de cele mai multe ori bazat pe Git. Astfel, pentru a ne putea folosi de acel pachet/program, trebuie să cunoaștem următoarele utilitare:

Git

Opțiuni și comenzi git:

  1. git clone <repo> - va aduce toate fișierele conținute de repository-ul repo pe mașina locală.
  2. git pull - actualizează un repository deja clonat local la ultima versiune remote.
  3. git checkout <branch> sau git checkout <tag> - actualizează fișierele locale la versiunea indicată de branch sau de tag. Un repository poate avea mai multe branch-uri, care pot fi văzute ca niște versiuni diferite ale repository-ului. Un exemplu uzual de folosire a branch-urilor este pentru organizarea diferitelor versiuni ale aceluiași program.
  4. git apply <patch file> - aplică pe fișierele locale modificările conținute de fișierul patch.

Exerciții

0. Pregăriri

  • Descarcati ultima varianta de kernel Linux pentru Raspberry Pi, de aici.
    git clone https://github.com/raspberrypi/linux.git --depth=1
  • Instalati pachetele bc, bison, flex, libssl-dev, libc6-dev, libncurses5-dev, crossbuild-essential-arm64.
  • Generati configurarea implicita a kernel-ului (tip: defconfig).
    cd linux/
    export KERNEL=kernel8
    make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- bcm2711_defconfig
  • Modificati din menuconfig numele imaginii de kernel

General Setup → Local Version

make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- menuconfig

  • Verificati daca driver-ul de mmc este activat (nu ca modul)

trebuie sa vedeti MMC [=y]

  • Compilati nucleul, device tree blob-urile si modulele de nucleu.
    make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- Image Image.gz modules dtbs -j$(nproc)

Procesul de compilare va dura destul de mult. Daca folositi o masina virtuala, dati-i cat mai multe nuclee, pentru a reduce timpul

  • Cat se compileaza nucleul, inspectati fisierul .dts corespunzator placii RaspberryPI 4B.
    arch/arm64/boot/dts/broadcom/bcm2711-rpi-4-b.dts
  • Kernelul nostru experimental se instalează folosind următorii pași:
    # se presupune că aveți deja montate imaginea / cardul SD al RPi-ului (citiți mai jos!)
    sudo env PATH=$PATH make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- INSTALL_PATH=<path_catre_partitia_de_boot> install
    sudo env PATH=$PATH make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- INSTALL_MOD_PATH=<path_catre_partitia_de_rootfs> modules_install
    
    # Notă: nu suprascrieți device tree-ul deoarece o versiune greșită ar strica u-boot-ul :D
    # Dar așa s-ar copia:
    #sudo cp arch/arm64/boot/dts/broadcom/bcm2711-rpi-4-b.dtb <path_catre_partitia_de_boot>/
    #sudo cp arch/arm64/boot/dts/overlays/bcm2711-rpi-4-b.dtb* <path_catre_partitia_de_boot>/overlays/
    
    # Redenumim imaginea de kernel (e deja cea compresata)
    sudo mv <path_catre_partitia_de_boot>/vmlinuz-<versiunea-noua> <path_catre_partitia_de_boot>/vmlinuz-student
    
    # Demontam partitiile
    sudo umount <path_catre_partitia_de_boot>
    sudo umount <path_catre_partitia_de_rootfs>
    
    # Oprim ums
  • Notă: dacă aveți probleme cu USB device passthrough-ul pe Raspberry PI, este suficient să copiem DOAR fișierul vmlinuz pe partiția de boot, cea FAT32. Pentru aceasta, bootați-l în modul ums din U-Boot, conectat la USB Type-C) și copiați DOAR fișierul arch/arm64/boot/Image.gz sub numele de vmlinuz-student.

Pentru a porni ums, urmati urmatorii pasi:

  • conectati adaptorul de seriala la laptop
  • picocom /dev/ttyUSB0 -b 115200
  • conectati cablu de alimentare
  • la un moment dat va aparea Hit any key to stop autoboot:; apasati orice tasta
  • daca nu ati intrat in meniul de U-Boot, apasati Ctrl-C
  • din meniul de U-Boot, dati comanda ums mmc 0
  • partitiile ar trebui sa fie vizibile: /dev/sda1(root) si /dev/sda2(rootfs)
  • daca nu vi le monteaza sistemul de operare automat, montati-le

U-boot din imaginea nouă este configurat să booteze automat vmlinuz-student, dacă există!

Acesta va aștepta 10 secunde, permițând să întrerupeți procesul pentru a putea re-accesa modul ums pentru a rescrie imaginea pe viitor.

Puteți vedea codul sursă al scriptului de u-boot aici.

Dacă nu ați copiat modulele externe pe rootfs-ul de pe Raspberry PI al noului kernel, Linux se va plânge când bootează că lipsesc o parte din modulele necesare de anumite dispozitive hardware / software non-esențiale (ahem: multe sunt, defapt, necesare pentru a avea suport pentru rețelistică în Linux), însă procesul de boot ar trebui să se finalizeze.

Referințe

si/laboratoare/07.txt · Last modified: 2023/11/22 12:59 by florin.stancu
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