Most kezdődő cikksorozatunkban napjaink legelterjedtebb processzor architektúráját, a 32 bites, x86-os architektúrát vizsgáljuk meg (rendszer)programozói oldalról. Ez az architektúra, pontosabban az ilyen architektúrájú processzorok és processzorú PC-k szinte kellékei életünknek, főképp az informatikusok életének. Ugyanakkor ezen architektúrájú processzorok mélyebb működéséről magyar szakirodalom híján, nem vagyunk oly tájékozottak, mint amilyen fontos szerepet tölt be az informatika, számítástechnika jelenlegi életében. Napjainkban, pontosabban 1985 óta vezető szerepet tölt be az Intel, processzorai illetve ezen 32 bites, x86-os architektúrája által, így számos rendszer ezen alapul. Az informatikusoknak, programfejlesztőknek ezen rendszerek működésének megértéséhez elengedhetetlen az architektúra alapos ismerete. Az architektúráról már megjelent egy-két kevésbé részletes magyar nyelvű kézikönyv, mely ugyan hiány pótló, ugyanakkor a szakemberek számára nem világítja meg a működést elég alapos módon, így nem alakulhat ki a rendszerek átfogó szemlélete. Így marad hiányos az operációs rendszerekről alkotott kép, így áll tétlenül az átlag informatikus a Windows 9x/ME kék képernyője és 0Eh vagy 0Dh kivétel hibája előtt. Vagy a Windows NT "ezer kis számmal" zsúfolt kék képernyője előtt csodálkozik, miközben tudatlanságában általánosságban szidja az operációs rendszert és készítőjét és sejtelme sincs, hogy hol a hiba, noha a Windows 9x/ME is megmondja: 0Eh vagy 0Dh, stb. vagy a Windows NT is, sőt jelentősen érthetőbb formában: PAGE_FAULT_IN_NON_PAGED_AREA, vagy IRQL_NOT_LESS_OR_EQUAL *** Address... - NTFS.sys, stb. Ha megérjük mi a hiba, "hol fáj az operációs rendszernek" sejthetjük milyen gyógyír segíthet. Talán - Windows NT-t alapul véve - az újonnan feltelepített program drivere (*.sys) a hibás, vagy vírusos a gépem, vagy csak egy jó fagyás eredménye képen íródott felül pár bájt vagy szektor és így megsérült pár rendszer driver? Nos reméljük sorozatunk mindezen kérdésekhez is segítséget nyújt.

A cikksorozatban a 32 bites, x86-os processzorok védett módú programozását taglaljuk, szorítkozva a legfontosabb kérdésekre: a rendszer architektúra, a védett módú memória kezelés, az architektúra által biztosított és követelt védelem mibenlétére. Tervezzük egy következő cikksorozatban a gyakorlati részek bemutatását, bepillantást nyújtva az operációs rendszerek "lelkébe", a kernelbe, egy saját gyakorlati példa bemutatása által, mely nagy mértékben elősegíti az informatikusokat és programozókat az operációs rendszerek jobb tolerálására. (A gyakorlati részekig eljutott Olvasó, a processzor és az operációs rendszer csodálatába kerül, ugyanis átlátja, hogy valóban "milyen csoda" egy operációs rendszer működése, az óriási komplexitás illetve precizitás igény miatt.)

Cikksorozatunk nagyrészt a processzor gyártó cégek - főképp az Intel - által kiadott hivatalos dokumentációkra épül. Ezek jól bevált tematikáját követjük, s saját rendszer programozói tapasztalataimmal magyarázom.

A 32 bites, x86-os architektúra története
Az x86-os architektúra kezdete

