jackyhwei 發(fā)布于 2011-01-28 09:59 點擊:
本篇我們介紹開發(fā)之前的準備工作,包括開發(fā)環(huán)境準備,、預備知識,。 開發(fā)環(huán)境準備 對于開發(fā)WDM驅動程序來說,我們有以下三個常用組合: 1.直接使用Windows DDK 2.使用DriverStudio 3.使用Windriver 下面我們分別比較三種方式的優(yōu)缺點,。 第一種:開發(fā)難度大一些,,而且有很多煩瑣的工作要作,大部分都是通用的基礎性的工作,。但如果選用這種方式的話你將對整個體系結構會有很好的理解和把握,。 第二種:難度低一些,工具軟件已經(jīng)幫你作了很多基礎性的工作,。也封裝了一些細節(jié),,你只要專心去作你需要的操作,但由于封裝的問題,,可能會帶來一些bug,。有可能導致項目的失敗。 第三種:幾乎沒有難度(從開發(fā)驅動的角度),。很容易,,但只能開發(fā)硬件相關的驅動,事實上你寫的只是定制和調(diào)用它提供的通用驅動而已,。效率上有問題,。工作頻率不是很高。但開發(fā)花費的時間很少,。是上面的幾 乃至幾十分之一,。 建議: 用windriver作驅動程序的原型,用driverstudio作最終發(fā)行的驅動程序,,如果驅動程序很復雜的話,,建議直接使用ddk開發(fā)。 上面的幾種情況都需要vc++作為輔助開發(fā)環(huán)境,。(ddk也可以直接用命令行工具,,但比較煩),前兩種情況都需要ddk,。開發(fā)時間上,,第一種最長,,第三種最短,,第二種可以認為是前面兩種方案的折衷。 如果更具體一點的話,,我們可以把以上三種形式比作三種開發(fā)工具,,那就是 ms c,vc++,Vb。 如果SDK沒bug的話,,用ms c開發(fā)的純sdk程序的bug是最少的,。Vc++由于對sdk進行了封裝,必然會引出一些新的bug,。Vb開發(fā)程序雖然快了一些,,但運行效率比前兩種方式差了很多。 這樣說明這三種方式的話,,大家一定會明白了,。 我們?yōu)榱撕啽闫鹨姡褂胐dk+VC的方式,。 首 先,,我們按正常方式安裝好vc++ 6.0。不過據(jù)微軟文檔說ddk98只支持vc++ 5.0,。我手里沒有vc++5.0,,在vc++6.0下試了一 下,證明可以使用,,不過設置很困難的,。當然,如果你不覺得煩的話,,也可以直接用build工具即可,。在安裝好vc++后再安裝ddk開發(fā)包,這樣不容易出 錯,。 如果你使用DriverStudio開發(fā)包,,請先安裝好vc++6.0,然后再安裝它,在安裝softice時注意選擇通用顯卡驅動,,這樣一般情況下都能正常使用,。
預備知識 在開發(fā)環(huán)境安裝完成后,我們將要步入開發(fā)過程,。在實際動手之前,,我們先要學習一些預備知識。 在設備驅動程序中,要作很多工作,,包括初始化,,設備對象創(chuàng)建等等工作,,其中一些是很重要的,必須實現(xiàn),,一些是可選的,如果你的驅動對這些功能的要求不是很高的話,,可以不實現(xiàn),。 要實現(xiàn)的功能主要有以下幾個: 初始化 創(chuàng)建和刪除設備 I/O請求的超時處理 I/O請求的撤消 訪問硬件資源 處理Windows的輸入/輸出請求 串行化對設備的訪問 調(diào)用其它驅動程序 處理一個可熱拔插的設備被加入或刪除的情況 處理電源管理請求 使用Windows管理診斷功能 處理Windows的打開和關閉文件句柄的請求 從 實際工作情況來看,只有初始化模塊是必不可少的,。但是只有初始化模塊的驅動程序什么工作也干不了,,只能說它僅僅是一個概念意義上的驅動程序而已,好比失去 感覺的植物人(軀體存在,,但已經(jīng)沒有了意志),。通常情況下,一個完整的驅動程序至少要能響應用戶態(tài)程序發(fā)出的I/O訪問請求,。大多數(shù)情況下驅動程序要訪問 它們所支持的硬件資源,,并且要支持簡單的電源管理功能和Windows管理診斷功能或能向系統(tǒng)日志寫入信息。 WDM驅動程序通常由PnP管理器載入內(nèi)存,,然后調(diào)用它之中的AddDevice例程來創(chuàng)建設備,。當然,在此時還要需要一個inf安裝文件而來指明該驅動程序需要的一些參數(shù),。 系 統(tǒng)內(nèi)核通常通過向驅動程序發(fā)送IRP包來運行驅動程序中的實現(xiàn)代碼,。我們以Windows向設備發(fā)出的ReadFile調(diào)用為例:此時Windows向驅 動程序發(fā)出一個“讀”請求的IRP包,讀取緩沖區(qū)的大小和位置作為IRP包中的參數(shù)指定(IRP實際上是一個數(shù)據(jù)結構,,包含幾個域),。如果你作過 Windows的程序,特別是用 VC作過開發(fā)的話,,你應該知道,,windows用戶態(tài)應用程序是消息驅動的,應用程序中的代碼是通過消息機制的觸發(fā)而獲 得運行的機會的,,需要的參數(shù)是通過消息的域(wParam,、lParam)傳給應用程序。實際上驅動程序的動作還是可以看作是一種消息驅動方式,,只不過內(nèi) 核態(tài)的“消息”已經(jīng)不再稱作消息,,而是被稱作I/O請求包(IRP)。 驅動程序通常使用DriverEntry作為入口點,,與我們在Windows應用程序中定義的WinMain相似,。通常情況下,它是驅動程序的默認入口點,。 注: 標準Build腳本將驅動程序入口點定為DriverEntry,,你最好遵守這個假設,,否則必須修改Build腳本。 在 這個入口函數(shù)中,,我們必須作必要的初始化設置,,并設置必要的回調(diào)函數(shù)。我們可以這樣理解:我們用c++(特別是用VC++)時,,我們在類的構造函數(shù)中要作 必要的初始化操作,,并要在類中作消息處理方法的映射,這樣才能讓需要的消息得到適當?shù)奶幚?。我們也要在DriverEntry例程中設置必要的IRP處理 函數(shù),。 一般情況下,DriverEntry例程要設置以下幾個IRP處理函數(shù): DriverUnload 指向驅動程序的清除例程,。I/O管理器會在卸載驅動程序前調(diào)用該例程,。通常,WDM驅動程序的DriverEntry例程一般不分配任何資源,,所以DriverUnload例程也沒有什么清除工作要做,。 DriverExtension->AddDevice 指向驅動程序的AddDevice函數(shù)。PnP管理器將為每個硬件實例調(diào)用一次AddDevice例程,。這樣將創(chuàng)建一個該設備對象,。 DriverStartIo 如果驅動程序使用標準的IRP排隊方式,應該設置該成員,,使其指向驅動程序的StartIo例程,。如果你不理解什么是“標準”排隊方式,不要著急,,能后的教程中你就會完全明白,,許多驅動程序都使用這種方法。 MajorFunction 是一個指針數(shù)組,,I/O管理器把每個數(shù)組元素都初始化成指向一個空函數(shù),,這個空函數(shù)僅返回失敗。驅動程序可能僅需要處理幾種類型的IRP,,所以至少應該設置與那幾種IRP類型相對應的指針元素,,使它們指向相應的派遣函數(shù)。 下面是一段DriverEntry例程的示例: - extern "C" NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject
- , IN PUNICODE_STRING RegistryPath)
- {
- DriverObject->DriverUnload = DriverUnload; <--1
- DriverObject->DriverExtension->AddDevice = AddDevice;
- DriverObject->DriverStartIo = StartIo;
- DriverObject->MajorFunction[IRP_MJ_PNP] = DispatchPnp;
-
- DriverObject->MajorFunction[IRP_MJ_POWER] = DispatchPower;
- DriverObject->MajorFunction[IRP_MJ_SYSTEM_CONTROL] = DispatchWmi;
- ... <--3
- servkey.Buffer = (PWSTR) ExAllocatePool(PagedPool
- , RegistryPath->Length + sizeof(WCHAR)); <--4
- if (!servkey.Buffer)
- return STATUS_INSUFFICIENT_RESOURCES;
- servkey.MaximumLength = RegistryPath->Length + sizeof(WCHAR);
- RtlCopyUnicodeString(&servkey, RegistryPath);
- return STATUS_SUCCESS; <--5
- }
1. 前三條語句為驅動程序的其它入口點設置了函數(shù)指針,。在這里,我們用了能表達其功能的名字命名了這些函數(shù):DriverUnload,、AddDevice,、StartIo。 2. 每 個WDM驅動程序必須能處理PNP,、POWER,、SYSTEM_CONTROL這三種請求,;應該在這里為這些請求指定派遣函數(shù)。在早期的 Windows 2000 DDK中,,IRP_MJ_SYSTEM_CONTROL曾被稱作IRP_MJ_WMI,,所以我把系統(tǒng)控制派遣函數(shù)命名為 DispatchWmi。 3. 在省略號處,,你可以插入設置其它MajorFunction指針的代碼,。 4. 如果驅動程序需要訪問設備的服務鍵,可以在這里備份RegistryPath串,。例如,如果驅動程序要作為WMI生產(chǎn)者,,則需要備份這個串,。這里我假設已經(jīng)在某處聲明了一個類型為UNICODE_STRING的全局變量servkey。 5. 返回STATUS_SUCCESS指出函數(shù)成功,。如果函數(shù)失敗,,應該返回NTSTATUS.H中的一個錯誤代碼,或者返回用戶定義的錯誤代碼,。STATUS_SUCCESS的值為0,。 關于DriverUnload例程的補充說明: 在WDM驅動程序中,DriverUnload例程的作用就是釋放DriverEntry例程在全局初始化過程中申請的任何資源,,但它幾乎沒什么可做,。如果你在DriverEntry中備份了RegistryPath串,應該在這里釋放備份所占用的內(nèi)存: VOID DriverUnload(PDRIVER_OBJECT DriverObject) { RtlFreeUnicodeString(&servkey);//釋放先前申請的資源 } 如果DriverEntry例程返回一個失敗狀態(tài)代碼,,系統(tǒng)將不再調(diào)用DriverUnload例程,。所以,不能讓DriverEntry例程出錯后產(chǎn)生任何副作用,,必須在它返回錯誤代碼前消除副作用(釋放掉申請的系統(tǒng)資源),。 一般情況下,一個驅動程序可以被多個設備利用,。WDM驅動程序有一個特殊的AddDevice函數(shù),,PnP管理器為每個設備實例調(diào)用該函數(shù)。以下為該函數(shù)的原型定義: NTSTATUS AddDevice(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT pdo) { } DriverObject參數(shù)指向一個驅動程序對象,,就是你在DriverEntry入口例程中初始化的那個驅動程序對象,。pdo參數(shù)指向設備堆棧底部的物理設備對象。 對于功能驅動程序,,其AddDevice函數(shù)的基本職責是創(chuàng)建一個設備對象并把它連接到以pdo為底的設備堆棧中,。相關步驟如下: 1. 調(diào)用IoCreateDevice創(chuàng)建設備對象,并建立一個私有的設備擴展對象,。 2. 注冊一個或多個設備接口,,以便應用程序能夠發(fā)現(xiàn)設備的存在,。另外,還可以給出設備名并創(chuàng)建符號連接,。 3. 初始化設備擴展和設備對象的Flag成員,。 4. 調(diào)用IoAttachDeviceToDeviceStack函數(shù)把新設備對象放到堆棧上。 下面我將詳細解釋這些步驟,。 創(chuàng)建設備對象 調(diào)用IoCreateDevice函數(shù)創(chuàng)建設備對象,,例如: - PDEVICE_OBJECT fdo;
- NTSTATUS status = IoCreateDevice(DriverObject,
- sizeof(DEVICE_EXTENSION),
- NULL,
- FILE_DEVICE_UNKNOWN,
- FILE_DEVICE_SECURE_OPEN,
- FALSE,
- &fdo);
第一個參數(shù)(DriverObject) 就是AddDevice的第一個參數(shù)。該參數(shù)用于在驅動程序和新設備對象之間建立連接,,這樣I/O管理器就可以向設備發(fā)送指定的IRP,。 第二個參數(shù)是設備擴展結構的大小。I/O管理器自動分配這個內(nèi)存,,并把設備對象中的DeviceExtension指針指向這塊內(nèi)存,。 第三個參數(shù)在本例中為NULL。它可以是命名該設備對象的UNICODE_STRING串的地址,。決定是否命名設備對象以及以什么名字命名還需要仔細考慮,,我將在后面深入認真地討論這個問題。 第 四個參數(shù)(FILE_DEVICE_UNKNOWN) 是設備類型,。這個值可以被設備硬件鍵(注冊表中包含該硬件信息的鍵值)或類鍵(注冊表中包含該類驅 動信息的鍵值)中的可替換值(overriding values)所替代,,如果這兩個鍵都含有該參數(shù)的替換值,那么硬件鍵中的可替換值具有更高的優(yōu)先 權,。對于屬于某個已存在類的設備,,必須在這些地方指定正確的值,因為驅動程序與外圍系統(tǒng)的交互需要依靠這個值,。另外,,設備對象的默認安全設置也依靠這個設 備類型值。 第五個參數(shù)(FILE_DEVICE_SECURE_OPEN) 為設備對象提供Characteristics標志,。這些標志主要關 系到塊存儲設備(如軟盤,、CDROM、Jaz等等),。未公開標志位FILE_AUTOGENERATED_DEVICE_NAME僅用于內(nèi)部使用,,并不是 DDK文檔忘記提到該標志。這個參數(shù)同樣也能被硬件鍵或類鍵中的對應值替換,,如果兩個值都存在,,那么硬件鍵中的可替換值具有更高的優(yōu)先權。 第六個參數(shù)(FALSE) 指出設備是否是排斥的,。通常,,對于排斥設備,I/O管理器僅允許打開該設備的一個句柄。這個值同樣也能被注冊表中硬件鍵和類鍵中的值替換,,如果兩個可替換值都存在,,硬件鍵中的可替換值具有更高的優(yōu)先權。 注意 排斥屬性僅關系到打開請求的目標是命名設備對象,。如果你遵守Microsoft推薦的WDM驅動程序設計方針,,沒有為設備對象命名,那么打開請求將直接指向 PDO(物理設備對象),。PDO通常不能被標記為排斥,,因為總線驅動程序沒有辦法知道設備是否需要排斥特征。把PDO標為排斥的唯一的機會在注冊表中,,即 設備硬件鍵或類鍵的Properties子鍵含有Exclusive可替換值,。為了完全避免依賴排斥屬性,你應該利用IRP_MJ_CREAT例程彈出任 何有違規(guī)行為的打開請求,。 第七個參數(shù)(&fdo) 是存放設備對象指針的地址,,IoCreateDevice函數(shù)使用該變量保存剛創(chuàng)建的設備對象的地址。 如 果IoCreateDevice由于某種原因失敗,,則它返回一個錯誤代碼,不改變fdo中的值,。如果IoCreateDevice函數(shù)返回成功代碼,,那么 它同時也設置了fdo指針。然后我們進行到下一步,,初始化設備擴展,,做與創(chuàng)建新設備對象相關的其它工作,如果在這之后又發(fā)現(xiàn)了錯誤,,那么在返回前應先釋放 剛創(chuàng)建的設備對象并返回狀態(tài)碼,。見下面例子代碼: - NTSTATUS status = IoCreateDevice(...);
- if (!NT_SUCCESS(status))
- return status;
- ...
- if (<some other error discovered>)
- {
- IoDeleteDevice(fdo);
- return status;
- }
為設備命名 Windows 使 用對象管理器集中管理系統(tǒng)中的大量的內(nèi)部數(shù)據(jù)結構(每個對象在系統(tǒng)中都表現(xiàn)為一個數(shù)據(jù)結構),包括驅動程序對象和設備對象,。為了便于區(qū)別,,每個對象都有名 稱,對象管理器用一個層次化的命名空間來管理這些名稱,。圖中是DevView(一個設備觀察工具,,在驅動開發(fā)網(wǎng) 站<http://www.有下載>)顯示的頂層對象名。此工具以文件夾形式顯示的對象是目錄對象,,它可 以包含子目錄或常規(guī)對象,,其它圖標則代表正常對象。 通常設備對象都把自己的名字放到\Device目錄中,。在Windows 2000中,,設備的 名稱有兩個用途。第一個用途,通過命名后,,其它內(nèi)核模式部件可以通過調(diào)用IoGetDeviceObjectPointer函數(shù)找到該設備,,找到設備對象 后,就可以向該設備的驅動程序發(fā)送IRP(I/O請求包),。 另一個用途,,允許用戶態(tài)的應用程序打開命名設備的句柄,這樣它們就可以向驅動程序發(fā)送 IRP,。應用程序可以使用標準的CreateFile API打開命名設備句柄,,然后用ReadFile、WriteFile,,和 DeviceIoControl向驅動程序發(fā)出請求(關于這些API函數(shù)的詳細說明和使用,,我們將在后面的文章中詳述)。應用程序打開設備句柄時使 用\\.\路徑前綴,。在C/C++語言程序中使用時,需要轉化為’\\\\.\\’,這是由語法規(guī)定的,。在內(nèi)部,I/O管理器在執(zhí)行名稱搜索前自動把 \\.\轉換成\??\,。為了把\??目錄中的名字與名字在其它目錄(例如,,在\Device目錄)中的對象相連接,對象管理器實現(xiàn)了一種稱為符號連接 (symbolic link)的對象,。 符號連接 符號連接有點象WIDOWS桌面上的快捷方式,,符號連接在Windows NT/2K中 的主要用途是把處于列表前面的DOS形式的名稱連接到設備上。符號連接可以使對象管理器在分析一個名稱時能跳到命名空間的某個地方,。例如我們通常見到的C 盤,,其實它是就是一個設備(磁盤)的符號鏈接。如果你用過unix/linux操作系統(tǒng)的話,,你會對符號鏈接有所理解,,這里的符號鏈接相當于unix /linux系統(tǒng)中的軟鏈接。 術語 類鍵 所有設備類的類鍵都出現(xiàn)在HKLM\System\CurrentControlSet\Control\Class鍵中,。它們的鍵名是由Microsoft賦予的GUID值,。 硬件鍵 硬件鍵包含單個設備的信息。
在上期中我們講了符號連接,,在應用層開發(fā)中我們可以調(diào)用以下函數(shù)來創(chuàng)建一個\??目錄下的符號鏈接: BOOL okay = DefineDosDevice(DDD_RAW_TARGET_PATH, "barf", "\\Device\\SECTEST_0"); 調(diào)用成功后,,將會在設備命名空間的\??目錄下生成一個名為”barf“的符號鏈接,該鏈接指向”“\\Device\\SECTEST_0“這個對象,。 在核心態(tài)的驅動程序中,,我們需要調(diào)用以下的函數(shù)來創(chuàng)建相應的符號鏈接: IoCreateSymbolicLink(linkname, targname); Linkname是要創(chuàng)建的符號鏈接名,相當于上面函數(shù)中的”barf”,targname是該鏈接指向的設備對象,。 如 果你創(chuàng)建了一個指向不存在的設備對象的符號鏈接,系統(tǒng)并不會作任何檢查,,當你訪問這個符號鏈接時只會收到一個錯誤報告,。所以你必須要自己保證鏈接的目的對 象真正存在。如果你想允許用戶模式程序能超越這個連接而轉到其它地方,,應使用IoCreateUnprotectedSymbolicLink函數(shù)替代上 面的IoCreateSymbolicLink函數(shù),。 給設備命名后我們就可以很方便地打開該設備進行訪問了。但在方便的同時你需要注意一個很嚴重 的問題:“安全性”,。一旦為設備命名后,,符何核心態(tài)的驅動程序都可以打開該設備的句柄,從而訪問此設備,。而且更糟的是,,任何用戶態(tài)的應用程序也可以通過建 立該設備名的符號鏈接而訪問到該設備。而這種情況可能是你不愿意看到的,。 一旦你決定要為你的設備命名時,,你應該將這個設備對象的名稱放到對象名空間的“\Device”目錄中,我們可以使用以下的核心態(tài)函數(shù)來創(chuàng)建設備,,同時給設備命名: - UNICODE_STRING devname;
- RtlInitUnicodeString(&devname, L"\\Device\\Simple0");
- IoCreateDevice(DriverObject, sizeof(DEVICE_EXTENSION), &devname, ...);
這 里的UNICODE_STRING devname就是用來存放設備名的地方,。RtlInitUnicodeString是unicode串初始化函數(shù), 第一個參數(shù)是要初始化的變量地址,,第二個為設備名常量,。第二個參數(shù)前的大寫L是將這個常量轉換成此函數(shù)需要的寬字符串。一般我們使用如下的格式為設備命 名: 設備名0 其中的0為設備的實例號(即產(chǎn)生實例的順序),。 說到這里我要提醒一下大家,,在驅動程序中一般不使用Ansi字符串,取而代之的是UniCode字符串,,它以16位表示一個字符。這點和在WinCE下開發(fā)軟件很相似,。 在 以前的老式驅動程序中(Win 3.2 or Win95)中大量使用設備命名(包括直接用名字和名字的符號鏈接)的方式來訪問設備,。這樣做有兩個很主要 的問題。一是安全性問題,。在上面我們已經(jīng)講了這樣做有潛在的安全性問題,,符何程序只要知道該設備的名字就可以訪問它。第二個問題是你的應用程序要訪問該設 備必須事先知道它的名字,,否則不能訪問,。這在測試用的設備或私有設備(只為你的應用程序服務而不向第三方提供接口)的情況下是可以的。但是如果你的硬件設 備還要為第三方的程序服務或者有可能有第三方的公司為你的設備寫驅動時就會有很多問題,??赡苣銓υ撛O備的命名會和其它的設備相重復。而且這樣的命名很依賴 程序員本身所使用的自然語言,。 為了解決這個問題,,微軟在設計WDM框架時引入了一個新的命名方案。該方案與任何自然語言無關,且易于擴展,,可廣泛 地用于軟件用硬件,,并且易于歸檔。該方案依靠一個設備接口的概念,。它基本上是軟件如何訪問硬件的一個說明,。一個設備接口由一個唯一的128位的GUID標 識。一般情況下我們可以使用GUIDGEN工具生成這個標識(GUIDGEN工具可以在VC++企業(yè)版的可執(zhí)行程序目錄下找到),。由于采用了獨特的生成算 法,,你永遠也不用擔心重復出現(xiàn)GUID的情況。這樣一個GUID就唯一標識了一種設備接口,。 生成的代碼如下所示 -
- DEFINE_GUID(<<name>>,
- 0xCAF53C68, 0xA94C, 0x11D2, 0xBB, 0x4A, 0x00, 0xC0, 0x4F, 0xA3, 0x30, 0xA6);
此為GUIDGEN程序工作時的截屏,。可以選擇四種格式輸出,。一般情況下我們選擇第二種,。并且為了便于管理,我們把要用于的GUID聲明集中放到一個頭文件中,。
你可以把設備接口想象成鎖和鑰匙,。這樣應用程序就可以準確地訪問需要訪問的設備。 我們可以在功能驅動程序的AddDevice例程序中注冊一個或多個設備接口,,程序如下: - #include <initguid.h>
- #include "guids.h"
-
- NTSTATUS AddDevice(...)
- {
-
- IoRegisterDeviceInterface(pdo, &GUID_SIMPLE, NULL, &pdx->ifname);
-
- }
其中的GUID_SIPMLE就是我們要注冊的接口的GUID的定義,。對此段代碼,我們作如下說明: 我 們包含了GUIDS.H頭文件,,那里定義了DEFINE_GUID宏,。DEFINE_GUID通常聲明一個外部變量。在驅動程序的某些地方,,我們不得不為 將要引用的每個GUID保留初始化的存儲空間,。系統(tǒng)頭文件INITGUID.H利用某些預編譯指令使DEFINE_GUID宏在已經(jīng)定義的情況下仍能保留 該存儲空間。 我使用單獨的頭文件來保存我要引用的GUID定義,。這是一個好的想法,,因為用戶模式的代碼也需要包含這些定義,但它們不需要那些僅與內(nèi)核模式驅動程序有關的聲明,。 IoRegisterDeviceInterface 的第一個參數(shù)必須是設備PDO的地址,。第二個參數(shù)指出與接口關聯(lián)的GUID,第三個參數(shù)指出額外的接口細分類名,。只有Microsoft的代碼才使用名稱 細分類方案,。第四個參數(shù)是一個UNICODE_STRING串的地址,該串用于接收設備對象的符號連接名,。 IoRegisterDeviceInterface 的返回值是一個Unicode 字符串,,這樣可以在不知道驅動程序的具體編碼的情況下(也就是說沒看過你的驅動程序的具體代碼),,應用程序可以確定并打開 該設備的句柄。這個返回值是很奇怪的,,形如以下情形:\DosDevices\0000000000000007#{CAF53C68-A94C- 11d2-BB4A-00C04FA330A6}. 即它的名字是0000000000000007#{CAF53C68-A94C-11d2-BB4A-00C04FA330A6} 注冊過程實際上是先創(chuàng)建一個符號鏈接,,然后把它記入注冊表。當驅動程序在響應PnP請求IRP+MN_START_DEVICE時,,驅動程序將調(diào)用IoSetDeviceInterfaceState函數(shù)”使能”該接口: IoSetDeviceInterfaceState(&pdx->ifname, TRUE); 所謂使能也就是使此符號鏈接指向具體的PDO對象,。 在 響應這個調(diào)用過程中,I/O管理器將創(chuàng)建一個指向設備PDO的符號連接對象,。以后,,驅動程序會執(zhí)行一個功能相反的調(diào)用禁止該接口(用FALSE做參數(shù)調(diào)用 IoSetDeviceInterfaceState)。最后,,I/O管理器刪除符號連接對象,,但它保留了注冊表項,即這個名字將總與設備的這個實例關 聯(lián),;但符號連接對象與硬件一同到來或消失,。 枚舉設備接口 內(nèi)核模式代碼和用戶模式代碼都能定位含有它們感興趣接口的設備。下面我將解釋如 何在用戶模式中枚舉所有含有特定接口的設備,。枚舉代碼寫起來十分冗長,,最后我不得不寫一個C++類來實現(xiàn)。你可以在DEVICELIST.CPP和 DEVICELIST.H文件中找到這些代碼,。它們聲明并實現(xiàn)了一個CDeviceList類,,該類包含一個CDeviceListEntry對象數(shù)組。 一些聲明代碼去掉了,,詳細的文章請看驅動開發(fā)網(wǎng)上的志寧專欄(http://www./column.php?sortid=3),。 所有實際的工作都發(fā)生在CDeviceList::Initialize函數(shù)中。其執(zhí)行過程大致是這樣:先枚舉所有接口GUID與構造函數(shù)得到的GUID相同的設備,,然后確定一個“友好”名,,我們希望向最終用戶顯示這個名字。最后返回找到的設備號,。下面是這個函數(shù)的代碼: - int CDeviceList::Initialize()
- {
- HDEVINFO info = SetupDiGetClassDevs(&m_guid, NULL, NULL
- , DIGCF_PRESENT | DIGCF_INTERFACEDEVICE);
- if (info == INVALID_HANDLE_VALUE)
- return 0;
- SP_INTERFACE_DEVICE_DATA ifdata;
- ifdata.cbSize = sizeof(ifdata);
- DWORD devindex;
- for (devindex = 0; SetupDiEnumDeviceInterfaces(info
- , NULL, &m_guid, devindex, &ifdata); ++devindex)
- {
- DWORD needed;
- SetupDiGetDeviceInterfaceDetail(info, &ifdata, NULL, 0, &needed, NULL);
-
- PSP_INTERFACE_DEVICE_DETAIL_DATA detail =
- (PSP_INTERFACE_DEVICE_DETAIL_DATA) malloc(needed);
- detail->cbSize = sizeof(SP_INTERFACE_DEVICE_DETAIL_DATA);
- SP_DEVINFO_DATA did = {sizeof(SP_DEVINFO_DATA)};
- SetupDiGetDeviceInterfaceDetail(info, &ifdata, detail, needed, NULL, &did));
- TCHAR fname[256];
- if (!SetupDiGetDeviceRegistryProperty(info,
- &did,
- SPDRP_FRIENDLYNAME,
- NULL,
- (PBYTE) fname,
- izeof(fname),
- NULL)
- && !SetupDiGetDeviceRegistryProperty(info,
- &did,
- SPDRP_DEVICEDESC,
- NULL,
- (PBYTE) fname,
- sizeof(fname),
- NULL)
- )
- _tcsncpy(fname, detail->DevicePath, 256);
- CDeviceListEntry e(detail->DevicePath, fname);
- free((PVOID) detail);
- m_list.Add(e);
- }
- SetupDiDestroyDeviceInfoList(info);
- return m_list.GetSize();
- }
該語句打開一個枚舉句柄,我們用它尋找包含了指定GUID接口的所有設備,。 循環(huán)調(diào)用SetupDiEnumDeviceInterfaces以尋找每個與此相匹配的設備,。 有兩項信息是我們需要的,接口的“細節(jié)”信息和設備實例信息,。這個“細節(jié)”信息就是設備的符號名,。因為它的長度可變,所以我們兩次調(diào)用了SetupDiGetDeviceInterfaceDetail,。第一次調(diào)用確定了長度,,第二次調(diào)用獲得了名字,。 通過詢問注冊表中的FriendlyName鍵或DeviceDesc鍵,我們獲得了設備的“友好”名稱,。 我們用設備符號名同時作為連接名和友好名創(chuàng)建了類CDeviceListEntry的一個臨時實例e,。 友好名 你可能會疑惑,注冊表怎么會有設備的FriendlyName名,。安裝設備驅動程序的INF文件中有一個指定設備參數(shù)的段,,這些參數(shù)將被添加到注冊表中。通常我們可以在這里為設備提供一個FriendlyName名,。 注:在windows 2k下和Windows 98下的inf文件有少許的不同,,即使是同一個設備的inf文件,也要作過適當修改后才能同時用于兩個平臺下,。 作好以上工作后,,我們還要初始化一些其它的數(shù)據(jù)結構才能完成設備加載工作。 在AddDevice中還需要加入其它一些步驟來初始化設備對象,,下面我將按順序描述這些步驟,。 設備擴展的內(nèi)容和管理全部由用戶決定。該結構中的數(shù)據(jù)成員應直接反映硬件的專有細節(jié)以及對設備的編程方式,。大多數(shù)驅動程序都會在這里放入一些數(shù)據(jù)項,,下面代碼聲明了一個設備擴展結構: - typedef struct _DEVICE_EXTENSION {
- PDEVICE_OBJECT DeviceObject;
- PDEVICE_OBJECT LowerDeviceObject;
- PDEVICE_OBJECT Pdo;
- UNICODE_STRING ifname;
- IO_REMOVE_LOCK RemoveLock;
- DEVSTATE devstate;
- DEVSTATE prevstate;
- DEVICE_POWER_STATE devpower;
- SYSTEM_POWER_STATE syspower;
- DEVICE_CAPABILITIES devcaps;
- ...
- } DEVICE_EXTENSION, *PDEVICE_EXTENSION;
我模仿DDK中官方的結構聲明模式聲明了這個結構。 我 們可以用設備對象中的DeviceExtension指針定位自己的設備擴展,。同樣,,我們有時也需要在給定設備擴展時能定位設備對象。因為某些函數(shù)的邏輯 參數(shù)就是設備擴展本身(這里有設備每個實例的全部信息),。所以,,我認為這里應該有一個DeviceObject指針。 我在一些地方曾提到過,,在調(diào)用IoAttachDeviceToDeviceStack函數(shù)時,,應該把緊接著你下面的設備對象的地址保存起來。LowerDeviceObject成員用于保存這個地址,。 有一些服務例程需要PDO的地址,,而不是堆棧中某個高層設備對象的地址。由于定位PDO非常困難,,所以最好的辦法是在AddDevice執(zhí)行時在設備擴展中保存一個PDO地址,。 無論你用什么方法(符號連接或設備接口)命名你的設備,都希望能容易地獲得這個名字,。所以,,這里我用一個Unicode串成員ifname來保存設備接口名。如果你使用一個符號連接名而不是設備接口,,應該使用一個有相關含義的成員名,,例如“l(fā)inkname”,。 當你調(diào)用IoDeleteDevice刪除這個設備對象時,需要使用一個自旋鎖來解決同步安全問題,,我將在第六章中討論同步問題,。因此,需要在設備擴展中分配一個IO_REMOVE_LOCK對象,。AddDevice有責任初始化這個對象,。 你可能需要一個成員來記錄設備當前的PnP狀態(tài)和電源狀態(tài)。DEVSTATE和POWERSTATE是枚舉類型變量,,我假設事先已經(jīng)在頭文件中聲明了這些變量類型,。我將在后面章節(jié)中討論這些狀態(tài)變量的用途。 電源管理的另一個部分涉及電源能力設置的恢復,,設備擴展中的devcaps結構用于保存這些設置,。 下面是AddDevice中的初始化語句(著重設備擴展部分的初始化): - NTSTATUS AddDevice(...)
- {
- PDEVICE_OBJECT fdo;
- IoCreateDevice(..., sizeof(DEVICE_EXTENSION), ..., &fdo);
- PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension;
- pdx->DeviceObject = fdo;
- pdx->Pdo = pdo;
- IoInitializeRemoveLock(&pdx->RemoveLock, ...);
- pdx->devstate = STOPPED;
- pdx->devpower = PowerDeviceD0;
- pdx->syspower = PowerSystemWorking;
- IoRegisterDeviceInterface(..., &pdx->ifname);
- pdx->LowerDeviceObject = IoAttachDeviceToDeviceStack(...);
- }
初始化默認的DPC對象 許 多設備使用中斷來完成操作。中斷服務例程(ISR)不能調(diào)用用于報告IRP完成的函數(shù)(IoCompleteRequest),。利用DPC(延遲過程調(diào) 用)可以避開這個限制,。你的設備對象中應包含一個輔助DPC對象,它可以調(diào)度你的DPC例程,,該對象應該在設備對象創(chuàng)建后不久被初始化,。DPC例程還有其 它的作用,比如你需要在中斷中處理很多很耗時的操作,這在通常情況下是不可以的,,這樣會影響操作系統(tǒng)的響應速度,,為此我們把這些處理操作移到DPC例程中去。 NTSTATUS AddDevice(...) { IoCreateDevice(...); IoInitializeDpcRequest(fdo, DpcForIsr); } 設置緩沖區(qū)對齊掩碼 執(zhí) 行DMA傳輸?shù)脑O備直接使用內(nèi)存中的數(shù)據(jù)緩沖區(qū)工作,。HAL(硬件抽象層)要求DMA傳輸中使用的緩沖區(qū)必須按某個特定界限對齊,,而且設備也可能有更嚴格 的對齊需求。設備對象中的AlignmentRequirement域表達了這個約束,,它是一個位掩碼,,等于要求的地址邊界減一。下面語句可以把任何地址 圈入這個界限: PVOID address = ...; SIZE_T ar = fdo->AlignmentRequirement; address = (PVOID) ((SIZE_T) address & ~ar); 還可以把任意地址圈入下一個對齊邊界: PVOID address = ...; SIZE_T ar = fdo->AlignmentRequirement; address = (PVOID) (((SIZE_T) address + ar) & ~ar); 在這兩段代碼中,,我使用了SIZE_T把指針類型(它可以是32位也可以是64位,,這取決于編譯的目標平臺)轉化成一個整型,該整型與原指針有同樣的跨度范圍,。 IoCreateDevice 把新設備對象中的AlignmentRequirement域設置成HAL要求的值,。例如,Intel的x86芯片沒有對齊需求,,所以 AlignmentRequirement的默認值為0。如果設備需要更嚴格的緩沖區(qū)對齊(例如設備有總線主控的DMA能力,,要求對齊數(shù)據(jù)緩沖區(qū)),,應該 修改這個默認值,,如下: if (MYDEVICE_ALIGNMENT - 1 > fdo->AlignmentRequirement) fdo->AlignmentRequirement = MYDEVICE_ALIGNMENT - 1; 我假設你在驅動程序某處已定義了一個名為MYDEVICE_ALIGNMENT的常量,它是2的冪,,代表設備的數(shù)據(jù)緩沖區(qū)對齊需求,。 其它對象 設備可能還有其它一些需要在AddDevice中初始化的對象。這些對象可能包括各種同步對象,,各種隊列頭(queue anchors),,聚集/分散列表緩沖區(qū),等等,。 初始化設備標志 設 備對象中有兩個標志位需要在AddDevice中初始化,,并且它們在以后也不會改變,它們是DO_BUFFERED_IO和DO_DIRECT_IO標 志,。你只能設置并使用其中一個標志,,它將決定你以何種方式處理來自用戶模式的內(nèi)存緩沖區(qū)。(我將在第七章中討論這兩種緩沖模式的不同,,以及你如何選 擇) 由于任何在后面裝入的上層過濾器驅動程序將復制你的標志設置,,所以在AddDevice中做這個選擇十分重要。如果你在過濾器驅動程序裝入后改變了 設置,,它們可能會不知道,。 設備對象中有兩個標志位屬于電源管理范疇。與前兩個緩沖區(qū)標志不同,,這兩個標志在任何時間都可以被改變,。我將在第八章中 詳細討論它們,但這里我先介紹一下,。DO_POWER_PAGABLE意味著電源管理器將在PASSIVE_LEVEL級上向你發(fā)送 IRP_MJ_POWER請求,。DO_POWER_INRUSH意味著你的設備在上電時將汲取大量電流,因此,,電源管理器將確保沒有其它INRUSH設備 同時上電,。 設置初始電源狀態(tài) 大部分設備一開始就進入全供電狀態(tài)。如果你知道你的設備的初始電源狀態(tài),,應該告訴電源管理器: POWER_STATE state; state.DeviceState = PowerDeviceD0; PoSetPowerState(fdo, DevicePowerState, state) 建立設備堆 每個過濾器驅動程序和功能驅動程序都有責任把自己的設備對象放到設備堆棧上,,從PDO開始一直向上。你可以調(diào)用IoAttachDeviceToDeviceStack完成你那部分工作: NTSTATUS AddDevice(..., PDEVICE_OBJECT pdo) { PDEVICE_OBJECT fdo; IoCreateDevice(..., &fdo); pdx->LowerDeviceObject = IoAttachDeviceToDeviceStack(fdo, pdo); } IoAttachDeviceToDeviceStack 的第一個參數(shù)是新創(chuàng)建的設備對象的地址,。第二個參數(shù)是PDO地址,。AddDevice的第二個參數(shù)也是這個地址。返回值是緊接著你下面的任何設備對象的地 址,,它可以是PDO,,也可以是其它低級過濾器設備對象。如果該函數(shù)失敗則返回一個NULL指針,,因此你的AddDevice函數(shù)也是失敗的,,應返回 STATUS_DEVICE_REMOVED,。 在AddDevice中最后一件需要做的事是清除設備對象中的DO_DEVICE_INITIALIZING標志: fdo->Flags &= ~DO_DEVICE_INITIALIZING; 當 這個標志設置時,I/O管理器將拒絕任何打開該設備句柄的請求或向該設備對象上附著其它設備對象的請求,。在驅動程序完成初始化后,,必須清除這個標志。在以 前版本的Windows NT中,,大部分驅動程序在DriverEntry中創(chuàng)建所有需要的設備對象,。當DriverEntry返回時,I/O管理器自動 遍歷設備對象列表并清除該標志,。但在WDM驅動程序中,,設備對象在DriverEntry返回后才創(chuàng)建,所以I/O管理器不會自動清除這個標志,,驅動程序 必須自己清除它,。 這是本節(jié)的內(nèi)容。 |