Specifika vývoje ovladačů 2
Tento článek navazuje na Pár faktů a mýtů o vývoji ovladačů a přináší další várku zajímavostí a specifik vývoje ovladačů na platformě Windows. Tentokrát se zaměříme zejména na paměť.
Procesy a vlákna
Při programování běžné aplikace víme, že ta má svůj vlastní virtuální adresový prostor, do něhož jí ostatní aplikace nemohou přímo sahat. V žargonu operačního systému je tento adresový prostor (a další prostředky, jako jsou otevřené soubory) zakomponován do entity s názvem proces. Proces dále obsahuje vlákna (v základu jedno) zodpovědná za vykonávání kódu umístěném v adresovém prostoru. Jako vývojáři aplikace víme, že nám patří jeden proces, ve kterém můžeme mít jedno či více vláken provádějících námi požadované operace.
Jak bylo řečeno na konci minulého článku, ovladače fungují velmi podobně jako DLL knihovny. Nejsou izolovány ve vlastním virtuálním adresovém prostoru, ale všechny sdílejí jeden prostor, který je navíc namapovaný do adresových prostorů všech procesů. Bezpečnostní mechanismy procesoru (konkrétně jednotky zodpovědné za stránkování) zajišťují, že aplikace, ač o ní vědí, nemohou do této oblasti provádět žádné přístupy (čtení, zápis, vykonávání instrukcí). Opačně žádné takové tvrzení neplatí – ovladače mohou přistupovat do celého adresového prostoru procesu, v jehož kontextu se právě nacházejí.
Tím se dostáváme k otázce, v kontextu jakého procesu (popř. jakého vlákna) vlastně ovladače vykonávají svůj kód. Odpověď je šalamounská: záleží na tom, kdo potřebuje jejich služeb. Například ovladač monitorující přístupy do registru je volán vždy v kontextu vlákna, které se snaží danou operaci s registrem provést. Volání funkce, kterou ovladač podstrčí systému, aby jej informoval o práci s registrem, je součástí každé z mnoha druhů registrových operací. Podobná pravidla platí pro ovladače dohlížející na otevírání a vytváření souborů na disku.
Ne vždy je ale situace takto jednoduchá. Ovladače často zpracovávají příchozí požadavky asynchronně – předávají je ke zpracování svým vlastním vláknům, jenž mohou komunikovat s dalšími komponentami jádra. Pro tyto komponenty pak často není možné dopátrat se toho, v kontextu jakého vlákna či procesu byl daný požadavek vytvořen.
Nejasnost kontextu procesu a vlákna vede k programování ovladačů tak, aby na těchto vlastnostech nezáleželo. Vzhledem k tomu, že oblast vyhrazená jádru se v každém adresovém prostoru nachází na stejném místě, ovladače pouze nesmí sahat mimo tuto oblast, pokud si nejsou absolutně jisty, v jakém procesu se právě nacházejí. A i potom je rozumné pracovat pouze s daty, která jim daný proces ukáže (předáním adresy a velikosti příslušného bufferu), protože strukturu celého adresového prostoru neznají (neví například, kde se nachází paměť s alokovanými daty).
Umístění paměti jádra vždy na stejném místě adresového prostoru procesu ukazuje následující obrázek. Adresové prostory Průzkumníka Windows (Explorer.exe), Firefoxu (firefox.exe) a Poznámkového bloku (Notepad.exe) sdílí svoji horní část (adresy 0x80000000-0xffffffff). Dolní část svého prostoru má každý proces sám pro sebe.
Ačkoliv by se tedy mohlo zdát, že přístup do libovolného adresového prostoru (protože ovladače opravdu mohou mezi adresovými prostory přecházet dle svých přání) znamená pro autory ovladačů obrovskou moc, prakticky této výhody příliš nevyužívají. Zejména pokud mluvíme o ovladačích, jejichž cílem je stabilní běh na co největším množství verzí Windows. Obvykle je totiž kontexty procesu a vlákna vůbec nezajímají.
Stránkovaná a nestránkovaná paměť
Podobně jako každá aplikace, i jádro disponuje haldou, ze které mohou ovladače alokovat paměť, případně ji tam vracet, pokud ji již nepotřebují. Hlavní rozdíl spočívá ve faktu, že ovladače mohou alokovat z různých hald (poolů), přičemž zásadní jsou následující dva:
- Nestránkovaná halda (nonpaged pool) obsahuje bloky, které se vždy budou nacházet ve fyzické paměti, nikdy neodcestují na disk do stránkovacího souboru. Nejdůležitější vlastností takto alokované paměti není to, že její čtení a zápisy budou vždy rychlé (nebude třeba ji hledat na disku), ale její přítomnost i za okolností, kdy se do stránkovacího souboru podívat nelze. Příkladem takových okolností jsou obslužné rutiny přerušení, jenž musí proběhnout vždy co možná nejrychleji a neměly by být příliš složité. Nestránkovaná paměť by měla být využívána pouze v oprávněných případech, protože její množství je omezeno velikostí RAM počítače. Nepodílí se na iluzi budované mechanismy virtuální paměti.
- Stránkovaná halda (paged pool) disponuje naopak bloky,
které mohou do stránkovacího souboru odcestovat kdykoliv. V podstatě se
jedná o ekvivalent haldy běžných aplikací spravované funkcemi
malloc()
afree()
.
I aplikace může jádro systému instruovat, aby s určitou
částí jejího adresového prostoru zacházelo jako z nestránkovanou haldou
(tzn. neodkládalo její obsah na disk). K tomuto účelu slouží funkce
VirtualLock()
.
Obsluha výjimek
Vyvolávání a ošetřování výjimek jistě patří mezi oblíbené postupy nejen v C++, ale i ve vyšších programovacích jazycích (Java, C#, Python ...). Pro příznivce tohoto stylu programování představuje prostředí jádra Windows velké zklamání – mechanismus výjimek není příliš podporován, rozhodně ne dostatečně pro C++, ve kterém jinak ovladače programovat lze.
Jak demonstruje následující kód, podpora výjimek přecejen není
nulová. Místo slov try
a except
se používají
alternativy __try
a __except
. V případě, že chceme
výjimku vyhodit, zavoláme funkci ExRaisestatus()
.
__try { // . . . Normální běh . . . __except (EXCEPTION_EXECUTE_HANDLER) { // . . . Obsluha výjimky . . . }
Až na několik funkcí je API jádra založeno čistě na chybových kódech – zda určitá funkce uspěla, se ovladač dozví z její návratové hodnoty. Snad jedinou výjimku tvoří případ, kdy ovladač reaguje na požadavek aplikace a potřebuje přenést jí specifikovaná data do paměti jádra.
Jelikož aplikace nemůže číst ani zapisovat do paměti jádra, předává potřebná data (například jméno souboru, který má být otevřen) tak, že jádru sdělí jejich adresu a délku. Jádro (nebo příslušný ovladač) pak musí z této adresy data překopírovat mimo dosah aplikace, případně je zabezpečit jiným způsobem. Aplikace totiž může v době, kdy jádro s jejími daty pracuje:
- měnit jejich obsah,
- měnit oprávnění příslušných paměťových stránek (a tak zamezit zápisu),
- paměť uvolnit, takže adresa předaná jádru přestane být platná.
Naštěstí všechny tyto ošklivé případy je možné řešit obsluhou
výjimek, která v tomto případě funguje, jak má. Pokud se během
kopírování do paměti jádra daný buffer stane neplatným, je vyvolána
výjimka STATUS_ACCESS_VIOLATION
, kterou jádro (či ovladač)
zachytí a obslouží. Pokud se však ovladač pokusí přistoupit na neplatnou
adresu v paměti jádra, ani sebelepší obsluha výjimek jej nezachrání před
modrou obrazovkou smrti.
Neexistuje žádný způsob, jak ovladač může zjistit, zda je
určitá adresa v paměti jádra platná. Může se maximálně dozvědět, zda
přístup na takovou adresu vyvolá výpadek stránky (funkce
MmIsAddressValid()
). Výpadek stránky ale nemusí znamenat, že je
daná adresa neplatná; příslušná oblast paměti se může nacházet ve
stránkovacím souboru na disku. Zde uvedená funkce navíc nezaručuje, zda
daná adresa nevyvolá výpadek stránky hned poté, co vrátí řízení
volajícímu.