A 32 bites, x86-os (Intel) architektúrához vezető fejlesztések visszavezethetőek a 8085-ös és a 8080-as mikroprocesszorokon keresztül egészen a 4004-es mikroprocesszorig (ez az első mikroprocesszor amit terveztek, ezt az Intel tervezte 1969-ben). Annak ellenére, hogy az Intel Architektúra (az x86-os) processzor család első mikroprocesszora a 8086-os, ezt gyorsan követte egy kisebb rendszerekbe szánt olcsóbb megoldás, a 8088-as mikroprocesszor. A tárgykódú programok, melyek ezekre a processzorokra készültek 1978-tól kezdődően, még a mai, 32 bites x86-os architektúrájú processzorok mindegyikén futnak, így pl. futhat a mai legújabb ilyen alap architektúrával - is - rendelkező Intel Pentium 4, vagy akár az AMD Athlon (K7) processzorokon (feltéve, hogy nem tartalmaz alaplap vagy időzítés specifikus megoldásokat).

Az Intel 8086-os processzor

A 8086 processzor 16 darab regiszterrel, 16 bites külső adatbusszal rendelkezik, és 20 bites memóriacímzést támogat ezáltal 1 MBájt címtartományt biztosítva. A 8088-as processzor a 8086-tal megegyezik, csak annyiban tér el, hogy 8 bites külső adatbusszal rendelkezik. Ezek a processzorok vezették be az Intel Architektúra szegmentálást, de csak "valós módban": a 16 bites regiszterek mutatóként használhatóak a szegmensen belüli címzésben egészen 64 KBájtos méretig. A négy szegmens regiszter (ténylegesen) tartalmazhatja a 20 bites kezdőcímét az éppen aktív szegmensnek; egészen 256 KByte címezhető meg anélkül, hogy szegmenst váltanánk, és a teljes címezhető tartomány pedig 1 MBájt.

Az Intel 80286-os processzor

Az Intel 80286-os processzor vezette be a Védett Módot (Protected Mode) az Intel, x86-os architektúrába. Ez az új mód a szegmens regiszterek tartalmát, mint kiválasztókat vagy mutatókat használja egy leíró táblázatban (GDT - Global Descriptor Table - Globális Leíró Táblázat). A leírók 24 bites kezdőcímet biztosítanak, ezzel maximálisan 16 MBájt fizikai memóriát engedélyezve, támogatva a virtuális memória kezelést (VMM - Virtual Memory Management) a szegmens alap, illetve különböző védelmi mechanizmusainak váltogatásának lehetőségével. Ebbe beletartozik a szegmens határ ellenőrzés, a csak olvasható és futtatható szegmensek lehetősége, és egészen négy privilégium szintet támogat, hogy megvédje az operációs rendszert az applikációtól, a felhasználói programoktól. Továbbá támogatja hardveresen a taszk váltást, és a Lokális Leíró Tábla (LDT - Local Descriptor Table) segítségével az operációs rendszer megvédheti az alkalmazásokat illetve felhasználói programokat egymástól.

Az Intel 386-os processzor

Az Intel386-os processzor vezette be a 32 bites regisztereket az architektúrába, mind a számításokhoz használt operandusoknál és mind a címzésnél használhatóak. Minden 32 bites regiszter alsó része megtartotta a tulajdonságait a két korábbi generáció processzorainak, így a 32 bites regiszterek alsó részét használhatjuk a korábbi generáció processzorainak 16 bites regisztereként. Így tartva meg a tökéletes visszamenőleges kompatibilitást (tehát nem igényel kód változtatást az új technológia). Egy új (működési) módot, a virtuális-8086-os módot vezették be, hogy nagyobb hatékonyságot érjenek el, amikor a 8086 illetve 8088-as processzorokra írt programokat futtatják ezen az új 32 bites gépen.

A 32 bites címzés egy 32 bites külső címsínnel lett megvalósítva, ezáltal biztosítva a 4 GBájtos címtartományt, és az architektúra fejlesztése révén minden egyes szegmens akár 4 GBájt méretű is lehetett. Az eredeti utasítások új, 32 bites operandusú és címzésű formákkal lettek kibővítve, továbbiakban teljesen új utasítások is bevezetésre kerültek, s természetesen a hozzájuk tartozó új bit manipulációs utasítások is helyet kaptak.

