V minulém díle jsme si uvedli pár základních faktů o ovladačích jádra a končili jsme ukázkovým kódem na načtení driveru do jádra. V tomto díle si řekneme, jak ovladač z jádra uvolnit. Dále si popíšeme jeden z velmi důležitých mechanismů, na který musíme brát ohled při programování ovladačů, a nakonec se dozvíme jeden způsob komunikace ovladačů s okolím.
Uvolnění ovladače z jádra je vcelku podobné jeho načtení. Zjednáme si přístup ke službě, která ovladač reprezentuje, a pošleme jí signál, že se má zastavit.
Var hSCM : THandle; hService : THandle; SS : _SERVICE_STATUS; begin hSCM:=OpenSCManager( Nil, Nil, SC_MANAGER_ALL_ACCESS); If hSCM > 0 Then begin hService:=OpenService( hSCM, PAnsiChar(DriverName), SERVICE_ALL_ACCESS); If hService > 0 Then begin ControlService( hService, SERVICE_CONTROL_STOP, SS); CloseServiceHandle(hService); end; CloseServiceHandle(hSCM); end; end;
Proměnná DriverName obsahuje název služby driveru, který chcete uvolnit z jádra. Pokud funkce ControlService vrátí nenulovou hodnotu (booleovsky TRUE), tak se uvolnění driveru podařilo. Pokud vrátí 0, došlo k chybě, jejíž číslo zjistíte přes funkci GetLastError.
Drivery a služby patří ke kritickým součástem operačního systému, tudíž se uvolnění ovladače zdařit nemusí. Vše totiž záleží na ovladači - pokud z jádra být uvolněn nechce, standardními prostředky jej nedonutíte. Existují prostředky nestandardní, leč jejich použití vám nedoporučuji. Driver může být například zavěšen na nějakém systémovém zařízení, může na sebe přesměrovat kód některých funkcí. Pokud ho tvrdě uvolníte, pravděpodobně velmi rychle dojde k modré obrazovce.
Pokud chcete operačnímu systému umožnit uvolnění vašeho ovladače z paměti, musíte ve zdrojovém kódu definovat příslušnou proceduru, která se většinou nazývá DriverUnload. Deklarace vypadá následovně:
VOID DriverUnload(IN PDRIVER_OBJECT DriverObject)
Procedura v parametru dostane odkaz na strukturu DRIVER_OBJECT, která obsahuje většinu důležitých informací, které je nutné při uvolňování složitějších ovladačů znát. Jakmile tato rutina skončí, ovladač je uvolněn. Rutina nijak nemůže zabránit uvolnění ovladače a není k tomuto účelu určena. Měla by obsahovat kód, který po ovladači uklidí (uvolní alokovanou paměť, zruší všechny vytvořené objekty atp.).
Samotná deklarace DriverUnload nestačí. Operační systém se nějak musí dozvědět, že taková rutina ve vašem ovladači existuje. Struktura DRIVER_OBJECT obsahuje položku s názvem DriverUnload, do které se systém podívá při uvolňování ovladače z jádra. Pokud tato položka obsahuje NULL, systém předpokládá, že ovladač uvolněn z jádra být nechce. Pokud je obsah položky adresa, systém na ni předá řízení. Pokud tedy chcete umožnit uvolnění ovladače, musíte do této položky přiřadit svoji rutinu.
NTSTATUS DriverEntry( IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegPath) { ... DriverObject->DriverUnload = DriverUnload; ... }
Zatím byste mohli usuzovat, že psát ovladač je stejně těžké, jako psát libovolnou jinou aplikaci. Leč není tomu tak. A jednou z příčin vyšší obtížnosti psaní ovladačů je mechanismus IRQL. IRQL je zkratka ze sousloví "Interrupt ReQuest Level" a jedná se v podstatě o prioritu. Pokud nějaké vlákno vykonává kód, běží s uričtým IRQL. Ale pozor! Narozdíl od priorit vláken a procesů, které jistě dobře znáte, mají IRQL zásadní dopad na vykonávání kódu. Dostatečně vysoké IRQL totiž zabraňuje multitaskingu na procesoru, kde příslušné vlákno běží. Vlákno nemůže být přerušeno vlákny, jejichž IRQL je menší nebo rovno jeho IRQL (výjimkou je nejnižší hodnota IRQL). Z toho také vyplývá, že pomocí IRQL lze maskovat i přerušení. Tato vlastnost má zásadní dopad na programování ovladačů - kód běžící s vysokou prioritou totiž nemůže používat žádné mechanismy, které jsou založeny na vykonávání kódu s prioritou nižší. Čím vyšší IRQL, tím méně toho může kód provádět.
Jak jsem již zmínil, IRQL je specifické pro procesor a vlákno, které je aktuálně aktivní. Pokud má váš počítač například dvoujádrový procesor, může na jednom jádře běžet vlákno s vysokým IRQL a na druhém vlákno s nízkým IRQL. Proto je nutné psát ovladače velmi opatrně a snažit se použít synchronizační primitiva všude tam, kde by mohlo dojít k race condition, jenž má obvykle fatální následky. A právě synchronizace někdy dělá z psaní ovladačů peklo na zemi.
IRQL může mít hodnotu od 0 do 255 (0xFF). V následujícím seznamu jsou uvedena a stručně charakterizována nejdůležitější IRQL:
Z popisu výše vyplývá, že ovladač by měl vykonávat co nejvíce kódu s IRQL PASSIVE_LEVEL, protože takový kód neblokuje procesor a může volat téměř všechny funkce, které mu operační systém poskytuje.
S IRQL se blíže sektáme v příštím díle seriálu, který bude věnován synchronizačním objektům, které nám jádro poskytuje.
Po trošku suchém teoretickém výkladu o IRQL se dostáváme k věcem, jejichž fungování si ukážeme na praktickém příkladu. Bude jím driver s názvem KMemReader, který umožní obyčejným aplikacím číst obsah paměti jádra. Z tohoto důvodu se v této podkapitolce zaměřím na komunikaci ovladače s normálním procesem běžícím v uživatelském režimu.
Existuje několik mechanismů, které může proces a driver využít k předávání zpráv. Jedním z nich jsou například namapované soubory (v terminologii jádra nazývané "Sections"). Těmi se zatím zabývat nebudeme. Ukážeme si jiný způsob komunikace, který lze považovat za ten nejpoužívanější. Tento mechanismus je založen na objektech zvaných Zařízení (Device). Zařízení si můžeme zjednodušeně představit jako rouru, na jejímž jednom konci je proces a na druhém driver. Aplikace přes objekt zařízení posílá zprávy driveru, který na ně odpovídá. Opačně tento princip nefunguje.
Zařízení se chová velmi podobně jako soubor - pomocí funkce CreateFile si k němu aplikace může zjednat přístup a potom používat další rutiny jako ReadFile, WriteFile, DeviceIoControl či CloseHandle.
Jakmile se volání některé z výše uvedených funkcí dostane do jádra, je vytvořen požadavek (IRP - Interrupt Request Packet), ve kterém jsou zakódovány všechny informace potřebné pro driver. Požadavek je poslán na zadané zařízení, které je kontrolováno driverem. Driver si přečte obsah požadavku, zareaguje a případně pošle zpět data (pokud to typ požadavku umožňuje).
My si ukážeme komunikaci pomocí funkce DeviceIoControl. Tato funkce způsobí vygenerování požadavku typu IRP_MJ_DEVICE_CONTROL, jenž obsahuje tyto položky:
Kód zprávy se většinou definuje pomocí makra CTL_CODE, které pohodlně umožňuje nastavit všechny parametry. Zmínili jsme zatím charaketer vstupního a výstupního bufferu a uživatelsky definovaný kód. Dále je nutné definovat, jakému typu zařízení lze zprávu doručit a jaký přístup musí aplikace k zařízení mít, aby mu zprávu s daným kódem mohla poslat.
Charakter vstupního a výstupního bufferu se charakterizuje pomocí konstant METHOD_XXX, které jsou následující:
Přesnou definici kódu zprávy a další informace najdete v nápovědě k DDK. Více do podrobností zabíhat nebudeme a vše si ukážeme na příkladu:
#define FILE_DEV_DRV 0x2A7B #define IOCTL_KMR_READ CTL_CODE ( FILE_DEV_DRV, 0x01, METHOD_BUFFERED, FILE_ALL_ACCESS)
Nejprve si zadefinujeme typ našeho zařízení (FILE_DEV_DRV) a poté definujeme vlastní kód zprávy. Bude použita bufferovaná metoda (oba buffery budou realokovány do nestránkované paměti) a naše zpráva bude mít uživatelský kód 1. Aby ji aplikace mohla poslat, musí získat k zařízení přístupová práva FILE_ALL_ACCESS.
Teď už máme mnoho informací o tom, co musí udělat aplikace, aby poslala zprávu ovladači. Ještě by bylo dobré vědět, co musí zařídit ovladač, aby komunikace mohla proběhnout. Teorie už však bylo dost. Proto si ukážeme příklad opravdu použitelného ovladače, který umožní číst paměť jádra. Vše potřebné bude vysvětleno v komentářích ve zdrojovém kódu. Použijeme definice kódu zprávy, jenž je uvedena výše.
KMemReader je jednoduchý driver, který umožňuje aplikacím číst paměť jádra. Přesněji řečeno: nestránkovanou paměť jádra vám přečte zaručeně. Bohužel jsem zatím nepřišel na jednoduchý postup, jak poznat nevalidní adresu od adresy paměti, jenž je uložena ve stránkovacím souboru. V ZIP archivu se nachází ovladač i se zdrojovými kódy, které jsem se snažil pečlivě komentovat, a jednoduchá konzolová aplikace, která přečte 2 byty z adresy specifikované parametrem (adresa musí být specifikována jako hexadecimální číslo "$XXXXXXXX"). Aplikace počítá s tím, že driver je již zaveden do jádra - k tomu můžete využít zdrojové kódy z minulého a tohoto dílu. Aplikaci můžete velmi jednoduše modifikovat, aby načítala více bytů či dělala jiné vylomeniny. Například pokud si necháte načíst 2 byty z počáteční adresy hlavního modulu jádra, uvidíte pravděpodobně signaturu jeho PE hlavičky ("MZ").
Odkaz na stažení: KMemReader.zip
Komentáre
KMemReader.zip
RE: nefunkční link