Poznámky ku Systémovému programovaniu (1-INF-526)

Pred rokmi, keď som bol prvák na matfyze a učil som sa na skúšku zo systémka, v rámci snahy ujasniť si pojmy a mechanizmy okolo správy pamäte som si začal písať poznámky. Tieto poznámky som zavesil a zopár ľuďom poslal odkaz; odvtedy si ich posunulo zopár ľudí z nasledujúcich generácií.

Pre lepšiu dostupnosť (a snáď aj čitateľnosť) som sa rozhodol ich skopírovať tutaj.

Okrem týchto poznámok ešte odporúčam ako študijný materiál aj prvú kapitolu zo skrípt k predmetu Operačné systémy; momentálne sú dostupné napríklad na katedrovej stránke. (Vopred sa ospravedlňujem budúcim generáciám, keď tento odkaz zomrie, ale nemám súhlas autora so zverejnením kópie. A vôbec, veď vy si nejaký spôsob, ako sa po ne dostať, nájdete aj tak. Každopádne, keď tento deň nastane, pokojne ma pingnite a ja sa pozriem, či s tým viem niečo spraviť.)

Segmentácia

V reálnom režime nie je o čom, žiadne prístupové práva nepoznáme, môžeme skákať a volať kamkoľvek chceme, odkiaľkoľvek chceme.

Čo teraz s chráneným režimom?

Máme nejaké registre CS, DS, SS, ... Tie obsahujú selektory, ktoré ukazujú do tabuľky deskriptorov. Selektor nám povie, na ktorý deskriptor sa chceme pozrieť a deskriptor nám prezradí, kde sa teda hľadaný segment nachádza, aký je veľký a čo to vlastne je zač – či je systémový (teda taký, čo obsahuje nejakú štruktúru potrebnú na riadenie skokov, volaní a podobne), alebo „obyčajný“, teda nejaký dátový alebo kódový.

CS ukazuje na kódový segment, v ktorom sa nachádza kód, ktorý práve vykonávame, teda nejaký náš program, poprípade nejaké systémové volanie, ktoré sme zavolali.

V selektore máme dvojicou bitov označených mystickou skratkou RPL – requested privilege level. V podstate rozlišujeme hlavne RPL 0, ktorý zodpovedá privilegovanému kódu, čiže systému, a RPL 3, ktorý patrí obmedzeným aplikáciám.

V selektore z registra CS hodnota RPL určuje, aké oprávnenia má práve vykonávaný kód a označujeme ju CPL – current privilege level. To znamená, že procesy, resp. vlákna kernelu, budú bežať na úrovni 0 bez obmedzení, teda budú mať počas behu CPL nastavené na 0, zatiaľ čo keď beží nejaká aplikácia, ktorá si nemôže dovoliť čokoľvek, bude mať CPL 3.

Len pre úplnosť, aby procesor nemusel pri každom prístupe do segmentu určeného selektorom pozrieť, kde je tabuľka deskriptorov, pohľadať v nej príslušný deskriptor a naládovať z neho údaje, každý register, v ktorom sa drží selektor, má neviditeľnú časť, do ktorej sa ukladá celý deskriptor z tabuľky.

Čo teraz, keď chceme pristúpiť k nejakým údajom? Na to potrebujeme najprv naplniť register DS selektorom, ktorý ukáže na ten správny deskriptor popisujúci segment údajov, ku ktorým chceme pristúpiť. Teda si skonštruujeme selektor, ktorý skúsime naládovať do registra DS. Tento selektor obsahuje aj nejaký RPL, ktorým povieme, s akými právami chceme k danému segmentu pristupovať.

Procesor sa teraz pozrie na deskriptor, ktorý má vlastný DPL, descriptor privilege level, ktorý určuje, s akými právami sa do segmentu dá pristupovať. Na to, aby sme my mohli k tomuto segmentu pristupovať, musíme my mať práva aspoň také, ako určuje DPL, teda musí platiť CPL <= DPL, ale okrem toho aj úroveň, ktorú sme vyžiadali pomocou RPL musí stačiť na prístup k tomuto segmentu, teda musí zároveň platiť RPL <= DPL.

