在前一節(jié)中,我講述了當(dāng)WDM驅(qū)動(dòng)程序被第一次裝入時(shí)如何初始化。通常,一個(gè)驅(qū)動(dòng)程序可以被多個(gè)設(shè)備利用。WDM驅(qū)動(dòng)程序有一個(gè)特殊的AddDevice函數(shù),PnP管理器為每個(gè)設(shè)備實(shí)例調(diào)用該函數(shù)。該函數(shù)的原型如下:
NTSTATUS AddDevice(PDRIVER_OBJECT DriverObject, PDEVICE_OBJECT pdo)
{
}
|
DriverObject參數(shù)指向一個(gè)驅(qū)動(dòng)程序?qū)ο?/strong>,就是你在DriverEntry例程中初始化的那個(gè)驅(qū)動(dòng)程序?qū)ο蟆?strong>pdo參數(shù)指向設(shè)備堆棧底部的物理設(shè)備對(duì)象。
對(duì)于功能驅(qū)動(dòng)程序,其AddDevice函數(shù)的基本職責(zé)是創(chuàng)建一個(gè)設(shè)備對(duì)象并把它連接到以pdo為底的設(shè)備堆棧中。相關(guān)步驟如下:
- 調(diào)用IoCreateDevice創(chuàng)建設(shè)備對(duì)象,并建立一個(gè)私有的設(shè)備擴(kuò)展對(duì)象。
- 寄存一個(gè)或多個(gè)設(shè)備接口,以便應(yīng)用程序能知道設(shè)備的存在。另外,還可以給出設(shè)備名并創(chuàng)建符號(hào)連接。
- 初始化設(shè)備擴(kuò)展和設(shè)備對(duì)象的Flag成員。
- 調(diào)用IoAttachDeviceToDeviceStack函數(shù)把新設(shè)備對(duì)象放到堆棧上。
下面我將詳細(xì)解釋這些步驟。
創(chuàng)建設(shè)備對(duì)象
調(diào)用IoCreateDevice函數(shù)創(chuàng)建設(shè)備對(duì)象,例如:
PDEVICE_OBJECT fdo;
NTSTATUS status = IoCreateDevice(DriverObject,
sizeof(DEVICE_EXTENSION),
NULL,
FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN,
FALSE,
&fdo);
|
第一個(gè)參數(shù)(DriverObject) 就是AddDevice的第一個(gè)參數(shù)。該參數(shù)用于在驅(qū)動(dòng)程序和新設(shè)備對(duì)象之間建立連接,這樣I/O管理器就可以向設(shè)備發(fā)送指定的IRP。
第二個(gè)參數(shù)是設(shè)備擴(kuò)展結(jié)構(gòu)的大小。正如我在本章前面講到的,I/O管理器自動(dòng)分配這個(gè)內(nèi)存,并把設(shè)備對(duì)象中的DeviceExtension指針指向這塊內(nèi)存。
第三個(gè)參數(shù)在本例中為NULL。它可以是命名該設(shè)備對(duì)象的UNICODE_STRING串的地址。決定是否命名設(shè)備對(duì)象以及以什么名字命名還需要仔細(xì)考慮,我將在本節(jié)后面深入討論這個(gè)問(wèn)題。
第四個(gè)參數(shù)(FILE_DEVICE_UNKNOWN) 是表2-4中列出的設(shè)備類型。這個(gè)值可以被設(shè)備硬件鍵或類鍵中的超越值所替代,如果這兩個(gè)鍵都含有該參數(shù)的超越值,那么硬件鍵中的超越值具有更高的優(yōu)先權(quán)。對(duì)于屬于某個(gè)已存在類的設(shè)備,必須在這些地方指定正確的值,因?yàn)?strong>驅(qū)動(dòng)程序與外圍系統(tǒng)的交互需要依靠這個(gè)值。另外,設(shè)備對(duì)象的默認(rèn)安全設(shè)置也依靠這個(gè)設(shè)備類型值。
第五個(gè)參數(shù)(FILE_DEVICE_SECURE_OPEN) 為設(shè)備對(duì)象提供Characteristics標(biāo)志(見(jiàn)表2-3)。這些標(biāo)志主要關(guān)系到塊存儲(chǔ)設(shè)備(如軟盤(pán)、CDROM、Jaz等等)。未公開(kāi)標(biāo)志位FILE_AUTOGENERATED_DEVICE_NAME僅用于內(nèi)部使用,并不是DDK文檔忘記提到該標(biāo)志。這個(gè)參數(shù)同樣也能被硬件鍵或類鍵中的對(duì)應(yīng)值超越,如果兩個(gè)值都存在,那么硬件鍵中的超越值具有更高的優(yōu)先權(quán)。
第六個(gè)參數(shù)(FALSE) 指出設(shè)備是否是排斥的。通常,對(duì)于排斥設(shè)備,I/O管理器僅允許打開(kāi)該設(shè)備的一個(gè)句柄。這個(gè)值同樣也能被注冊(cè)表中硬件鍵和類鍵中的值超越,如果兩個(gè)超越值都存在,硬件鍵中的超越值具有更高的優(yōu)先權(quán)。
注意
排斥屬性僅關(guān)系到打開(kāi)請(qǐng)求的目標(biāo)是命名設(shè)備對(duì)象。如果你遵守Microsoft推薦的WDM驅(qū)動(dòng)程序設(shè)計(jì)方針,沒(méi)有為設(shè)備對(duì)象命名,那么打開(kāi)請(qǐng)求將直接指向PDO。PDO通常不能被標(biāo)記為排斥,因?yàn)榭偩€驅(qū)動(dòng)程序沒(méi)有辦法知道設(shè)備是否需要排斥特征。把PDO標(biāo)為排斥的唯一的機(jī)會(huì)在注冊(cè)表中,即設(shè)備硬件鍵或類鍵的Properties子鍵含有Exclusive超越值。為了完全避免依賴排斥屬性,你應(yīng)該利用IRP_MJ_CREAT例程彈出任何有違規(guī)行為的打開(kāi)請(qǐng)求。
第七個(gè)參數(shù)(&fdo) 是存放設(shè)備對(duì)象指針的地址,IoCreateDevice函數(shù)使用該變量保存剛創(chuàng)建設(shè)備對(duì)象的地址。
如果IoCreateDevice由于某種原因失敗,則它返回一個(gè)錯(cuò)誤代碼,不改變fdo中的值。如果IoCreateDevice函數(shù)返回成功代碼,那么它同時(shí)也設(shè)置了fdo指針。然后我們進(jìn)行到下一步,初始化設(shè)備擴(kuò)展,做與創(chuàng)建新設(shè)備對(duì)象相關(guān)的其它工作,如果在這之后又發(fā)現(xiàn)了錯(cuò)誤,那么在返回前應(yīng)先釋放剛創(chuàng)建的設(shè)備對(duì)象并返回狀態(tài)碼。見(jiàn)下面例子代碼:
NTSTATUS status = IoCreateDevice(...);
if (!NT_SUCCESS(status))
return status;
...
if (<some other error discovered>)
{
IoDeleteDevice(fdo);
return status;
}
|
NTSTATUS狀態(tài)代碼和NT_SUCCESS宏的解釋見(jiàn)下一章。
為設(shè)備命名
Windows NT使用對(duì)象管理器集中管理大量的內(nèi)部數(shù)據(jù)結(jié)構(gòu),包括我們討論過(guò)的驅(qū)動(dòng)程序?qū)ο蠛驮O(shè)備對(duì)象。David Solomon在《Inside Windows NT, Second Edition (Microsoft Press, 1998)》的第三章“System Mechanisms”中給出了關(guān)于Windows NT對(duì)象管理器和命名空間的一個(gè)比較完整的闡述。對(duì)象都有名稱,對(duì)象管理器用一個(gè)層次化的命名空間來(lái)管理這些名稱。圖2-16是DevView顯示的頂層對(duì)象名。圖中以文件夾形式顯示的對(duì)象是目錄對(duì)象,它可以包含子目錄或常規(guī)對(duì)象,其它圖標(biāo)則代表正常對(duì)象。(從這一點(diǎn)上看,DevView與平臺(tái)SDK中的WINOBJ工具相類似,但WINOBJ不能給出設(shè)備對(duì)象和驅(qū)動(dòng)程序的相關(guān)信息)

