Treceți la conținutul principal

Crearea unei micro distribuții Linux

 Articol tradus si adaptat cu Google Translate dupa https://popovicu.com/posts/making-a-micro-linux-distro/


În acest articol, vom vorbi despre construirea unei mici (micro) „distribuții” Linux de la zero. Această distribuție nu va face prea multe, dar va fi construită de la zero.

Vom construi singuri kernelul Linux și vom scrie niște programe pentru a împacheta micro-distribuția noastră.

În cele din urmă, facem acest exemplu pe arhitectura RISC-V, în special riscv64 virtpe mașina QEMU. Există foarte puține informații în acest articol specifice acestei arhitecturi, așa că ați putea la fel de bine să faceți un exercițiu aproape identic pentru alte arhitecturi, cum ar fi x86. Am parcurs recent procesul de bootare RISC-V cu SBI și programare bare metal pentru RISC-V , deci aceasta este doar o continuare a stivei de software.

Atenție: Acest articol oferă o imagine foarte simplificată a unei distribuții Linux. Mai jos sunt informații care nu sunt 100% corecte, ci mai degrabă 99,9%. Acest articol este destinat începătorilor și îi ajută să-și formeze un cadru mental de bază pentru înțelegerea sistemelor Linux. Utilizatorii mai avansați ar putea fi deranjați de simplificarea excesivă a unor părți.

Ce este un nucleu de sistem de operare?

Să presupunem că lucrăm pe o mașină cu un singur nucleu. Acestea sunt încă prezente, poate nu în laptopuri și telefoane, dar în unele dispozitive mai mici și, din punct de vedere istoric, au fost de fapt utilizate pe scară largă chiar și în dispozitivele noastre personale „mari”, cum ar fi desktop-urile. Acestea din urmă au fost capabile să ruleze mai multe programe simultan timp de mulți ani, chiar și ca nuclee individuale. Vom discuta despre ce înseamnă cu adevărat simultan puțin mai târziu, dar deocamdată să observăm doar că una dintre marile sarcini ale nucleului sistemului de operare este de a face acest lucru să se întâmple.

Dacă vă întoarceți la articolele despre programarea bare metal și SBI pe RISC-V, puteți vedea cum interacționăm cu dispozitivele noastre I/O la cele mai joase niveluri de software. De obicei (cel mai adesea, dar nu neapărat întotdeauna ) se reduce la scrierea de date de către procesor la adresa corespunzătoare. Imaginați-vă dacă dezvoltatorii de aplicații ar trebui să țină cont de toate aceste adrese și ar trebui să știe exact ce valori să trimită la acele adrese! Asta ar însemna că am avea mult mai puține aplicații astăzi, dar nu avem, iar acest lucru se datorează nucleelor ​​sistemului de operare care abstractizează aceste detalii și oferă în schimb niște interfețe simple de nivel înalt. În articolul despre RISC-V SBI , am analizat un exemplu de astfel de interfață pentru Linux x86- în loc să știm la ce adrese să scriem și ce valori să trimitem acolo, ne-am concentrat pe logică și , practic, i-am spus nucleului sistemului de operare că „vrem ca mesajul cutare și cutare să fie scris la ieșirea standard”, iar apoi nucleul sistemului de operare s-a ocupat de detaliile interacțiunii cu hardware-ul. Deci, aceasta este o altă sarcină importantă pentru kernelul sistemului de operare: gestionarea hardware-ului de pe mașină și facilitarea interacțiunii cu acesta.

Mergând mai departe, kernelul sistemului de operare oferă niște interfețe de programare de nivel înalt, cum ar fi sistemele de fișiere . Aceasta poate fi sau nu legată de gestionarea anumitor componente hardware și abstractizarea operațiunilor asupra acestora. De exemplu, cel mai comun caz pentru sistemele de fișiere, desigur, este stocarea unor date pe disc și recuperarea lor ulterior, iar acest lucru are legătură cu gestionarea de către kernelul sistemului de operare a hardware-ului aferent discurilor de pe mașină (adică trimiterea unor date către anumite adrese, ceea ce face ca acele dispozitive de hard disk să răspundă într-un fel). Cu toate acestea, acest lucru nu este întotdeauna cazul, fișierele nu sunt întotdeauna date stocate pe disc, așadar sistemul de fișiere este o interfață expusă nouă, ceea ce înseamnă că este o modalitate de a comunica cu kernelul sistemului de operare, nu neapărat o modalitate de a comunica cu datele. Vom acoperi sistemele de fișiere în detaliu într-un alt articol, dar să ținem cont de acest lucru deocamdată - kernelul sistemului de operare trebuie să ofere o modalitate simplă de a face lucruri de nivel înalt prin intermediul mai multor interfețe.

În cele din urmă, ultimul lucru pe care am vrut să-l abordez despre kerneluri este faptul că acestea oferă un model de programare . Vă amintiți cum am menționat (după cum sunt sigur că știți deja) că mai multe programe pot rula simultan chiar și pe un dispozitiv cu un singur nucleu? Sistemul de operare permite ca aplicațiile care rulează să fie programate să nu știe nici măcar unele despre altele, cu alte cuvinte, o aplicație își poate trăi ciclul de viață comportându-se ca și cum ar fi singura aplicație care rulează pe computer și nimeni altcineva nu îi atinge memoria. Imaginați-vă o lume în care serverul dvs. Python Django trebuie să știe despre acea aplicație de mesaje text de pe dispozitivul dvs. pentru a funcționa - am avea mult mai puține aplicații Django și aplicații de mesaje text, cu siguranță, deoarece codarea lor ar deveni rapid complicată. Cu toate acestea, aplicațiile pot ști, de asemenea, despre existența reciprocă pe aceeași mașină. Nucleul sistemului de operare facilitează ambele . Acesta oferă un model de programare în care puteți izola aplicațiile unele de altele sau puteți uni câteva aplicații izolat de alte aplicații etc.

Practic, kernelul sistemului de operare face o muncă grea pentru a vă permite să rulați codul cu ușurință pe o mașinărie foarte generică și complicată, cum ar fi smartphone-ul. Ceea ce este scris mai sus probabil nu face dreptate pe deplin kernelurilor, acestea fac o mulțime de lucruri, dar cele câteva paragrafe de mai sus ar trebui să ofere o idee destul de bună despre principalele sarcini ale kernelului, și sunt multe.

Linux este un kernel de sistem de operare extrem de popular. Poate fi construit să ruleze pe multe arhitecturi (de fapt, foarte multe ), este open source și gratuit. Și mulți oameni sunt „utilizatori Linux”, dar ce înseamnă exact că cineva „folosește Linux”? Acei utilizatori Linux instalează de obicei ceva de genul Debian sau Ubuntu pe mașinile lor și folosesc Linux în acest fel, și ce înseamnă asta?

Ce este o distribuție Linux?

Am vorbit mai sus despre ce fac nucleele, adică care sunt sarcinile lor, și am spus că Linux este un nucleu de sistem de operare, dar putem pur și simplu să luăm Linux simplu și, ca utilizatori finali care vor doar să se uite la YouTube, să facem ceva cu el? Răspunsul este probabil nu, avem nevoie de mult mai multe niveluri pe lângă Linux pentru a putea porni un browser Chrome și a ne uita la YouTube.

