簡介
這些驅動程式開發指導方針是由Microsoft的驅動程序開發人員開發多年。 隨著時間的推移,當司機行為不當並從中學到教訓時,這些教訓被吸取並演變成一套指導原則。 Microsoft Surface 硬體小組會使用這些最佳做法來開發和維護支援獨特 Surface 硬體體驗的裝置驅動程式程式代碼。
和任何一組指導方針一樣,會有合法的例外狀況和替代方法同樣有效。 請考慮將這些指導方針納入您的開發標準,或利用它們開始制定適合您開發環境和特殊需求的領域專用指導方針。
驅動程式開發人員犯的常見錯誤
處理 I/O
- 存取從 IOCTLs 所擷取的緩衝區,而不驗證其長度。 請參閱 無法檢查緩衝區的大小。
- 在使用者線程或隨機線程的執行緒上下文中執行阻塞 I/O。 請參閱 核心發送器對象的簡介。
- 將同步 I/O 傳送至另一個驅動程式,而不會超時。 請參閱 同步傳送 I/O 要求。
- 在不瞭解安全性影響的情況下,使用 neither-io IOCTL。 請參閱 不使用緩衝區亦或直接 I/O 的方法。
- 未檢查 WdfRequestForwardToIoQueue 的傳回狀態,或未正確處理失敗,導致遺棄的 WDFREQUEST。
- 將 WDFREQUEST 保持在佇列外部,處於無法取消的狀態。 請參閱 管理 I/O 佇列、 完成 I/O 要求 和 取消 I/O 要求。
- 嘗試使用 Mark/UnmarkCancelable 函式來管理取消,而不是使用 IoQueues。 請參閱 Framework Queue 物件。
- 不知道文件句柄清除操作和關閉操作之間的差異。 請參閱 處理清除和關閉作業中的錯誤。
- 忽略 I/O 完成和從完成例程重新提交的潛在遞歸。
- 未明確說明 WDFQUEUE 的電源管理屬性。 未清楚地記錄電源管理選擇。 這是 WDF 驅動程式中 DRIVER_POWER_STATE_FAILURE 的 錯誤檢查 0x9F 主要原因。 拿掉裝置時,架構會在移除程式的不同階段,從電源受控佇列和非電源受控佇列清除 IO。 收到最終IRP_MN_REMOVE_DEVICE時,會清除未受電源管理的隊列。 因此,如果您在非電源受控佇列中保存 I/O,最好在 EvtDeviceSelfManagedIoFlush 的內容中明確清除 I/O,以避免死結。
- 不遵循處理 IRP 的規則。 請參閱 處理清除和關閉作業中的錯誤。
同步
- 針對不需要保護的程式代碼保留鎖定。 當只有少數作業需要保護時,請勿保留整個函式的鎖定。
- 列出被鎖定的驅動程式。 這是死結的主要原因。
- 使用互鎖原語來建立鎖定機制,而不是使用系統提供的適當鎖定原語,例如 mutex、信號量和自旋鎖。 請參閱 Mutex 物件簡介、 信號量物件 和 自旋鎖簡介。
- 使用自旋鎖,而某些類型的被動鎖可能會更合適。 請參閱 快速互斥鎖和保護互斥鎖 和 事件物件。 如需鎖的使用的其他觀點,請查看OSR的文章 - 同步化狀態。
- 選擇加入 WDF 同步化及執行層級模型,卻未完全了解其中的含意。 請參閱 使用架構鎖定。 除非您的驅動程式是整合式最上層驅動程式直接與硬體互動,否則請避免選擇使用 WDF 同步處理,因為它可能會導致因遞歸而導致死結。
- 在多個執行緒中取得 KEVENT、Semaphore、ERESOURCE、UnsafeFastMutex,而不需要進入臨界區。 這樣做可能會導致 DOS 攻擊,因為持有其中一個鎖定的線程可以暫停。 請參閱 核心發送器對象的簡介。
- 在線程堆疊上配置KEVENT,並在EVENT仍在使用時返回呼叫端。 通常搭配 IoBuildSyncronousFsdRequest 或 IoBuildDeviceIoControlRequest使用時完成。 這些呼叫的呼叫者應該確定它們不會從堆疊中退出,直到 I/O 管理員在 IRP 完成時發出事件信號為止。
- 無限期地在分派例程中等候。 一般而言,分派例程中的任何等候都是一種不良的做法。
- 在刪除物件之前,不適當地檢查物件的有效性(如果 blah == NULL)。 這通常表示作者對控制物件存留期的程式代碼沒有完整瞭解。
物件管理
- 未明確管理 WDF 物件。 請參閱 Framework 物件簡介。
- 將WDF物件父系至WDFDRIVER,而不是將父系至提供更佳存留期管理和優化記憶體使用量的物件。 例如,將 WDFREQUEST 設為 WDFDEVICE 的父代,而非 IOTARGET。 請參閱 使用一般 Framework 物件、 Framework 物件生命週期 和 Framework 物件的摘要。
- 不執行對跨驅動程式存取的共用記憶體資源的執行流程保護。 請參閱 ExInitializeRundownProtection 函式。
- 在上一個工作專案已經在佇列中或已經執行時,錯誤地將相同的工作專案排入佇列。 如果客戶端假設每個已排入佇列的工作專案都會執行,就會發生此問題。 請參閱 使用框架工作項目。 如需佇列 WorkItems 的詳細資訊,請參閱驅動程式模組架構 (DMF) 專案中 的 DMF_QueuedWorkitem 模組 - https://github.com/Microsoft/DMF。
- 在張貼定時器預期要處理的訊息之前,佇列定時器。 請參閱 使用定時器。
- 在工作項目中執行某項操作,它可能會導致封鎖或花很長時間才能完成。
- 設計一個會導致大量工作項目排入佇列的解決方案。 如果壞人能控制操作,可能會導致系統無反應或 DOS 攻擊(例如,將 I/O 灌輸至每個 I/O 都會排入新工作項目的驅動程式)。 請參閱 使用 Framework 工作項目。
- 未確保工作專案 DPC 回呼已執行完成,就刪除物件。 請參閱 撰寫 DPC 例程 和 WdfDpcCancel 函式的指導方針。
- 建立執行緒,而不是在短期/非輪詢任務中使用工作項目。 請參閱 系統工作執行緒。
- 在刪除或卸除驅動程式之前,未確保線程已經執行完成。 如需有關線程執行同步處理的詳細資訊,請參閱驅動程式模塊架構(DMF)專案中與DMF_Thread模組相關的程式代碼。
- 使用單一驅動程式來管理不同但相互依賴的裝置,以及使用全域變數來共用資訊。
記憶體
- 盡可能不要將被動執行程式代碼標示為PAGEABLE。 分頁驅動程式程式代碼可以減少驅動程式程式代碼使用量的大小,從而釋放系統空間以供其他用途使用。 請謹慎將代碼標記為可分頁,如果它會引發 IRQL 等於或高於 DISPATCH_LEVEL,或可能在提高的 IRQL 下被呼叫。 請參閱 何時應該讓程式代碼和數據可分頁 ,並 讓驅動程式可分頁 並 偵測可分頁的程序代碼。
- 在堆疊上宣告大型結構時,應該使用堆或池來代替。 請參閱 使用 KernelStack 和 配置 System-Space 記憶體。
- 不必要地將 WDF 對象內容重設為零。 這可能表示對於記憶體何時會自動清零缺乏明確性。
一般驅動程式指導方針
- 混合 WDM 和 WDF 基本類型。 在可使用 WDF 基本類型的情況下,使用 WDM 基本類型。 使用 WDF 原語可以保護您免於意想不到的問題、改善除錯,並且更重要的是,讓您的驅動程式可以移植到使用者模式。
- 在不需要時命名 FDO 並建立符號連結。 請參閱 管理驅動程式訪問控制。
- 從範例驅動程式複製貼上和使用 GUID 和其他常數值。
- 請考慮在驅動程式專案中使用驅動程式模組架構 (DMF) 開放原始碼。 DMF 是 WDF 的延伸模組,可為 WDF 驅動程式開發人員啟用額外的功能。 請參閱 驅動程式模組架構簡介。
- 使用登錄做為進程間通知機制或信箱。 如需替代方案,請參閱 DMF 專案中可用的 DMF_NotifyUserWithEvent 和 DMF_NotifyUserWithRequest 模組 - https://github.com/Microsoft/DMF。
- 假設登錄的所有部分在系統早期開機階段都可供存取。
- 依賴另一個驅動程式或服務的載入順序。 由於載入順序可以在驅動程式的控制之外變更,這可能會導致一開始運作的驅動程式,但稍後會以無法預測的模式失敗。
- 重新建立已存在的驅動程式庫,例如 WDF 提供的針對 PnP 的功能,如「在驅動程式中支援 PnP 和電源管理」中所述,或者是總線介面中提供的庫,如 OSR 文章「使用適用於驅動程式對驅動程式通訊的總線介面」中所述。
PnP/Power
- 以不支援 PNP 的方式與另一個驅動程式互動 - 未註冊 PNP 裝置變更通知。 請參閱 註冊裝置介面變更通知。
- 建立 ACPI 節點來列舉裝置,並在其中建立電源相依性,而不是使用總線驅動程式或系統,以簡潔的方式提供軟體裝置建立介面給 PNP 和電源相依性。 請參閱 在函式驅動程式中支援 PnP 和電源管理。
- 標示裝置不可停用 - 強制在驅動程式更新時重新啟動。
- 將裝置隱藏在設備管理器中。 請參閱 從設備管理器隱藏裝置。
- 假設驅動程式只會用於裝置的一個實例。
- 你以為驅動程序永遠不會被卸除。 請參閱 PnP 驅動程式的卸除例程。
- 不處理虛假介面出現通知。 這可能會發生,且驅動程式預期會安全地處理此狀況。
- 未實施 S0 閒置電源政策,這對受DRIPS限制或其子裝置很重要。 請參閱 支援待機電源關閉。
- 未檢查 WdfDeviceStopIdle 傳回狀態會導致因 WdfDeviceStopIdle /ResumeIdle 不平衡而導致電源參考流失,最終會檢查 9F 錯誤。
- 不知道 PrepareHardware/ReleaseHardware 可能會因為資源重新平衡而被多次呼叫。 這些回呼函式應僅限於初始化硬體資源。 請參閱 EVT_WDF_DEVICE_PREPARE_HARDWARE。
- 使用 PrepareHardware/ReleaseHardware 來配置軟體資源。 如果裝置的軟體資源配置是固定的,應在 AddDevice 中完成;若資源配置需要與硬體互動,則在 SelfManagedIoInit 中進行。 請參閱 EVT_WDF_DEVICE_SELF_MANAGED_IO_INIT。
程式代碼撰寫指導方針
- 不使用安全字串和整數函式。 請參閱 使用安全字串函式 和使用 安全整數函式。
- 不使用 typedefs 來定義常數。
- 使用全域和靜態變數。 避免將每個裝置的內容儲存在全域變數中。 全域變數旨在於多個裝置實例之間共享資訊。 或者,請考慮使用 WDFDRIVER 對象內容,在多個裝置實例之間共用資訊。
- 不使用變數的描述性名稱。
- 在命名變數中不一致 - 大小寫一致性。 對現有程式代碼進行更新時,不要遵循現有的程式代碼撰寫樣式。 例如,針對不同函式中的通用結構使用不同的變數名稱。
- 未對重要的設計選擇進行評論 - 電源管理、鎖定、狀態管理、使用工作專案、DPC、計時器、全域資源使用、資源預先配置、複雜運算式/條件語句。
- 對從呼叫的 API 名稱中顯而易見的事項進行評論。 將批注設為相當於函式名稱的英文語言(例如在呼叫 WdfDeviceCreate 時撰寫批注「建立裝置物件」)。
- 請勿建立具有傳回呼叫的巨集。 請參閱函式 (C++)。
- 沒有或不完整的原始程式碼註釋(SAL)。 請參閱 適用於 Windows 驅動程式的 SAL 2.0 註釋。
- 運用巨集而非內嵌函式。
- C++ 中可使用巨集來取代 constexpr。
- 使用 C 編譯程式編譯驅動程式,而不是 C++ 編譯程式,以確保您能進行強型別檢查。
錯誤處理
- 未報告重大驅動程式錯誤,並正常標記裝置無法運作。
- 未返回能對應至有意義的 WIN32 錯誤狀態的適當 NT 錯誤狀態。 請參閱 使用 NTSTATUS 值。
- 不使用NTSTATUS 巨集來檢查系統函式的傳回狀態。
- 視需要不要判斷狀態變數或旗標。
- 檢查指標是否有效,再存取指標以因應競爭條件。
- 對 NULL 指標進行斷言。 如果您嘗試使用 NULL 指標來存取記憶體 Windows,將會檢查錯誤。 錯誤檢查的參數將提供修正 Null 指標的必要資訊。 加班時,當許多不需要的 ASSERT 語句新增至程式代碼時,它們會耗用記憶體並讓系統變慢。
- 對象內容指標上的 ASSERTING。 驅動程式架構保證物件始終會連同上下文一同配置。
追踪
- 未定義 WPP 自定義類型,並在追蹤呼叫中使用它來取得人類可讀取的追蹤訊息。 請參閱 將 WPP 軟體追蹤新增至 Windows 驅動程式。
- 不使用 IFR 追蹤。 請參閱 在 KMDF 和 UMDF 2 驅動程式中使用 Inflight Trace Recorder (IFR)。
- 在 WPP 追蹤呼叫中呼叫函式名稱。 WPP 已經追蹤函式名稱和行號。
- 不使用 ETW 事件來測量影響事件的效能和其他重要用戶體驗。 請參閱 將事件追蹤新增至 Kernel-Mode 驅動程式。
- 未在事件記錄檔中報告重大錯誤,並正常標記裝置無法運作。
驗證
- 在開發和測試期間,未同時執行標準和進階設定的驅動程序驗證器。 請參閱 驅動程式驗證器。 在進階設定中,建議啟用所有規則,但與低資源模擬相關的規則除外。 最好以隔離方式執行低資源模擬測試,以便更輕鬆地偵錯問題。
- 在驅動程式或所屬的裝置類別上未執行 DevFund 測試,因為已啟用了進階驗證設定。 請參閱 如何透過命令行執行DevFund測試。
- 未驗證驅動程式是否符合 HVCI 規範。 請參閱 實作 HVCI 相容性程式代碼。
- 在開發及測試使用者模式驅動程序期間,未在 WUDFhost.exe 上執行 AppVerifier。 請參閱 應用程式驗證器。
- 在運行時間不使用 !wdfpoolusage 調試程序擴充功能檢查記憶體使用量,以確保不會放棄 WDF 物件。 記憶體、要求和工作專案是這些問題的共同受害者。
- 不使用 !wdfkd 調試程式擴充模組來檢查物件樹,以確保物件的父物件設置正確,並檢查主要物件(如 WDFDRIVER、WDFDEVICE、IO)的屬性。