KIV/OS Flashcards
80186 a 80286
Oba - 1982
Oba - Instrukční sada x86-16 (16 bitový procesor)
80186 - sice se už nevyrábí, ale zato dodnes existují RISCové procesory, které mají x86-16 instrukční sadu
80286 - měl protected mode, procesor po zapnutí však startuje real-mode kvůli zpětné kompatibilitě (to ostatně dělají i dnešní x86-64)
80286 – vybrané registry (5x)
1) AX, BX, CX a DX – obecné 16-bitové registry, které se dále dělí na dvojice 8-bitových registrů
2) CS, DS, SS, ES – segmentové registry pro kód, data, zásobník a extra segmentový registr
3) SI, DI, BP, SP – indexové registry; source, destination, base, stack
4) Flags – stavový registr, výsledky operací
5) MSW (386+: CR0-4, atd.) – stav CPU (machine status word)
80286 – adresace
Adresa v paměti ji dána dvojicí registrů segment:index (např CS:IP)
Adresa má 20 bitů
- segment i index 16 bitů
- adresa = segment posunutý o 4 bity dolevo, bitové or s indexem
MS-DOS
- význam
- co poskytuje
- druhy
- Disk Operating System
- Jeden z nejvýznamnějších (tj. ne nutně nejlepších) operačních systémů
- Poskytuje konzoli a souborový systém
(konzole se myslí klávesnice pro standard input) - Existovalo mnoho DOSů
◦ MS-DOS, PC-DOS, DR-DOS, QDOS…
◦ MSDOS.SYS, IO.SYS a COMMAND.COM se pak jmenují jinak
◦ A dodnes aktivní FreeDOS
80286 – MS-DOS Bootstrap - od začátku až do nalezení MBR (tj. kroky 1 - 4)
1) Po zapnutí/hw resetu (tj. hot reset počítače) se procesor uvede do aktivního stavu a do režimu real-mode, tzn. má přístup pouze k 1MB paměti a kód může číst a zapisovat na libovolné místo v paměti
2) Registry CS:IP se nastaví na hodnoty F000:FFF0 a CPU začne vykonávat instrukce od této adresy
◦ Tj. skočí na první instrukci ROM-BIOSu, čímž spustí jeho kód – proto se této adrese říká reset vector
◦ Cold reset počítače: předání se řízení na adresu určenou reset vectorem; od 386 je to fyz. lineární adr. 0xFFFFFFF0
3) Podle nakonfigurovaného pořadí BIOS hledá disky
4) Při nalezení prvního disku BIOS načte do paměti, adresa 0x7c00, jeho první sektor, tj. prvních 512 bytů, předá řízení CPU na tuto adresu – tj. nastaví CS:IP
◦ První sektor se nazývá Master Boot Record (MBR)
80286 – MS-DOS Bootstrap - od nalezení MBR až po převzetí řízení IO.SYS (tj. kroky 4-7)
4) Při nalezení prvního disku BIOS načte do paměti, adresa 0x7c00, jeho první sektor, tj. prvních 512 bytů, předá řízení CPU na tuto adresu – tj. nastaví CS:IP
◦ První sektor se nazývá Master Boot Record (MBR)
5) Kód načtený z MBR má za úkol načíst zbytek zavaděče ze správného/aktivního/vybraného oddílu disku
◦ Může být zavaděč přímo operačního systému
◦ Anebo manažer, který dá vybrat, který OS se má zavést, je-li jich nainstalováno více
◦ Anebo to také může být vir, který infikuje počítač ještě před načtením OS
◦ MS-DOS startuje z FAT, active&bootable&primary oddílu
6) Poté, co kód z MBR identifikoval použitelný diskový oddíl, pokračuje s načítáním OS do paměti
◦ Jedná se o soubory IO.SYS a MSDOS.SYS, které musí být uloženy kontinuálně na začátku oddílu
7) Jakmile jsou sobory načteny, řízení převezme IO.SYS
◦ Rozhraní mezi DOSem a I/O subsystémem, které zpřístupní základní periferie
80286 – MS-DOS Bootstrap - od převzetí řízení IO.SYS až do konce (tj. kroky 7 - 12)
7) Jakmile jsou sobory načteny, řízení převezme IO.SYS
◦ Rozhraní mezi DOSem a I/O subsystémem, které zpřístupní základní periferie
8) IO.SYS předá řízení MSDOS.SYS
◦ Jádro OS, které poskytuje abstrakci od HW pomocí poskytovaných služeb
9) Pokud existuje, MSDOS.SYS načte a zparsuje CONFIG.SYS
◦ Zavede ovladače paměti (XMS, EMS), periferií (CDROM, Myš, zvuková karta, atd.)…
10) MS-DOS.SYS načte a spustí, tj. předá řízení, COMMAND.COM (interpret příkazů, aneb shell)
11) Pokud existuje, spustí se AUTOEXEC.BAT
◦ Dávkový soubor s příkazy pro COMMAND.COM
12) C:\ aneb Hotovo
Windows - 9X Bootstrap
- 16 bitové Windows se spouštěly příkazem win.com
◦ Zavaděč Windows/386 přepnul procesor do tzv. protected-mode - Windows 9x se zaváděly tak, že IO.SYS provedl konfiguraci počítače v real-mode a pak spustil win.com po dokončení AUTOEXEC.BAT
◦ Komplikované zavedení ovladačů, některá zařízení mají ovladače jen pro real-mode, zatímco Windows běží v protected-mode => potřeba virtualizace –V86 mode
Pozn:
O trochu komplikovanější
Méně pádů
Pády kvůli driverům
Problém jádra většího než 1MB
- CPU startuje v real-mode pouze s 20-bitovou adresou (2^20 = 10^6)
Jak zavést tak velké jádro?
1) 386+ procesory lze přepnout do tzv. unreal-mode, který zpřístupní dostupnou paměť jako 4GB flat address space
- > A pak lze načíst a spustit jádro větší než 1MB
2) Anebo máme UEFI – což nikdy nebyl případ MS-DOSu
- > Ale stále existuje např. FreeDOS
Unreal mode
- Není to oficiálně garantovaná vlastnost, ale existuje protože ji potřebuje System Management Mode
- Aby mohly programy běžet v protected-mode (32-bitová adresa, segmentace, stránkování, izolace procesů), je třeba vytvořit GDT a LDT
- Globální a lokální tabulky deskriptorů segmentů – organizace paměti
- Segmentům se nastaví maximální velikost (limity) a procesor se přepne zpět do real-mode
◦ Respektive do unreal-mode protože limity zůstanou zachovány
Problém příliš velkého programu (DOS)
Co když máme program pro real-mode, který ale
vyžaduje více paměti, než kolik jí je volné?
◦ XMS, EMS – paměťové manažery, které přepnuly procesor do protected mode a na rozdíl od unreal mode v něm zůstaly
◦ Real-mode programům, pak umožnily využít větší paměť po max 64kB velkých oknech, které kopírovaly mezi adresami nad a pod 1MB
Programy pak dynamicky překrývaly blok paměti různými částmi, moduly, svého kódu => tzv. overlays
Overlays
- Technika, která umožňuje spustit program, který je větší než velikost dostupné paměti
◦ Předchůdce virtuální paměti (příští přednáška)
◦ Stále se používá u vestavěných zařízení, kde virtuální paměť není k dispozici - Program se rozdělí do funkčních modulů, overlays, které mají stromovou strukturu
◦ V paměti mohou být zavedeny jenom moduly od na cestě kořene, tj. fce main, až k listům
◦ Návrh stromů a explicitní zavádění a uvolňování musí zařídit programátor
UEFI
- Unified Extensible Firmware Interface
- Nástupce BIOSu, který má řešit jeho nedostatky
- Nespoléhá se na boot (tj. první) sektor, ale definuje boot manager
◦ Umí zavádět pouze důvěryhodně podepsaný kód – což nejsou např. viry – BIOS uměl zamezit přepsání MBR
◦ Nicméně má legacy mode, ve kterém se chová jako BIOS - Umí přepnout procesor do cílového režimu, např. protected-mode – zavaděč OS to ale musí očekávat
UEFI boostrap - Stejná control flow jako MS-DOS
EFI executable
spustitelný soubor v UEFI bytecodu (ne Java bytecode)!
Guid Partition Table (GPT)
načisto udělaná tabulka diskových oddílů, tak aby se nemusel vláčet omezující balast z minulost (UEFI)
EFI System Partition
oddíl naformátovaný souborovým systémem FAT dle specifikace UEFI
MS-DOS API
Chce-li program zavolat službu operačního systému, de-facto volá požadovanou rutinu, která je už někde
v paměti – ale jak ji najde?
Řešením je, aby byla na předem známé adrese, kam se předá řízení procesoru nastavením CS:IP
Rutin implementujících jednotlivé služby může být mnoho => použije se další registr, např. ax na určení konkrétní služby
Volání:
Služba MS-DOS se zavolá pomocí přerušení 21h
◦ Tzn. adresa hlavní rutiny služeb OS je zapsána na 0x21. pozici tabulky vektorů přerušení
Příklad volání:
mov ah, 0x30h
int 21h
;po návratu jsou hlavní a vedlejší čísla verze v AL a AH
MS-DOS obsluha API
- Program nastaví příslušné registry vykoná int 21h
- Do zásobníku, na jehož vrchol ukazuje SS:SP, se uloží registry CS:IP (ukazující ve volajícím programu na další instrukci po int 21h) a registr Flags
◦ Provede procesor v rámci zpracování instrukce int 21h - Jádro OS získá kontrolu nad CPU a vidí všechny registry volajícího programu
- Jádro OS provede příslušnou akci a nastaví příslušní registry podle výsledku akce
EGA-BIOS
- Co když budeme chtít využít služeb, které neposkytuje BIOS, ale např. grafický adaptér?
- Např. budeme chtít změnit režim obrazovky na
320x200x256. Program vykoná následující instrukce
mov ah, 0 ;služba Změň videorežim
mov al, 13h ;režim 320x200 256 barev
int 10h
- Z hlediska procesoru se stane to samé jako při instrukci int 21h, pouze vykonávaný kód bude v paměti někde jinde
Zápis do video paměti (asi MS-DOS)
- Na adrese a000:0000 je první pixel právě nastaveného videorežimu, levý horní roh
Na každý pixel je jeden byte, byte je index do palety barev
Přímým zápisem do namapované videopaměti měníme barvy jednotlivých pixelů
V textovém režimu je znak v levém horním rohu na
b800:0000
◦ Má dva byty – 1. byte kód znaku, 2. byte atributy, např. barva
VESA - motivace
Videorežim 320x200x256 potřebuje méně než 64kB na uložení indexů pixelů do palety barev
Videorežim 640x480x16 také
Ale videorežim 640x480x256 už ne – navíc to dříve nebýval standardní režim, dokud se neobjevila specifikace VESA - SuperVGA
Před VESA sice bylo možné na některých kartách takový režim aktivovat, ale postup byl proprietární
Proto vznikl rezidentní software, který VESA emuloval
VESA - stránkování
Video režim 1280x1024 při 24-bitové barevné hloubce potřebuje více než 64kB paměti
Řešením bylo stránkování
◦ Obraz se rozdělil na několik stránek po 64kB
◦ 64kB od A000:0000 umožňovalo přímý zápis a čtení do aktivní stránky
Aktivní stránka se zvolí funkcí VESA BIOSu
◦ Buď pomocí int 10h – opakované volání int 10h je pomalé
◦ Anebo call na konkrétní adresu obslužné rutiny; adresu získáme přes int 10h – call je daleko rychlejší než int, který je potřeba pouze jednou
VESA – Linear Frame Buffer
I když je stránkování video paměti pomocí call rychlejší než pomocí int, je stále pomalé
Rychlejší je přímý přístup do video paměti, jako tomu bylo u videorežimů, kterým stačilo 64kB
VESA umožňuje získat adresu Linear Frame Buffer (LFB)
◦ Obdoba A000:0000 u EGA, ale adresa LFB není pevně daná
◦ Je třeba LFB „povolit“ a následně získat adresu od grafické karty
◦ Blok paměti, který nelze použít pro data a kód programů
Zřetězení obsluhy přerušení
Např. chceme-li sw emulovat VESA
1. Program si do vlastní proměnné načte adresu stávajícího vektoru přerušení – int 10h u sw VESA
2. Program zapíše do tabulky vektorů přerušení adresu své rutiny, která bude přerušení nově obsluhovat
◦ Např. u sw VESA obsluhuje služby s AH=4Fh (VESA extension)
◦ Nová rutina přerušení může, případně musí (např. int 08h hodiny), zavolat (už ne int!) předcházející obsluhu přerušení
3. Program skončí službou OS Terminate and Stay Resident (TSR)
Ukončení TSR
TSR program měl smysl pouze tehdy, pokud obsluhoval některé přerušení
V paměti mohl být načteno několik TSR obsluhujících stejné přerušení
Ale co když se měl jeden z nich ukončit, a nebyl to ten poslední?
◦ Byl to problém, protože neexistoval standardizovaný protokol, jak vyjmout ze zřetězeného seznamu obsluh přerušení někoho uprostřed
Interrupt request (IRQ)
Instrukce int je sw-vyvolané přerušení
Pokud přerušení vyvolá hw, pak se bavíme o IRQ
◦ Každé IRQ má svoji prioritu – Level aka IRQL
Při IRQ procesor zastaví vykonávání aktuálního programu, uloží Flags, CS a IP, a začne vykonávat příslušenou obsluhu přerušení dle tabulky vektorů přerušení
Programmable Interrupt Controller (PIC) mj. překládá číslo IRQ na index do tabulky vektorů přerušení
Časovač
Např. tik hodin je IRQ0 (nelze změnit ani maskovat)
a vyvolá obsluhu přerušení int 08h
◦ Proběhne každých 55ms
Na tomto přerušení závisí mnoho důležitých činností a je proto nutné, aby
a) Bylo co nejrychlejší
b) Zavolalo původní obsluhu přerušení
Pomocí časovače se implementuje preemptivní
multithreading (a následně multitasking)
Časovač - implementace
Nejprve se do proměnné oldVec8 uloží adresa původní
obsluhy přerušení, takže obsluha může vypadat následovně:
pushf;simulace volání obsluhy přerušení
call dword ptr cs:[oldVec8] ;pro původní obsluhu
… ;naše vlastní činnost
iret ;návrat do přerušeného programu
I/O Porty
Input/Ouput base address – adresa prvního portu
Periferie lze také ovládat pomocí portů – jedno zda blikáme s LED klávesnice, nebo programujeme PIC
◦ Zápis odešle příkaz; instrukce out
◦ Čtení čte stav nebo výsledek operace; instrukce in
Např. při obsluze časově závislých činností v int 08h je nutné poslat řadiči přerušení informaci, že přerušení již skončilo
mov al, 20h ;signál Konec přerušení
out 20h, al ;port řadiče přerušení 8259
Viry
MS-DOS umožnil ovládat počítač a jeho periferie
Ale špatně nebo záměrně napsaný program mohl číst
a přepisovat jeho vnitřní proměnné, a libovolně měnit činnost systému
◦ Např. na přerušení se mohl pověsit vir, který se spouštěl z infikovaného MBR disku a infikoval MBR disket, a díky zavedení před jádrem OS mohl utajit své soubory na disku filtrováním systémových volání
Antiviry musely skenovat paměť, což byla příležitost pro polymorfní viry, které měnily svůj kód v paměti za běhu
Režím jádra a uživatelský režim - motivace
..mohlo zároveň běžet několik procesů
..proces nemusel vědět, že zároveň s ním běží další procesy
..bylo jedno, kde v paměti běží který proces
..proces nemohl přistupovat do paměti jiného procesu či jádra
..výsledkem nebylo zpomalení počítače
..výsledkem bylo zefektivnění práce na počítači
Memory Management Unit
- Výše uvedené cíle (motivace režim jádra a uživatelský režim) implikují, že procesy budou pracovat se
svým formátem (tj. virtuální) adresy do paměti, která bude následně převedena na fyzickou adresu do paměti
Protože sw implementovaný převod by byl pomalý, převod virtuální na fyzickou adresu obstará hw
◦ Konkrétně Memory Management Unit (MMU)
◦ Detaily převodu zadá MMU přímo OS
CPU –(virutální)–> MMU –(fyzická)–> Paměť
x86 virtuální adresa
x86 pracuje s virtuální adresou ve formátu segment:offset
Z ní je třeba získat tzv. lineární adresu, a teprve tu lze převést na fyzickou adresu v režimu, ve kterém dokážeme od sebe izolovat jednotlivé procesy a jádro
CPU –(virutální/logická)–> MMU –(lineární)–> Stránkování –(fyzická)–> Paměť
Protected mode
V prvé řadě musíme zabránit uživatelským procesům, aby nemohly modifikovat kód a data jádra
Jelikož x86 adresuje pomocí segmentu a offsetu (vůči
segmentu), řešením bylo připojit dodatečné informace ke konkrétním segmentům
◦ => už nepracujeme se segmentem, ale se segment deskriptorem
◦ Procesor běží v tzv. Protected mode
Segment Descriptor
Segment má: ◦ Základní (base) adresu (lineární adresa) ◦ Velikost (limit) ◦ Typ (kód, data, atd.) ◦ Přístupová práva ◦ Další vlajky
Izolace jádra
Pro izolaci je důležitá hodnota bitů CPL/DPL
CPL = current privilege level?
DPL = descriptor privilege level?
Číslo oprávnění/privilege level aktuálního kódu
◦ 00 - většinou jádro
◦ 01 –může být jádro, když na 00 poběží hypervizor virtualizace
◦ 10 – může být ovladač, který nesmí do jádra
◦ 11 – většinou uživatelský proces
Kód s nižším číslem oprávnění může přistupovat do segmentu s vyšším číslem oprávnění
Opačně to nelze => izolace jádra od uživ. procesů
Izolace procesů
Před vlastní inicializací protected mode musí jádro OS
vytvořit tabulku deskriptorů segmentů
Tabulky existují dvě
1) Globální používaná jádrem
- > uložena v registru gdtr privilegovanou instrukcí lgdt
2) Lokální používaná konkrétním jedním procesem
- > Uložena v registru ldtr privilegovanou instrukcí lldt
- > Tj. při přepnutí kontextu může lldt vykonat pouze jádro s CPL=0 a tudíž si uživatelský proces nemůže měnit jemu přidělenou paměť
Segment deskriptory v kódu
Uživatelský proces může mít až 6 segmentů s deskriptory uloženými v cs, ds, ss, es, fs a gs
◦ V 64-bitovém long-mode jsou nulové až na fs a gs
Adresa v paměti má prefix požadovaného segmentu
◦ call cs:[adresa_funkce]
◦ mov eax, ds:[adresa_retezce]
◦ mov ecx, ss:[ebp-offset_parametru_funkce]
V ebp bývá hodnota esp při volání funkce, tzv. frame pointer
Výhody a nevýhody segmentace
\+ Izoluje procesy a jádro \+ Lze sdílet segmenty – např. read-only programový kód sdílených knihoven \+ Lze relokovat i pouze jeden segment \+ Není nutné alokovat nevyužitou paměť \+ Tabulka deskriptorů se vejde do MMU
- Segmenty mohou mít různou délku => jak je poskládat do paměti?
- Segmenty mohou být velké => fragmentace paměti
- Jak efektivně implementovat sdílenou paměť mezi procesy?
- Jak efektivně odkládat paměť z RAM na disk, abychom zvýšili celkovou dostupnou paměť počítače?
Stránkování
- Odstraňuje nevýhody segmentace
◦ x86 umožňuje kombinovat segmentaci a stránkování
◦ Nelze povolit stránkování bez protected mode
◦ x86 ji umožňuje použít v protected (32-bit) a long (64-bit) mode - Celá paměť se rozdělí do stránek o pevné velikosti
◦ 4kB, 2MB, 4MB a 1GB
◦ Virtuální stránka: page (o velikosti frame)
◦ Fyzická stránka: frame (o velikosti page)
Page Table
- Jádro OS vytvoří tabulku stránek
◦ Řídící registr (x86) cr3 ukazuje na tuto tabulku stránek
◦ Pouze jádro s CPL=0 může měnit obsah cr3
-> Tj. dosáhneme stejného efektu jako s GDT a LDT
-> Procesor nastaví cr3 při přepnutí kontextu - Každá stránka má své vlajky, mj. zahrnující
◦ Jádro vs. Uživatelský proces
◦ Writeable, NX – do not execute
◦ Present (zda je fyzicky v RAM), Dirty a Accessed
Hierarchická Page Table
Jenom několik málo procesů využije celý dostupný paměťový prostor, např. 2^32
Řešením je vytvořit vnější/nadřízenou/page directory tabulku stránek, která bude odkazovat na další tabulku stránek
◦ Paměťový prostor procesu pak nemusí být spojitý, ale mohou v něm být díry na místech, které program nepoužívá
Translation look-aside buffer
Běžící proces často potřebuje jenom omezené množství stránek
◦ Jak tedy zrychlit převod virtuální adresy na fyzickou?
◦ Pomocí asociativní pamětí Translataion look-aside buffer (TLB)
-> Musí se vyprázdnit při změně kontextu
-> Je rychlá, ale také je malá
—> Větší hit rate když je větší, ale také je pak pomalejší
-> Má zásadní vliv na výkon
Sdílená paměť
- Efektivní cesta jak sdílet data mezi různými procesy
- Procesy jsou zodpovědné za konsistenci dat ve sdílené paměti
- OS pouze zajistí sdílení paměti
- Do tabulky stránek procesu OS přidá page entry, která ukazuje na ten samý rámec (fyzická stránka) jako page entries v tabulkách stránek ostatních procesů, které tímto „trikem“ sdílejí tu samou paměť
Sdílení kódu
Pokud několik procesů používá sdílený programový kód, proč ho do paměti nahrávat vícekrát?
Příslušné stránky s kódem se označí jako read-only
a namapují se do paměťových prostorů příslušných procesů
◦ Tj. stále jde o sdílenou paměť
Příkladem jsou dynamické knihovny
◦ dll
◦ so
Copy on write
Co když několik procesů na začátku sdílí stejná data, která považují za privátní, ale mění je jenom zřídka?
1. Příslušná stránka se označí jako read-only
◦ Dokud z ní proces jenom čte, nic se neděje
2. Jakmile se proces pokusí zapsat do read-only stránky, procesor vygeneruje vyjímku, kterou zachytí OS
3. OS pak alokuje novou stránku, zkopíruje do ní původní read-only stránku, a aktualizuje tabulku stránek procesu
4. Po návratu z obsluhy proces normálně zapíše data už do nové stránky, aniž by o něčem vůbec věděl
Swapování
Chceme-li poskytnout procesům více paměti, než kolik máme fyzicky RAM, nezbývá než paměť z RAM dočasně odložit do jiné paměti – flash nebo pevný disk
Working Set – dynamická množina stránek, které má proces aktuálně ve fyzické paměti
Chce-li dát OS více paměti některému procesu a není volný rámec, zmenší Working Set jiného procesu odebráním stránky, přičemž uloží obsah rámce na disk
Page Fault
- Proces chce číst data ze stránky, která není v RAM
- MMU nedokáže přeložit virtuální adresu na fyzickou adresu
◦ => vygeneruje vyjímku PageFault - OS uloží rámec některé jiné stránky, třeba i jiného procesu, na disk a načte do něj stránku požadovanou aktuálním procesem
- OS příslušně upraví tabulky stránek dotčených procesů
- Po dokončení proces dál normálně pokračuje aniž by něco poznal
Následky:
Ve skutečnosti OS nebude měnit working sets při každém Page Fault
◦ Namísto toho se požadavky mohou nasbírat a vyřídit později – hromadně
◦ Page Fault-ovaný proces se mezitím může pozastavit
Swapování má extrémně negativní vliv na výkon systému
◦ Zaměstnává procesor, disk a příslušnou i/o sběrnici
Mapování souborů
Chceme-li zrychlit práci se souborem, je možné OS požádat o namapování souboru do paměti
Proces pak zapisuje a čte přímo z rychlé paměti, namísto práce s pomalejším diskem
OS namapuje soubor do paměti po stránkách jako
u swap souboru
Při ukončení práce se souborem se pak na disk zapíší pouze ty stránky, které mají nastavený dirty bit
◦ Pokud by měl soubor např. několik GB, proč zapisovat vše?
Změna privilege level
Uživatelský proces běží ve svém paměťovém prostoru
Pokud volá funkci jádra operačního systému, které vyžaduje vyšší úroveň oprávnění, např. CPL=0, je nutné nějak změnit privilege level
Dělá to procesor v okamžiku, kdy obsluhuje přerušení ať už int nebo IRQ
◦ Úroveň oprávnění určí z adresy/vektoru obsluhy přerušení
◦ Obsluha přerušení běží se zvýšeným CPL tak dlouho, dokud neudělá iret
Physical Address Extension (PAE)
Éra 32-bitových x86 s více než 4GB RAM
Ačkoliv procesor, tj. i jádro OS, dokázalo adresovat pouze 4GB RAM, tabulky stránek mohly adresovat více než 4GB RAM
V podstatě šlo první kroky ke 64-bitovým tabulkám stránek – u PAE s 36-bitovou fyzickou adresou
◦ Zavedeno s Pentium Pro
◦ Nicméně, už 386 by teoreticky zvládnula 64TB virtuální paměti – technická realizace v praxi je ovšem něco jiného
80386 virtuální adresový prostor
Virtuální adresy mají 48-bitů – dáno MMU
◦ 16 bitů segment selektor, 32 bitů offset v rámci segmentu
16+32= 48 bitů virtuálního adresy
◦ Ale 2 bity selektoru jsou použity na privilege level
◦ 1 bit selektoru indikuje globální/lokální tabulku
◦ => 46 k adresování použitelných bitů => 2^46=64TB
Jedná se ale jenom o teoretický limit! V praxi nikdy nebylo použito.
Kde se používá segmentace?
Ačkoliv je segmentace považovaná za historický artefakt, nedá se říci, že by byla nepoužívaná
◦ Např. Vx32 user-level sandboxing na x86 ji používá ke spouštění nedůvěryhodných programů na FreeBSD, Linuxu a Mac OS
◦ Implementace TLS ve Windows
Segmented Paging
Tabulky stránek jsou segmentovány
Virtuální adresa je logical_page:offset
logical_page je segment_number:segment_offset
Paged Segmentation
Segmenty se skládají ze stránek
Virtuální adresa je seg:offset
Offset je page_number:page_offset
Way to go on x86
Monolitické jádro (privilege level)
Všechny služby jádra OS běží s CPL=0
Ovladače mohou běžet jako moduly jádra také s CPL=0
K drahému přepnutí kontextu dojde jenom 2x
◦ Při volání služby OS
◦ Po dokončení služby OS
=> Pozor, řízení se může předat jinému procesu
Proces běží -> Jádro obsluhuje -> Proces běží
Monolitické jádro - problém s velikostí
Jádro může být příliš velké, než aby se vešlo do paměti
◦ Nebo nechá příliš málo volné paměti
◦ Problém zejména u Embedded systémů
◦ U PC s Linuxem to zase takový problém není
Např. AIX a MULTICS umí dynamicky načítat a uvolňovat moduly
Kernel Panic
Když selže jádro, co budeme dělat?
◦ Nejspíš už nic.
Vlastní jádro bývá obvykle dobře odladěné
Daleko větší problém představují ovladače běžící s CPL=0
Chyba v ovladači pak s sebou vezme celé jádro
◦ Bez ohledu na to, jak má být dotčené jádro dobré
Příčinou může být i vadný hw
Mikrojádro – proč?
Když přesuneme co nejvíce kódu mimo CPL jádra, pak zvyšujeme šanci, že jádro přežije
◦ A následně můžeme restartovat pouze ten kód, který selhal
Mikrojádro obsahuje pouze základní, nezbytné služby
◦ Plánovač
◦ Alokace paměti (ale ne celý správce paměti)
◦ Meziprocesová komunikace
Když „nejaderný“ kód OS poběží mimo CPL(plánovač má CPL jádra), pak ho můžeme napsat jako preemptivní a plánovat jako běžný proces
◦ Požadavky na vykonání služeb se pak dají seskupovat a tím se zvýší efektivita jejich obsluhy systémem
Monolitické a hybridní jádra mají Bottom-Half, viz dále, které mají stejný cíl
◦ Kód pak může běžet na různých CPU,
Dokonce i na jiném počítači u distribuovaných OS
Mikrojádro – skutečný výkon
Mikrojádro je pomalé kvůli příliš velkému počtu
přepínání kontextu
1. Proces volá službu OS – přepne se kontext do CPL mikrojádra
2. Mikrojádro určí příslušný obslužný kód mimo CPL mikrojádra a předá mu řízení => přepnutí kontextu
3. Když se dokončí obsluha mimo CPL mikrojádra, předá se opět řízení do kontextu s jiným CPL
–> Může být opět mikrojádro a z něj pak následně
přepnutí do CPL uživatelského procesu
Mikrojádro – optimalizace
Protože mikrojádro přeposílá požadavky jinam, optimalizace by spočívala v tom, jak při předání výsledků ušetřit alespoň jedno přepnutí kontextu
◦ Případně jak rovnou volat kód mimo CPL mikrojádra
Taková možnost musí ale počítat s tím, že kód mimo mikrojádra mohl být nahrán znovu na novou adresu v paměti
Nejrychlejší mikrokernel měla AmigaOS v době, kdy ještě Amiga neměla obdobu Protected-Mode
◦ Pak už na tom byla stejně jako jiné mikrokernely
Hybridní jádro
Protože je mikrojádro pomalé, vývoj OS začne monolitickým jádrem
Výkonnostně kritické části kódu pak zůstanou v jádře, zatímco ostatní se přesunou na úroveň s méně privilegovanou CPL
◦ Např. ovladače třetích stran, které často padají a zákazníci si myslí, že je to špatným OS
GetTickCount - co to je + problém
RTL knihovny často nabízejí funkci podobného názvu, která vrací počet tiků od resetu procesoru
Získání korektní hodnoty není na moderních procesorech triviální => proto se vyplatí, aby RTL knihovna zavolala příslušnou funkci OS
80286 měla na adrese 0000:046c real-mode 4-bytovou proměnnou ROM-BIOSu, která udávala počet 55ms tiků od resetu procesoru
Pozdější procesory mají tick-counter registr, který je dnes větší a má daleko nižší režii přístupu, ale..
◦ Registry nejsou synchronizované mezi jednotlivými jádry v systému - resp. není to garantované
◦ Power-saving na konkrétním jádru ovlivní hodnotu čítače
◦ Out-of-order může samotnou čtecí instrukci vykonat příliš brzy
=> ať si takové varianty ošetří jádro namísto volajícího
threadu
GetTickCount - volání
Pro jednoduchost předpokládejme x86 uniprocesor
Čítač tiků přečte instrukce RDTSC
V uživatelském procesu tak kód může vypadat takto:
size_t tickcount = GetTickCount();
Přičemž předpokládejme, že se návratová hodnota
vrátí v registru EAX
Instrukci RDTSC lze vykonat v uživatelském režimu
Kód příslušné funkce namapuje OS do adresového prostoru příslušného procesu
◦ Sdílení kódu
◦ Dynamické knihovny
Obsluha takové funkce tedy vůbec nevyžaduje přepnutí kontextu => rychlost dokončení služby OS
◦ Což neznamená, že k němu nemůže dojít vlivem časovače
Volání služby:
- GetTickCount se přeloží jako call instrukce, která předá řízení na adresu, na které je příslušný RTL kód, nejpravděpodobněji kód od výrobce překladače
- RTL kód zatím blíže neurčeným způsobem zná adresu, kam OS namapoval svůj sdílený kód (dynamická knihovna), který dělá požadovanou činnost - RTL kód udělá příslušný call
- Kód OS mj. zapíše tick count do EAX a udělá ret
- RTL kód udělá ret
- Proces zná počet ticků (zanedbali jsme errno)
Dynamická knihovna
Naprostá většina programů používá knihovny
◦ Statické knihovny se během překladu stanou součástí výsledného kódu spustitelného programu
◦ U dynamických knihoven se to nestane, knihovna bude existovat jako samostatný soubor
Soubor může na disku existovat jenom jednou
Soubor může mít do paměti namapováno několik
procesů
Viz koncept sdílení kódu
Dynamic loading
Proces explicitně stanoví kdy a která knihovna se má načíst do paměti – tj. proces zavolá jednu z následujících funkcí, které předá cestu ke knihovně
◦ dlopen – Linux, MacOS
◦ LoadLibrary – WinAPI
OS volajícímu procesu inkrementuje reference counter, kolikrát už danou knihovnu načetl
◦ Pokud knihovna nebyla dosud načtena, OS alokuje procesu paměť, do které nahraje knihovnu
◦ OS vyřeší, která část nově alokovaná data bude označena jako spustitelná a která jako datová
Library Hijacking
Během načítání knihovny nemusí být cesta k ní
jednoznačná
◦ Např. bude-li to pouze jméno souboru bez cesty, systém bude soubor hledat v adresáři se spustitelným souborem
A když tam nebude, tak v adresářích specifikovaných nějakou proměnnou – např. PATH ve Windows
Bude-li program pod Windows specifikovat pouze jméno souboru knihovny v cestě dané PATH, lze podvrhnout falešnou knihovnu do adresáře k jeho .exe souboru, a tato podvržená knihovna tak bude načtena namísto té v cestě PATH
Dynamic unloading
Během procesu dynamic loading získá proces deskriptor načtené knihovny
Proces explicitně stanoví, kdy knihovna
identifikovaná příslušným deskriptorem, uvolní z
paměti
◦ dlclose, FreeLibrary
◦ Uvedené funkce sníží reference counter knihovny o jedna
–> OS uvolní knihovnu z paměti teprve až reference counter = 0
–> Ukazatele do této paměti se tak stanou neplatnými
Dynamic GetAddress
Máme-li knihovnu načtenou v paměťovém prostoru procesu, potřebujeme ještě získat adresy požadovaných funkcí
◦ Programátor procesu musí znát prototyp těchto funkcí
Proces zavolá funkci, které předá název funkce, a OS
mu vrátí pointer na adresu této funkce
◦ dlsym
◦ GetProcAddress
Programátor knihovny označí, které funkce exportovat
Dynamic knihovna - inicializace
Dynamická knihovna není nic jiného než spustitelný program – má svůj main, který se spustí při jejím
načtení
◦ dllmain pod Windows
◦ .interp sekce v ELF formátu pod Linuxem
Dynamická knihovna má tak možnost inicializovat se
◦ A stejně tak má možnost deinicializace před uvolněním
dllmain
sekce .fini
Dynamic linking - proč
Dynamic loading se hodí např. pro načítání plug-inů
Ale co když budeme namapovat dynamickou knihovnu hned při spuštění programu?
◦ Buď v případě, že program knihovnu vyžaduje a tudíž nemá smysl řešit její podmíněné načítání
◦ Anebo v případě, kdy knihovna patří OS, který jejím prostřednictvím poskytuje služby procesu
V hlavičce programu se označí, které knihovny má OS
dynamically load rovnou při zavádění programu
Dynamic linking - PLT
Ve zdrojovém kódu se proměnné ukazující na
symboly (funkce, proměnné, atd.) knihovny označí
◦ Např. pomocí __declspec (dllimport) pod Windows
◦ Značky jsou uvedeny v samostatné sekci v programu
–> Procedure Linkage Table (PLT)
–> Např. .rel.text pod Linuxem
◦ Značka říká, že se má na příslušnou adresu zapsat
hodnota získaná dlsym/GetProcAddress
–> Zařídí zavaděč knihovny - OS
+ example v přednáškách (3/26CZ)
Dynamic linking - PIC
Do paměti se může zavést několik knihoven, tj. každá knihovna se může pokaždé zavést na jinou virtuální adresu
◦ Potřebujeme tzv. Position Independent Code (PIC)
◦ Překladač musí zajistit, aby se veškerý kód adresoval relativně k program counteru (IP registr)
◦ Alternativě se používala relokovatelný kód (např. před Vista), kdy zavaděč programu modifikoval kód knihovny ještě před spuštěním tak, aby šla spustit z adresy, kam byla zavedena
Dynamic linking - GOT
Relokace vyžaduje přepsání kódu knihovny jejím zavaděčem
Lepší je vytvořit tabulku, Global Offset Table (GOT), ve které budou adresy symbolů knihovny, a které (adresy) tam zapíše zavaděč knihovny
◦ Kód tedy bude symbol dereferencovat 2x
◦ GOT bude privátní pro každý proces
A nezměněný kód knihovny tak bude sdílený pro všechny procesy
Lazy Binding (Dynamické knihovny)
Adresy symbolů knihovny není nutné resolvovat ihned, ale až bude třeba
1. Proces volá funkci knihovny
◦ Každá knihovnou exportovaná funkce má své ordinální číslo n – tj. volá se adresa PLT[n]
2. PLT[n] ale zatím ukazuje na rutinu zavaděče, která se zavolá jako první a resolvuje adresu rutiny do GOT než zavolá vlastní funkci
◦ Než je funkce zavolána, GOT se upraví tak, aby se příště už rovnou volala funkce knihovny, ne rutina zavaděče
Randomizace adres kódu (Dynamické knihovny)
Některé útoky se spoléhají na to, že se předem ví, co se bude nacházet na konkrétních adresách
◦ Např. je možné změnit adresu v GOT tak, aby ukazovala na jinou funkci, než na kterou má
Jednou z možností obrany je randomizace adres kódu tak, aby adresy byly náhodné a nedaly se uhádnout
◦ Což jde tím lépe, čím větší je virtuální adresový prostor
Volání služby OS přes int
Některé služby OS nelze vyřídit pouze v uživatelském adresovém prostoru, ale musí se změnit CPL na CPL jádra a předat jádru řízení programu
◦ Toto dokáže sw přerušení - x86 instrukce int
Proces buď sám vygeneruje int, s číslem přerušení dedikovaného OS pro tyto účely, aneb zavolá rutinu v dynamické knihovně OS, která sama posléze vygeneruje příslušný int
Stejné princip jako u volání MS-DOS API
Předání parametrů jádru:
Při použití int se parametry předávají v registrech, nebo přes zásobník
Vybrané registry mohou obsahovat virtuální adresu do virtuálního adresového prostoru procesu, kde je připravena nějaká struktura blíže specifikující, co má OS udělat
Volání služby v jádře - enter
Procesor použije číslo přerušení jako index do tabulky vektorů přerušení, aby získal adresu obsluhy přerušení
◦ Tabulka nemusí být na adrese 0000:0000 jako po startu v real mode, ale její pozici udává registr IDTR
Procesor nastaví CS:EIP na získaný vektor přerušení
◦ V zásobníku jsou uloženy registry přerušeného vlákna flags, cs a eip; cs:eip ukazují na instrukci, která se má vykonat po návratu
Jádro OS považuje zásobník přerušeného vlákna za nedůvěryhodný, protože v něm nemusí být místo, a tak bude používat svůj zásobník
Volání služby v jádře - main
Obsluha přerušení vykoná pouze nezbytnou část
požadované činnosti
◦ Lze-li něco odložit na později, odloží se to – viz Bottom Half dále
◦ V takovém případě ale nelze vrátit řízení přerušenému vláknu, protože operace nebyla dokončena => vlákno se musí uspat a řízení se předá jinému vláknu
Jádro také předá řízení jinému vláknu tehdy, když bylo cílem vlákno uspat nebo ukončit
Plánovač vybere jiné vlákno, které je ve stavu runnable
Volání služby v jádře - exit
Po dokončení obsluhy přerušení už jádro ví, kterému procesu předá řízení
Před ukončením obsluhy přerušení tedy jádro vybere zásobník vlákna, kterému předá řízení
◦ Tj. v zásobníku jsou registry CS:EIP a flags tohoto vlákna
◦ Pokud jde o jiné vlákno, musí se obnovit i zbývající registry
Obsluha přerušení udělá instrukci iret, která nastaví registry CS:EIP a flags z aktuálního zásobníku SS:ESP
◦ A z CS:EIP procesor určí svůj režim - CPL
Task State Segment
x86 (IA-32) struktura, kam se ukládá kontext přerušené činnosti procesoru
◦ V x86 terminologii task, jinak jde o vlákno
◦ Lze vytvořit pro každé vlákno v systému
◦ Používá se např. k implementaci syscall jako rychlejší varianty int
Deprecated na x86-64
Syscall - co to je + sdílená stránka
int generovaný programem je synchronní událost vůči běhu programu => lze toho využít
syscall/sysret jsou speciální instrukce, které toho využívají a dokáží přepnout procesor do režimu jádra cca 3x rychleji než int
◦ int – původní, pomalý mechanismus volání jádra
◦ sysenter/sysexit – protected mode, compatibility mode, vyžaduje TSS
◦ syscall/sysret – long mode
Jádro musí nastavit zásobník v době, kdy může dojít např. nemaskovatelnému přerušení
Kdy lze použít int, sysenter/sysexit či syscall/sysret
závisí na dostupném hw a jádru OS
◦ V každém případě se ale jedná o netriviální činnost, které by mělo být volající vlákno ušetřeno
OS namapuje do virtuálního adresového prostoru procesu speciální stránku, která zavolá službu jádra jádrem očekávaným způsobem
◦ Kód na této stránce poskytne jádro OS, volající vlákno pouze udělá call na adresu této stránky
syscall - volání
libc má funkci syscall
ntdll.dll má KiFastSystemCall a KiFastSystemCallRet
Adresa musí být fixní, aby byla známa RTL v době
jejího psaní
◦ Buď fixní adresa, např. 32-bit Win
call dword ptr [adresa]
◦ Nebo se použijí registry fs (Win) či gs (Linux), přičemž segmentový registr odkazuje na Thread Control Block
call fs:[offset od začátku TCB]
Výjimky
Výjimka je přerušení generované procesorem, když dojde k
◦ Trap – např. breakpoint
◦ Fault – opravitelná chyba, program může pokračovat
Např. Page Fault
◦ Abort – neopravitelná chyba
Např. Double Fault – dojde k Page Fault, ale handler je ve stránce, která není v RAM
◦ Některé výjimky přidávají na zásobník extra hodnotu s chybovým kódem, který je třeba odstranit před návratem z obsluhy přerušení, aby se na vrcholu byly cs, ip a flags
Exception handler
V případě, že procesor nedokáže zavolat obsluhu
výjimky, dojde k Double Fault
◦ Pokud ani pro ni nebude obsluha, dojde k Triple Fault
Což je samo o sobě známkou, že je něco špatně v obsluze první výjimky
Výjimku nelze zamaskovat
=> buď budeme psát perfektní, bezchybný kód
Anebo alespoň spolehlivé obsluhy výjimek
Význam exception handleru na příkladu dělení nulou
Pokud by OS neměl handler vyjímek, došlo by
k restartu počítače
◦ OS ho má – tj. dojde-li k vyjímce v uživatelském procesu, OS vyjímku zachytí
Např. dělení nulou je Fault
◦ Pokud proces nenastavil svůj handler, OS program ukončí
◦ Pokud ho proces nastavil, OS mu předá řízení
◦ Opět se k tomu použije Thread Control Block