這篇博客詳細介紹了UMDF驅動的基本概念、生命週期、代碼實現和應用交互,適合初學者入門
• 目的:創建一個“虛擬設備”(軟件模擬的設備),讓用户模式應用程序(比如你的C#或C++程序)能與之“對話”。它不控制真實硬件(如USB設備),而是演示驅動的基本流程:加載、創建設備、處理請求。
• 為什麼用UMDF? UMDF讓驅動運行在用户模式(非內核),更安全穩定。內核驅動(如KMDF)風險高,UMDF適合簡單任務。
• 示例功能:驅動加載後,註冊一個設備接口。應用能打開設備、發送“控制請求”(如自定義命令),驅動簡單響應(目前只返回成功)。你可以擴展它做監控、數據處理等。
• 文件結構(從ReadMe.txt):
• Device.c & Device.h:設備創建和上下文。
• Driver.c:驅動入口和全局回調。
• Queue.c:I/O請求隊列處理。
• Public.h:共享定義(如設備接口GUID)。
• UMDFTest.inf:安裝文件(定義硬件ID、服務等)。
• 其他:跟蹤日誌、項目配置。
驅動的生命週期(從加載到運行)
驅動像“服務”一樣工作。安裝後,它不直接運行,而是等系統調用。
• 入口點在哪裏?
• 全局入口:DriverEntry 函數(在Driver.c)。這是驅動的“起點”,系統加載DLL時第一個調用它。
• 它初始化WDF框架、註冊回調(如設備添加事件)、設置跟蹤日誌。
• 如果失敗,驅動不加載。
• 設備入口:UMDFTestEvtDeviceAdd(也在Driver.c),PnP(即插即用)系統添加設備時調用。它調用UMDFTestCreateDevice(在Device.c)創建設備對象。
• 安裝後:驅動作為UMDF服務運行在WUDFHost.exe進程中(用户模式主機)。系統啓動它時,調用DriverEntry,然後創建設備。
• 安裝過程(手動做的):
• 用pnputil /add-driver UMDFExample.inf /install安裝INF文件。 //安裝後出現在system devices下面名字叫做UMDFExampledevices
• 系統註冊設備(硬件ID Root\UMDFExample),加載DLL到WUDFHost。
• 設備出現在設備管理器(“UMDFExample Device”),狀態“正常”。 //沒找到
-
其他程序怎麼用這個驅動?入口點在哪裏?
驅動不是“程序”,它是系統服務。其他程序(應用)通過設備接口與之交互,像打開文件一樣。
• 入口點main(應用側):
• 設備接口GUID:GUID_DEVINTERFACE_UMDFExample(在Public.h定義,值是{8d385c4d-4fa8-413c-9307-a8fa3e768390})。這是“門牌號”,應用用它找到設備。
• 打開設備:用CreateFile API(Windows函數),路徑是\.\GUID字符串(如\.{8d385c4d-4fa8-413c-9307-a8fa3e768390})。成功返回句柄(handle),像文件句柄。
• 發送消息: 使用DeviceIoControl API(windows函數),成功的話會返回DeviceIoControl succeeded 返回的實際內容如下:bytesReturned,BOOL result = DeviceIoControl(hDevice, // 1. 設備句柄 IOCTL_UMDFTEST_HELLO, // 2. 控制碼 (LPVOID)inputBuffer, // 3. 輸入緩衝區指針 inputSize, // 4. 輸入緩衝區大小 outputBuffer, // 5. 輸出緩衝區指針 outputSize, // 6. 輸出緩衝區大小 &bytesReturned, // 7. 接收實際返回字節數的變量指針 nullptr); // 8. 用於異步操作,此處為同步3.驅動層先不細講,在驅動裏面UMDFTestEvtIoDeviceControl是一個回調函數,用於處理來自用户模式應用程序的 設備控制請求(IOCTL)。當你的應用調用 DeviceIoControl 時,UMDF 框架會自動調用這個函數來處理請求。再博客的最後會寫個例子講解這個函數詳細的代碼內容
VOID UMDFTestEvtIoDeviceControl( _In_ WDFQUEUE Queue, // 隊列句柄(框架管理的 I/O 隊列) _In_ WDFREQUEST Request, // 請求句柄(包含輸入/輸出緩衝區和請求詳情) _In_ size_t OutputBufferLength, // 輸出緩衝區大小(字節) _In_ size_t InputBufferLength, // 輸入緩衝區大小(字節) _In_ ULONG IoControlCode // IOCTL 控制碼(唯一標識請求類型,如 IOCTL_UMDFTEST_HELLO) )
• 返回類型:VOID(無返回值,因為處理結果通過 Request 對象傳遞)。
• 參數:
• Queue:指向隊列對象的句柄(你不需要直接操作它)。
• Request:核心對象,封裝了請求的所有數據(輸入緩衝區、輸出緩衝區等)。這是你處理數據的入口。
• OutputBufferLength / InputBufferLength:緩衝區大小,用於安全檢查(防止緩衝區溢出)。
• IoControlCode:一個數字,標識具體的 IOCTL 類型(例如,你的代碼中定義的 IOCTL_UMDFTEST_HELLO)。
• In 註解:SAL (Source Annotation Language) 註解,表示這些參數是輸入的(只讀)。
檢查是否還有 pnputil /enum-drivers | findstr /i UMDFTest
我們講解一下exe要加載驅動的時候傳入的參數和驅動的inf的對應關係
SW_DEVICE_CREATE_INFO createInfo = { 0 };
createInfo.cbSize = sizeof(SW_DEVICE_CREATE_INFO);
createInfo.pszInstanceId = L"umdftest"; // 實例ID
createInfo.pszzHardwareIds = L"Root\\UMDFTest\0\0"; // 硬件ID,與INF文件匹配
createInfo.pszzCompatibleIds = nullptr; // 兼容ID,無需設置
createInfo.pContainerId = nullptr; // 容器ID
createInfo.CapabilityFlags = SWDeviceCapabilitiesDriverRequired; // 能力標誌,需要驅動程序
createInfo.pszDeviceDescription = L"UMDF Test Device"; // 設備描述
createInfo.pszDeviceLocation = nullptr; // 設備位置
createInfo.pSecurityDescriptor = nullptr; // 安全描述符
HRESULT hr = SwDeviceCreate(L"umdftest", L"HTREE\\ROOT\\0", &createInfo, 0, nullptr, SwDeviceCreatedCallback, nullptr, &g_SwDevice);
• createInfo.pszInstanceId(實例ID,如L"umdfexample"):這不是直接對應INF中的某個字段,而是軟件設備的唯一標識符,用於區分多個實例。它與INF中的硬件ID(Root\UMDFExample)配合使用,但INF本身不定義實例ID。你可以自定義,只要在代碼中保持一致即可。
• SwDeviceCreate的第一個參數(pszEnumeratorName,如L"umdfexample"):這是枚舉器名稱,通常設置為與pszInstanceId相同的值,用於標識創建設備的枚舉器。它也不直接對應INF中的特定字段,而是系統內部使用的標識符。
每次編譯並修改代碼後重復安裝INF時,系統會將每個INF作為獨立的驅動包添加,導致多個oemXX.inf文件(例如oem40.inf和oem54.inf)。即使硬件ID相同,pnputil也會創建新包,而不是覆蓋舊的。這是因為驅動包是基於INF內容的哈希值管理的,如果內容不同(即使硬件ID相同),會被視為新包。
對於軟件設備(如SWD\umdfexample\umdfexample),可能會出現多個設備實例或衝突。
正常更新驅動的處理步驟:
- 停止相關應用程序和服務:確保沒有進程使用驅動。
- 刪除舊驅動包:
• 運行 pnputil /enum-drivers 查看所有驅動包。
• 刪除舊的:pnputil /delete-driver oem40.inf(替換為實際名稱)。 - 卸載設備實例(如果存在):
• 使用設備管理器:找到設備(例如“UMDF Test Device”),右鍵卸載。
• 或使用命令:pnputil /remove-device "SWD\umdfexample\umdfexample"(替換為實際設備ID)。 - 安裝新驅動:
• 複製新編譯的INF和DLL到目錄。
• 運行 pnputil /add-driver UMDFExample.inf /install。 - 重啓系統(如果需要):有時軟件設備需要重啓才能生效。
- 測試:運行應用程序驗證新代碼。
如果只想強制更新而不刪除,可以嘗試 pnputil /add-driver UMDFExample.inf /install /force,但這可能不總是可靠。建議始終刪除舊包以避免衝突。
驅動層代碼詳細分析
1.INF安裝階段(驅動註冊)
我們從編譯完成之後的安裝開始:
• 首先命令執行:pnputil /add-driver UMDFExample.inf /install 將INF文件添加到系統驅動存儲中,生成一個oemXX.inf文件(例如oem54.inf)。
• 驅動代碼經歷:此時驅動代碼(UMDFExample.dll)尚未加載。INF只是註冊了驅動包,包括硬件ID(Root\UMDFExample)、服務名(UMDFExample)和DLL路徑。系統知道“如果有匹配的硬件ID,就加載這個驅動”。
• 結果:驅動包已安裝,但沒有設備實例。驅動代碼靜止。
2.設備枚舉與發現階段(PnP觸發)
• 觸發條件:當應用程序運行 SwDeviceCreate(在UMDFConsole.cpp中)時,系統創建軟件設備實例(SWD\umdfexample\umdfexample),硬件ID為Root\UMDFExample。
• PnP管理器動作:PnP檢測到新設備,匹配INF中的硬件ID,決定加載對應的驅動。
• 驅動代碼經歷:WUDFHost進程啓動(如果未運行)。系統啓動WUDFHost.exe(用户模式主機進程),並加載UMDFExample.dll到該進程中。
• DriverEntry調用:這是驅動的入口點(在Driver.c中)。DriverEntry 初始化WDF框架,註冊設備創建回調(UMDFExampleCreateDevice)。
• 此時,驅動開始“活起來”,但設備對象尚未創建。
3. 設備創建與初始化階段
• UMDFExampleCreateDevice(在Device.c中):創建WDFDEVICE對象,初始化設備上下文(deviceContext->PrivateDeviceData = 0),並創建設備接口(WdfDeviceCreateDeviceInterface,使用GUID_DEVINTERFACE_UMDFExample)。這允許應用程序通過SetupAPI找到設備路徑。
• UMDFExampleQueueInitialize(在Queue.c中):創建I/O隊列(WDFQUEUE),配置並行處理,註冊事件回調(如EvtIoDeviceControl和EvtIoStop)。
• 設備現在“可用”,應用程序可以打開設備句柄(CreateFile)。
4. I/O請求處理階段(運行時)
• 觸發條件:應用程序調用 DeviceIoControl 發送IOCTL請求(例如IOCTL_UMDFTEST_HELLO)。
• 驅動代碼經歷:
• 請求進入隊列,調用 UMDFExampleEvtIoDeviceControl(在Queue.c中)。
• 檢查IoControlCode(如果是IOCTL_UMDFTEST_HELLO)。
• 獲取輸入/輸出緩衝區。
• 處理邏輯:拼接字符串("hi client this as drive,i received message as " + 輸入),設置bytesReturned。
• 調用 WdfRequestCompleteWithInformation 完成請求,返回數據給應用程序。
• 如果不支持的IOCTL,調用 WdfRequestComplete 返回STATUS_INVALID_DEVICE_REQUEST。
• 其他事件如EvtIoStop在電源管理時調用(目前返回,無額外處理)。
5.設備移除與卸載階段
• 觸發條件:應用程序調用 SwDeviceClose,或系統重啓/卸載驅動。
• 驅動代碼經歷:
• PnP調用設備移除回調(如果有),清理資源。
• WUDFHost進程卸載DLL,驅動對象銷燬。
• 如果刪除驅動包(pnputil /delete-driver),INF被移除,下次需要重新安裝。
關鍵注意事項
• 用户模式特性:UMDF運行在WUDFHost中,崩潰不會藍屏,但權限受限(不能直接訪問硬件)。
• 調試:使用WDF Verifier或ETW跟蹤(TraceEvents)查看日誌。
• 錯誤處理:如果SwDeviceCreate失敗(權限問題),驅動不會加載。確保管理員運行應用程序。
• 生命週期總結:從安裝到卸載,驅動代碼只在設備存在時活躍。軟件設備依賴應用程序創建/銷燬。
UMDFExampleEvtIoDeviceControl 函數的詳細講解
當應用程序(UMDFConsole.cpp)調用 DeviceIoControl 發送字符串 "Hello from App" 時,驅動端(UMDFExample.dll,在WUDFHost進程中運行)會經歷以下步驟處理請求。假設IOCTL碼為IOCTL_UMDFTEST_HELLO(匹配驅動中的檢查):###
我自己的代碼如下:
VOID
UMDFExampleEvtIoDeviceControl(
_In_ WDFQUEUE Queue,
_In_ WDFREQUEST Request,
_In_ size_t OutputBufferLength,
_In_ size_t InputBufferLength,
_In_ ULONG IoControlCode
)
/*++
Routine Description:
This event is invoked when the framework receives IRP_MJ_DEVICE_CONTROL request.
Arguments:
Queue - Handle to the framework queue object that is associated with the
I/O request.
Request - Handle to a framework request object.
OutputBufferLength - Size of the output buffer in bytes
InputBufferLength - Size of the input buffer in bytes
IoControlCode - I/O control code.
Return Value:
VOID
--*/
{
if (IoControlCode == IOCTL_UMDFTEST_HELLO) { // 檢查控制碼
PVOID inputBuffer = NULL;
PVOID outputBuffer = NULL;
size_t bytesReturned = 0;
// 獲取緩衝區
if (NT_SUCCESS(WdfRequestRetrieveInputBuffer(Request, 0, &inputBuffer, NULL)) &&
NT_SUCCESS(WdfRequestRetrieveOutputBuffer(Request, 0, &outputBuffer, NULL))) {
// 處理:拼接字符串並返回
const char* prefix = "hi client this as drive,i received message as ";
size_t prefixLen = strlen(prefix);
size_t inputLen = InputBufferLength > 0 ? InputBufferLength - 1 : 0; // 假設輸入是null-terminated字符串
size_t totalLen = prefixLen + inputLen + 1; // +1 for null terminator
if (OutputBufferLength >= totalLen) {
memcpy(outputBuffer, prefix, prefixLen);
if (inputLen > 0) {
memcpy((char*)outputBuffer + prefixLen, inputBuffer, inputLen);
}
((char*)outputBuffer)[totalLen - 1] = '\0'; // null terminate
bytesReturned = totalLen;
} else {
// 輸出緩衝區太小,返回錯誤
WdfRequestComplete(Request, STATUS_BUFFER_TOO_SMALL);
return;
}
}
TraceEvents(TRACE_LEVEL_INFORMATION,
TRACE_QUEUE,
"%!FUNC! Queue 0x%p, Request 0x%p OutputBufferLength %d InputBufferLength %d IoControlCode %d",
Queue, Request, (int)OutputBufferLength, (int)InputBufferLength, IoControlCode);
// 完成請求並指定返回字節數
WdfRequestCompleteWithInformation(Request, STATUS_SUCCESS, bytesReturned);
}
else {
WdfRequestComplete(Request, STATUS_INVALID_DEVICE_REQUEST); // 不支持的 IOCTL
}
return;
}
1.請求到達驅動
• DeviceIoControl 通過設備句柄發送IRP(I/O Request Packet)到內核,內核轉發給UMDF框架。
• UMDF將請求分發到I/O隊列(在Queue.c的UMDFExampleQueueInitialize中創建)。
• 調用 UMDFExampleEvtIoDeviceControl 回調函數(在Queue.c中)。
2.檢查IOCTL碼
• 驅動檢查 IoControlCode 是否等於 IOCTL_UMDFTEST_HELLO(定義在Public.h中)。
• 如果匹配,繼續處理;否則,返回 STATUS_INVALID_DEVICE_REQUEST。
3.獲取輸入/輸出緩衝區
• 調用 WdfRequestRetrieveInputBuffer 獲取輸入緩衝區指針(包含 "Hello from App",長度為 InputBufferLength,包括null終止符)。
• 調用 WdfRequestRetrieveOutputBuffer 獲取輸出緩衝區指針(應用程序提供的256字節緩衝區)。
4.處理邏輯(拼接字符串)
• 計算前綴:"hi client this as drive,i received message as "(長度約50字節)。
• 輸入長度:InputBufferLength - 1(假設null-terminated,去掉null)。
• 總長度:前綴 + 輸入 + 1(null終止符)。
• 檢查輸出緩衝區是否足夠大(OutputBufferLength >= totalLen)。
• 如果足夠:
• 使用 memcpy 複製前綴到輸出緩衝區。
• 複製輸入字符串到輸出緩衝區後面。
• 添加null終止符。
• 設置 bytesReturned = totalLen。
• 如果不夠,返回 STATUS_BUFFER_TOO_SMALL。
5.完成請求
• 調用 WdfRequestCompleteWithInformation(Request, STATUS_SUCCESS, bytesReturned),將拼接後的字符串(例如 "hi client this as drive,i received message as Hello from App")返回給應用程序。
• 應用程序在 outputBuffer 中接收數據,bytesReturned 指示實際返回字節數。
關鍵點
• 輸入:"Hello from App"(字符串,驅動假設null-terminated)。
• 輸出:拼接後的字符串,長度取決於輸入。
• 錯誤處理:如果緩衝區小,返回錯誤;不支持IOCTL也返回錯誤。
• 日誌:TraceEvents 記錄調試信息(隊列、請求、緩衝區大小等)。