Az Intel386-os processzor vezette be a lapozást (paging) az Intel, x86-os, immár 32 bites architektúrába, a fix 4 KBájtos méretű lapok lehetőséget nyújtanak egy olyan virtuális memória kezelésre, mely jelentőségteljesen jobb egy általános szegmens használatnál (ez sokkal hatékonyabb az operációs rendszerek számára, a felhasználói programok illetve applikációk számára pedig abszolút áttetsző és ezt jelentősebb - végrehajtási - sebesség csökkenés nélkül valósítja meg).

Továbbá az a lehetőség, hogy akkora szegmenst definiálhatunk, amekkora a fizikai címtartomány - akár 4 GBájtosat -, a lapozással együtt, megvalósítható a védett sík modell, mely címzési rendszert széles körben használt a UNIX típusú operációs rendszerekben.

Mindezen újítások által 1985-ben definiálta az Intel a 32 bites x86-os architektúrát, mely a legelterjedtebb processzor architektúra napjainkban. Az így kifejlesztett architektúrát azóta sok más processzor gyártó cég is alkalmazza, mely nagy mértékben elősegítette az architektúra elterjedését. Ma felhasználói számítógépeink, PC-ink jelentős többsége ilyen, a 32 bites, x86-os architektúrájú processzoron fut. Ez az architektúra szinte nem változott az elmúlt 16 évben (1985-2001) és mégis újabb és újabb processzorok jelennek meg. Ennek oka, hogy az 1985-ben avagy 1985-re megalkotott 32 bites, akkor forradalmian új, Intel, x86 architektúra teljesen átgondolt, konzekvens. Látható, hogy nem volt szükség az architektúra jelentősebb - gyökeresebb - módosítására a 16 év során, hisz az jól használható a modernebb és jócskán megváltozott körülmények között. Ami még meglepőbb, hogy egyes szakértők szerint még 18-25 évig használatban marad a 32 bites, x86-os architektúra (én ennél az idő intervallumnál azért óvatosabb vagyok, "csak": 12-18 évet prognosztizálok). Az architektúra változatlansága (a kisebb változásokról alább) mellett újabb és újabb processzorok jöttek, jogosan merül fel a kérdés, hogy akkor pontosan mi is változott. Röviden: A 32 bites, x86-os architektúrát (utasítás készlet és felépítés) kiszolgáló mikro-architektúra (a processzor belső áramkörei). Újabb és újabb belső, fizikai felépítésű processzorok jelentek meg, amik nem csak az órajelben gyorsultak, hanem egy órajel alatt is egy, majd egyre több (3-4) utasítást tudtak, tudnak végrehajtani. Így futva be a pályát a kezdeti CISC 32 bites, x86-os processzoroktól a mai OOO (Out Of Order) végrehajtású, inkább RISC jellegű, 32 bites x86-os processzorokig. Tehát az utasításkészlet, az utasításkészlet architektúra szinte változatlan az elmúlt 16 év során.

Architektúrai változások az Intel386-os processzor óta

Az Intel386-os processzor valósította meg, "alapította meg" a 32 bites, x86-os architektúrát, s azóta csak kis számú változtatás történt magában az architektúrában, az utasítás architektúrában. Igazából az 1993-ben bejelentett Intel Pentium processzor tartalmazott egy-két új utasítás architektúra szintű újítást. Bevezették a 4 MBájtos lapok használatát a 4 KBájtos lapok mellett, illetve a virtuális-8086-os módban kisebb fejlesztések történtek. 1995-ben a Pentium Pro-val történtek további kis fejlesztések az utasítás architektúrában. Kibővítették az ekkora - nagy gépes környezetben - lassan szűkössé váló 4 GBájtos, 32 bites címzési módot 36 bites címzésűre, mellyel már 64 GBájt fizikai memóriát kezelhetünk. A kompatibilitás megőrzése végett csak új biteket - melyek eddig fenntartottak voltak - definiáltak az újítások lefedésére. Így lehetőség nyílik a kompatibilitás megőrzése mellett az új funkciókat implementálva 64 GBájt fizikai memória elérésére, az immár 2 MBájtos új méretű lapok segítségével (ez jobb, mint a Pentium 4 MBájtos nagyságú, túl nagy lapmérete). 1995 óta napjainkig nem történt változás az architektúrában, s jó ideig nem is várható, bár elvi lehetősége természetesen létezik, hisz vannak még módosítható, eddig fenntartott bitek. Bár a jelenlegi fejlődés nem ebbe az irányba mutat, hisz nagy erőkkel fejleszti az Intel a "jövő architektúráját" az IA-64-et, a 64 bites új architektúrát, melynek első működő processzora az Itanium és rendszere már működik és "kapható".