Cum să ajungem în vârful stivei de software, unde putem folosi acele aplicații super simple și intuitive, cum ar fi browserele web grafice? Am discutat anterior despre procesul de bootare și am parcurs întregul proces, de la primele operațiuni pe mașină după pornire, până în momentul în care ajungem în kernelul sistemului de operare. Nu am acoperit bootloader-ele în detaliu, le-am menționat doar pe scurt, deoarece am reușit să facem QEMU să încarce direct kernelul nostru fals în memorie dintr-o singură mișcare, ceea ce de obicei nu este posibil cu sisteme complete, cum ar fi Linux desktop (există o etapă intermediară de bootare în care bootloader-ul preia imaginea sistemului de operare de pe ceva precum un disc sau poate chiar din rețea și o încarcă în memorie). Nucleul pe care l-am scris era un mic stub fals care practic nu face nimic, așa că am încheiat ultimul nostru articol în punctul în care kernelul sistemului de operare este în memorie și gata de funcționare, doar că nu aveam niciun kernel de rulat.

Pe baza a ceea ce vedem mai sus, cred că modelul mental corect pentru kernel în acest moment este că acesta reprezintă infrastructura pentru rularea aplicațiilor utilizatorului pe o mașină complexă, dar nu face nimic pentru logica de business a utilizatorului. La asta m-am referit când am spus că Linux-ul simplu nu poate porni Chrome și să te lase să te uiți la YouTube - este doar infrastructura pe care dezvoltatorul aplicației o folosește pentru a implementa Chrome și capacitățile sale de streaming.

Totuși, kernelul în sine nu constituie infrastructura necesară rulării Chrome. Trebuie să rulăm un fel de „infrastructură peste infrastructură” pentru a obține infrastructura completă necesară rulării Chrome. Din nou, la fel ca în articolul SBI, doar suprapunem abstracțiuni una peste alta într-un fel, așa că, în esență, nu este nimic nou aici, ci doar modul în care procedăm.

De exemplu, pentru ca o mașină să se conecteze la internet, kernelul sistemului de operare trebuie mai întâi să poată comanda dispozitivul de rețea de pe mașină pentru a trimite semnalele în afara mașinii (către switch, router, o altă mașină sau orice altceva la care este conectată). Cu toate acestea, în Linux, există mai mult sau mai puțin un punct unde kernelul se oprește. La ce rețele te conectezi, folosești VPN, cum atribui adrese IP mașinii tale (static sau dinamic) și acest tip de activitate se întâmplă în straturile superioare ale infrastructurii.

Acum probabil ghiciți unde se îndreaptă toate acestea - o distribuție Linux este, de fapt, kernelul Linux plus infrastructura de deasupra infrastructurii kernelului . Haideți să aprofundăm.

Cum funcționează „infrastructura peste infrastructură”?

Din nou, nucleul face o grămadă de lucruri, de un milion de ori mai multe decât putem acoperi într-un singur articol, dar cu siguranță are limitele sale și nu face toată munca grea pe dispozitivul personal de zi cu zi - și aici intervine ceva din afara nucleului.

Disclaimer: Poți fi cu adevărat creativ cu Linux în o mulțime de moduri diferite, iar de acum înainte vom aborda o perspectivă foarte simplă, de bază, ca într-un manual, asupra a ceea ce se întâmplă în distribuțiile mainstream. Există multe lucruri super complexe pe care le putem face și există o mulțime de detalii pe care le omitem, dar speranța mea este că vă veți face o idee generală și suficiente cunoștințe pentru a putea înțelege materiale mai avansate pe acest subiect; există o mulțime de astfel de informații pe internet.

Motivul pentru care am scris această declinare de responsabilitate de mai sus este în principal pentru că vom presupune că Linux-ul dvs. va avea un sistem de fișiere de acum înainte, deoarece aceasta este cea mai comună cale. De câte ori ați văzut o implementare Linux fără un sistem de fișiere? Cu siguranță pare posibil, dar ar putea fi aproape inutil, cu excepția unor cazuri super edge/avansate, pe care le vom ignora în acest articol. Consultați această pagină pentru a vă face o idee mai clară despre ce vorbesc.

Deci, ce este în afara kernelului? Este ceea ce numim cod utilizator ! Este doar un cod normal care rulează în mediul Linux, la fel cum rulezi practic orice pe mașina ta Linux. Sigur, unele coduri sunt mai privilegiate decât altele și există un milion de alte detalii care pot fi implicate, dar haideți să ne concentrăm doar pe distincția principală aici: atunci când rulezi Linux pe o mașină, rulează cod kernel , precum și cod utilizator , și tot ceea ce face parte din kernelul în sine rulează în spațiul kernelului , iar tot ceea ce rulează pe mașină și nu face parte din kernel rulează în spațiul utilizator , și sunt destul de izolate unele de altele.

Deci, această „infrastructură peste infrastructură” despre care am vorbit rulează în spațiul utilizatorului. Sigur, trebuie să se extindă până la kernel pentru multe primitive și am văzut deja cum se întâmplă asta. Linux are un ABI bine definit care expune un set de servicii pe care codul din spațiul utilizatorului le poate invoca în spațiul kernelului. Și unde intră în scenă acest cod din spațiul utilizatorului?

Procesul init(și „copiii” săi)

După ce kernelul termină de încărcat și se acomodează pe mașină, acesta lansează prima parte a codului în spațiul utilizator - initprocesul. Aceasta este o bucată de cod din spațiul utilizator care se află într-un fișier binar aflat undeva în sistemul de fișiere, iar kernelul o va căuta în câteva locații, începând cu /init(dacă nu o găsește acolo, va mai încerca câteva locații diferite înainte de a renunța la ea). Să presupunem că kernelul a găsit un fișier binar în sistemul de fișiere la /init— îl va porni și îi va atribui ID-ul 1. Acesta este practic singurul proces utilizator pe care kernelul îl va porni: initprocesul este apoi strămoșul tuturor celorlalte procese din spațiul utilizator. Aceasta înseamnă că initva porni alte procese, aceste alte procese vor porni la rândul lor alte procese și așa mai departe. În scurt timp, veți avea o grămadă de procese care rulează pe mașina dvs., sperând că fiecare dintre ele este util pentru operațiunile dorite pe mașină. Mașina ar trebui în acest moment să înceapă să interacționeze activ cu lumea din jurul ei: fie că vorbim despre un smartphone care oferă interfața utilizatorului său, un dispozitiv încorporat care colectează date de la senzori și le trimite în cloud etc. În plus, mașina va avea adesea diverse instrumente disponibile care nu rulează activ pe mașină, dar pot fi invocate în anumite situații pentru unele operațiuni de nivel înalt (de exemplu, un script Python poate invoca câteva instrumente precum lscatsau ceva de genul , pentru a obține o instantanee a ceea ce se întâmplă cu mașina și apoi trimite datele undeva). O notă rapidă este că chiar și aceste instrumente pornite periodic sau ad-hoc sunt într-un fel descendente ale init; nu este prea important de știut acum, dar este bine de ținut minte.

Colecția de kernel, procesele care sunt lansate imediat după kernel și instrumentele disponibile reprezintă distribuția Linux . Este, în esență, un pachet pentru kernel, alături de toate aceste instrumente utile care fac mai multe în jurul mașinii decât ceea ce face kernelul singur (dar oferă în continuare infrastructura pentru ca tot ce este în afara kernelului să ruleze, nimic nu ocolește kernelul).