圖2-16. 用DevView觀察命名空間
通常設(shè)備對(duì)象都把自己的名字放到\Device目錄中。在Windows 2000中,設(shè)備的名稱有兩個(gè)用途。第一個(gè)用途,設(shè)備命名后,其它內(nèi)核模式部件可以通過(guò)調(diào)用IoGetDeviceObjectPointer函數(shù)找到該設(shè)備,找到設(shè)備對(duì)象后,就可以向該設(shè)備的驅(qū)動(dòng)程序發(fā)送IRP。
另一個(gè)用途,允許應(yīng)用程序打開(kāi)命名設(shè)備的句柄,這樣它們就可以向驅(qū)動(dòng)程序發(fā)送IRP。應(yīng)用程序可以使用標(biāo)準(zhǔn)的CreateFile API打開(kāi)命名設(shè)備句柄,然后用ReadFile、WriteFile,和DeviceIoControl向驅(qū)動(dòng)程序發(fā)出請(qǐng)求。應(yīng)用程序打開(kāi)設(shè)備句柄時(shí)使用\\.\路徑前綴而不是標(biāo)準(zhǔn)的UNC(統(tǒng)一命名約定)名稱,如C:\MYFILE.CPP或\\FRED\C-Drive\HISFILE.CPP。在內(nèi)部,I/O管理器在執(zhí)行名稱搜索前自動(dòng)把\\.\轉(zhuǎn)換成\??\。為了把\??目錄中的名字與名字在其它目錄(例如,在\Device目錄)中的對(duì)象相連接,對(duì)象管理器實(shí)現(xiàn)了一種稱為符號(hào)連接(symbolic link)的對(duì)象。
符號(hào)連接
符號(hào)連接有點(diǎn)象桌面上的快捷方式,符號(hào)連接在Windows NT中的主要用途是把處于列表前面的DOS形式的名稱連接到設(shè)備上。圖2-17顯示了\??目錄的部分內(nèi)容,這里就有一些符號(hào)名,例如,“C:”和其它一些用DOS命名方案命名的驅(qū)動(dòng)器名稱,它們被連接到\Device目錄中,而這些設(shè)備對(duì)象的真正名稱就放在\Device目錄中。符號(hào)連接可以使對(duì)象管理器在分析一個(gè)名稱時(shí)能跳到命名空間的某個(gè)地方。例如,如果我用CreateFile打開(kāi)名稱為“C:\MYFILE.CPP”的對(duì)象,對(duì)象管理器將以下面過(guò)程打開(kāi)該文件:
- 內(nèi)核模式代碼最開(kāi)始看到的名稱是\??\C:\MYFILE.CPP。對(duì)象管理器在根目錄中查找“??”。
- 找到\??目錄后,對(duì)象管理器在其中查找“C:”。它發(fā)現(xiàn)找到的對(duì)象是一個(gè)符號(hào)連接,所以它就用這個(gè)符號(hào)連接組成一個(gè)新的內(nèi)核模式路徑名:\Device\HarddiskVolume1\MYFILE.CPP,然后析取它。
- 使用新路徑名后,對(duì)象管理器重新在根目錄中查找“Device”。
- 找到\Device目錄后,對(duì)象管理器在其中查找“HarddiskVolume1”,最后它找到一個(gè)以該名字命名的設(shè)備。

