This shows you the differences between two versions of the page.
si:laboratoare:06 [2024/10/27 09:23] florin.stancu removed |
si:laboratoare:06 [2024/11/11 12:43] (current) florin.stancu [Exerciții] |
||
---|---|---|---|
Line 1: | Line 1: | ||
- | ====== Laboratorul 06. Root filesystem și servicii de sistem ====== | + | ====== Laboratorul 06. The embedded boot process ====== |
- | ===== Root file system ===== | + | Atunci când un microprocesor primește semnalul de reset (prin alimentare ori transmiterea unor comenzi / întreruperi interne etc.), acesta începe să ruleze un program inițial numit bootloader. |
+ | Sistemele moderne folosesc un proces de boot multi-stagiu, însă primul este mereu încărcat dintr-un ROM (Read Only Memory) care, de cele mai multe ori, este integrat în același chip. | ||
- | Pentru ca sistemul să fie inițializat corect după pornirea kernel-ului, este necesar ca toate script-urile și executabilele necesare pentru a porni daemon-ul de inițializare, //init//, și restul proceselor //user-space//, să existe în anumite locații în sistemul de fișiere. Acest sistem minimal de fișiere necesar la inițializare poartă numele de **root file system** sau **rootfs**. În cazul sistemelor Unix, //rootfs//-ul are ca rădăcină directorul ''/'' și înglobează o serie de directoare ce contin restul de fișiere necesare. De obicei, în directorul ''/'' nu se află niciun fișier, doar subdirectoare. Aceste subdirectoare sunt organizate în funcție de fișierele pe care le conțin: | + | ==== Procesul de boot standard ARM ==== |
- | * ''/bin'' - conține programe și comenzi necesare la inițializare ce pot fi folosite apoi de către un utilizator neprivilegiat în timpul unei sesiuni | + | Arhitectura ARMv8 propune următoarea structură: |
- | * ''/sbin'' - conține programe asemănătoare cu cele din /bin ca utilitate, dar care pot fi utilizate în general doar de utilizatorul privilegiat //root// | + | |
- | * ''/etc'' - conține fișierele de configurare specifice sistemului | + | |
- | * ''/lib'' - conține bibliotecile folosite de programele din //rootfs// (cele din ''/bin'' și ''/sbin'') | + | |
- | * ''/dev'' - conține referințe la toate dispozitivele periferice; aceste referințe sunt în general fișiere speciale | + | |
- | * ''/boot'' - conține fișierele necesare bootării și imaginea kernel-ului; este posibil ca acestea să fie păstrate pe un sistem de fișiere separat de //rootfs// | + | |
- | * ''/tmp'' - conține fișiere temporare și este de obicei curățat la repornirea sistemului | + | |
- | * ''/opt'' - conține în principal aplicațiile third-party | + | |
- | * ''/proc'', ''/usr'', ''/mnt'', ''/var'', ''/home'' - sunt directoare care, în general, reprezintă puncte în care se pot monta alte sisteme de fișiere pentru a stoca log-uri, biblioteci și alte aplicații și programe folosite de către utilizatori. | + | |
- | <note> | + | {{:si:laboratoare:arm_booting_process.png?700}} |
- | Organizarea unui rootfs Linux în termeni de directoare este bine definită de standardul ierarhiei sistemului de fișiere: [[https://refspecs.linuxfoundation.org/fhs.shtml|Filesystem Hierarchy Standard]], ultimul standard FHS 3.0 fiind definit in 2015. | + | |
- | </note> | + | |
- | Standardul Unix recomandă ca //rootfs//-ul să aibe dimensiuni relativ mici, deoarece orice problemă sau corupere a acestuia poate împiedica inițializarea corectă a sistemului. Dacă, totuși, rootfs-ul devine corupt, există modalități prin care poate fi remediat. O soluție des folosită este de a monta acest sistem de fișiere într-un alt sistem, funcțional, pentru a putea fi explorat și verificat. | + | Se observă numărul mare de pași decât metoda naivă pe care sistemul trebuie să-i realizeze până să pornească kernelul sistemului de operare. |
+ | Acest lucru se datorează punerii accentului pe securitatea soluțiilor incorporate (prin tehnologia ARM TrustZone, de care doar vom menționa scurt, pe Raspberry PI nefiind implementată în totalitate), însă necesitatea a 3 stagii diferite este dată de un motiv simplu: memoria disponibilă în diferitele momente ale procesului. | ||
- | În cazul sistemelor embedded, de multe ori spațiul de stocare pe care îl avem la dispoziție este deja limitat: < 32MB. De cele mai multe ori, o bună parte din acest spațiu este ocupat de imaginea kernel-ului. De aceea, în majoritatea cazurilor, //rootfs//-ul pentru aceste sisteme este fie compus doar din minimul de programe necesar pentru funcționare, fie este stocat pe un server accesibil prin rețea și montat de către kernel la inițializare (e.g., folosind protocolul NFS). | + | Așadar, prima dată se începe prin rularea primului stagiu, ''BL1'', stocat în memoria ROM. Acesta va încărca, de pe o memorie flash externă (de obicei, prin SPI: eMMC sau SD), următoarele stagii. Dintre acestea, ''BL2'' este, de obicei, un firmware foarte mic (//10-100KB//) ce este încărcat în memoria SRAM a SOC-ului (care, uneori, funcționează și pe post de cache) și care, mai departe, inițializează toate perifericele chip-ului (printre care, foare important este DRAM-ul -- folosit de următoarele stagii cu consum ridicat de RAM!). |
- | ===== Pseudo Filesystems ===== | + | Stagiile ''BL3x'' devin opționale, totul depinzând dacă ''BL2'' conține funcționalitatea de a porni și Kernel-ul de Linux și este suficient de configurabil pentru a putea acoperi o mare parte a cazurilor de utilizare ale sistemului (ceea ce este adevărat pentru RPI, însă deseori nu pentru celelalte SoC-uri) sau dezvoltatorul software dorește să beneficieze de funcționalități avansate de boot (aici, ''BL31'' poate oferi partiționare A/B cu toleranță la defecte, actualizări la distanță etc.) sau încărcarea unui sistem de operare securizat (Trusted OS, la pasul ''BL32''). |
- | Un kernel Linux expune mai multe pseudo-sisteme de fișiere, precum ''/dev'' (unde există noduri pentru toate dispozitivele detectate), ''/proc'' sau ''/sys''. Unele aplicații ce gestionează sistemul de operare au nevoie ca aceste mountpointuri să existe, ele fiind folosite pentru a interfața cu kernelul (mai ales în sisteme incorporate,unde programele trebuie să comunice cu hardware-ul). | + | În final, ultimul pas al bootloaderului va fi să citească configurația (de pe o partiție de boot sau dintr-o altă memorie ROM re-programabilă) și componentele de rulare (kernel, initrd, device tree blob -- de cele mai multe ori, toate 3 fiind necesare) ale sistemului de operare, să încarce în RAM și apoi să paseze execuția CPU-ului către kernel. |
- | ==== Sistemul virtual proc ==== | + | ==== Procesul de boot al Raspberry PI ==== |
- | Acest sistem de fișiere există de când există și Linux-ul și permite expunerea de statistici despre procesele rulate în sistem dar și ajustarea la runtime a diverșilor parametrii ce implică managementul proceselor, ale memoriei etc. Este utilizat de majoritatea aplicațiilor standard. Aplicații precum **ps** și **top** nu pot funcționa fără acest filesystem. | + | Deși procesul de boot diferă între //Raspberry PI// versiuni mai vechi sau egale cu **3** (ce folosește SoC-ul ''BCM2837'') și cele **după 4** (cu ''BCM2711'', și, pe viitor, v5), stagiile se încadrează în arhitectura propusă de ARM. |
- | Pentru a monta **proc** se poate folosi comanda: <code> mount -t proc nodev /proc </code> | + | Toate versiunile procesoarelor Broadcom folosite la RPI încep prin rularea stagiului ''BL1'' pe microcontrollerul de gestionare a GPU-ul integrat (da, pe bune!), care, mai departe, inițializează memoria cache, interfața SPI (pentru accesarea memoriei flash din eMMC / card SD) și, folosind o bibliotecă incorporată de citire de pe partiții FAT32, scanează după existența firmware-ului pentru următorul stagiu, ''BL2'', pe care îl va încărca în cache-ul L2 al procesorului (DRAM-ul încă nu a fost inițializat). |
- | Printre conținutul aflat în **proc** putem găsi: | + | Ordinea de scanare a perifericelor pentru continuarea procesului de boot (e.g., SD Card / eMMC, extenal SPI, USB Mass Storage, LAN boot) diferă în funcție de starea unor GPIO-uri sau a unor regiștri OTP (One Time Programmable). |
- | * ///proc/interrupts, /proc/iomem, /proc/cpuinfo//: ce conțin informații specifice de device | + | |
- | * ///proc/<pid>, /proc/3840/cmdline//: conține detalii despre fișierele deschise de proces, utilizarea memoriei, procesorului etc. | + | |
- | * ///proc/cmdline//: conține linia de comandă cu care a fost pornită imaginea kernel-ului | + | |
- | * ///proc/sys//: conține fișiere care pot fi scrise pentru a ajusta parametrii ai kernel-ului. Poartă numele de **sysctl**. Mai multe detalii despre asta aici: [[https://www.kernel.org/doc/html/latest/admin-guide/sysctl/|documentație sysctl]]. | + | |
- | ==== Sistemul virtual sys ==== | + | Așadar, un Raspberry PI are nevoie ca dispozitivul de pe care se efectuează bootarea să conțină o primă partiție FAT32 cu cel puțin firmware-urile de inițializare a platformei (''bootcode.bin'', ''start*.elf'', ''config.txt'' și altele câteva ce depind de modelul efectiv). |
- | Permite reprezentarea în userspace a viziunii pe care o are kernel-ul asupra magistralelor, dispozitivelor și driverelor din sistem. Este util pentru diverse aplicații din userspace care trebuie să enumere și să interogheze hardware-ul disponibil, de exemplu **udev** sau **mdev**: | + | <note important> |
+ | Fiecare microprocesor are propriile convenții de stabilire a adreselor de încărcare a stagiului secundar. | ||
+ | Cei de la Broadcom au ales să folosească partiții FAT32, însă majoritatea producătorilor de SoC-ul incorporate cu ARM (e.g., Allwinner, NXP) preferă să încarce o imagine specială de la un anumit offset al disk-ului (e.g., la adresa ''32KB'' de la începutul cardului SD). De aici vine și recomandarea de a crea prima partiție abia începând cu offset-ul de ''1MB''. | ||
+ | </note> | ||
- | <code>ls /sys/ | + | Pentru [[https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#boot-sequence|Raspberry PI 3]], se va încărca fișierul ''bootcode.bin'' ca stagiu secundar, care, după inițializarea RAM-ului, va citi și încărca următorul stagiu (care este, de obicei, sistemul de operare propriu-zis, însă se poate interpune un alt bootloader -- ''BL31'', e.g., [[https://www.denx.de/project/u-boot/|U-Boot]]). |
- | block bus class dev devices firmware fs kernel module power | + | |
- | </code> | + | |
- | ===== Utilitare folosite pentru crearea / inspecția unui RootFS ===== | + | La versiunile de Raspberry PI ''>=4'', ''BL2'' poate fi încărcat doar dintr-un EEPROM prin interfață SPI (și NU de pe SD / eMMC-ul extern -- însă există procedură de recovery în caz că se strică ceva), fapt ce ușurează aplicațiile care făceau network boot. De asemenea, firmware-ul este open-source (ceea ce nu era adevărat până acum). |
- | Uneori vom fi în situația în care nu avem acces la un server și imaginea de care dispunem pentru sistemul nostru nu este satisfăcătoare. În aceste condiții va trebui să ne creăm propriul //rootfs// pe care să îl scriem în memoria sistemului embedded. | + | Mai departe, //Secondary Program Loader//-ul va încărca firmware-uri adiționale pentru GPU (pentru a putea afișa text prin HDMI), va analiza conținutul fișierului ''config.txt'' și va pune în aplicare procedurile configurate (încărcarea în memorie a fișierelor de kernel / device tree / initramfs). |
- | Imaginea recomandată pentru RaspberryPi, Raspbian, conține două partiții: o partiție ''FAT'' folosită pentru boot și o partiție ''ext4'' pentru //rootfs//. Fiecare dintre aceste partiții începe de la un anumit offset în cadrul imaginii. Atunci când dorim să creăm o imagine nouă pentru RaspberryPi, trebuie să ne asigurăm că respectăm aceste partiții și formatele lor. Pașii pe care trebuie să îi urmăm sunt: | + | ==== Componentele pentru boot ale Linux ==== |
- | * Stabilirea dimensiunii imaginii și inițializarea acesteia cu zero-uri | + | După cum a fost menționat mai sus, pentru a porni un sistem de operare pe bază de Linux se folosesc, de cele mai multe ori, 3 componente: |
- | * Crearea tabelei de partiții și a celor două partiții necesare | + | |
- | * Formatarea partițiilor cu formatul corespunzător | + | |
- | * Pentru popularea rootfs-ului, putem fie să montam partiția și să copiem manual directoarele și fișierele, fie putem să copiem o partiție întreagă de pe o altă imagine | + | |
- | Pentru fiecare dintre acești pași există utilitare ce ne ajută să realizăm operațiile necesare. | + | - **Imaginea Kernel-ului** (''kernel*.img''), ce conține codul executabil al nucleului Linux; |
+ | - **Device Tree Blob-ul** (''*.dtb''): conține descrierea și configurația tuturor componentelor specifice platformei hardware (i.e., pentru un anumit model + versiune a unei plăci de bază), însă se pot codifica și anumite setări și meta-informații pentru software (e.g., kernel command line, partiția rădăcină etc.); | ||
+ | - **RamDisk inițial** (''initrd*.img''): opțional, poate conține o imagine a unui sistem de fișiere minimalist (max. câțiva zeci de MB) cu module și scripturi necesare pentru a monta sistemul de fișiere rădăcină (e.g., dacă se dorește montarea unui sistem la distanță, trebuie mai întâi să se conecteze la rețea și să primească IP prin DHCP). | ||
- | ==== dd ==== | + | Bootloaderul (ori ''BL2''-ul integrat, ori ''BL31'' -- dacă a fost inclus) va încărca aceste fișiere în DRAM-ul sistemului și va completa anumiți regiștri (cu pointerii la locația de încărcare a tuturor componentelor necesare) și va executa o instrucțiune de //branch// pentru a lansa kernelul în execuție, toate în condordanță cu [[https://www.kernel.org/doc/html//v5.8/arm64/booting.html|protocolul definit de kernel pentru arhitectura dată]]. |
- | Pentru copierea, și eventual convertirea, unui fișier, la nivel de byte, se poate folosit utilitarul //dd//.. Folosind //dd//, putem de asemenea genera fișiere de anumite dimensiuni și le putem stabili conținutul. În cazul nostru, //dd// este util pentru a inițializa o imagine și pentru a copia în acea imagine conținutul care ne interesează. Dintre parametrii lui //dd//, cei mai des utilizați sunt: | + | Obiectivul final al componentelor de boot Linux va fi să caute și să monteze sistemul de fișiere rădăcină (**roofs**-ul) ce conține toată configurația și programele din //user-space//, de unde se va lansa procesul ''init'' care va continua prin pornirea serviciilor predefinite (care pot, la rândul lor, să inițializeze dispozitive hardware noi și să ruleze procese de automatizare). |
- | * ''if'' - fișierul de intrare; dacă nu se specifică acest parametru, se va citi de la //standard input// | + | ==== U-Boot ==== |
- | * ''of'' - fișierul de ieșire; ca și la ''if'', dacă nu este specificat, se scrie la //standard output// | + | |
- | * ''count'' - numărul de blocuri de input ce vor fi copiate | + | |
- | * ''bs'' - numărul de bytes dintr-un bloc | + | |
- | <note tip> | + | U-boot este probabil cel mai popular bootloader open-source folosit atât în lumea embedded / IoT / industrial, cât și în platformele mobile (majoritatea telefoanelor pe Android). |
- | Un exemplu de utilizare a lui ''dd'' pentru a inițializa cu zerouri un fișier de o dimensiune exactă: | + | |
- | <code shell> | + | Acesta este scris în C și folosește KConfig (as expected) pentru personalizarea funcționalităților incluse pentru a se putea încadra în cerințele restrictive de memorie a anumitor sisteme. |
- | $ dd if=/dev/zero of=<file> bs=1M count=2048 | + | De asemenea, U-Boot are implementată o cantitate vastă de drivere necesare pentru a interfața cu cât mai multe periferice de pe care să se încarce sistemul de operare (SPI, MMC, USB devices, SATA / M.2 PCI-E, chiar și Rețea, prin PXE + TFTP) și conține chiar și un mini-limbaj de scripting (similar bash, însă compilat în binar) ce permite implementarea de proceduri avansate de boot cu redundanță și failover (necesare sistemelor realtime). |
- | </code> | + | |
- | Observați valorile lui ''count'' si ''bs''. Se vor copia în total 2048 de blocuri de câte 1MB fiecare, rezultând o dimensiune de 2GB. Fișierul de intrare ''/dev/zero'' este un fișier special din care se pot citi oricâte caractere ASCII NUL (0x00). | + | Noi vom folosi un U-Boot la laboratoare pentru a putea scrie SD card-ul direct de pe PC, prin conectarea Raspberry PI4-ului direct la PC prin USB OTG (fără a utiliza un cititor extern de carduri SD!), cât și pentru a avea o vizibilitate mult mai bună a procesului de boot Linux. |
- | </note> | + | |
- | ==== parted ==== | + | ===== Exerciții ===== |
- | Pentru manipularea tabelei de partiții (crearea sau ștergerea partițiilor) se poate folosi utilitarul //parted//. Acesta recunoaște și poate manipula multiple formate de partiții: DOS / SUN / GNU / GPT etc. Utilitarul primește ca parametru device-ul a cărui tabelă de partiții dorim să o modificăm. Un exemplu de apel este: | + | <note> |
+ | **În laborator, vom folosi echipamente Raspberry PI 4!** conectate prin USB Type-C și un adaptor UART la USB pentru vizualizarea consolei dispozitivului (din păcate, nu dispunem de suficiente monitoare HDMI în laborator + cabluri adaptoare). | ||
- | <code shell> | + | Înainte de a începe exercițiile, asigurați-vă că aveți cel puțin 10GB de storage disponibili în mașină virtuala de laborator. |
- | $ parted /dev/<device> | + | </note> |
- | </code> | + | |
- | Rezultatul acestei comenzi este un prompt nou (similar cu //fdisk//) în care putem folosi tasta ''m'' pentru a afișa un meniu cu opțiunile disponibile. Opțiuni utile pentru crearea și ștergerea de partiții sunt: | + | **0.** Descărcați [[https://github.com/cs-pub-ro/SI-rpi-debian-scripts/releases|de aici o arhivă unui sistem de fișiere folosit ca referință laborator]] pentru RPI4 + imaginea partiției ''rpi-boot.img'' (utilă în caz că se strică bootloaderul). |
- | + | ||
- | * ''print'' (sau simplu, ''p'') - afișează tabela curentă; | + | |
- | * ''mklabel <type>'' - șterge tot și crează o tabelă nouă de partiții (type poate fi ''msdos'', ''gpt'' etc.); | + | |
- | * ''mkpart [type] [fstype] <START> <END>'' - crează o partiție nouă (type poate fi ''primary'' / ''logical'' / ''extended''); singurii parametrii obligatorii sunt cele 2 offseturi; | + | |
- | * ''set <partition> <flag> <state>'' - setează un flag unei partiții; | + | |
- | * ''rm <NUMBER>'' - șterge o partiție; | + | |
- | * ''quit'' - iese din program (toate comenzile salvează automat). | + | |
- | Trebuie reținut că, în mod normal //parted// operează cu sectoare și nu cu octeți. Așadar, este de preferat să punem unitatea de măsură (K, M, G etc.) ca sufix la orice offset cerut de comenzi (e.g., ''120MB''). | + | * Dezarhivați arhiva într-un subdirector (prin ''tar'', folosiți argumentul ''-C'' pentru a preciza directorul destinație, însă va trebui să îl creați înainte). **Atenție**: folosiți contul de ''root'', deoarece dorim să dezarhivăm fișierele și să păstrăm permisiunile originale (imaginea este un root filesystem de Linux pre-instalat!); |
+ | * Inspectați căile ''/boot'' (rețineți / copiați într-un fișier text output-ul, e util de comparat mai încolo). | ||
+ | * Apoi, ne vom pregăti să pornim Raspberry PI 4! | ||
+ | * Asigurați-vă că firele ce conectează Raspberry PI-ul la adaptorul de serială nu sunt ieșite. Dacă da, [[https://pinout.xyz/|conectați-le corespunzător]] (UART RX (alb) / TX (verde) la TX (pin 8)/RX (pin 10) al RPI-ului!) + chemați asistentul să verifice! | ||
+ | * TLDR: începeți cu negru (GND) la pin 6 (al treilea de pe margine), apoi imediat lângă (pe același rând), firele alb + verde (în această ordine); **firul roșu SĂ RĂMÂNĂ NECONECTAT**! | ||
- | <note> | + | <note warning> |
- | * Dimensiunea standard a unui sector de disc este ''512 bytes''. Aceasta poate fi schimbată folosind opțiunea ''-b''. | + | Pentru orice eventualitate, **vă rugăm să chemați asistentul înainte de a alimenta dispozitivul la laptop!** |
- | * Există două tipuri de partiții: ''primary'' și ''extended''. Un device ce folosește sistemul de partiții ''DOS'' nu poate avea mai mult de 4 partiții primare (la GPT s-au eliminat aceste restricții). | + | |
- | * Exemplu de creare a celor 2 partiții necesare pentru o imagine bootabilă Raspberry PI:<code> | + | |
- | (parted) mklabel msdos | + | |
- | (parted) mkpart primary fat32 1MiB 120MiB | + | |
- | (parted) mkpart primary ext4 120MiB 100% | + | |
- | (parted) print | + | |
- | (parted) quit | + | |
- | </code> | + | |
- | * Observați, în exemplu anterior, la ultima partiție s-a folosit o unitate specială, procentul, ceea ce înseamnă că ultimei partiții i s-a repartizat tot spațiul rămas. | + | |
</note> | </note> | ||
- | ==== losetup ==== | + | * În final, înainte de a-l alimenta, conectați-vă adaptorul serial la USB în laptop, faceți USB passthrough la mașina virtuală (dacă este cazul) și porniți programul preferat de consolă serială (e.g., ''picocom''), folosind baud rate-ul ''115200'': <code> |
+ | sudo dmesg | tail -20 && ls -l /dev/ttyUSB* | ||
+ | picocom -b 115200 /dev/ttyUSB0 # înlocuiți cu ce dispozitiv aveți | ||
+ | </code> | ||
- | Dacă se lucrează cu imagini din fișiere, este util să le putem //monta// pentru a vedea ce conțin. Însă acest lucru nu se poate realiza pe Linux fără a avea un dispozitiv de tip bloc. Din fericire, utilitarul ''losetup'' ne poate salva în aceste situații, el permițând conectarea unui fișier imagine la un dispozitiv bloc virtual de forma ''/dev/loop<N>'', unde //N// este un număr din intervalul //[0-9]//. | + | <note important> |
+ | Din păcate, în laborator avem ca adaptoare Serial to USB niște clone chinezești de Prolific PL2303, pe care driverele oficiale Microsoft [[http://wp.brodzinski.net/2014/10/01/fake-pl2303-how-to-install/|le dezactivează automat ca fiind "counterfeit" (găsiți o soluție aici)]]. De regulă, în mașina virtuală cu Linux nu ar trebui să conteze, doar că mecanismul driverelor de a dezactiva automat dispozitivele "copiate" poate interfera cu passthrough-ul USB care se face în mod normal. :(( problema apare frecvent pe VirtualBox (pe VMware nu au fost raportate probleme). | ||
+ | </note> | ||
- | **Atenție**: ''losetup'' nu poate fi utilizat decât cu privilegii de **root** (e.g., folosiți **sudo** la toate comenzile de manipulare a dispozitivelor de sistem)! | + | **1.** Alimentați RPI-ul și urmăriți mesajele din consolă. Ar trebui să vă intre în U-Boot (preinstalat pe cardurile SD ca ''BL31''), apoi: |
- | Exemplu folosire: <code bash> | + | * Dacă apare că rulează un proces de network boot, apăsați Ctrl+C de foarte multe ori (//yep... scriptul de boot face un for cu vreo 10 iterații//) pentru a-l întrerupe; |
- | # conectăm fișierul la un dispozitiv loop | + | * După ce vă apare prompt-ul ''U-Boot> '', rulați comanda ''mmc list''. O să vă apară o listă de dispozitive MMC (ar trebui să vedeți cardul SD ca fiind detectat); încercați-le pe fiecare cu ''mmc dev <N>'' și apoi ''mmc info'' până găsiți cardul SD; |
- | losetup /dev/loop0 ./path/to/your-image.bin | + | * Apoi rulați comanda ''ums mmc <N>'', unde ''<N>'' este numărul perifericului MMC. Acesta va lansa în execuție un program care emulează un Mass Storage Device pe interfața USB Type-C folosită la conectarea la laptop; |
- | # scanăm dispozitivul bloc de partiții: | + | * Inspectați noul dispozitiv montat (nu uitați să faceți pass-through la noul device USB în mașina virtuală, dacă este cazul!). Mai precis, montați prima partiție (posibil să fie și singura) pe mașina voastră virtuală cu Linux, e.g., în /mnt. Ce fișiere există? Comparați cu partiția ''/boot'' a rootfs-ului. |
- | partprobe /dev/loop0 | + | |
- | ls -l /dev/loop0* | + | |
- | # ^ ar trebui să vedeți cele 2 partiții ale imaginii RPI: loop0p1 și loop0p2 | + | |
- | </code> | + | |
- | **Important / NU uitați:** la final, trebuie să deconectăm dispozitivul pentru a sincroniza cele scrise înapoi în fișier: ''losetup -d /dev/loop<N>''! | + | <note important> |
+ | Comanda ''ums'' este blocantă (poate fi întreruptă cu Ctrl+C). | ||
+ | Dacă folosiți un soft de virtualizare (VirtualBox / VMWare), va trebui să faceți passthrough la acest dispozitiv (se numește "Netchip USB Download Gadget"). Apoi verificați prin ''lsblk'' / ''blkid'' care este dispozitivul nou apărut în VM. | ||
- | ==== mkfs ==== | + | Cât timp rulează, vă puteți conecta cu laptopul (prin interfața USB Type-C) la cardul SD introdus în Raspberry pentru a-l inspecta / [re]scrie (cu grijă să nu suprascrieți bootloaderul u-boot). |
+ | </note> | ||
- | Crearea unei partiții nu este suficient pentru a putea stoca fișiere. Avem nevoie ca partiția să conțină și un sistem de fișiere. Utilitarul folosit pentru a crea (formata) sisteme de fișiere Linux este //mkfs//. Parametrii pe care îi primește //mkfs// sunt device-ul ce trebuie formatat și tipul sistemului de fișiere dorit, specificat cu parametrul ''-t''. | + | **2.** Dorim să compilăm **U-Boot** local (pentru a învăța cum se face și explora ce funcționalități are) și să construim imaginea de boot de la zero. |
- | Spre exemplu, comanda <code shell> | + | * Aveți nevoie de toolchain-ul pentru Aarch64 să fie în ''$PATH'' (lucru adevărat pe VM-ul de la laborator); |
- | $ mkfs -t ext4 -L MyRootFS /dev/fd0 | + | * Urmați pașii: <code> |
+ | # pe Lab VM 2023 lipsește acest pachet, instalați-l: | ||
+ | sudo apt install libssl-dev | ||
+ | git clone --branch=v2023.07.02 https://github.com/u-boot/u-boot.git | ||
+ | cd u-boot | ||
+ | # ne pregatim de compilare: | ||
+ | export CROSS_COMPILE="aarch64-linux-gnu-" | ||
+ | # initializam configul default pentru RPI4 | ||
+ | make rpi_4_defconfig | ||
+ | make menuconfig | ||
+ | # aici, căutați și activați următoarele opțiuni (ambele sunt necesare!): | ||
+ | # CONFIG_USB_FUNCTION_MASS_STORAGE=y | ||
+ | # CONFIG_CMD_USB_MASS_STORAGE=y | ||
+ | make -j4 | ||
</code> | </code> | ||
- | Va crea pe device-ul //fd0// un sistem de fișiere //ext4// cu label-ul (denumire logică) //MyRootFS//. În Linux, utilitarul //mkfs// este împărțit în câte un executabil pentru fiecare tip de sistem de fișiere suportat. Pentru a le vedea pe cele disponibile, deschideți un terminal, scrieți ''mkfs.'' (punct la coadă!) și apasați tab. | + | * O dată terminată compilarea, copiați fișierele ''u-boot.bin'' și ''arch/arm/dts/bcm2711-rpi-4-b.dtb'' din directorul lui U-Boot către un director nou creat (să zicem, ''~/rpi-boot-firmware''). |
+ | * De asemenea, stagiul secundar (''BL2'') al RPI4 are nevoie de firmware-ul GPU-ului, ce poate fi descărcat de pe GitHub de la adresele: | ||
+ | <code> | ||
+ | cd ~/rpi-boot-firmware/ | ||
+ | wget "https://github.com/raspberrypi/firmware/raw/master/boot/start4.elf" | ||
+ | wget "https://github.com/raspberrypi/firmware/raw/master/boot/fixup4.dat" | ||
+ | # însă le găsiți și pe sistemul rădăcină referință, la calea /boot/firmware | ||
+ | </code> | ||
+ | * Ultimul lucru care lipsește este fișierul ''config.txt''. Strictul necesar este următorul: <code> | ||
+ | # config.txt contents to load U-Boot as BL31: | ||
+ | arm_64bit=1 | ||
+ | kernel=u-boot.bin | ||
+ | enable_uart=1 | ||
+ | </code> | ||
+ | * Acum directorul ''rpi-boot-firmware'' ar trebui să fie identic ca imaginea referință din laborator! | ||
+ | * //Notă: Pentru a boota Linux, este necesar și ''cmdline.txt'' (folosiți-l pe cel din imaginea referință de bootloader);// | ||
- | <note> | + | <note important> |
- | Dacă etichetați sistemele de fișiere, le puteți accesa ulterior după căi speciale din ''/dev/disk/by-label/''! | + | Dacă doriți să testați noua imagine, ar trebui să copiați aceste fișiere pe partiția FAT32 a Raspberry PI-ului. |
+ | Însă faceți asta doar dacă aveți încredere că nu stricați bootloaderul existent! | ||
+ | Dacă ați pățit totuși asta și doriți să refaceți, există mai multe tehnici de recuperat, cea mai simplă fiind folosirea unui SD card reader extern și scrierea imaginii ''rpi-boot.img'' descărcate la ex. 0. Alternativ, puteți scrie imaginea pe un stick USB și introduce în RPI, apoi folosiți comanda ''ums'' pentru a scrie cardul SD introdus în dispozitiv și apoi ''dd'' pentru a scrie bootloaderul referință:<code> | ||
+ | sudo dd if=~/Downloads/rpi-boot.img.bin of=/dev/sd<x> bs=4k status=progress && sudo sync | ||
+ | </code> | ||
</note> | </note> | ||
- | ==== mount ==== | + | **3.** Ne dorim să instalăm sistemul de fișiere Linux referință pe cardul SD din Raspberry PI (folosind USB Mass Storage-ul prin USB Type-C). |
- | În sistemele Unix, sistemul de fișiere este unul arborescent unde directoarele pot fi considerate noduri, iar fișierele frunze. Utilitarul //mount// ne permite să atașăm unui arbore existent un alt subarbore (sistem de fișiere). Apelată fără niciun parametru, această comandă va afișa toate device-urile montate, locația în care sunt montate și opțiunile cu care au fost montate. | + | * Folosiți comanda ''lsblk'' pentru a descoperi cum se numesc device-urile (și, desigur, aveți grijă la capacitate: să nu ștergeți partiția rădăcină a OS-ului real din VM / fizic)! |
+ | * Primul lucru, va trebui să creați o partiție ''ext4'' pe cardul SD, **după cea de boot, FAT32 (NU O ȘTERGEȚI)!**. Putem folosi ''fdisk'' sau utilitarul mai modern, ''parted'' (tot în linia de comandă :D ): | ||
+ | <code> | ||
+ | lsblk # NOTAȚI CU ATENȚIE CARE E CARDUL SD DE ~16GB !!!! | ||
+ | # probabil SDB/SDC..., poate să fie sda dacă aveți ssd pe NVME și rulați Linux nativ... | ||
+ | sudo parted /dev/sd<X> # înlocuiți <X>-ul | ||
+ | # în parted, dați comenzile 'print' pentru a vedea partițiile curente, 'help' pentru comenzi | ||
+ | # creați o partiție nouă (primary, ext2 sunt setări bune) pe restul de spațiu liber (puteți scrie ''100%''), apoi quit | ||
+ | # apoi, pentru a încărca tabela nouă de partiții, folosiți comanda: | ||
+ | sudo partprobe /dev/sd<X> | ||
+ | </code> | ||
- | Formatul general al comenzii este ''mount -t <type> <device> <mount path>'', unde //<type>// este același ca la //mkfs//. De cele mai multe ori, argumentul ''-t <type>'' poate fi omis, acesta fiind autodetectat de către kernel. | + | <note warning> |
+ | **Mare atenție aici**: sunteți pe cale de a rula operații cu potențial destructiv! | ||
- | Și, desigur, pentru de-montarea de la final a dispozitivelor, putem folosi ''umount'' cu argumentul ori dispozitivul sursă, ori mountpointul (oricare ne este mai la îndemână -- va face același lucru). | + | Dacă folosiți mașina virtuală, este recomandat să faceți snapshot (riscați să ștergeți partițiile sistemului de operare real dacă nu aveți grijă!). |
- | ===== Exerciții ===== | + | Dacă sunteți pe un Linux în mașină fizică și nu sunteți siguri de ce faceți, întrebați un asistent dacă dați comenzile bune **ÎNAINTE DE A LE DA**! |
+ | </note> | ||
+ | * Formatați noua partiție (cea de-a doua, e.g. ''/dev/sdX2'') folosind utilitarul ''mkfs.ext4''. Folosiți și argumentul ''-L RPI_ROOTFS'' pentru a-i da un label (nume). | ||
+ | * Copiați conținutul arhivei ''rootfs.tar.xz'' (descărcată de pe GitHub la începutul laboratorului) pe partiția ''ext4'' nou-creată: | ||
+ | <code> | ||
+ | # montăm partiția în /media/rootfs (pe care trebuie să-l cream) | ||
+ | sudo mkdir /media/rootfs | ||
+ | sudo mount /dev/sdX2 /media/rootfs | ||
+ | # TODO: mount sdX1 undeva (veți avea nevoie mai jos), creați un dir nou sau folosiți /mnt | ||
+ | sudo tar xf rootfs.tar.xz -C /media/rootfs | ||
+ | sudo sync # sincronizează datele cacheuite în RAM către dispozitivele fizice | ||
+ | # dacă comanda sync durează mult, NU O ÎNTRERUPEȚI!!! citiți notița | ||
+ | # când a terminat, nu dați unmount, mai avem de copiat ceva de pe partiție!! | ||
+ | </code> | ||
<note> | <note> | ||
- | **În laborator, vom folosi echipamente Raspberry PI 4!** conectate prin USB Type-C și un adaptor UART la USB pentru vizualizarea consolei dispozitivului (din păcate, nu dispunem de suficiente monitoare HDMI în laborator + cabluri adaptoare). | + | Cardurile SD sunt extrem de lente, iar Linux are un obicei prost de a cache-ui fișierele copate în RAM-ul local și a le scrie în background după ce returnează comenzile de dezarhivare / copiere! Rulați comanda ''sudo sync'', care este blocantă până toate operațiile de scriere pe disk se vor completa. |
- | Înainte de a începe exercițiile, asigurați-vă că aveți cel puțin 10GB de storage disponibili în mașină virtuala de laborator. | + | Pentru a monitoriza progresul, utilizați one-liner-ul ''watch grep -e Dirty: -e Writeback: /proc/meminfo'' (așteptați până Writeback devine aproape de zero, iar Dirty sub 1MB). |
</note> | </note> | ||
- | **0.** Descărcați [[https://github.com/cs-pub-ro/SI-rpi-debian-scripts/releases|de aici arhiva rootfs referință pentru laborator (v2)]] + imaginea partiției de boot ''rpi-boot.img'' (pe care o vom clona mai târziu și îi vom adăuga partiție nouă pentru Linux). | + | * Așa cum a fost menționat, avem nevoie de câteva fișiere din sistemul rădăcină pentru a boota linux-ul (pe care le găsiți în ''/media/rootfs/boot'' pe partiția a doua, sau în arhiva rootfs.tar.xz în directorul de boot), anume: |
+ | * ''vmlinuz--*''; | ||
+ | * ''initrd.img-*''; | ||
+ | * //device tree blob-ul ar trebui să fie bun cel de la BL2, încărcat automat și de către u-boot, nu-l preluați pe acesta!//; | ||
- | * Pentru cei cu Windows, descărcați și instalați utilitarul [[https://www.raspberrypi.com/software/|Raspberry Pi Imager]], pe care îl vom folosi să scriem imaginea pe dispozitivul fizic. | + | * NU UITAȚI: după ce procesul de copiere s-a terminat (și ''sync'' nu mai blochează!), demontați toate device-urile implicate (i.e., cele 2 partiții ale cardului SD). |
+ | * După copiere, puteți vizualiza fișierele de pe partiția FAT32 din U-Boot: ''fatls mmc 1'' ;) | ||
- | 1. Dorim să creăm propria imagine pe care, ulterior, s-o urcăm pe Raspberry PI și să bootăm Linux-ul (prin U-Boot și modul ''ums'' -- USB Mass Storage Device): | + | **4.** Pornim Raspberry PI-ul, din nou (avem mare grijă la firele de la serială, să nu facă contact cu alte părți ale dispozitivului!). |
- | * Imaginea de //rpi-boot.img// a RaspberryPi are dimensiunea de ~150MB. Folosiți ''dd'' (citiți și documentația din laborator mai sus) și creați o imagine nouă, plină de zerouri, de 2GB (să zicem, ''rpi-full.img''). | + | * Încercați, mai întâi, fără initramfs, apoi dați și comenzile care îl încarcă și îl dau argument la ''booti'': |
- | * Dorim să copiem de-a întregul prima partiție din imaginea ''rpi-boot.img'' în cea de 2GB proaspăt creată. Putem folosi ''dd'' pentru asta:<code> | + | |
- | dd if=rpi-boot.img of=rpi-full.img conv=notrunc | + | |
- | </code> | + | |
- | * Folosind ''parted'', inspectați dacă există partiția de boot în noua imagine (cea //full//). Apoi creați pe cea de-a doua partiție (ce va conține un sistem de tip ext4). Vedeți mai sus în laborator pentru exemple de folosire a utilitarului; | + | |
- | * Formatați sistemul de fișiere a celei de-a doua partiții în ext4. Pentru aceasta, va trebui, mai întâi, să conectați imaginea într-un dispozitiv de tip bloc prin ''losetup'', apoi ''mkfs'' pentru a o formata (urmați documentația din laborator a acestor utilitare); | + | |
- | * Montați noua partiție ''ext4'' (puteți folosi ''/mnt'' ca mountpoint) și copiați conținutul arhivei rootfs descărcate mai sus; folosiți argumentul ''-C'' al ''tar'' pentru a preciza directorul destinație, adică mountpointul partiției). **Atenție**: folosiți contul de ''root'', deoarece dorim să dezarhivăm fișierele și să păstrăm permisiunile originale (imaginea este un root filesystem de Linux pre-instalat!): <code bash> | + | |
- | tar xf <ARCHIVE_FILE> -C <DEST_MOUNTPOINT> | + | |
- | # inspectați calea dezarhivată: | + | |
- | ls -l <DEST_MOUNTPOINT> | + | |
- | # ar trebui să vedeți toate directoarele unui rootfs clasic: /bin, /usr, /dev, /opt etc. | + | |
- | </code> | + | |
- | * Încă nu am terminat. Montați partiția de boot (prim partiție din loop device) într-o cale (puteți crea directorul ''/mnt/boot/rpi'' și să-l folosiți); apoi copiați fișierele ''/boot/vmlinuz-<versiune>'' ''/mnt/boot/initrd.img-<versiune>'' pe prima partiție (în mountpointul acesteia, ofc); acestea sunt necesare deoarece bootloaderul RPI nu știe să citească partiții ''ext4'' (ci doar FAT32). | + | |
- | * Tot pe partiția de boot, creați fișierul ''cmdline.txt'' și scrieți argumentele kernelului Linux:<code> | + | |
- | earlycon=pl011,mmio32,0xfe201000 console=tty1 console=serial0,115200 root=/dev/mmcblk0p2 rw rootwait fsck.repair=yes | + | |
- | </code> | + | |
- | * **Acum (atenție!):** demontați partițiile (mai întâi pe cea de boot, montată ultima oară, apoi pe cea ext4) și deconectați dispozitivul ''loop''! | + | |
- | + | ||
- | 2. Acum că imaginea noastră este gata, vrem să vedem că funcționează. Pentru aceasta, va trebui să pornim u-boot în modul ''ums'' (vedeți [[:si:laboratoare/05|laboratorul anterior]]) și folosim un utilitar de Raw Disk Imager (precum cel descărcat mai sus) sau ''dd'' (pentru cei cu Linux nativ sau cărora le funcționează USB Device Passthrough în mașina virtuală) pentru a scrie imaginea obținută anterior. | + | |
- | + | ||
- | <note important> | + | |
- | * **Atenție**: Va trebui, mai întâi, să aduceți imaginea pe Windows (folosiți ''scp'' cu ''username@<ip_sursa>:/cale/catre/fisier.img'' ca prim argument, iar, ca destinație, o cale de pe PC-ul fizic, e.g., ''C:\Users\..etc'')! | + | |
- | * **Notă pentru VirtualBox**: va trebui să folosiți argumentul ''-P 2023'' și ''student@localhost:<cale>'' ca sursă. | + | |
- | * **Notă pentru cei cu gazda pe Linux**: dacă utilizați ''dd'', va trebui să rulați comanda ''sync'' și să așteptați să se termine! Asta e important deoarece kernelul modern cache-uiește în memoria RAM și scrierea se termină, aproape instant (însă rămâne să se copieze pe SD în background, această operațiune fiind mult mai lentă). | + | |
- | </note> | + | |
- | 3. Reporniți Raspberry PI-ul. Vom încerca să bootăm kernelul și rootfs-ul din prompt-ul ''u-boot'': | + | <code> |
- | + | # inițial, fără initramfs: | |
- | * Deoarece kernelul este compresat, operațiunea este un pic mai complicată, trebuind să-i spunem o zonă de memorie folosibilă pentru extracția imaginii: <code> | + | fatload mmc <N>:1 ${kernel_addr_r} vmlinuz-6.1.61-rpi+ |
- | # load bootargs from device tree (contains BL2-modified cmdline.txt data) | + | #fatload mmc <N>:1 ${ramdisk_addr_r} initrd.img-6.1.61-rpi+ # e degeaba, momentan :D |
- | fdt addr ${fdt_addr} && fdt get value bootargs /chosen bootargs | + | # astea sunt setate deja în cmdline.txt, apoi sunt încărcate în DTB (FDT) mai departe de către BL2 |
+ | # putem explora device tree-ul din memorie în felul următor: | ||
+ | fdt addr ${fdt_addr} | ||
+ | fdt print /chosen | ||
+ | # verificați valoarea bootargs să conțină cel puțin: | ||
+ | # bootargs = "... earlycon=pl011,mmio32,0xfe201000 console=serial0,115200 root=/dev/mmcblk0p2 rw rootwait" | ||
+ | booti ${kernel_addr_r} - ${fdt_addr} | ||
+ | # se va plânge că nu aveți zonă de memorie de decompresie alocată! facem asta: | ||
# set decompression zone in RAM at 400MB, 64MB in size | # set decompression zone in RAM at 400MB, 64MB in size | ||
setenv kernel_comp_addr_r 0x19000000 | setenv kernel_comp_addr_r 0x19000000 | ||
setenv kernel_comp_size 0x04000000 | setenv kernel_comp_size 0x04000000 | ||
- | # load kernel from file | + | # try again: |
- | fatload mmc 0:1 ${kernel_addr_r} vmlinuz-6.1.61-rpi+ | + | |
- | # boot without initrd, for now | + | |
booti ${kernel_addr_r} - ${fdt_addr} | booti ${kernel_addr_r} - ${fdt_addr} | ||
+ | # ramdisk-ul momentan nu merge folosit, dar e opțional la debian | ||
</code> | </code> | ||
- | * Dacă vă printează mesajul cu ''waiting for device /dev/mmcblk0p2'' și nu se termină procesul, verificați dacă ați rulat kernel-ul 6.1 (este în comanda de mai sus) și dacă partiția a doua există (''part list mmc 0''). | ||
- | 4. Dorim să instalăm pachete în imagine. Deoarece pe Raspberry PI nu avem Internet, vom face asta pe mașina virtuală, lucrând direct cu imaginea și utilitarele ''systemd-nspawn'' (pornește facil un chroot container) și ''qemu-user-static'' (ce ne va permite emularea unei arhitecturi străine direct dintr-un container)! | + | <note> |
+ | Pentru a avea vizibilitate la ce face kernelul când bootează pe serială, [[https://www.raspberrypi.com/documentation/computers/configuration.html#enabling-early-console-for-linux|am urmat pașii de aici]] să activăm earlycon pe RPI. | ||
+ | </note> | ||
- | * Folosind ''losetup'', ''partprobe'' și ''mount'', montați partiția a doua (e.g., în ''/mnt''). | + | * Este posibil ca ''root=<device>'' să nu fie bun, încercați să îi dați ''root=LABEL=RPI_ROOTFS'' sau prin UUID (depinde cum ați creat partiția și dacă ați dat label la ext4). |
- | * Copiați utilitarul ''qemu-<arch>-static'' pentru arhitectura emulată (AArch64) în rootfs (în ''/mnt/usr/bin''): <code> | + | |
- | # vedem unde e executabilul: | + | <note> |
- | which qemu-aarch64-static | + | Login-ul pentru rootfs este ''pi'' (fără parolă)! |
- | # /usr/bin/qemu-aarch64-static | + | </note> |
- | cp -f /usr/bin/qemu-aarch64-static /mnt/usr/bin | + | |
- | chmod +x /mnt/usr/bin/qemu-aarch64-static | + | |
- | </code> | + | |
- | * Rulăm containerul: <code> | + | |
- | systemd-nspawn --as-pid2 --resolv-conf=copy-host -D "/mnt" bash | + | |
- | </code> | + | |
- | * Ar trebui să fiți în containerul de Debian pe arhitectură străină (AArch64), emulat prin qemu-user-static cu ajutorul funcționalității din kernel ''binfmt_misc''. Rulați ''apt update'' și instalați pachetele ''wpasupplicant'' (pentru autentificare prin WiFi) și ce alte utilitare mai doriți. | + | |
- | * La final, **nu uitați** să de-montați + deconectați dispozitivele loop utilizare! | + | |
- | * Bonus: testați noua imagine pe dispozitivul RPI4 (va trebui să repetați pașii copiere + burn folosind Disk Imager)! | + | |
===== Resurse ===== | ===== Resurse ===== | ||
+ | * [[https://www.raspberrypi.com/documentation/computers/raspberry-pi.html#boot-sequence|Procesul de boot al Raspberry PI]] | ||
+ | * [[https://hechao.li/2021/12/20/Boot-Raspberry-Pi-4-Using-uboot-and-Initramfs/|Mod utilizare U-Boot pentru Raspberry PI 4]] | ||
+ | * [[https://github-wiki-see.page/m/lulu98/projects-with-sel4/wiki/RPi4-Boot-Files|Explicație proces de boot al RPI 4 și fișierele firmware implicate]] | ||
* [[https://github.com/cs-pub-ro/SI-rpi-debian-scripts|Cod sursă scripturi compilare bootloader / kernel / generare rootfs]] | * [[https://github.com/cs-pub-ro/SI-rpi-debian-scripts|Cod sursă scripturi compilare bootloader / kernel / generare rootfs]] | ||