Chiar și o distribuție minim utilă pentru uzul zilnic poate deveni destul de repede cam nepotrivită. Dacă alegi să-ți construiești propria distribuție personalizată, așa cum vom face acum, aproape inevitabil vei întâlni o mulțime de obstacole în care ceva ce te aștepți să funcționeze pur și simplu nu funcționează, iar soluția completă este fie să programezi o parte din propriul software pentru a comunica cu kernelul pentru a realiza ceva în sistem, fie să folosești pur și simplu un software standard pentru a face acest lucru. Ultima este calea cu cea mai mică rezistență și probabil vei continua să adaugi lucruri până când vei ajunge la o implementare care poate face ceva cât de cât util pentru tine. În acest moment, probabil vei fi acumulat un număr semnificativ de pachete software.

Pe de altă parte, probabil ați auzit oameni criticând anumite distribuții ca fiind „umflate”, probabil însemnând că au acumulat atât de multă complexitate în pachetele lor, încât irosesc o mulțime de resurse hardware făcând lucruri care nu sunt utile etc. Fără disciplină, îmi pot imagina cu ușurință dezvoltatorii de distribuții aruncând la întâmplare diferite instrumente în sistem doar pentru a pune în funcțiune acel lucru lipsă, fără a curăța retroactiv excesul mai târziu și a trece pur și simplu la următoarea caracteristică unde fac același lucru - un model (din păcate) comun în ingineria software.

Unele distribuții stabilesc limite în diferite puncte, unde pur și simplu iau o decizie pentru utilizator și fac ceva asupra sistemului, în loc să-l lase pe utilizator să ia decizia completă și să fie mai implicat. De exemplu, poți instala Arch Linux într-un mod minimal, unde este doar puțin mai mult decât kernelul pornit cu un shell. Toate deciziile ulterioare îți aparțin și trebuie să fii foarte implicat pentru a ajunge la un punct în care devine foarte grafic și extrem de interactiv. Sau poți decide că pur și simplu nu merită să-ți petreci atât de mult timp configurendu-l și poți instala pur și simplu o distribuție Ubuntu foarte ușor de utilizat, care poate fi „umflată” pentru gustul cuiva, dar te face să funcționezi foarte repede (personal, îmi place).

Construind micro-distribuția noastră Linux aproape inutilă

Hai să ne murdărim mâinile și să construim ceva care este practic inutil, dar pe care îl vom porni de fapt. Poate vrei să-ți reîmprospătezi memoria cu privire la procesul de pornire RISC-V , cred că va fi util aici.

În primul rând, haideți să construim kernelul.

Construirea unui sistem de operare Linux pentru RISC-V

Sunt pe o x86platformă aici, așa că voi depinde în mare măsură de lanțul de instrumente multi-platformă pentru a construi lucruri pentru RISC-V. Probabil veți face ceva similar (nu sunt sigur că am văzut pe cineva construind nucleul RISC-V chiar pe RISC-V).

Să obținem codul sursă pentru Linux. Dezvoltarea Linux se face pe sistemul de control al versiunilor Git, dar vom alege o scurtătură aici și vom descărca doar un fișier tarball cu sursele pentru o ramură, nu vom sincroniza întreaga bază de cod Linux cu toate ramurile Git, chestii experimentale și așa mai departe. Vom descărca fișierul tarball de la kernel.orgpentru versiunea 6.5.2aici ). De asemenea, puteți descărca orice fișier tarball pentru cea mai recentă versiune stabilă de pe pagina principală kernel.org. După ce este descărcată, puteți dezarhiva-o. Să intrăm și cdîn acel director.

Acum este momentul să configurați versiunea. Primul pas este să creați, defconfigcare practic inițiază fișierul de configurare.

Notă: Aici și mai jos, este posibil să doriți să utilizați un CROSS_COMPILEprefix diferit, în funcție de modul în care instrumentul de compilare încrucișată este identificat pe mașina dvs.

make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- defconfig

Sperăm că a fost rapid și că .configfișierul ar trebui generat. Fișierul de configurare ar trebui să conțină o mulțime de ID-uri pentru configurații individuale și valorile acestora, adesea în format da/nu (de exemplu, CONFIG_FOO=ysau CONFIG_FOO=n). Puteți edita fișierul manual, dar personal nu aș recomanda acest lucru, mai ales ca începător (nici eu nu mă consider expert în acest sens). O modalitate mai bună de a edita acest lucru este prin intermediul cursespseudo-interfeței bazate pe . Puteți ajunge acolo rulând

make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- menuconfig

Această interfață are câteva avantaje.

  1. Ai o prezentare generală a configurațiilor mai ușor de citit, asemănătoare unui folder.
  2. Există informații despre dependențele dintre configurații, adică ar putea avea sens să se poată activa configurația doar foodacă barși bazsunt, de asemenea, activate.
  3. Această interfață are o funcție de căutare, activată prin apăsarea /butonului (nu cred că veți ajunge departe căutând acolo în limbaj natural; modul meu de a mă descurca aici este să caut pe Google și să găsesc exact ce cheie de configurare caut, de exemplu CONFIG_TTY_PRINTK). Când găsiți ceea ce căutați, apăsați butonul pe care îl vedeți între paranteze.

Nu vom modifica nimic aici deocamdată, hai să ieșim și să mergem mai departe.

E timpul să construim kernelul! O notă rapidă: procesul make are faimosul -jflag, care practic setează concurența în procesul de compilare, ceea ce înseamnă că permite procesului de compilare să ruleze mai multe lucruri simultan. Dacă vrei să construiești mai repede, dar nu ești sigur ce să faci, numără numărul de nuclee și, dacă este ceva de genul 8, transmite pur și simplu flag-ul -j8mai jos, așa cum este. Voi rula comanda astfel (sunt pe o mașină cu 16 nuclee):

make ARCH=riscv CROSS_COMPILE=riscv64-linux-gnu- -j16

Acest lucru poate dura ceva timp, deși pentru versiunea RISC-V nu ar trebui să dureze foarte mult, dar m-aș aștepta la cel puțin câteva minute.

După ce ați terminat, probabil veți vedea ceva de genul acesta aproape de jos:

OBJCOPY arch/riscv/boot/Image

și acesta este fișierul pe care îl vom trimite către QEMU.

Grozav, hai să pornim QEMU!

qemu-system-riscv64 -machine virt -kernel arch/riscv/boot/Image

Trecând la vizualizarea UART, vedem că OpenSBI a pornit în mod ordonat și Linux a preluat controlul! Grozav! Vedem chiar și câteva referințe la nivelul SBI despre care am discutat anterior:

[    0.000000] Linux version 6.5.2 (uros@uros-debian-desktop) (riscv64-linux-gnu-gcc (Debian 10.2.1-6) 10.2.1 20210110, GNU ld (GNU Binutils for Debian) 2.35.2) #1 SMP Mon Sep 11 00:45:40 PDT 2023
[    0.000000] Machine model: riscv-virtio,qemu
[    0.000000] SBI specification v0.2 detected
[    0.000000] SBI implementation ID=0x1 Version=0x8
[    0.000000] SBI TIME extension detected
[    0.000000] SBI IPI extension detected
[    0.000000] SBI RFENCE extension detected

