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.

A Limine menüje a QEMU-ban: a zsozs_os bejegyzés, 3 másodperces visszaszámlálással

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                 │   │
│   └──────────────────────────────────────────────────────┘   │
└──────────────────────────────────────────────────────────────┘

2. A műhely: mit telepítettünk és miért

EszközMire kell
rustup + Rust nightlyA 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épA 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-toolsA Rust alapkönyvtárának forrása, és eszközök, amikkel belenézhetünk a lefordított kernelbe.
QEMU + KVMA virtuális PC.
OVMF (edk2-ovmf)UEFI-firmware a QEMU-hoz: ugyanaz a szerep, mint egy valódi alaplap „BIOS”-ának.
LimineA bootloader, a „recepciós”.
xorrisoBootolható lemezképet (ISO) készít.
straceMegmutatja, mit kér egy program a kerneltől. Ezzel mértünk.
git + GitHubMinden 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áltKérések a kerneltőlKülönböző fajták
Csak kiírta a verziószámát42340
Egy igazi munkamenet egy ls-sel93 057112

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.

HexaTízesMegjegyzés
0x1016
0x10004096egy memórialap mérete (4 KB)
0x10b44276
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:

  1. A szegmensek listája: melyik darab hová kerüljön, és milyen jogokkal (readelf -lW).
  2. A belépési pont: hol kezdje a processzor a futtatást (ENTRY(_start) → 0xffffffff80001010).
  3. 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

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:


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átunkMit jelentMikor lesz fontos
RIP=ffffffff800010b4Hol tart a processzor: a következő utasítás címe. A mi utcánkban, a megall()-ban.most
HLT=1A processzor alszik.most
CPL=0A 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=ffffffff80001000Egy 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=ffffffff80000020Valószínűleg a kérőlapunk címe: a kód utoljára azt nézte meg.–
RSP=ffff80001a08dfb8A 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=0Nincs 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=1bf13000A lapozótábla (a „fordítótábla”) fizikai címe.4. fázis
EFER=…d00Többek között: be van kapcsolva a 64 bites mód.–
FPR, XMMLebegő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:

  1. 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.
  2. 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.