A rendszer szintű architektúra áttekintése

A 32 bites x86-os architektúrájú processzorok széleskörű támogatást nyújtanak az operációs rendszerek, illetve a rendszerfejlesztő szoftverek részére. A processzor rendszer szintű architektúrája az alábbi lehetőségeket támogatja:

  • Memória menedzsment

  • A program modulok védelme

  • Multitaszking

  • Kivétel (exception) és megszakítás (interrupt) kezelés

  • Több processzoros rendszerek

  • Cache (gyorsító memória) kezelés

  • A hardware erőforrások illetve az energiakezelés menedzsment

  • Debugging és teljesítmény felügyelet

A továbbiakban az alacsony - rendszer - szintű felépítést tekintjük át. Szó lesz továbbá a rendszer és vezérlő regiszterekről, illetve az operációs rendszer által alkalmazható utasítások közül is foglalkozunk a fontosabbakkal.

Alábbi ábránkon a 32 bites x86-os processzorok rendszer szintű felépítését mutatjuk be. Ábránk regiszterek, adatstruktúrák, rendszer szintű utasítások, és a memória menedzsment blokk sémáját mutatja be.

32 bites x86-os processzorok rendszer szintű felépítése

Globális és lokális leíró táblák

Amikor védett módban működik processzorunk (ezt alább, a Processzor működési módjainál részletezzük) az összes memória hozzáférés az úgynevezett globális leíró táblán (GDT - Global Descriptor Table) vagy az opcionális lokális leíró táblán (LDT - Local Descriptor Table) keresztül történik. Az LDT használata opcionális, nagyobb jelentősége multitaszkingnál van, míg a GDT használata ki nem kerülhető. (A fenti ábránk mutatja be a GDT és LDT táblákat.) Ezen tábláknak a bejegyzéseit szegmens leírónak (segmet descriptor) nevezzük. A szegmens leírók tartalmazzák a szegmens kezdő címét (segment base address), az elérési jogait, típusát, és használati információkat. Minden szegmens leírót (tehát azt a bejegyzést, ami a szegmensről információt tárol) egy szegmens kiválasztóval (segment selector) érhetjük el. Ez nem más, mint a leíró "indexe". A kiválasztókat később részletesen tárgyaljuk. (Összefoglalva: ha egy szegmenst el akarunk érni, pl. a DS regiszterbe szoktuk valós módban a fizikai címet tölteni, akkor védett módban DS-be a kiválasztót írjuk, s az ezen kiválasztó által prezentált leíró által meghatározott szegmenshez férünk hozzá. A leírót - amire a kiválasztóval hivatkozunk - már előbb elkészítettük egy kernel utasítással. Az, hogy a GDT illetve LDT az-az globális illetve lokális leírótábla szegmens leírójára hivatkozunk az úgyszintén a DS-ben, a 2. bitben adjuk meg. Ezzel bővebben, a következő fejezetben foglalkozunk.)