După ce am citit despre procesul de pornire , ar trebui să înțelegem pe deplin ce se întâmplă aici. Acest lucru s-a întâmplat foarte devreme în faza de pornire. Se întâmplă multe lucruri în aceste jurnale și voi evidenția câteva lucruri:

[    0.000000] riscv: base ISA extensions acdfim

Se pare că Linux este capabil să determine dinamic capacitatea hardware-ului RISC-V subiacent. Nu sunt sigur care este exact mecanismul din spatele acestui lucru, ar putea fi cumva transmis prin arborele de dispozitive menționat în articolul anterior sau ceva din ISA-ul însuși îi spune acest lucru kernelului, nu sunt sigur.

[    0.000000] Kernel command line:

Interesant este, un kernel are o linie de comandă? Se pare că kernelul, la fel ca binarele obișnuite, are flag-uri de pornire. Bootloader-ul kernelului le configurează de obicei - la urma urmei, știe cum să pornească kernelul, iar acest lucru ar putea fi pur și simplu o parte a procesului de pornire. Cu QEMU, amintiți-vă, scurtcircuităm cumva întreaga chestie cu bootloader-ul, iar prin transmiterea -kernelflag-ului, permitem și lui QEMU să joace rolul de bootloader, încărcând imaginea kernelului în memorie și pornindu-l. QEMU are de fapt un flag numit -appendcu care puteți adăuga la această linie de comandă a kernelului. Linia de comandă în sine este inclusă în fișierul de configurare Boot optionsundeva, las cititorul să o caute, iar flag-ul QEMU vă permite practic să îl ajustați cu o lansare a mașinii virtuale, în loc să fie nevoie să reconstruiți kernelul pentru a modifica linia de comandă. În acest caz, linia de comandă este pur și simplu goală în mod implicit.

[    0.003376] printk: console [tty0] enabled

Presupun că asta înseamnă că printkwill now write to tty0printkeste practic o modalitate de a scrie mesaje din spațiul kernelului. Rețineți că limbajul tipic printfdin C stdio.heste destinat rulării în spațiul utilizatorului, nu în spațiul kernelului, deci spațiul kernelului trebuie să aibă propria soluție și este printk.

[    0.211634] Serial: 8250/16550 driver, 4 ports, IRQ sharing disabled
[    0.221544] 10000000.uart: ttyS0 at MMIO 0x10000000 (irq = 12, base_baud = 230400) is a 16550A
[    0.222659] printk: console [ttyS0] enabled

Grozav, Linux știe că există UART la 0x10000000, așa cum am stabilit anterior. Linux poate acum alege dacă să utilizeze interfața SBI pentru a controla UART-ul sau să comunice direct cu acesta (dacă modul S permite acest lucru pe acea mașină, adică). Pe multe platforme, sistemul de operare poate ignora faptul că un software de nivel inferior, cum ar fi BIOS-ul, s-ar putea oferi să interacționeze cu hardware-ul și, din câte am auzit, acest lucru se întâmplă într-adevăr des.

Există și multe alte lucruri în jurnalele kernelului:

[    0.250030] SuperH (H)SCI(F) driver initialized

Nu cred că avem nevoie de asta? Cred că putem reveni la configurația kernelului și să nu integrăm acest driver în kernel și astfel să subțiem kernelul. Ceea ce construim aici este, de fapt, o versiune generică. Nu am personalizat nimic și, probabil, autorii configurației implicite au considerat că aceasta este o configurație implicită rezonabilă, care ar trebui să ruleze pe o mulțime de configurații diferite, așa că probabil au inclus o mulțime de lucruri pentru a fi în siguranță. Dacă lucrați pe hardware mai mic, cu memorie, CPU etc. mai puțin generoase, trebuie să alegeți cu atenție ce este integrat în kernel și ce nu.

În plus, această compilare generică este suficient de inteligentă pentru a identifica faptul că consola ar trebui să fie conectată la dispozitivul UART corect, ceea ce este foarte util pentru noi. Altfel, probabil că ar trebui să facem o grămadă de configurații, cum ar fi să ne asigurăm că TTY (să nu ne concentrăm prea mult asupra acestui aspect acum) este activat, dorim să activăm imprimarea către UART la pornirea kernelului etc. Toate acestea sunt practic configurabile în menuconfiginterfață.

Vom păstra lucrurile simple în acest articol și nu vom personaliza nimic în kernel decât dacă este necesar.

Primele obstacole

Derulând în jos, mai aproape de partea de jos a rezultatului, vedem acest lucru:

[    0.330411] /dev/root: Can't open blockdev
[    0.330743] VFS: Cannot open root device "" or unknown-block(0,0): error -6
[    0.330984] Please append a correct "root=" boot option; here are the available partitions:
[    0.331648] List of all bdev filesystems:
[    0.331785]  ext3
[    0.331803]  ext2
[    0.331882]  ext4
[    0.331950]  vfat
[    0.332028]  msdos
[    0.332098]  iso9660
[    0.332181]
[    0.332405] Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0)
[    0.332756] CPU: 0 PID: 1 Comm: swapper/0 Not tainted 6.5.2 #1
[    0.333018] Hardware name: riscv-virtio,qemu (DT)
[    0.333248] Call Trace:
[    0.333442] [<ffffffff8000537a>] dump_backtrace+0x1c/0x24
[    0.333940] [<ffffffff808890f8>] show_stack+0x2c/0x38
[    0.334138] [<ffffffff80894a48>] dump_stack_lvl+0x3c/0x54
[    0.334318] [<ffffffff80894a74>] dump_stack+0x14/0x1c
[    0.334493] [<ffffffff80889500>] panic+0x102/0x29e
[    0.334683] [<ffffffff80a015c6>] mount_root_generic+0x1e8/0x29c
[    0.334891] [<ffffffff80a0186c>] mount_root+0x1f2/0x224
[    0.335108] [<ffffffff80a01a68>] prepare_namespace+0x1ca/0x222
[    0.335320] [<ffffffff80a010c8>] kernel_init_freeable+0x23e/0x262
[    0.335539] [<ffffffff80896264>] kernel_init+0x1e/0x10a
[    0.335714] [<ffffffff800034c2>] ret_from_fork+0xa/0x1c
[    0.336208] ---[ end Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(0,0) ]---

Ups, ne-am blocat! Nucleul a intrat în panică .

Vă amintiți cum am spus că aproape întotdeauna Linux are nevoie de un sistem de fișiere pentru a fi util și cum toată „infrastructura peste infrastructură” se află în spațiul utilizatorului? Ei bine, nu am transmis nimic explicit legat de sistemul de fișiere și cu siguranță nu am transmis niciun cod în spațiul utilizatorului care să servească drept init, deși nici măcar nu am ajuns la acesta din urmă.

V-ați putea imagina că sistemul de fișiere trebuie să fie pe un disc, dar nu este neapărat cazul. Vom vorbi altă dată despre sistemele de fișiere în detaliu, dar puteți avea un sistem de fișiere susținut și de memoria RAM. Și acest lucru este de fapt foarte des folosit de Linux, mai ales în faza de pornire. Când kernelul ajunge la punctul în care s-a blocat pentru noi chiar acum, într-o situație normală, tipică, va găsi întregul sistem de fișiere complet funcțional încărcat în RAM. Dacă acest lucru vă derutează, gândiți-vă în felul următor - un disc este doar o grămadă de octeți, la fel ca RAM, deși RAM este mai rapid, dar mult mai mic; conceptual, sunt practic la fel. Cine și cum încarcă această memorie?

