動態

詳情 返回 返回

UMDF驅動開發入門:一 創建虛擬設備,從安裝到I/O交互全解析 - 動態 詳情

這篇博客詳細介紹了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”),狀態“正常”。 //沒找到

  1. 其他程序怎麼用這個驅動?入口點在哪裏?
    驅動不是“程序”,它是系統服務。其他程序(應用)通過設備接口與之交互,像打開文件一樣。
    • 入口點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),可能會出現多個設備實例或衝突。
正常更新驅動的處理步驟:

  1. 停止相關應用程序和服務:確保沒有進程使用驅動。
  2. 刪除舊驅動包:
    • 運行 pnputil /enum-drivers 查看所有驅動包。
    • 刪除舊的:pnputil /delete-driver oem40.inf(替換為實際名稱)。
  3. 卸載設備實例(如果存在):
    • 使用設備管理器:找到設備(例如“UMDF Test Device”),右鍵卸載。
    • 或使用命令:pnputil /remove-device "SWD\umdfexample\umdfexample"(替換為實際設備ID)。
  4. 安裝新驅動:
    • 複製新編譯的INF和DLL到目錄。
    • 運行 pnputil /add-driver UMDFExample.inf /install。
  5. 重啓系統(如果需要):有時軟件設備需要重啓才能生效。
  6. 測試:運行應用程序驗證新代碼。
    如果只想強制更新而不刪除,可以嘗試 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 記錄調試信息(隊列、請求、緩衝區大小等)。

user avatar sherlocked93 頭像 donnytab 頭像 puxiaoke6 頭像 jkkang 頭像 hedzr 頭像 manxisuo 頭像 qiyuxuanangdelvdou 頭像 hello_5adf4e51b4f3e 頭像 aixiaodewulongcha_ehoerm 頭像 keen_626105e1ef632 頭像 thinkerdjx 頭像 mrbone11 頭像
點贊 16 用戶, 點贊了這篇動態!
點贊

Add a new 評論

Some HTML is okay.