圖2-17. \??目錄和部分符號(hào)連接
現(xiàn)在,對(duì)象管理器要創(chuàng)建一個(gè)IRP,然后把它發(fā)到HarddiskVolume1設(shè)備的驅(qū)動(dòng)程序。該IRP最終將使某個(gè)文件系統(tǒng)驅(qū)動(dòng)程序或其它驅(qū)動(dòng)程序定位并打開(kāi)一個(gè)磁盤(pán)文件。描述文件系統(tǒng)驅(qū)動(dòng)程序的工作過(guò)程已經(jīng)超出了本書(shū)的范圍。如果我們使用設(shè)備名COM1,那么最終收到該IRP的將是\Device\Serial0的驅(qū)動(dòng)程序。
用戶模式程序可以調(diào)用DefineDosDevice創(chuàng)建一個(gè)符號(hào)連接,如下例:
BOOL okay = DefineDosDevice(DDD_RAW_TARGET_PATH, "barf", "\\Device\\SECTEST_0");
|
圖2-17中顯示了上面調(diào)用的結(jié)果。
如果你需要在WDM驅(qū)動(dòng)程序中創(chuàng)建一個(gè)符號(hào)連接,可以調(diào)用IoCreateSymbolicLink函數(shù):
IoCreateSymbolicLink(linkname, targname);
|
linkname是要?jiǎng)?chuàng)建的符號(hào)連接名,targname是要連接的名字。順便說(shuō)一下,對(duì)象管理器并不關(guān)心targname是否是已存在對(duì)象的名字,如果連接到一個(gè)未定義的符號(hào)名,那么訪問(wèn)該符號(hào)連接將簡(jiǎn)單地收到一個(gè)錯(cuò)誤。如果你想允許用戶模式程序能超越這個(gè)連接而轉(zhuǎn)到其它地方,應(yīng)使用IoCreateUnprotectedSymbolicLink函數(shù)替代上面函數(shù)。
應(yīng)該命名設(shè)備對(duì)象嗎?
決定為設(shè)備對(duì)象命名之前,你應(yīng)該多想一想。如果命名了設(shè)備對(duì)象,那么任何內(nèi)核模式程序都可以打開(kāi)該設(shè)備的句柄。另外,任何內(nèi)核模式或用戶模式程序都能創(chuàng)建連接到該設(shè)備的符號(hào)連接,并可以使用這個(gè)符號(hào)連接打開(kāi)設(shè)備的句柄。你可能允許也可能不允許這種事情發(fā)生。
是否命名設(shè)備對(duì)象的主要考慮是安全問(wèn)題。當(dāng)有人打開(kāi)一個(gè)命名對(duì)象的句柄時(shí),對(duì)象管理器將檢查他是否有權(quán)這樣做。當(dāng)IoCreateDevice為你創(chuàng)建設(shè)備對(duì)象時(shí),它也為設(shè)備對(duì)象設(shè)置了一個(gè)默認(rèn)安全描述符(基于第四個(gè)參數(shù)中的設(shè)備類型)。下面是三個(gè)基本分類,I/O管理器基于這些分類來(lái)選擇安全描述符。(參考表2-4中的第二列)
- 大部分文件系統(tǒng)設(shè)備對(duì)象(磁盤(pán)、CD-ROM、文件、磁帶)將得到“public default unrestricted”ACL(訪問(wèn)控制表)。該表對(duì)系統(tǒng)(SYSTEM)和管理員(administrator)之外的所有賬戶給予了SYNCHRONIZE、READ_CONTROL、FILE_READ_ATTRIBUTES、FILE_TRAVERSE訪問(wèn)權(quán)限。順便說(shuō)一下,文件系統(tǒng)設(shè)備對(duì)象就是作為CreateFile函數(shù)的目標(biāo)而存在,CreateFile函數(shù)將打開(kāi)一個(gè)由文件系統(tǒng)管理的文件。
- 磁盤(pán)設(shè)備和網(wǎng)絡(luò)文件系統(tǒng)對(duì)象將得到與文件系統(tǒng)對(duì)象相同的ACL,但做了一些修改。例如,任何人對(duì)命名軟磁盤(pán)設(shè)備對(duì)象都有全部訪問(wèn)權(quán),管理員有足夠的權(quán)限運(yùn)行ScanDisk。(用戶模式的網(wǎng)絡(luò)支持DLL需要更大的權(quán)限來(lái)訪問(wèn)其對(duì)應(yīng)文件系統(tǒng)驅(qū)動(dòng)程序的設(shè)備對(duì)象,這就是網(wǎng)絡(luò)文件系統(tǒng)需要與其它文件系統(tǒng)區(qū)別對(duì)待的原因)
- 所有其它的設(shè)備對(duì)象將得到“public open unrestricted”ACL,它允許任何有設(shè)備句柄的人不受限制地使用該設(shè)備。
可以看出,如果非磁盤(pán)設(shè)備的驅(qū)動(dòng)程序在調(diào)用IoCreateDevice時(shí)給出設(shè)備對(duì)象名,那么任何人都可以讀寫(xiě)這個(gè)設(shè)備,因?yàn)槟J(rèn)安全設(shè)置幾乎允許用戶有全部的訪問(wèn)權(quán)限,而且在創(chuàng)建符號(hào)連接時(shí)根本不進(jìn)行安全檢查。安全檢查僅發(fā)生在對(duì)設(shè)備的打開(kāi)操作上,基于命名對(duì)象的安全描述符。這對(duì)于在同一堆棧中的有更嚴(yán)格安全限制的其它設(shè)備對(duì)象也是這樣。
DevView可以顯示設(shè)備對(duì)象的安全屬性。你可以通過(guò)測(cè)試一個(gè)文件系統(tǒng)、一個(gè)磁盤(pán)設(shè)備、或者任何其它隨機(jī)存取設(shè)備了解到我剛描述過(guò)的默認(rèn)操作規(guī)則。
PDO也得到一個(gè)默認(rèn)安全描述符,但這個(gè)安全描述符可能被存儲(chǔ)在硬件鍵或類鍵的Properties子鍵中的安全描述符超越(當(dāng)兩者都存在時(shí),硬件鍵中的超越值有更高的優(yōu)先權(quán))。即使沒(méi)有指定安全描述符超越,如果硬件鍵或類鍵的Properties子鍵中有設(shè)備類型或特征的超越值,那么I/O管理器也會(huì)基于新類型為對(duì)象構(gòu)造一個(gè)新的默認(rèn)安全描述符。但I(xiàn)/O管理器不會(huì)超越PDO上面的任何其它設(shè)備對(duì)象的安全設(shè)置。因此,由于超越的影響,你不應(yīng)該命名你的設(shè)備對(duì)象。但不要失望,應(yīng)用程序仍可以使用注冊(cè)的接口(interface)訪問(wèn)你的設(shè)備。
關(guān)于安全問(wèn)題的最后一點(diǎn):當(dāng)對(duì)象管理器析取對(duì)象名時(shí),對(duì)于名字的中間部分僅需要具有FILE_TRAVERSE訪問(wèn)權(quán),它僅在最終對(duì)象名上執(zhí)行全部的安全檢查。所以,假設(shè)某個(gè)設(shè)備對(duì)象可以通過(guò)\Device\SECTEST_0名或符號(hào)連接\??\SecurityTest_0名到達(dá),那么,如果設(shè)備對(duì)象的安全描述符設(shè)置為拒絕寫(xiě),則試圖以寫(xiě)方式打開(kāi)\\.\SecurityTest_0的用戶模式應(yīng)用程序?qū)⒈蛔枞5绻麘?yīng)用程序試圖打開(kāi)名為\\.\SecurityTest_0\ExtraStuff的對(duì)象,那么打開(kāi)請(qǐng)求(IRP_MJ_CREATE形式)將被允許,而此時(shí)用戶對(duì)\\.\SecurityTest_0\僅有FILE_TRAVERSE權(quán)限。I/O管理器希望設(shè)備驅(qū)動(dòng)程序自己去處理額外名稱部件的安全檢查。
為了避免涉及到我剛描述過(guò)的安全問(wèn)題,你可以在調(diào)用IoCreateDevice時(shí)指定設(shè)備特征參數(shù)為FILE_DEVICE_SECURE_OPEN。該標(biāo)志將使Windows 2000在額外名稱部件存在的情況下仍檢查調(diào)用者是否有權(quán)限打開(kāi)設(shè)備句柄。
設(shè)備名稱
如果你決定命名設(shè)備對(duì)象,通常應(yīng)該把對(duì)象名放在名稱空間的\Device分支中。為了命名設(shè)備對(duì)象,首先應(yīng)該創(chuàng)建一個(gè)UNICODE_STRING結(jié)構(gòu)來(lái)存放對(duì)象名,然后把該串作為調(diào)用IoCreateDevice的參數(shù):
UNICODE_STRING devname;
RtlInitUnicodeString(&devname, L"\\Device\\Simple0");
IoCreateDevice(DriverObject, sizeof(DEVICE_EXTENSION), &devname, ...);
|
我將在下一章中討論RtlInitUnicodeString的用法。
通常,驅(qū)動(dòng)程序用設(shè)備類型串后加上一個(gè)以0開(kāi)始的實(shí)例號(hào)作為設(shè)備對(duì)象名(如上面的Simple0)。一般,你不希望象我上面做的那樣使用帶有硬編碼性質(zhì)的名稱。你希望用串操作函數(shù)動(dòng)態(tài)地合成一個(gè)名字:
UNICODE_STRING devname;
static LONG lastindex = -1;
LONG devindex = InterlockedIncrement(&lastindex);
WCHAR name[32];
_snwprintf(name, arraysize(name), L"\\Device\\SIMPLE%2.2d", devindex);
RtlInitUnicodeString(&devname, name);
IoCreateDevice(...);
|
我將在后兩章中解釋上面代碼中出現(xiàn)的服務(wù)函數(shù)。如上面代碼所示,從私有設(shè)備類型得出的實(shí)例號(hào)應(yīng)該是一個(gè)靜態(tài)變量。
設(shè)備接口
用舊的命名方法命名設(shè)備對(duì)象,并創(chuàng)建一個(gè)應(yīng)用程序能夠使用的符號(hào)連接,存在著兩個(gè)主要問(wèn)題。命名設(shè)備對(duì)象所帶來(lái)的潛在安全問(wèn)題我們已經(jīng)討論過(guò)。此外,訪問(wèn)設(shè)備的應(yīng)用程序需要先知道設(shè)備采用的命名方案。如果你的硬件僅由你的應(yīng)用程序訪問(wèn),那么不會(huì)有什么問(wèn)題。但是,如果有其它公司想為你的硬件寫(xiě)應(yīng)用程序,并且有許多硬件公司想制作相似的設(shè)備,那么設(shè)計(jì)一個(gè)合適的命名方案是困難的。最后,許多命名方案將依賴于程序員所說(shuō)的自然語(yǔ)言,這不是一個(gè)好的選擇。
為了解決這些問(wèn)題,WDM引入了一個(gè)新的設(shè)備命名方案,該方案是語(yǔ)言中立的、易于擴(kuò)展的、可用于許多硬件和軟件廠商,并且易于文檔化。該方案依靠一個(gè)設(shè)備接口(device interface)的概念,它基本上是軟件如何訪問(wèn)硬件的一個(gè)說(shuō)明。一個(gè)設(shè)備接口被一個(gè)128位的GUID唯一標(biāo)識(shí)。你可以用平臺(tái)SDK中的UUIDGEN工具或者GUIDGEN工具生成GUID,這兩個(gè)工具輸出同一種數(shù),但格式不同。這個(gè)想法就象某些工業(yè)組織聯(lián)合起來(lái)共同制定某種硬件的標(biāo)準(zhǔn)訪問(wèn)方法一樣。在標(biāo)準(zhǔn)制作過(guò)程中,產(chǎn)生了一些GUID,這些GUID將永遠(yuǎn)關(guān)聯(lián)到某些接口上。