O modalitate este de a integra sistemul de fișiere direct în imaginea kernelului. În acest caz, pe măsură ce kernelul se încarcă, se încarcă și sistemul de fișiere inițial, bazat pe memorie, iar sistemul nostru ar fi gata de funcționare dacă am fi făcut asta. Dacă nu doriți să încărcați imaginea kernelului în masă și doriți ca sistemul de fișiere inițial să fie încărcat prin alte mijloace, cum ar fi printr-un bootloader sau ceva de genul acesta, atunci îl împachetați separat. În cazul QEMU, putem scurtcircuita din nou lucrurile și să-i facem să aibă câteva roluri suplimentare - îl vom face să încarce și sistemul de fișiere inițial în memorie. Dacă sunteți interesat să integrați sistemul de fișiere în kernel, citiți discuția de aici și încercați ca exercițiu după ce ați terminat acest ghid.

Acest sistem de fișiere inițial are un nume: initramfs. Îl veți auzi adesea numit initrdși (îmi imaginez rdcă este o prescurtare de la ramdisk?). Acesta din urmă este modul în care QEMU preia sistemul de fișiere pentru încărcare ( -initrdflag).

Sistemul de fișiere este ambalat ca o cpioarhivă, care este similară din punct de vedere conceptual cu tar, dar nu este același format binar. O scurtă discuție poate fi citită aici .

Construireainitramfs

Singura cerință reală pentru ca procesul să fie executat initramfsdin kernel este să aibă un fișier binar pe care să îl poată porni ca initproces, iar primul loc unde kernelul îl va căuta este la rădăcina sistemului de fișiere, deci calea este /init. Dacă nu aveți absolut nimic altceva pe sistemul de fișiere, este îndoielnic util, dar aceasta este cerința simplă. Să începem prin a scrie procesul initîn C. Acest proces poate fi de fapt orice, Linux nu vă va împiedica să scrieți un proces inutil init, îl va executa pur și simplu. Putem merge cu un „salut lume” atunci?

#include <stdio.h>

int main(int argc, char *argv[]) {
  printf("Hello world\n");
  return 0;
}

Grozav, acum hai să-l împachetăm într-o cpioarhivă.

riscv64-linux-gnu-gcc -static -o init init.c
cpio -o -H newc < file_list.txt > initramfs.cpio

Are file_list.txto singură linie:

init
  1. Construim un fișier binar static deoarece nu vrem să depindem dinamic de biblioteca C standard. Sistemul de fișiere nu îl va avea, creăm un sistem de fișiere cu initalone.
  2. Linux se așteaptă ca initramfsarhiva să fie construită cu -H newcsteagul.

Să rulăm QEMU.

qemu-system-riscv64 -machine virt -kernel arch/riscv/boot/Image -initrd /PATH/TO/NEWLY_BUILT/initramfs.cpio

Nucleul tot intră în panică, dar într-una diferită!

[    0.351894] Run /init as init process
Hello world
[    0.379006] Kernel panic - not syncing: Attempted to kill init! exitcode=0x00000000
[    0.379360] CPU: 0 PID: 1 Comm: init Not tainted 6.5.2 #1
[    0.379597] Hardware name: riscv-virtio,qemu (DT)
[    0.379812] Call Trace:
[    0.380005] [<ffffffff8000537a>] dump_backtrace+0x1c/0x24
[    0.380724] [<ffffffff808890f8>] show_stack+0x2c/0x38
[    0.380906] [<ffffffff80894a48>] dump_stack_lvl+0x3c/0x54
[    0.381095] [<ffffffff80894a74>] dump_stack+0x14/0x1c
[    0.381283] [<ffffffff80889500>] panic+0x102/0x29e
[    0.381447] [<ffffffff80013fd0>] do_exit+0x760/0x766
[    0.381623] [<ffffffff80014154>] do_group_exit+0x24/0x70
[    0.381806] [<ffffffff800141b8>] __wake_up_parent+0x0/0x20
[    0.382009] [<ffffffff80895482>] do_trap_ecall_u+0xe6/0xfa
[    0.382218] [<ffffffff8000337c>] ret_from_exception+0x0/0x64
[    0.382808] ---[ end Kernel panic - not syncing: Attempted to kill init! exitcode=0x00000000 ]---

Presupun că asta înseamnă pur și simplu că initnu ar trebui să se termine, deci ar trebui să fie ușor de remediat? Hai să-l facem să afișeze ceva la fiecare 10 secunde și să nu se oprească niciodată. Important de reținut: rezultatul nostru a funcționat, vedem un șir de caractere „Salut, lume”!

Vom scrie un nou init, dar haideți să facem și al nostru initramfspuțin mai complex. Să ne amintim cum am spus că initpornește toate celelalte procese de pe mașină. Nu ar fi frumos dacă am avea într-adevăr un fel de shell? La urma urmei, asta avem de obicei cu Linux - shell-urile merg bine cu Linux. Vom construi un shell inutil, cel care ne spune doar ce i-am cerut să facă (repetă intrarea).

Să scriem mai întâi initprocesul. Înainte de a începe să se repetă și să afișeze ceva la fiecare 10 secunde, are o sarcină importantă de a genera „mica noastră shell”. Modul în care un proces poate genera un alt proces în Linux este prin 2 operații: forkși execforkva porni un nou proces prin clonarea literală a procesului curent în momentul fork. Modul în care codul subiacent poate diferenția procesele „părinte” și „copil” după aceea este prin verificarea valorii returnate de forkoperație. Dacă este 0, aceasta înseamnă că procesul este procesul copil, iar în caz contrar este un părinte (-1 este returnat într-un caz de eroare).

În continuare, nu este util pentru noi să executăm programul initîn 2 procese diferite. Aici execintervine una dintre numeroasele operații. Când spun că există multe execoperații disponibile pe Linux, mă refer la execlexeclpexecle, etc. Aruncați o privire la mai multă documentație aici , vă rugăm. Vom merge cu execlaici, iar primul parametru este ce binar dorim să lansăm. Vom împacheta shell-ul nostru fals ca binar little_shellpe rădăcină. Restul parametrilor nu contează cu adevărat (după cum o demonstrează valoarea celui de-al doilea parametru). Mai important, mecanismul acestei operații este că apelăm în kernel pentru a lua orice rulează în procesul curent și a-l înlocui cu programul care este încărcat pentru execuție din binarul listat ca prim parametru. Așa se lansează programele pe Linux și când lucrați în shell-ul Bash și ajungeți să lansați un program, iată ce se întâmplă - o secvență de apeluri în stilul forkși .exec

#include <stdio.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
  pid_t pid = fork();

  if (pid == -1) {
    printf("Unable to fork!");
    return -1;
  }

  if (pid == 0) {
    // This is a child process.
    int status = execl("/little_shell", "irrelevant", NULL);

    if (status == -1) {
      printf("Forked process cannot start the little_shell");
      return -2;
    }
  }

  int count = 1;

  while (1) {
    printf("Hello from the original init! %d\n", count);
    count++;
    sleep(10);
  }

  return 0;
}

Construim în initacelași mod ca înainte:

riscv64-linux-gnu-gcc -static -o init init.c

Pentru „învelișul” pe care îl construim, vreau să fiu puțin mai creativ. De ce nu scriem asta în Go în loc de C-ul clasic?

