0. fázis – A műhely és az első, néma kernel
Az első fázis: felállítjuk a műhelyt, és elkészül egy kernel, ami még semmit nem csinál, de már bebootol. A szakaszok önállóan is érthetők; a „Miért?” részek kinyithatók.
Röviden: mit értünk el?
Egy QEMU-ablakban bebootol egy operációs rendszer, amit mi írtunk. Még semmit nem csinál: a processzor belép a kódunkba, és elalszik. Ez viszont bizonyítja, hogy a teljes lánc működik:
bekapcsolás → firmware → bootloader → A MI KERNELÜNK
Közben felépült a műhely (eszközök, projekt, git), és megtanultunk belenézni a processzor fejébe.
Az első képernyő, ami már a miénk: a Limine menüje a zsozs_os bejegyzéssel és a limine.conf-ban megadott felirattal. Három másodperc múlva betölti a kernelt, és onnantól a képernyő fekete marad: a kernel fut, de még nem tud írni.
1. A nagy kép: dobozok a dobozban
┌──────────────────────────────────────────────────────────────┐
│ A fejlesztő gép: Arch Linux, Intel i9-14900K │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ QEMU: egy teljes, virtuális PC egy ablakban │ │
│ │ (processzor, 512 MB memória, lemez, képernyő) │ │
│ │ │ │
│ │ 1. firmware (UEFI, OVMF) │ │
│ │ 2. bootloader (Limine) │ │
│ │ 3. A MI KERNELÜNK ◄── ezt építjük │ │
│ └──────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────┘
- A QEMU egy teljes számítógépet játszik el. A kernelünk nem tudja, hogy nem valódi vason fut, és a Linuxot sem látja.
- A KVM miatt gyors: a kernelünk utasításait közvetlenül a gép valódi processzora hajtja végre, nem egy szimulátor „utánozza”.
- Biztonságos: ha a kernel összeomlik, csak a QEMU-ablak áll meg. A gépedben nem tehetünk kárt.
2. A műhely: mit telepítettünk és miért
| Eszköz | Mire kell |
|---|---|
rustup + Rust nightly | A fordító. A nightly (napi) változat azért kell, mert később olyan funkciót használunk, ami még csak ott érhető el (a megszakításkezelők hívási módja, 2. fázis). A verziót rögzítettük (rust-toolchain.toml: nightly-2026-09-29), hogy egy frissítés ne törjön el semmit a hátunk mögött. |
x86_64-unknown-none célgép | A Rust szótárában ez azt jelenti: „x86_64 processzor, operációs rendszer nélkül”. Nincs println!, nincs fájl, nincs hálózat, csak a core könyvtár (számok, szövegek, tömbök). |
rust-src, llvm-tools | A Rust alapkönyvtárának forrása, és eszközök, amikkel belenézhetünk a lefordított kernelbe. |
| QEMU + KVM | A virtuális PC. |
OVMF (edk2-ovmf) | UEFI-firmware a QEMU-hoz: ugyanaz a szerep, mint egy valódi alaplap „BIOS”-ának. |
| Limine | A bootloader, a „recepciós”. |
xorriso | Bootolható lemezképet (ISO) készít. |
strace | Megmutatja, mit kér egy program a kerneltől. Ezzel mértünk. |
| git + GitHub | Minden lépés el van mentve; bármikor visszanézhető, mi változott. |
3. Miért építünk kernelt? – a mérés
Az strace-szel megnéztük, mit kér a Claude Code (egy AI-alapú fejlesztőeszköz, amelyet a projekt egyik célja szerint a saját rendszerünkön is futtatni szeretnénk) a Linux kerneltől:
| Mit csinált | Kérések a kerneltől | Különböző fajták |
|---|---|---|
| Csak kiírta a verziószámát | 423 | 40 |
Egy igazi munkamenet egy ls-sel | 93 057 | 112 |
Egy program szinte semmit nem tud egyedül. Memóriát, fájlt, hálózatot, szálakat, sőt egyetlen kiírt sort is csak a kerneltől kérhet. Ezeket a kéréseket rendszerhívásnak hívjuk (pl. read, write, mmap, futex).
A kernel tehát egy szolgáltató: a programok kérnek, ő kiszolgál. Mi ezt a szolgáltatót építjük fel, és ez a 112 tételes lista nagyjából megmutatja, mi mindent kell majd tudnia a rendszerünknek, hogy a Claude Code fusson rajta.
4. Bekapcsolástól a _start-ig
⚡ bekapcsolás
│
▼
┌─────────────────┐ A processzor egy ősi, 1978-as üzemmódban ébred
│ 1. FIRMWARE │ (16 bites „valós mód”), és mindig ugyanarról a
│ (UEFI/OVMF) │ rögzített címről kezd olvasni: ott a firmware van.
│ │ • felébreszti a memóriát, felméri az eszközöket
│ │ • átkapcsolja a processzort 64 bites módba
│ │ • a lemezen megkeresi: \EFI\BOOT\BOOTX64.EFI
└────────┬────────┘
▼
┌─────────────────┐ • beolvassa a limine.conf-ot, megmutatja a menüt
│ 2. BOOTLOADER │ • betölti a memóriába a kernelt (/boot/kernel)
│ (Limine) │ • berendezi a lapozótáblákat („térkép”, lásd 5.)
│ │ • ad egy vermet (jegyzetfüzet, legalább 64 KB)
│ │ • kitölti a kérőlapjainkat (lásd 7.)
│ │ • elengedi a firmware-t, és ráugrik a _start-ra
└────────┬────────┘
▼
┌─────────────────┐ Innen minden utasítás a miénk. Senki nem segít:
│ 3. KERNEL │ nincs képernyőkezelés, nincs memóriakezelés,
│ _start │ nincs hibakezelés. Mindent mi építünk fel.
└─────────────────┘
Hasonlattal: a firmware a gondnok, aki reggel bekapcsolja a villanyt és kinyitja az épületet. A bootloader a recepciós: felkísér az irodádba, a kezedbe ad egy alaprajzot (memóriatérkép) és a hirdetőtábla kulcsát (a képernyő), aztán hazamegy. A kernel te vagy, egyedül az épületben.
5. Az „utca”: hogyan látja a processzor a memóriát
5.1 Címek és hexadecimális számok
A memória egy hosszú sor bájt (egy bájt = 8 bit = egy 0 és 255 közötti szám). Minden bájtnak van egy sorszáma: ez a cím, mint egy házszám.
A címeket hexadecimálisan (16-os számrendszerben) írjuk: a 0–9 számjegyek után jön az a b c d e f (10–15). Azért így, mert egy hexa jegy pontosan 4 bitet jelent, így a számok rövidek és „kerekek” maradnak. A 0x előtag jelzi, hogy hexa szám következik.
| Hexa | Tízes | Megjegyzés |
|---|---|---|
0x10 | 16 | |
0x1000 | 4096 | egy memórialap mérete (4 KB) |
0x10b4 | 4276 | |
0xffffffff80000000 | ~18,4 trillió | a kernelünk címe: 16 hexa jegy = 64 bit |
5.2 Egy furcsaság: a kernel címe nagyobb, mint a memória
A QEMU-nak 512 MB memóriát adtunk. A kernelünk címe viszont 0xffffffff80000000, ami sok milliárdszor nagyobb szám. Hogy lehet ez?
Úgy, hogy a processzor nem a valódi (fizikai) címeket látja, hanem virtuálisakat. Van egy „fordítótábla”, a lapozótábla, amely minden virtuális címhez megmondja, hol van a hozzá tartozó valódi memória:
amit a processzor lát ahol ténylegesen van
(virtuális cím) (fizikai cím, a 512 MB-on belül)
0xffffffff80001000 ──── lapozótábla ────► valahol a RAM-ban
A lapozótáblát a Limine rendezte be nekünk. A helyét a CR3 nevű regiszter mutatja (a mérésünkben CR3=1bf13000, kb. 447 MB-nál a fizikai memóriában). Hogy ez pontosan hogyan működik, az a 4. fázis témája. Addig elég annyi: a kernel virtuális utcában lakik, és a „házszám” nem a RAM-beli helyet jelenti.
5.3 Az utca térképe
Egy 64 bites címből a mai processzorok 48 bitet használnak. Emiatt az utcának csak két lakott szakasza van: az alsó és a felső fél. A kettő között egy hatalmas, tiltott szakasz húzódik: ha oda nyúlnánk, a processzor azonnal hibát jelezne.
0x0000_0000_0000_0000 ┐
... │ ALSÓ FÉL
... │ Később a felhasználói programoké lesz
... │ (shell, bash, Claude Code...).
0x0000_7fff_ffff_ffff ┘
░░░░░░░░░░░░░░ TILTOTT SZAKASZ (nem létező címek) ░░░░░░░░░░░░░░
0xffff_8000_0000_0000 ┐ FELSŐ FÉL – a kernelé
│ Itt kezdődik a „tükör” (HHDM): a Limine ide
│ a teljes fizikai memóriát bemásolta, mint egy
│ tükörbe. A vermünk is itt van (RSP=ffff8000...).
... │
0xffff_ffff_8000_0000 │ ◄── A KERNELÜNK (a legfelső 2 GB)
0xffff_ffff_ffff_ffff ┘ az utca vége
Miért éppen a legfelső 2 GB?
A Limine ezt kéri, és ez a szokásos megoldás: így a kernel nem foglal helyet az alsó félből, amely egészen a felhasználói programoké lehet.
5.4 Közelebbről: a kernelünk háza
A linker script (kernel/linker.ld) írja le, hogy a kernel darabjai hová kerüljenek. Négy részre bontottuk, mindegyik külön memórialapon (4 KB) kezdődik:
0xffffffff80000000 ┌──────────────────────┐ KÉRŐLAPOK (írható)
│ kezdőjel │ A Limine ide írja
│ ALAP_REVIZIO (…0020) │ a válaszait.
│ zárójel │
0xffffffff80001000 ├──────────────────────┤ KÓD (futtatható, NEM írható)
│ kernel::fo (…1000) │
│ _start (…1010) │ ◄── itt lép be a processzor
│ megall (…10b0) │ ◄── itt alszik (RIP=…10b4)
│ indulas (…11f0) │
0xffffffff80002000 ├──────────────────────┤ ÁLLANDÓK (csak olvasható)
0xffffffff80003000 ├──────────────────────┤ VÁLTOZÓK (írható, NEM futtatható)
└──────────────────────┘
Miért kell a külön jog?
Biztonság. Ha a kód nem írható, egy hiba nem írhatja át a programot. Ha az adat nem futtatható, egy támadó nem tudja „kódként” lefuttatni, amit becsempészett. Ez a W^X elv: vagy írható, vagy futtatható, de soha nem egyszerre. A jogokat a Limine a lapozótáblában állítja be, a processzor pedig minden egyes memóriaelérésnél ellenőrzi őket.
6. A kernelfájl: bájtok pontos helyen
A lefordított kernel egy ELF formátumú fájl (target/x86_64-unknown-none/debug/kernel). Ugyanez a formátum, mint minden Linux-programé. Három dolog van benne, ami nekünk fontos:
- A szegmensek listája: melyik darab hová kerüljön, és milyen jogokkal (
readelf -lW). - A belépési pont: hol kezdje a processzor a futtatást (
ENTRY(_start)→0xffffffff80001010). - A tartalomjegyzék (szimbólumtábla): melyik függvény melyik címen van (
nm -C).
$ nm -C target/x86_64-unknown-none/debug/kernel | sort
ffffffff80000020 keret::limine::ALAP_REVIZIO
ffffffff80001000 kernel::fo
ffffffff80001010 _start
ffffffff800010b0 keret::cpu::megall
...
Ebből tudjuk, hogy a RIP=ffffffff800010b4 a megall() belsejében van: a megall …10b0-nál kezdődik, a következő függvény …10c0-nál.
7. A kérőlapok: hogyan beszél a kernel a Limine-nal?
A probléma: amikor a kernel fut, a Limine már nincs sehol, így nem lehet megkérdezni.
A megoldás: a kérdéseket előre beleírjuk a kernelfájlba. A Limine betöltéskor végigolvassa a fájlt, megkeresi a kérdéseket egy-egy varázsszám alapján (egy hosszú, véletlenszerűnek látszó szám, ami máshol nem fordul elő), és beírja a válaszokat. Olyan, mint egy kitöltendő űrlap, amit a recepciós pultján hagyunk.
Egyelőre egyetlen kérdést teszünk fel: „A protokoll 6-os változata szerint beszélünk, rendben?”
a fájlban (előtte) a memóriában (utána, a QEMU-monitorból)
[0] varázsszám 0xf9562b2d5c95a6c8 0xf9562b2d5c95a6c8
[1] varázsszám 0x6a7b384944536bdc → 0x0000000000000006 ← „6-os szerint töltöttelek be”
[2] kért változat 6 → 0 ← „támogatom”
A kernel első dolga (keret::indulas), hogy megnézi a [2] mezőt: ha nem nulla, nem bízunk a Limine-ban, és megállunk.
Egy csapda, amit elkerültünk
a fordító azt látja, hogy ezt az értéket a kódunk sehol nem írja át, ezért „okosan” behelyettesítené a kezdőértéket (6), és azt hinné, hogy a Limine nem támogat minket. Ezért egy speciális olvasást használunk (read_volatile), ami azt mondja a fordítónak: „ne találgass, tényleg nézd meg a memóriát”.
A pontos számok a Limine hivatalos leírásából valók, nem emlékezetből.
8. A projekt felépítése és a biztonsági határ
zsozs_os/
├── keret/ ← a kernel alsó rétege: MINDEN unsafe kód itt
│ └── src/
│ ├── lib.rs (a _start makró, a pánikkezelő, az indulás)
│ ├── cpu.rs (megall: cli + hlt)
│ └── limine.rs (a kérőlapok)
├── kernel/ ← a kernel többi része: unsafe TILOS
│ ├── src/main.rs (fo: a mi fő függvényünk)
│ ├── linker.ld (az alaprajz)
│ └── build.rs (megmondja a linkernek, hol az alaprajz)
├── eszkozok/ ← a gazdagépen fut: fordít, lemezképet készít, QEMU-t indít
├── limine.conf ← a recepciós utasítása
├── DEVLOG.md ← a terv és a napló
└── CLAUDE.md ← útmutató az AI-val (Claude) közös munkához
Mi az az unsafe?
A Rust fordító sok hibát megakadályoz: nem engedi, hogy egy program felszabadított memóriához nyúljon, vagy túlírjon egy tömböt. Egy kernelnek viszont olyan dolgokat is csinálnia kell, amiket a fordító nem tud ellenőrizni: közvetlenül a hardverrel beszél, és nyers processzorutasításokat ad ki. Ezeket unsafe („nem biztonságos”) blokkba kell tenni. Ez azt jelenti: „itt én felelek, nem a fordító”.
A határ
- Minden
unsafekód akeret-ben van, és mindegyik fölött ott a// BIZTONSÁG:megjegyzés: miért helyes. - A
kernelcrate#![forbid(unsafe_code)]alatt fordul: ott a fordító megtagadja azunsafe-et. - Kipróbáltuk: betettünk egy
unsafeblokkot a kernelbe, és a fordító elutasította (usage of an unsafe block).
Miért jó ez a határ?
Ha valaha memóriahiba történik, csak a kicsi keret-ben kell keresni, nem az egész kernelben. Ahogy nő a rendszer, ez egyre többet ér.
Egy trükk: ki írja a _start-ot?
A processzor a _start nevű ponton lép be. Ehhez a függvénynek pontosan ezen a néven kell szerepelnie a fájlban, és ennek a beállítása is unsafe művelet. Ezért a _start-ot a keret egy makrója írja meg (keret::belepesi_pont!(fo)), a kernel csak megmondja, melyik a fő függvénye.
9. Mi történik a cargo run után?
cargo run
│
├─ 1/3 kernel fordítása
│ cargo build -p kernel --target x86_64-unknown-none
│
├─ 2/3 lemezkép összerakása (target/zsozs_os.iso)
│ iso_gyoker/
│ ├── boot/kernel ← a mi kernelünk
│ ├── boot/limine/limine.conf ← a Limine beállításai
│ ├── boot/limine/limine-*.bin/sys ← a Limine darabjai
│ └── EFI/BOOT/BOOTX64.EFI ← ezt keresi az UEFI
│ xorriso … → ISO limine bios-install → BIOS-on is bootol
│
└─ 3/3 QEMU indítása
qemu-system-x86_64 -machine q35 -enable-kvm -cpu host -m 512M
-bios OVMF.4m.fd -no-reboot -cdrom zsozs_os.iso
-serial stdio
Minden parancs kiíródik a terminálba, hogy lásd, mi történik. Fontosabb kapcsolók:
-no-reboot: ha a kernel végzetes hibába fut (Triple Fault), a QEMU nem indul újra, hanem megáll, így látjuk a hibát;-serial stdio: a virtuális gép soros portja a terminálodba van kötve. Az 1. fázisban ezen szólal meg először a kernel.
10. Hogyan látja a processzor: a regiszterek
A regiszterek a processzor saját, belső „jegyzetlapjai”: kevés van belőlük, de villámgyorsak. A QEMU-monitor info registers parancsa megmutatja az összeset. A fontosak:
| Amit látunk | Mit jelent | Mikor lesz fontos |
|---|---|---|
RIP=ffffffff800010b4 | Hol tart a processzor: a következő utasítás címe. A mi utcánkban, a megall()-ban. | most |
HLT=1 | A processzor alszik. | most |
CPL=0 | A legmagasabb jogosultsági szint (kernel mód). A felhasználói programok majd CPL=3-on futnak, és nem nyúlhatnak a kernelhez. | 9. fázis |
RAX=ffffffff80001000 | Egy jegyzetlap. Érdekesség: ez a kernel::fo címe. Az indulas itt tartotta a fő függvényünk címét, mielőtt meghívta. | – |
RCX, RDI=ffffffff80000020 | Valószínűleg a kérőlapunk címe: a kód utoljára azt nézte meg. | – |
RSP=ffff80001a08dfb8 | A verem teteje (a Limine adta jegyzetfüzet), a „tükör” szakaszban. | 2. fázis |
CS=0028, DS=0030, GDT=… | Az 1980-as évekből megmaradt szegmensbeállítások. Most a Limine-éi; pontosan azok az értékek, amelyeket a Limine leírása ígér. | 2. fázis |
IDT=0 | Nincs hibakezelő tábla. Ha most hiba történne, a processzor nem tudná, kit hívjon, és a gép leállna (Triple Fault). | 2. fázis |
CR3=1bf13000 | A lapozótábla (a „fordítótábla”) fizikai címe. | 4. fázis |
EFER=…d00 | Többek között: be van kapcsolva a 64 bites mód. | – |
FPR, XMM | Lebegőpontos és SIMD-regiszterek, csupa nulla: a kernelünk nem használja őket. | 9., 20. fázis |
A megall() a processzor szemével
Ezt írtuk Rustban:
loop {
unsafe { asm!("cli", "hlt", options(nomem, nostack)); }
}
Ez lett belőle (x/3i $pc a monitorban, vagy objdump -d):
cím bájtok utasítás
…10b0: eb 00 jmp (a ciklus eleje)
…10b2: fa cli „ne zavarjon senki” (megszakítások tiltása)
…10b3: f4 hlt „aludj”
…10b4: eb fc jmp …10b2 ◄── RIP ITT ÁLL
A cli–hlt párból szó szerint két bájt lett: fa f4. A processzor ezeket a számokat olvassa, semmi mást. A hlt után a RIP már a következő utasításra mutat, és ha a processzor valamiért felébredne, a jmp visszaküldené a cli-re, és újra elaludna.
11. Fogalomtár
A fázisban előforduló fogalmak a közös fogalomtárban vannak, ábécérendben.
12. Ellenőrizd magad
1. Mi a firmware, a bootloader és a kernel dolga?
A firmware felébreszti a gépet, felméri az eszközöket, és elindítja a bootloadert. A bootloader betölti a kernelt, berendezi neki a memóriát (lapozótábla, verem), válaszol a kérőlapjaira, és átadja a vezérlést. A kernel innen mindent maga csinál.
2. Mi a RIP, és miből látszott, hogy a mi kódunk fut?
A RIP a következő utasítás címe (ahol a processzor „tart”). Az értéke ffffffff8… kezdetű volt, ahová a linker scriptben a kernelt tettük. A szimbólumtáblából pedig kiderült, hogy azon belül a megall() függvényben.
3. Hogyan lehet a kernel címe nagyobb, mint a gépben lévő memória?
Mert a processzor virtuális címeket lát. A lapozótábla fordítja le őket a valódi, fizikai címekre, így a „házszám” nem a RAM-beli helyet jelenti.
4. Miért van a kód és az adat külön memórialapon?
Hogy külön jogokat kaphassanak (W^X): a kód futtatható, de nem írható; az adat írható, de nem futtatható. Így egy hiba vagy egy támadó nehezebben tudja átírni vagy kódként lefuttatni a memóriát.
5. Miért nem írhatunk unsafe kódot a kernel crate-be, és hol van helyette?
A kernel crate forbid(unsafe_code) alatt fordul, ezt a fordító kényszeríti ki. Minden unsafe kód a kicsi keret crate-ben van, így ha memóriahiba történik, csak ott kell keresni.
6. Mit jelent az IDT=0, és miért veszélyes?
Nincs hibakezelő tábla. Ha most bármilyen hiba történne (pl. rossz memóriacím), a processzor nem tudná, kit hívjon, és Triple Faulttal leállna. Ezt a 2. fázisban javítjuk.
13. Hogyan tovább?
A kernelünk él, de néma. Az 1. fázisban megtanul írni:
- Soros port: betűnként bájtokat küld egy ősi csatornán, amit a QEMU a terminálodba továbbít. Ez lesz a kernel naplója.
- Képernyő: a Limine-tól elkérjük a képernyő memóriáját, és pixelenként rajzolunk ki minden betűt.
A cél: Szia! Itt a zsozs_os. Élek. – a QEMU-ablakban és a terminálodban is.
Kipróbálható parancsok (a projektmappában):
cargo run # indítás
nm -C target/x86_64-unknown-none/debug/kernel | sort # tartalomjegyzék
readelf -lW target/x86_64-unknown-none/debug/kernel # szegmensek és jogok
objdump -d -C target/x86_64-unknown-none/debug/kernel # a gépi kód
QEMU-monitor az ablakban: Ctrl+Alt+2 (vissza: Ctrl+Alt+1) → info registers, x/3i $pc.