圖2-18. 使用GUIDGEN生成GUID
我想接口類似于蛋白質(zhì)合成器,它能制作活細(xì)胞的細(xì)胞膜。訪問(wèn)特定種類設(shè)備的應(yīng)用程序有自己的蛋白質(zhì)合成器,它就象一把鑰匙,可以插入到所有有匹配合成器的設(shè)備驅(qū)動(dòng)程序中。如圖2-19。

圖2-19. 用設(shè)備接口匹配應(yīng)用程序和設(shè)備
注冊(cè)設(shè)備接口 調(diào)用IoRegisterDeviceInterface函數(shù),功能驅(qū)動(dòng)程序的AddDevice函數(shù)可以注冊(cè)一個(gè)或多個(gè)設(shè)備接口:
#include <initguid.h> <--1
#include "guids.h" <--2
...
NTSTATUS AddDevice(...)
{
...
IoRegisterDeviceInterface(pdo, &GUID_SIMPLE, NULL, &pdx->ifname); <--3
...
}
|
- 我們包含了GUIDS.H頭文件,那里定義了DEFINE_GUID宏。DEFINE_GUID通常聲明一個(gè)外部變量。在驅(qū)動(dòng)程序的某些地方,我們不得不為將要引用的每個(gè)GUID保留初始化的存儲(chǔ)空間。系統(tǒng)頭文件INITGUID.H利用某些預(yù)編譯指令使DEFINE_GUID宏在已經(jīng)定義的情況下仍能保留該存儲(chǔ)空間。
- 我使用單獨(dú)的頭文件來(lái)保存我要引用的GUID定義。這是一個(gè)好的想法,因?yàn)橛脩裟J降拇a也需要包含這些定義,但它們不需要那些僅與內(nèi)核模式驅(qū)動(dòng)程序有關(guān)的聲明。
- IoRegisterDeviceInterface的第一個(gè)參數(shù)必須是設(shè)備PDO的地址。第二個(gè)參數(shù)指出與接口關(guān)聯(lián)的GUID,第三個(gè)參數(shù)指出額外的接口細(xì)分類名。只有Microsoft的代碼才使用名稱細(xì)分類方案。第四個(gè)參數(shù)是一個(gè)UNICODE_STRING串的地址,該串用于接收設(shè)備對(duì)象的符號(hào)連接名。
IoRegisterDeviceInterface的返回值是一個(gè)Unicode串,這樣在不知道驅(qū)動(dòng)程序編碼的情況下,應(yīng)用程序能用該串確定并打開(kāi)設(shè)備句柄。順便說(shuō)一下,這個(gè)名字比較丑陋;后面例子是我在Windows 98中為Sample設(shè)備生成的名字:\DosDevices\0000000000000007#{CAF53C68-A94C-11d2-BB4A-00C04FA330A6}。
注冊(cè)過(guò)程實(shí)際就是先創(chuàng)建一個(gè)符號(hào)連接名,然后再把它存入注冊(cè)表。之后,當(dāng)響應(yīng)PnP請(qǐng)求IRP_MN_START_DEVICE時(shí),驅(qū)動(dòng)程序?qū)⒄{(diào)用IoSetDeviceInterfaceState函數(shù)“使能”該接口:
IoSetDeviceInterfaceState(&pdx->ifname, TRUE);
|
在響應(yīng)這個(gè)調(diào)用過(guò)程中,I/O管理器將創(chuàng)建一個(gè)指向設(shè)備PDO的符號(hào)連接對(duì)象。以后,驅(qū)動(dòng)程序會(huì)執(zhí)行一個(gè)功能相反的調(diào)用禁止該接口(用FALSE做參數(shù)調(diào)用IoSetDeviceInterfaceState)。最后,I/O管理器刪除符號(hào)連接對(duì)象,但它保留了注冊(cè)表項(xiàng),即這個(gè)名字將總與設(shè)備的這個(gè)實(shí)例關(guān)聯(lián);但符號(hào)連接對(duì)象與硬件一同到來(lái)或消失。
因?yàn)榻涌诿罱K指向PDO,所以PDO的安全描述符將最終控制設(shè)備的訪問(wèn)權(quán)限。這樣比較好,因?yàn)橹挥泄芾韱T才可以通過(guò)控制臺(tái)控制PDO的安全屬性。
枚舉設(shè)備接口 內(nèi)核模式代碼和用戶模式代碼都能定位含有支持它們感興趣接口的設(shè)備。下面我將解釋如何在用戶模式中枚舉所有含有特定接口的設(shè)備。枚舉代碼寫(xiě)起來(lái)十分冗長(zhǎng),最后我不得不寫(xiě)一個(gè)C++類來(lái)實(shí)現(xiàn)。你可以在DEVICELIST.CPP和DEVICELIST.H文件中找到這些代碼,這些文件是第八章“電源管理”中WDMIDLE例子的一部分。它們聲明并實(shí)現(xiàn)了一個(gè)CDeviceList類,該類包含一個(gè)CDeviceListEntry對(duì)象數(shù)組。這兩個(gè)類聲明如下:
class CDeviceListEntry
{
public:
CDeviceListEntry(LPCTSTR linkname, LPCTSTR friendlyname);
CDeviceListEntry(){}
CString m_linkname;
CString m_friendlyname;
};
class CDeviceList
{
public:
CDeviceList(const GUID& guid);
~CDeviceList();
GUID m_guid;
CArray<CDeviceListEntry, CDeviceListEntry&> m_list;
int Initialize();
};
|
該類使用了CString類和CArray模板,它們都是MFC的一部分。這兩個(gè)類的構(gòu)造函數(shù)僅簡(jiǎn)單地把它們的參數(shù)復(fù)制到數(shù)據(jù)成員中:
CDeviceList::CDeviceList(const GUID& guid)
{
m_guid = guid;
}
CDeviceListEntry::CDeviceListEntry(LPCTSTR linkname, LPCTSTR friendlyname)
{
m_linkname = linkname;
m_friendlyname = friendlyname;
}
|
所有實(shí)際的工作都發(fā)生在CDeviceList::Initialize函數(shù)中。其執(zhí)行過(guò)程大致是這樣:先枚舉所有接口GUID與構(gòu)造函數(shù)得到的GUID相同的設(shè)備,然后確定一個(gè)“友好”名,我們希望向最終用戶顯示這個(gè)名字。最后返回找到的設(shè)備號(hào)。下面是這個(gè)函數(shù)的代碼:
int CDeviceList::Initialize()
{
HDEVINFO info = SetupDiGetClassDevs(&m_guid, NULL, NULL, DIGCF_PRESENT | DIGCF_INTERFACEDEVICE); <--1
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) <--2
{
DWORD needed;
SetupDiGetDeviceInterfaceDetail(info, &ifdata, NULL, 0, &needed, NULL); <--3
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]; <--4
if (!SetupDiGetDeviceRegistryProperty(info,
&did,
SPDRP_FRIENDLYNAME,
NULL,
(PBYTE) fname,
sizeof(fname),
NULL)
&& !SetupDiGetDeviceRegistryProperty(info,
&did,
SPDRP_DEVICEDESC,
NULL,
(PBYTE) fname,
sizeof(fname),
NULL)
)
_tcsncpy(fname, detail->DevicePath, 256);
CDeviceListEntry e(detail->DevicePath, fname); <--5
free((PVOID) detail);
m_list.Add(e);
}
SetupDiDestroyDeviceInfoList(info);
return m_list.GetSize();
}
|
- 該語(yǔ)句打開(kāi)一個(gè)枚舉句柄,我們用它尋找寄存了指定GUID接口的所有設(shè)備。
- 循環(huán)調(diào)用SetupDiEnumDeviceInterfaces以尋找每個(gè)匹配的設(shè)備。
- 有兩項(xiàng)信息是我們需要的,接口的“細(xì)節(jié)”信息和設(shè)備實(shí)例信息。這個(gè)“細(xì)節(jié)”信息就是設(shè)備的符號(hào)名。因?yàn)樗拈L(zhǎng)度可變,所以我們兩次調(diào)用了SetupDiGetDeviceInterfaceDetail。第一次調(diào)用確定了長(zhǎng)度,第二次調(diào)用獲得了名字。
- 通過(guò)詢問(wèn)注冊(cè)表中的FriendlyName鍵或DeviceDesc鍵,我們獲得了設(shè)備的“友好”名稱。
- 我們用設(shè)備符號(hào)名同時(shí)作為連接名和友好名創(chuàng)建了類CDeviceListEntry的一個(gè)臨時(shí)實(shí)例e。
其它全局性的設(shè)備初始化操作
在AddDevice中還需要加入其它一些步驟來(lái)初始化設(shè)備對(duì)象,下面我將按順序描述這些步驟。
初始化設(shè)備擴(kuò)展
設(shè)備擴(kuò)展的內(nèi)容和管理全部由用戶決定。該結(jié)構(gòu)中的數(shù)據(jù)成員應(yīng)直接反映硬件的專有細(xì)節(jié)以及對(duì)設(shè)備的編程方式。大多數(shù)驅(qū)動(dòng)程序都會(huì)在這里放入一些數(shù)據(jù)項(xiàng),下面代碼聲明了一個(gè)設(shè)備擴(kuò)展結(jié)構(gòu):
typedef struct _DEVICE_EXTENSION { <--1
PDEVICE_OBJECT DeviceObject; <--2
PDEVICE_OBJECT LowerDeviceObject; <--3
PDEVICE_OBJECT Pdo; <--4
UNICODE_STRING ifname; <--5
IO_REMOVE_LOCK RemoveLock; <--6
DEVSTATE devstate; <--7
DEVSTATE prevstate;
DEVICE_POWER_STATE devpower;
SYSTEM_POWER_STATE syspower;
DEVICE_CAPABILITIES devcaps; <--8
...
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;
|
- 我模仿DDK中官方的結(jié)構(gòu)聲明模式聲明了這個(gè)結(jié)構(gòu)。
- 我們可以用設(shè)備對(duì)象中的DeviceExtension指針定位自己的設(shè)備擴(kuò)展。同樣,我們有時(shí)也需要在給定設(shè)備擴(kuò)展時(shí)能定位設(shè)備對(duì)象。因?yàn)槟承┖瘮?shù)的邏輯參數(shù)就是設(shè)備擴(kuò)展本身(這里有設(shè)備每個(gè)實(shí)例的全部信息)。所以,我認(rèn)為這里應(yīng)該有一個(gè)DeviceObject指針。
- 我在一些地方曾提到過(guò),在調(diào)用IoAttachDeviceToDeviceStack函數(shù)時(shí),應(yīng)該把緊接著你下面的設(shè)備對(duì)象的地址保存起來(lái)。LowerDeviceObject成員用于保存這個(gè)地址。
- 有一些服務(wù)例程需要PDO的地址,而不是堆棧中某個(gè)高層設(shè)備對(duì)象的地址。由于定位PDO非常困難,所以最好的辦法是在AddDevice執(zhí)行時(shí)在設(shè)備擴(kuò)展中保存一個(gè)PDO地址。
- 無(wú)論你用什么方法(符號(hào)連接或設(shè)備接口)命名你的設(shè)備,都希望能容易地獲得這個(gè)名字。所以,這里我用一個(gè)Unicode串成員ifname來(lái)保存設(shè)備接口名。如果你使用一個(gè)符號(hào)連接名而不是設(shè)備接口,應(yīng)該使用一個(gè)有相關(guān)含義的成員名,例如“linkname”。
- 當(dāng)你調(diào)用IoDeleteDevice刪除這個(gè)設(shè)備對(duì)象時(shí),需要使用一個(gè)自旋鎖來(lái)解決同步安全問(wèn)題,我將在第六章中討論同步問(wèn)題。因此,需要在設(shè)備擴(kuò)展中分配一個(gè)IO_REMOVE_LOCK對(duì)象。AddDevice有責(zé)任初始化這個(gè)對(duì)象。
- 你可能需要一個(gè)成員來(lái)記錄設(shè)備當(dāng)前的PnP狀態(tài)和電源狀態(tài)。DEVSTATE和POWERSTATE是枚舉類型變量,我假設(shè)事先已經(jīng)在頭文件中聲明了這些變量類型。我將在后面章節(jié)中討論這些狀態(tài)變量的用途。
- 電源管理的另一個(gè)部分涉及電源能力設(shè)置的恢復(fù),設(shè)備擴(kuò)展中的devcaps結(jié)構(gòu)用于保存這些設(shè)置。
下面是AddDevice中的初始化語(yǔ)句(著重設(shè)備擴(kuò)展部分的初始化):
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(...);
}
|
初始化默認(rèn)的DPC對(duì)象
許多設(shè)備使用中斷來(lái)報(bào)告操作完成。我將在第七章“讀寫(xiě)數(shù)據(jù)”中討論中斷處理,其中對(duì)中斷服務(wù)例程(ISR)能做什么做了嚴(yán)格的限定。特別是ISR不能調(diào)用用于報(bào)告IRP完成的例程(IoCompleteRequest)。利用DPC(推遲過(guò)程調(diào)用)可以繞過(guò)這個(gè)限制。你的設(shè)備對(duì)象中應(yīng)包含一個(gè)輔助DPC對(duì)象,它可以調(diào)度你的DPC例程,該對(duì)象應(yīng)該在設(shè)備對(duì)象創(chuàng)建后不久被初始化。
NTSTATUS AddDevice(...)
{
IoCreateDevice(...);
IoInitializeDpcRequest(fdo, DpcForIsr);
}
|
設(shè)置緩沖區(qū)對(duì)齊掩碼
執(zhí)行DMA傳輸?shù)脑O(shè)備直接使用內(nèi)存中的數(shù)據(jù)緩沖區(qū)工作。HAL要求DMA傳輸中使用的緩沖區(qū)必須按某個(gè)特定界限對(duì)齊,而且設(shè)備也可能有更嚴(yán)格的對(duì)齊需求。設(shè)備對(duì)象中的AlignmentRequirement域表達(dá)了這個(gè)約束,它是一個(gè)位掩碼,等于要求的地址邊界減一。下面語(yǔ)句可以把任何地址圈入這個(gè)界限:
PVOID address = ...;
SIZE_T ar = fdo->AlignmentRequirement;
address = (PVOID) ((SIZE_T) address & ~ar);
|
還可以把任意地址圈入下一個(gè)對(duì)齊邊界:
PVOID address = ...;
SIZE_T ar = fdo->AlignmentRequirement;
address = (PVOID) (((SIZE_T) address + ar) & ~ar);
|
在這兩段代碼中,我使用了SIZE_T把指針類型(它可以是32位也可以是64位,這取決于編譯的目標(biāo)平臺(tái))轉(zhuǎn)化成一個(gè)整型,該整型與原指針有同樣的跨度范圍。
IoCreateDevice把新設(shè)備對(duì)象中的AlignmentRequirement域設(shè)置成HAL要求的值。例如,Intel的x86芯片沒(méi)有對(duì)齊需求,所以AlignmentRequirement的默認(rèn)值為0。如果設(shè)備需要更嚴(yán)格的緩沖區(qū)對(duì)齊(例如設(shè)備有總線主控的DMA能力,要求對(duì)齊數(shù)據(jù)緩沖區(qū)),應(yīng)該修改這個(gè)默認(rèn)值,如下:
if (MYDEVICE_ALIGNMENT - 1 > fdo->AlignmentRequirement)
fdo->AlignmentRequirement = MYDEVICE_ALIGNMENT - 1;
|
我假設(shè)你在驅(qū)動(dòng)程序某處已定義了一個(gè)名為MYDEVICE_ALIGNMENT的常量,它是2的冪,代表設(shè)備的數(shù)據(jù)緩沖區(qū)對(duì)齊需求。
其它對(duì)象
設(shè)備可能還有其它一些需要在AddDevice中初始化的對(duì)象。這些對(duì)象可能包括各種同步對(duì)象,各種隊(duì)列頭(queue anchors),聚集/分散列表緩沖區(qū),等等。事實(shí)上,在本書(shū)的其它地方討論這些對(duì)象的初始化更合適。
初始化設(shè)備標(biāo)志
設(shè)備對(duì)象中有兩個(gè)標(biāo)志位需要在AddDevice中初始化,并且它們?cè)谝院笠膊粫?huì)改變,它們是DO_BUFFERED_IO和DO_DIRECT_IO標(biāo)志。你只能設(shè)置并使用其中一個(gè)標(biāo)志,它將決定你以何種方式處理來(lái)自用戶模式的內(nèi)存緩沖區(qū)。(我將在第七章中討論這兩種緩沖模式的不同,以及你如何選擇) 由于任何在后面裝入的上層過(guò)濾器驅(qū)動(dòng)程序?qū)?fù)制你的標(biāo)志設(shè)置,所以在AddDevice中做這個(gè)選擇十分重要。如果你在過(guò)濾器驅(qū)動(dòng)程序裝入后改變了設(shè)置,它們可能會(huì)不知道。
設(shè)備對(duì)象中有兩個(gè)標(biāo)志位屬于電源管理范疇。與前兩個(gè)緩沖區(qū)標(biāo)志不同,這兩個(gè)標(biāo)志在任何時(shí)間都可以被改變。我將在第八章中詳細(xì)討論它們,但這里我先介紹一下。DO_POWER_PAGABLE意味著電源管理器將在PASSIVE_LEVEL級(jí)上向你發(fā)送IRP_MJ_POWER請(qǐng)求。DO_POWER_INRUSH意味著你的設(shè)備在上電時(shí)將汲取大量電流,因此,電源管理器將確保沒(méi)有其它INRUSH設(shè)備同時(shí)上電。
設(shè)置初始電源狀態(tài)
大部分設(shè)備一開(kāi)始就進(jìn)入全供電狀態(tài)。如果你知道你的設(shè)備的初始電源狀態(tài),應(yīng)該告訴電源管理器:
POWER_STATE state;
state.DeviceState = PowerDeviceD0;
PoSetPowerState(fdo, DevicePowerState, state);
|
電源管理的細(xì)節(jié)請(qǐng)見(jiàn)第八章。
建立設(shè)備堆
每個(gè)過(guò)濾器驅(qū)動(dòng)程序和功能驅(qū)動(dòng)程序都有責(zé)任把設(shè)備對(duì)象放到設(shè)備堆棧上,從PDO開(kāi)始一直向上。你可以調(diào)用IoAttachDeviceToDeviceStack完成你那部分工作:
NTSTATUS AddDevice(..., PDEVICE_OBJECT pdo)
{
PDEVICE_OBJECT fdo;
IoCreateDevice(..., &fdo);
pdx->LowerDeviceObject = IoAttachDeviceToDeviceStack(fdo, pdo);
}
|
IoAttachDeviceToDeviceStack的第一個(gè)參數(shù)是新創(chuàng)建的設(shè)備對(duì)象的地址。第二個(gè)參數(shù)是PDO地址。AddDevice的第二個(gè)參數(shù)也是這個(gè)地址。返回值是緊接著你下面的任何設(shè)備對(duì)象的地址,它可以是PDO,也可以是其它低級(jí)過(guò)濾器設(shè)備對(duì)象。如果該函數(shù)失敗則返回一個(gè)NULL指針,因此你的AddDevice函數(shù)也是失敗的,應(yīng)返回STATUS_DEVICE_REMOVED。
清除DO_DEVICE_INITIALIZING標(biāo)志
在AddDevice中最后一件需要做的事是清除設(shè)備對(duì)象中的DO_DEVICE_INITIALIZING標(biāo)志:
fdo->Flags &= ~DO_DEVICE_INITIALIZING;
|
當(dāng)這個(gè)標(biāo)志設(shè)置時(shí),I/O管理器將拒絕任何打開(kāi)該設(shè)備句柄的請(qǐng)求或向該設(shè)備對(duì)象上附著其它設(shè)備對(duì)象的請(qǐng)求。在驅(qū)動(dòng)程序完成初始化后,必須清除這個(gè)標(biāo)志。在以前版本的Windows NT中,大部分驅(qū)動(dòng)程序在DriverEntry中創(chuàng)建所有需要的設(shè)備對(duì)象。當(dāng)DriverEntry返回時(shí),I/O管理器自動(dòng)遍歷設(shè)備對(duì)象列表并清除該標(biāo)志。但在WDM驅(qū)動(dòng)程序中,設(shè)備對(duì)象在DriverEntry返回后才創(chuàng)建,所以I/O管理器不會(huì)自動(dòng)清除這個(gè)標(biāo)志,驅(qū)動(dòng)程序必須自己清除它。