package main

import (
	"bufio"
	"fmt"
	"os"
)

func main() {
	fmt.Println("Hello world from Go!")

	reader := bufio.NewReader(os.Stdin)

	for {
		fmt.Print("Enter your command: ")
		line, _ := reader.ReadString('\n')
		fmt.Printf("Your command is: %s", line)
	}
}

Pot să compilez acest lucru în RISC-V direct cu gocompilatorul meu.

GOOS=linux GOARCH=riscv64 go build little_shell.go

Un lucru frumos care îmi place foarte mult la Go este că este foarte ușor să faci referire la alte depozite la distanță pe GitHub pentru a include biblioteci, iar lucrurile sunt împachetate static cu grijă. Nu voi minți, fișierul little_shellbinar Go este destul de gros, cântărind 1,9 MB pe mașina mea, comparativ cu doar 454K pentru init-ul simplu cu link-uri statice, dar în zilele desktop-urilor/laptopurilor/telefoanelor cu sute de GB de stocare, dacă construiești o distribuție pentru astfel de dispozitive, s-ar putea să vrei să iei în considerare acest compromis.

Rețineți că există situații în care este posibil să nu puteți rula pur și simplu binarul Go pe un kernel bare, acesta putând începe să genereze panică Go peste tot. Pentru a rula Go, trebuie să construiți kernelul cu caracteristicile corecte, suportul futex fiind una dintre ele (cred că am identificat doar 2 în experiența mea anterioară). Dacă întâmpinați probleme la rularea aplicațiilor Go și suspectați că nu aveți suportul corect pentru kernel, citiți cu atenție panicările și veți putea identifica ce lipsește. Vestea bună este că configurația implicită pentru kernelul RISC-V este suficient de bună pentru a rula Go.

Să actualizăm file_list.txt:

init
little_shell

Împachetați totul din nou:

cpio -o -H newc < file_list.txt > initramfs.cpio

Hai să o rulăm!

qemu-system-riscv64 -machine virt -kernel arch/riscv/boot/Image -initrd /PATH/TO/NEWLY_BUILT/initramfs.cpio
[    0.356314] Run /init as init process
Hello from the original init! 1
Hello world from Go!
Enter your command: [[[mkdir hello]]]
Your command is: mkdir hello
Enter your command: [[[ls]]]
Your command is: ls
Enter your command: Hello from the original init! 2
[[[echo 123]]
Your command is: echo 123
Enter your command: [[[exit]]]
Your command is: exit
Enter your command: Hello from the original init! 3
[[[I give up!]]]
Your command is: I give up!

Biții din acest fragment de consolă incluși între paranteze pătrate triple reprezintă datele mele de intrare furnizate de utilizator prin UART. Puteți vedea 3 lucruri intercalate pe UART.

  1. initIeșirea perioadei originalului la fiecare 10 secunde .
  2. Ieșire de la little_shell.
  3. Introducerea de informații de la utilizator.

Folosim singurul dispozitiv UART de pe mașina virtuală pentru toate acestea, dar acesta nu este singurul motiv pentru care totul este amestecat aici. initProcesul imprimă la ieșirea standard, la fel cum little_shellface și acum, și este posibil să nu fiți conștienți de asta, dar orice fel de imprimare pe Linux este o imprimare către un fișier deschis . Ieșirea standard, din câte știe Linux, este un fișier care este deschis de un proces și imprimați la ieșirea standard scriind în acel fișier. Când am aplicat fork-ed ` little_shellfrom` init, procesul little_shell a moștenit fișierele deschise `from` init. Deci, ei partajează literalmente toate fluxurile standard de intrare și ieșire. Chiar dacă am avea mai multe dispozitive I/O pe care le-am folosi pe această mașină, acestea ar trimite în continuare ieșiri către același flux de ieșire. Când inita fost pornit, ieșirea sa standard a fost setată să producă conținut către UART, iar acest comportament a fost pur și simplu moștenit de ` little_shell.`.

Și iată-le, avem o distribuție Linux destul de inutilă, dar făcută manual! Trimiteți-o prietenilor voștri! :)

Glume la o parte, poți face un exercițiu din asta și implementa un fel de mini-shell little_shell. În loc să redai doar comenzile care îi sunt date, ai putea să-l faci să înțeleagă ce mkdireste. Poți chiar să-l faci să dezvolte un proces pentru a executa acel proces în altă parte. Nu există limite, ești în spațiul de utilizator Linux!

Să facem un pas înapoi și să vedem dacă kernelul Linux și-a îndeplinit promisiunile inițiale:

  1. Elimină hardware-ul. Nici noi init, nici shell-ul nostru nu știau nimic despre UART. Tot ce știau era că scriu într-un anumit identificator de fișier Linux. Se întâmplă să fie mapat la ceva abstract în kernelul Linux care invocă driverul UART din kernelul Linux, care poate sau nu să utilizeze SBI-ul în interior (sincer, nu am verificat dacă kernelul își elimină dependența de SBI după ce pornește).

  2. Oferă câteva paradigme de programare la nivel înalt, cum ar fi sistemele de fișiere. initProcesul nostru a localizat celălalt binar prin sistemul de fișiere (calea era banală, binarul era chiar în rădăcină, dar totuși, paradigma este acolo).

  3. Există o izolare destul de clară între procesele care rulează. Odată ce shell-ul a fost separat de init, procesele rulau practic independent. Memoria nu era partajată între ele și nu trebuiau să-și facă griji cu privire la configurația memoriei reciproce. Totuși, împărtășeau altceva, cum ar fi identificatorii de fișiere, dar aceasta este o consecință a modului în care au fost lansate pentru a rula. Linux vă permite să schimbați o parte din acest comportament, de exemplu, puteți configura o memorie partajată între procese, dacă doriți în mod explicit acest lucru.

Există multe alte lucruri pe care nucleul le face pentru noi, dar haideți să ne oprim aici deocamdată și să apreciem acestea. Poate că nu pare mult, dar nucleul ne oferă o infrastructură destul de solidă și portabilă cu care putem dezvolta software de nivel înalt, ignorând adesea complexitățile mașinii subiacente.

Deci, ce este un sistem de operare?

În opinia mea, acesta este acum un joc de cuvinte. În opinia mea, ceea ce contează este ca cititorul să înțeleagă acum ce este Linux ca kernel, ce „infrastructură” oferă și ce rulează în spațiul utilizatorului și ce rulează în spațiul kernelului.

Unii oameni ar putea numi kernelul în sine un sistem de operare, alții se vor referi la întreaga distribuție drept sistem de operare sau ar putea veni cu o idee complet diferită. Sper că în acest moment ați înțeles bine ce se întâmplă pe o mașină odată ce Linux este pornit și unde se termină responsabilitățile fiecărei componente (sau cel puțin vă puteți imagina limitele pe un sistem mai complex).

Sper că acest lucru a fost util!

Secțiune bonus: crearea unei microdistribuții cu adevărat utile cuu-root

M-am gândit să închei aici, dar nu ar fi o demonstrație extravagantă. De ce nu am porni în schimb ceva care este cu adevărat util, adică să poți face lucruri pe care le-ai face în mod normal pe un sistem bazat pe Linux, cum ar fi să rulezi lsmkdirechoși alte așa ceva? Haideți să rămânem la kernelul pe care l-am construit anterior și să adăugăm o „infrastructură peste infrastructură” utilă în domeniul spațiului utilizator pentru a face întreaga mașină mai utilă.