Načo je to dobré? Predstavme si, že sme kód nejakého systémového volania, ktoré má CPL 0. Aplikácia s úbohou úrovňou 3 nás zavolá a povie nám: „Prečítaj mi, prosím ťa, toľkoto bajtov zo siete a ulož ich do tohto segmentu na túto adresu.“ Teda takýto kód si nastaví RPL pre tento segment na 3, čím dosiahne, že aj keď samotný tento kód má CPL 0, do segmentu určeného aplikáciou bude môcť zapisovať iba vtedy, keď je prístupný aj tejto aplikácii a nebude môcť prepisovať segmenty, ktoré sa dajú prepisovať iba s vyššími oprávneniami.

Ako by sme presunuli riadenie do nejakého iného segmentu?

Priamo:

Zavoláme far jump / far call, ktorému rovno povieme selektor kódového segmentu, do ktorého chceme skočiť. V takom prípade sa procesor pozrie, akého typu je zadaný kódový segment.

nekonformný:
Do takéhoto segmentu sa dá skočiť iba vtedy, keď práve vykonávaný kód beží na tej istej úrovni oprávnenia, ako má tento segment. Okrem toho ešte v selektore nastavujeme aj RPL, ktorým hovoríme, aké najväčšie práva chceme mať po skoku. Tie však ostanú rovnaké ako pred skokom. (Význam RPL mi tu ešte uniká, takže uvítam nejaké vysvetlenie.)
konformný:
Do takéhoto segmentu vieme skočiť, pokiaľ jeho práva sú aspoň také ako naše. To znamená, že do takéhoto segmentu môžeme skočiť aj pokiaľ máme nižšie práva, ako tento segment, ale CPL ostane po skoku rovnaké ako pred ním.

Teda priamym skokom nevieme zmeniť CPL vykonávaného kódu, jediný rozdiel je ten, že do konformného vieme skočiť aj s nižšími právami.

Call gate:

Call gate je vlastne modifikácia deskriptora, ktorá obsahuje informácie o type (potrebné na odlíšenie od obyčajného deskriptora), selektor segmentu, do ktorého sa ňou dá skočiť, konkrétny offset, na ktorý sa cez ňu skáče, počet parametrov, ktorý očakáva funkcia, ktorá sa cezeň volá a ako každý descriptor, DPL. DPL brány hovorí o tom, kedy máme právo použiť túto bránu, teda aby sme ju mohli použiť, CPL aj RPL musia obsahovať práva aspoň také, ako je DPL použitej brány. Cez call gate sa dá skákať alebo volať iba do kódu s vyššími alebo rovnakými právami, ako má práve vykonávaný kód. Veď predsa nebude privilegovaný operačný systém skákať do kódu nejakej špinavej aplikačnej spodiny. Teda DPL segmentu, na ktorý sa odkazuje call gate, musí obsahovať číslo menšie alebo rovné CPL. Teraz podľa toho, či skáčeme pomocou jump alebo call a takisto podľa toho, či skáčeme do konformného alebo nekonformného segmentu sa deje toto:

call do konformného segmentu:
Ostaneme na rovnakom CPL.
call do nekonformného segmentu:
CPL sa nastaví na DPL nového segmentu, teda si v podstate zvýšime práva na práva tohto segmentu.
jump
Pri jumpe sa nikdy nezmení CPL, ale pokiaľ sa pokúšame skákať do nekonformného segmentu, znamená to, že tento segment musí mať DPL zhodné s CPL, aby sa skok dal vykonať. Skok do konformného sa podarí, ale bude sa vykonávať na rovnakom CPL, ako bolo pred skokom.