Ahhoz, hogy a szegmensben hozzáférjünk egy bájthoz a szegmens kiválasztót és a 32 bites ofszetet is be kell állítani. (Miért 32 bites? Mert a 32 bites x86-os processzorok általános illetve címző-ofszet regiszterei 32 bitesek. Ezeket a regisztereket Exx módon nevezzük meg: EAX, EBX, ECX, EDX, ESP, EBP, ESI, EDI) A szegmens leíróból veszi a processzor a szegmens lineáris kezdőcímét. (Mi a lineáris cím: az operatív memória bájtjai egymás után szépen sorban: 0. bájttól egészen 4 GB vagy 64 GB-ig.) Az ofszet pedig ehhez a kezdő címhez képest határozza meg relatív módon a bájt helyét. Ofszetnek pozitív számot adunk, illetve csak pozitívat adhatunk meg. Tehát a szegmens leíró, illetve az ofszet segítségével elérhetünk bármely bájtot, így használható ez a mechanizmus mind kód, adat, illetve verem elérésére. (Az, hogy melyik szegmens mit - kód / adat / verem - tartalmaz azt a szegmens leíró határozza meg.) A fenti ábrán jól látható a lineáris címtartomány, a GDT, LDT illetve ezeken keresztül a szegmensek elérése.

Azt is láthatjuk, hogy a GDT-re egy regiszter mutat. Ez a GDTR (GDT regiszter), mely többek között tartalmazza a táblázat lineáris címét is. Innét tudja a processzor, hogy hol kezdődik a GDT. (Bővebben alább, a memória menedzselő regisztereknél írunk róla.) Hasonlóan a LDT-re is az LDTR mutat, de nem a lineáris címét, hanem az LDT kiválasztóját tartalmazza.

Rendszer szegmensek, szegmens leírók, és kapuk (gates)

Mindamellett, hogy a kód-, adat-, illetve a veremszegmensek már megteremtették egy program futásának a lehetőségét, létezik még két rendszer szegmens a taszk állapot szegmens (TSS - Task-State Segment) és az LDT. (A GDT nem tekintjük külön szegmensnek mert nem érhető el kiválasztóval, illetve nincs leírója.)

Továbbá még létezik sok speciális leíró amiket funkciójuk alapján kapuknak nevezünk (call gate, interrupt gate, és task gate). A kapuk szolgálnak átjáróként védett módban a különböző védelmi szinten álló procedúrák, rutinok, illetve megszakítások között.

(Röviden a védelmi szintekről: van az operációs rendszer, pl. Windows NT, Linux. Az operációs rendszernek olyan fontos rendszer részei vannak, mint a kernel és eszközvezérlők, melyeket nem érhet el egy átlagos felhasználói program pl. egy szövegszerkesztő. Annak kiküszöbölésére, hogy egy sima program beleírjon az operációs rendszer memória területébe vezették be a védelmi szinteket, így megelőzve azt, hogy az össze-vissza irkálástól lefagyjon az operációs rendszer. Különböző szintek vannak a védelemre, a több joggal rendelkező eléri az alatta lévőket, viszont fordítva nem.) A védelmi szinteket is később tárgyaljuk.

Példa a kapuk használatára: pl. egy program hívás a CALL kapun keresztül valósulhat meg, így férhetünk hozzá egy olyan kódszegmensben lévő rutinhoz (procedúrához) amelyik ugyanaz, vagy alacsonyabb számú (0-3) az-az privilegizáltabb. (0 - operációsrendszer kernel, 3 - felhasználói program). Tehát ha van lehetőség ilyen hívásra, akkor azt így lehet megvalósítani, pl. alkalmazás hívja a kernel rutint. Az ilyen hívás megvalósításához egy call kapu kiválasztót kell megadni. Lásd a fenti ábrát. Miután kijelöltük a kiválasztóval a call kapu leírót a processzor megnézi, hogy van e jogunk elérni a kívánt rutint. Összehasonlítja a CPL-t (CPL - Current Privilege Level; aktuális privilégium szint. Az a privilégium szint mellyel a éppen futó programrész rendelkezik.) a call kapu privilégium szintjével és a meghívandó kódszegmensét a kapuéval. Ha a meghívás megengedett a védelem megállapítása alapján, akkor a processzor a call kapuban a leíró által meghatározott szegmenssel illetve a megadott ofszettel elérhetjük a kívánt rutint, memória címet. Ha a meghívás (call) privilégium szintváltást igényel, akkor a processzor áttér ennek a privilégium szintnek a vermére. Ugyanis minden privilégium szint - mint sok más is - külön veremmel rendelkezik. Az új szegmens kiválasztót a veremhez a TSS-ből veszi a processzor. (Az éppen futó program taszk állapot szegmenséből, TSS-éből.) Végül, ha nem lehetséges a hozzáférés a más privilégiumú szegmenshez, akkor kivétel (~ hiba jelző-, feldolgozó rutin) generálódik. A kapuk továbbá használatosak a 16 illetve 32 bites kód keverésekor, illetve ezek közötti átjáráskor. Még sok esetben fogunk találkozni kapun keresztüli hozzáféréssel.