Îmi place foarte mult proiectul u-root pentru asta.

Notă: Titlul proiectului lor menționează bootloaderele Go, iar acest lucru s-ar putea să vă surprindă, deoarece, în calitate de cititor atent, știți că programele Go nu sunt chiar ceva ce poate fi rulat pe bare metal. Aceste bootloadere sunt bootloadere oarecum exotice în spațiul utilizatorului , ceea ce înseamnă că vor rula de fapt pe un kernel Linux live și apoi vor folosi acest mecanism Linux uimitor kexecpentru a reîncărca un kernel diferit în memorie din spațiul utilizatorului. Nu vom folosi aceste bootloadere deocamdată, ne vom concentra doar pe celelalte funcții disponibile în spațiul utilizatorului, dar m-am gândit că un scurt paragraf aici i-ar ajuta pe cititorii confuzi.

Motivul pentru care îmi place u-rootproiectul este pentru că este incredibil de ușor de utilizat. Utilizarea sa este însă puțin creativă, așa că există de fapt doi pași aici:

  1. Instalați u-rootconform instrucțiunilor lor. Ar trebui să obțineți un u-rootfișier binar în fișierul PATH.
  2. Acum, pentru a genera efectiv o funcțională initramfscu u-root, cea mai ușoară cale este să clonezi depozitul lor Git și cdsă accesezi directorul pe care tocmai l-ai clonat. De acolo, poți compila încrucișat un set de instrumente complet funcționale în spațiul utilizatorului cu o singură comandă.
git clone https://github.com/u-root/u-root.git
cd u-root
GOOS=linux GOARCH=riscv64 u-root

Primesc câteva linii de ieșire, ultima fiind:

18:31:31 Successfully built "/tmp/initramfs.linux_riscv64.cpio" (size 14827284).

Și cam atât, acest cpiofișier poate fi acum rulat pur și simplu cu QEMU și veți porni direct dintr-un shell! Consultați documentația u-rootpentru a înțelege cum puteți personaliza initramfsimaginea pe care o obțineți, inclusiv ce fel de modificări puteți face comportamentului initprocesului, dar cred că configurația implicită este extraordinară pentru a fi explorată.

qemu-system-riscv64 -machine virt -kernel arch/riscv/boot/Image -initrd /tmp/initramfs.linux_riscv64.cpio

Uau, a pornit foarte ușor! Oferă partea de jos a ieșirii UART.

[    0.400269] Run /init as init process
2023/09/12 01:34:33 Welcome to u-root!
                              _
   _   _      _ __ ___   ___ | |_
  | | | |____| '__/ _ \ / _ \| __|
  | |_| |____| | | (_) | (_) | |_
   \__,_|    |_|  \___/ \___/ \__|

Și, după cum puteți vedea din micul /#prompt, vă aflați de fapt într-o interfață de shell! u-rootinitderivat un proces de shell și i-a dat acestuia controlul asupra UART-ului.

/# ls
bbin
bin
buildbin
dev
env
etc
go
init
lib
lib64
proc
root
sys
tcz
tmp
ubin
usr
var
/# pwd
/
/# echo "Hello world!"
Hello world!

Această mică cochilie care u-rootoferă suport chiar și pentru completarea cu tabulatorul! Trebuie să spun că am întâmpinat unele probleme ocazionale cu ea, cu siguranță nu este Bash-ul în toată regula, dar este mai mult decât o simplă jucărie.

Instrumentele standard, cum ar fi, lspar să ia steagurile standard:

/# ls -lah
dtrwxrwxrwx root 0 420 B  Sep 12 01:35 .
drwxr-xr-x  root 0 2.1 kB Jan  1 00:00 bbin
drwxr-xr-x  root 0 80 B   Jan  1 00:00 bin
drwxrwxrwx  root 0 40 B   Sep 12 01:34 buildbin
drwxr-xr-x  root 0 12 kB  Sep 12 01:34 dev
drwxr-xr-x  root 0 40 B   Sep 12 01:35 directory
drwxr-xr-x  root 0 40 B   Jan  1 00:00 env
drwxr-xr-x  root 0 80 B   Sep 12 01:34 etc
drwxrwxrwx  root 0 60 B   Sep 12 01:34 go
Lrwxrwxrwx  root 0 9 B    Jan  1 00:00 init -> bbin/init
drwxrwxrwx  root 0 40 B   Sep 12 01:34 lib
drwxr-xr-x  root 0 40 B   Jan  1 00:00 lib64
dr-xr-xr-x  root 0 0 B    Sep 12 01:34 proc
drwx------  root 0 40 B   Sep 11 07:43 root
dr-xr-xr-x  root 0 0 B    Sep 12 01:34 sys
drwxr-xr-x  root 0 40 B   Jan  1 00:00 tcz
dtrwxrwxrwx root 0 60 B   Sep 12 01:34 tmp
drwxr-xr-x  root 0 40 B   Jan  1 00:00 ubin
drwxr-xr-x  root 0 60 B   Jan  1 00:00 usr
drwxr-xr-x  root 0 60 B   Jan  1 00:00 var

Vizitați google.com de aici!

Un ultim lucru interesant - hai să ne conectăm la google.com din această mașină virtuală cu user-land-ul nostru personalizat!

Mai întâi, trebuie să atașăm un dispozitiv de rețea. Îl adăugăm -device virtio-net-device,netdev=usernet -netdev user,id=usernet,hostfwd=tcp::10000-:22la interfața noastră de comandă QEMU. Cred că ultimele 2 numere nu contează cu adevărat, deoarece nu ne vom conecta prin SSH la această mașină (poate puteți face acest exercițiu singuri, dar mă tem că nu va fi ușor). Versiunea implicită a kernelului ar trebui într-adevăr să includă virtiodriverele de dispozitive de rețea, așa că acest lucru ar trebui să funcționeze mai mult sau mai puțin.

Vom avea nevoie de o adresă IP funcțională și vom folosi ceva de la u-rootpentru a o obține. Acel ceva necesită 3 lucruri prezente în configurația kernelului: CONFIG_VIRTIO_PCICONFIG_HW_RANDOM_VIRTIOși CONFIG_CRYPTO_DEV_VIRTIO. Setările mele implicite pentru kernel au toate acestea comutate la y, așa că sunt gata de plecare și ar trebui să fii la fel și tu, dar poți verifica de două ori pentru orice eventualitate. Dacă ai modificat setările kernelului, te rugăm să reconstruiești imaginea kernelului.

În cele din urmă, trebuie să atașăm un dispozitiv RNG (indiferent ce este) la mașina noastră QEMU pentru a putea obține adresa IP. Pur și simplu îl adăugăm -device virtio-rng-pcila interfața noastră de comandă QEMU.

qemu-system-riscv64 -machine virt -kernel arch/riscv/boot/Image -initrd /tmp/initramfs.linux_riscv64.cpio -device virtio-net-device,netdev=usernet -netdev user,id=usernet,hostfwd=tcp::10000-:22 -device virtio-rng-pci

Odată ce intrăm, putem rula ip addrsă vedem care este adresa noastră IP.

/# ip addr
1: lo: <UP,LOOPBACK> mtu 65536 state UNKNOWN
    link/loopback
    inet 127.0.0.1 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1 scope host
       valid_lft forever preferred_lft forever