Keď sa mení CPL, prepína sa na iný zásobník; pozície zásobníkov a selektory ich segmentov pre levely 0, 1, 2 sú v Task state segmente. Teda sa do nového zásobníka pushne selektor a ESP starého zásobníka, skopíruje sa toľko parametrov, koľko je zadané v call gate a pushne sa selektor pre návratovú adresu a samotná adresa. Pokiaľ sa nemení CPL, pushne sa iba návratový selektor a adresa. Pri lret sa rozlišuje, či sa vraciame do tej istej CPL, alebo do nižšej úrovne; ak meníme úroveň, na zásobníku sa nájde selektor a ESP toho zásobníka, ku ktorému sa vraciame; ak použijeme ret n, n parametrov sa odstráni z oboch zásobníkov, overí sa, či sú v poriadku privilege levely návratového zásobníka a code segmentu, vynulujú sa segmentové registre obsahujúce selektory segmentov, ku ktorým po návrate nebude prístup a vráti sa kontrola volajúcemu kódu (s tým, že sa mu vráti jeho pôvodný privilege level). Za zmienku asi stojí, že vďaka tomu stačí v TSS pamätať si iba zásobníky pre úrovne 0, 1 a 2, pretože do úrovne 3 sa dá dostať iba návratom, pričom na aktuálnom zásobníku vtedy sú informácie aj o tom, do ktorého sa chceme vrátiť. Takisto za zmienku stojí fun fact, že žiadnym jumpom ani callom nevieme pomocou call gate ani priamo skočiť do segmentu s nižším privilege levelom, než je CPL, ale toto sa dá odrbať správne pripraveným lret, teda far returnom.

Multitasking

Máme takú imba vec, čo sme už spomínali, ktorej nadávame TSS, teda task state segment. Je to ďalší systémový segment, do ktorého sa ukladajú všetky údaje relevantné k práve bežiacemu procesu / threadu, teda selektor pre lokálnu tabuľku deskriptorov, registre všeobecné a segmentové, flagy, už spomínané stacky pre úrovne vyššie ako 3, poloha aktuálnej inštrukcie, odkaz na predchádzajúci task etc etc.

Selektor pre TSS aktuálne bežiacej úlohy býva uložený v registri TR, teda task register.

Task v rámci seba môže pokojne robiť volania cez call gate, jumpy a podobne a okrem toho môže spraviť jump/call na nejaký TSS alebo task gate, čo je akýsi „odkaz“ na TSS. V TSS deskriptore, resp. task gate deskriptore je určené, z akých CPL sa dá skákať. Vtedy sa uloží kompletný stav doteraz bežiaceho tasku do jeho TSS, ak sme robili call, tak sa do TSS nového procesu uloží odkaz na predchádzajúci a natiahne sa stav z TSS, do ktorého sme skočili. Tým sme teda prepli na iný task.

Pokiaľ sme použili call, nastaví sa aj flag NT, nested task. Potom ak tento zavolaný task príde po inštrukciu iret, táto sa pozrie na flag NT, ak je nastavený na 1, vie, že sa má vrátiť ku predchádzajúcemu tasku, na ktorý ukazuje odkaz v TSS, takže uloží stav do aktuálneho TSS, natiahne stav z TSS, do ktorého sa vracia a pokračuje tam.

Takto sa teda dá naimplementovať jednoduchý kooperatívny multitasking: Scheduler bude bežať ako hlavný task, ktorý si vždy vyberie niektorý proces, ktorému predá riadenie. Tento bude bežať ako nested task, chvíľu pobeží a následne zavolá iret, ktorý vráti riadenie scheduleru. Tento si môže vybrať ďalší proces, opäť ho zavolá ako vnorený a tak ďalej.

V praxi sa však toto už veľmi nepoužíva, pretože všetky možné koprocesory majú ešte kopy ďalších registrov, ktoré sa v TSS neukladajú, preto si to moderné OS riešia vo vlastnej réžii.

No a ešte sa oplatí vedieť, že okrem globálnej tabuľky deskriptorov, ktorej poloha sa drží v registri GDTR (global descriptor table register), existujú aj lokálne tabuľky, kde každý task má svoju vlastnú. Lokálna tabuľka deskriptorov (LDT) je vlastne systémový segment, ktorého deskriptor je v GDT. Selektor pre LDT sa udržiava vždy v registri LDTR (názov sa dá ľahko uhádnuť zo skratky).

Teda keď máme akýkoľvek selektor pre nejaký dátový alebo kódový segment, v ňom je určené aj to, či ide o selektor do globálnej alebo lokálnej tabuľky.

Výnimky a prerušenia

Rozdiel medzi výnimkou a prerušením je skôr sémantický, než nejaký implementačný. Procesor oboje spracúva rovnako.

Typy výnimiek poznáme tri:

  • fault: „Nevydalo, skúsme ešte raz.“
  • trap: „Nevydalo, no čo, pokračujeme ďalej.“
  • abort: „Ja sa na to celé môžem vysrať...“