Taszk állapot szegmens és taszk kapuk

A taszk állapot szegmens (TSS - Task-State Segment) nagyon fontos a multitaszkos környezetekben. A TSS (lásd a fenti ábrát) határozza meg az éppen futó - vagy csak a memóriában bent lévő - taszk végrehajtási környezetét és állapotát. (Több program látszólag egyszerre történő futása során az alkalmazásokat, programokat taszkoknak nevezzük.) A TSS tartalmazza az általános regiszterek, a szegmens regiszterek, az EFLAG regiszter, az EIP regiszter (32 bites utasítás mutató regiszter, Instruction Pointer regiszter, az éppen végrehajtandó utasítás ofszetjét tartalmazza) elmentett értékét, illetve három szegmens kiválasztót és mutatót a három verem szegmens részére. (Három verem szegmens: mindegyik privilégium szintnek egyet: privilégium szint 0, 1, 2. Ezt tekintettük át fent, hogy innen veszi a másik privilégium szint verem szegmensének címét.) Továbbá az adott taszkhoz tartozó LDT-re mutató kiválasztó és a lapcímtár lineáris címét. (Lapcímtár (page directory): a spec. védett memóriakezeléshez, a védett módú memória-kezelés fejezetben foglalkozunk részletesen vele) A lapcímtár lineáris címe a TSS-ből közvetlenül a CR3 (PDBR) regiszterbe kerül a taszk váltás során.

Minden védett módban futtatott program taszkként fut. Tehát, ha egy program fut csak, akkor is használt a multitaszk környezet rendszer regiszterei - típusai, csak az adott taszkra. (Szükséges megjegyezni, felettébb ritka, hogy csak egy taszk fusson, hisz az operációs rendszer kernelje, meg egy felhasználói program is már több mint két taszkot takar.) Az éppen futó taszknak a TSS kiválasztója egy e célra létrehozott regiszterben a taszk regiszterben (TR) van tárolva.

A legegyszerűbb módja a taszk váltásnak egy meghívás (call) vagy egy ugrás (jump) a másik taszkra. Híváskor, illetve ugráskor szegmens kiválasztónak a másik taszk TSS-ét adjuk meg.

A processzor az alábbiakat teszi meg - automatikusan - taszk váltásának bekövetkezésekor:

  1. Az éppen futó taszk (az a taszk amiről éppen elváltunk) állapotát elmenti az aktuális TSS-be.

  2. A taszk regiszterbe - TR - az új, futtatandó taszk szegmens kiválasztója kerül

  3. Az új TSS-hez a GDT egy leíróján keresztül fér hozzá

  4. Betölti az új taszk állapotát az új taszk TSS-éből a megfelelő általános regiszterekbe, szegmens regiszterekbe, az LDTR-be, a CR3 vezérlő regiszterbe (a lapcímtár kezdőcíme), az EFLAG regisztert és az EIP regisztert is beállítja.

  5. Ezek után kezdődik el az új taszk futása az CS-ben lévő kiválasztó és az EIP által meghatározott helyen.

Egy taszkhoz taszk kapun keresztül is hozzá lehet férni. A taszk kapu a call kapuhoz hasonló, annyi kivétellel, hogy a taszk kapu nem kódszegmenshez, hanem TSS-hez ad elérést, ha adható elérés.