2: eth0: <BROADCAST,MULTICAST> mtu 1500 state DOWN
    link/ether 52:54:00:12:34:56
3: sit0: <0> mtu 1480 state DOWN
    link/sit

Rețeaua noastră Ethernet nu este configurată. Să activăm rețeaua IPv4 (nu avem nevoie de 6). În această mică configurație, QEMU rulează o rețea virtualizată și încorporează un mic server DHCP care poate atribui dinamic IP-uri (documentația este aici ). Așadar, haideți să rulăm un program de asistență DHCP u-rootpentru aceasta, rulând

dhclient -ipv6=false

Rezultatul pe care l-am obținut a fost următorul:

2023/09/12 03:46:59 Bringing up interface eth0...
2023/09/12 03:47:00 Attempting to get DHCPv4 lease on eth0
2023/09/12 03:47:00 Got DHCPv4 lease on eth0: DHCPv4 Message
  opcode: BootReply
  hwtype: Ethernet
  hopcount: 0
  transaction ID: 0x05f008e1
  num seconds: 0
  flags: Unicast (0x00)
  client IP: 0.0.0.0
  your IP: 10.0.2.15
  server IP: 10.0.2.2
  gateway IP: 0.0.0.0
  client MAC: 52:54:00:12:34:56
  server hostname:
  bootfile name:
  options:
    Subnet Mask: ffffff00
    Router: 10.0.2.2
    Domain Name Server: 10.0.2.3
    IP Addresses Lease Time: 24h0m0s
    DHCP Message Type: ACK
    Server Identifier: 10.0.2.2
2023/09/12 03:47:00 Configured eth0 with IPv4 DHCP Lease IP 10.0.2.15/24
2023/09/12 03:47:00 Finished trying to configure all interfaces.

Documentația QEMU vă va spune de ce ping-ul nu funcționează, așa că haideți să nu ne mai obosim cu ping-ul. Haideți pur și simplu să „vizităm” google.com!

wget http://google.com

Acum puteți citi index.htmlfișierul descărcat!

cat index.html

Vei primi o mulțime de JavaScript ofuscat, dar asta e minunat! Înseamnă că am accesat cu succes google.com prin wget! Sper că asta îți stârnește imaginația de a face și alte lucruri interesante cu u-root.

Manageri de pachete

Poate înțelegeți intuitiv în acest moment că unul dintre cele mai importante programe software ale unei distribuții Linux este managerul de pachete. Este de fapt poarta de acces către funcționalitatea de care aveți nevoie pe mașina dvs. Ceea ce am parcurs aici este mai degrabă un flux integrat: am generat aceste imagini software oarecum monolitice și, dacă vrem să actualizăm ceva, reconstruim întreaga imagine și reimaginăm dispozitivul. Acest lucru nu funcționează pentru desktop-uri, telefoane etc. Managerii de pachete sunt acolo pentru a actualiza, adăuga sau elimina software de pe mașinile noastre. Nu vom vorbi despre ei aici, doar le vom oferi o scurtă prezentare și, sperăm, vă puteți imagina de la nivel general cum funcționează și ce fac.

Monstrul dininit

Ceea initce am creat este cu siguranță doar o jucărie și, în final, a pornit doar un fel de shell. Totuși, să fim siguri că initeste un lucru incredibil de important pe un sistem Linux, iar a-l face corect este o știință. Veți vedea o mulțime de opinii puternice despre diferite initsisteme Linux online. initDe obicei, nu generează un singur proces și îl termină pe loc, ci poate configura o mulțime de lucruri, cum ar fi dispozitive diferite, de exemplu. Ca exercițiu, rulați pur și simplu ls /devdin u-rootversiunea dvs. bazată pe și vedeți toate acele dispozitive configurate. Multe dintre ele provin din initconfigurarea de și multe sunt extrem de utile. Puteți apoi citi o parte din u-rootcodul sursă pentru a vedea ce se întâmplă acolo în init.

Depozit GitHub

Codul pentru acest ghid este disponibil aici , unde puteți pur și simplu sincroniza și construi initramfsimaginile.

Comentarii

Postări populare de pe acest blog

Containerizare nativă pe macOS: Apple lansează propriul „Distrobox”

  Editorial de: Andrei Popescu, Penguin Reviews În cadrul Conferinței Globale a Dezvoltatorilor (WWDC25), Apple a prezentat „Containerization” – un nou framework open‑source care aduce pe macOS, bazat pe Apple Silicon, un mecanism performant și sigur pentru rularea distribuțiilor Linux in containere, similar ideii de Distrobox sau WSL . 🔹 Ce este Containerization? Framework în Swift : scris integral într-un limbaj modern, se bazează pe Virtualization.framework și rulează fiecare container Linux într‑o mașină virtuală ultra‑ușoară , asigurând izolare completă la nivel de kernel . Performanţă ridicată : containerele pornesc în câteva milisecunde, folosind dinamic doar resursele necesare, datorită accelerării hardware oferite de cipurile ARM Apple . Protecție avansată : fiecare container rulează separat, eliminând riscurile asociate scăpărilor de procese între medii sau către sistemul gazdă . 🛠 Funcționalități cheie Funcționalitate Detalii Imagini OCI standard Compatibile c...

Kali GPT: asistentul AI care transformă pentesting‑ul

  Editorial de: Elena Marinescu, Penguin Reviews Într‑o mișcare revoluționară pentru comunitatea de securitate cibernetică, XIS10CIAL a lansat Kali GPT , un asistent AI conceput special pentru Kali Linux, bazat pe GPT‑4, care integrează inteligența artificială direct în terminal, redefinind modul în care se realizează testele de penetrare . 🔍 Ce aduce Kali GPT? Integrare în terminalul Kali – Kali GPT înțelege comenzi în limbaj natural, generează payloads, interpretează scanări Nmap, configurează Metasploit și explică tool‑uri precum Burp Suite, fără să părăsești shell‑ul . Asistent contextual – adaptează răspunsurile în funcție de nivel (începători vs. experți), oferind explicații simplify sau tehnice avansate . Automatizare AI – generează comenzi, script‑uri și payloads, reduce erorile umane și accelerează ciclul pentesting‑ului . Beneficii clare Productivitate sporită – reducerea semnificativă a timpului de research și documentare Învățare accelerată – e...

De ce Danemarca renunță la Microsoft Office și Windows în favoarea LibreOffice și Linux

  de Mihai Georgescu, editor colaborator Danemarca face un pas major în orientarea către suveranitatea digitală: sectorul public a început să înlocuiască Microsoft Office și Windows cu LibreOffice și distribuții Linux. Misunea este de a readuce controlul datelor în spațiul UE și de a diminua dependența de furnizori extra-comunitari. Motivele deciziei Guvernul danez a început tranziția pe baza a trei obiective prioritare: Suveranitate digitală – datele rămân sub control european, nu sunt transmise către servere non‑UE . Reducerea costurilor – eliminarea licențelor Microsoft și direcționarea fondurilor către dezvoltatori și companii locale. Securitate și interoperabilitate – standardizarea pe formate deschise (ODF) pentru o colaborare mai eficientă între instituții și cetățeni . Strategia de implementare Pilot gradual – implementări în etape, începând cu LibreOffice și Windows, urmate de trecerea la Linux — fără tranziția “brutală” în toate birourile deodată . Form...