Procesor si drží v premennej IDTR odkaz na interrupt descriptor table, ktorá je podobná GDT. Keď procesor dostane výnimku alebo prerušenie, pozrie sa do IDT na deskriptor, ktorý jej prislúcha a zavolá ho.

Tento deskriptor môže byť task gate, vtedy nastane v podstate to isté, ako pri calle na tento gate, interrupt gate, alebo trap gate; pri týchto nastane niečo podobné ako pri použití call gate, ale do zásobníka, ktorý bude používaný obslužným programom, sa pushne aj obsah EFLAGS a niektoré flagy sa natvrdo vynulujú.

Pri návrate z kažého z týchto typov handlerov sa používa iret. Ak išlo o task gate, už vieme, čo sa stane. Ak to bol interrupt gate alebo trap gate, nested task flag bude nulový, teda iret robí niečo podobné ako lret pri návrate spoza call gate, len s tým rozdielom, že ešte restorne aj EFLAGS zo zásobníka.

V reálnom móde sa nerieši žiadna IDT, vtedy IDT obsahuje iba adresu interrupt vector table, čo je tabuľka, ktorá obsahuje iba priamo adresy, na ktoré sa skočí pri danom prerušení; žiadne gate, žiadne tasky, iba skok, pred ktorým sa pushne FLAGS a návratová adresa. Na návrat opäť slúži iret.

Stránkovanie

Keď už sme posegmentovali, hurá, zistili sme, že na adresu XYZ môžeme pristupovať a že tam chceme držať nejaké údaje, alebo že tam máme uložený kód. Ale... To, či máme ku nejakej pamäti prístup, alebo nie, sme rozlišovali iba na základe úrovní privilégií, ale nerozlišovali sme, či náhodou daný segment nepatrí nejakému inému procesu s tou istou úrovňou privilégií.

Ako to teraz, sakra, vyriešime? Na záchranu nám na bielom koni cvála stránkovanie.

Idea je taká, že každému procesu vytvoríme nejaký virtuálny pamäťový priestor, ktorý začína na nule a končí až kdesi hentam. Vo vnútri tohto priestoru môžeme veselo riešiť všetky stránky, úrovne privilégií a podobné nezmysly, bez toho, aby sme akokoľvek ohrozili pamäť prislúchajúcu iným procesom.

Tento virtuálny priestor teraz nasekáme na nejaké rovnako veľké kusy a tieto nejak umiestnime do fyzickej pamäte. Dokonca ich ani nemusíme umiestniť v takom istom poradí. Lenže na to si musíme pamätať nejaké tabuľky, v ktorých si pamätáme, kde sa nachádza ktorá stránka. Teda si v registri CR3 zapamätáme adresu tabuľky, ktorú nazveme page directory, v tej budú odkazy na tabuľky, kde každá tabuľka obsahuje riadky ukazujúce na fyzické miesta v pamäti.

Teda keď dostaneme logickú adresu, prvých 10 bitov použijem na pohľad do adresára, ten mi vypľuje odkaz na tabuľku. Druhých 10 bitov adresy použijem na pohľad do tabuľky, ktorá mi vypľuje adresu stránky. Potom posledných 12 bitov bude pozícia vo vnútri samotnej stránky.

V adresári a tabuľkách sa držia informácie o tom, či bola stránka alebo tabuľka od poslednej kontroly použitá, poprípade či bola stránka zmenená, čo potom jadro použije na počítanie štatistických údajov, na základe ktorých sa rozhoduje, ktoré stránky odsunúť napríklad na disk a podobne. Navyše je ku každej stránke určené, či je read-only, alebo read/write; či vôbec existuje a či k nej má prístup kód na CPL 3.

Za zmienku stojí výnimka PF (page fault), ktorá nastane pri pokuse o prístup do neplatnej stránky; to môže nastať napr. vtedy, keď kernel odsunie stránku na disk, uvoľní z RAM a proces, ktorému patrila, si zmyslí, že k jej obsahu pristúpi. Vtedy kernel musí pozrieť na disk, či ju tam neodložil, ak hej, znovu natiahnuť do pamäte a vrátiť sa z obsluhy prerušenia, pričom sa inštrukcia vykoná znova (výnimka typu fault) a tentokrát už (snáď) úspešne.

Comments

Comments powered by Disqus