Surface Team Driver 開發最佳做法
簡介
這些驅動程式開發指導方針已由 Microsoft 的驅動程式開發人員開發多年。 隨著時間的推移,當司機錯誤和吸取教訓時,這些課程被擷取並演變成這一組指導。 Microsoft Surface 硬體小組會使用這些最佳做法來開發和維護支援獨特 Surface 硬體體驗的裝置驅動程式程式代碼。
和任何一組指導方針一樣,會有合法的例外狀況和替代方法同樣有效。 請考慮將這些指導方針納入您的開發標準,或使用它們來啟動開發環境的特定指導方針,以及您獨特的需求。
驅動程式開發人員犯的常見錯誤
處理 I/O
- 存取從 IOCTLs 擷取的緩衝區,而不驗證長度。 請參閱 無法檢查緩衝區的大小。
- 在使用者線程或隨機線程內容的內容中執行封鎖 I/O。 請參閱 核心發送器對象的簡介。
- 將同步 I/O 傳送至另一個驅動程式,而不會逾時。 請參閱 同步傳送 I/O 要求。
- 在不瞭解安全性影響的情況下,使用無ioIOCTL。 請參閱 不使用緩衝處理或直接 I/O。
- 未檢查 WdfRequestForwardToIoQueue 的傳回狀態,或未正確處理失敗,並導致放棄的 WDFREQUEST。
- 將 WDFREQUEST 保留在佇列外部處於無法取消的狀態。 請參閱 管理 I/O 佇列、 完成 I/O 要求 和 取消 I/O 要求。
- 嘗試使用 Mark/UnmarkCancelable 函式來管理取消,而不是使用 IoQueues。 請參閱 Framework Queue 物件。
- 不知道檔句柄清除和關閉作業之間的差異。 請參閱 處理清除和關閉作業中的錯誤。
- 忽略 I/O 完成和從完成例程重新提交的潛在遞歸。
- 未明確說明 WDFQUEUE 的電源管理屬性。 未清楚地記錄電源管理選擇。 這是錯誤檢查0x9F的主要原因 :WDF 驅動程式中的DRIVER_POWER_STATE_FAILURE 。 拿掉裝置時,架構會在移除程式的不同階段,從電源受控佇列和非電源受控佇列清除 IO。 收到最終IRP_MN_REMOVE_DEVICE時,會清除非電源受控佇列。 因此,如果您在非電源受控佇列中保存 I/O,最好在 EvtDeviceSelfManagedIoFlush 的內容中明確清除 I/O,以避免死結。
- 不遵循處理 IRP 的規則。 請參閱 處理清除和關閉作業中的錯誤。
同步處理
- 針對不需要保護的程式代碼保留鎖定。 當只有少數作業需要保護時,請勿保留整個函式的鎖定。
- 叫出鎖定的驅動程式。 這是死結的主要原因。
- 使用聯結基本類型來建立鎖定配置,而不是使用適當的系統提供鎖定基本類型,例如 mutex、旗號和微調鎖定。 請參閱 Mutex 物件簡介、旗號物件和微調鎖定簡介。
- 使用同步鎖定,其中某些類型的被動鎖定會更合適。 請參閱 Fast Mutexes和 Guarded Mutexes 和 事件物件。 如需鎖定的其他觀點,請檢閱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 函式。
- 在上一個工作專案已經在佇列中或已經執行時,錯誤地將相同的工作專案排入佇列。 如果客戶端假設每個已排入佇列的工作專案都會執行,就會發生此問題。 請參閱 使用 Framework WorkItems。 如需佇列 WorkItems 的詳細資訊,請參閱 驅動程式模組架構 (DMF) 專案中的 DMF_QueuedWorkitem 模組 - https://github.com/Microsoft/DMF。
- 在張貼定時器預期要處理的訊息之前,佇列定時器。 請參閱 使用定時器。
- 在工作專案中執行作業,可以封鎖或無限期地完成。
- 設計會導致大量工作專案排入佇列的解決方案。 如果壞人可以控制動作,可能會導致沒有回應的系統或 DOS 攻擊(例如,將 I/O 抽入至排入每個 I/O 的新工作專案的驅動程式)。 請參閱 使用 Framework 工作專案。
- 刪除物件之前,不會再執行工作專案 DPC 回呼以完成。 請參閱撰寫 DPC 例程和 WdfDpcCancel 函式的指導方針。
- 建立線程,而不是在短期/非輪詢工作中使用工作專案。 請參閱 系統背景工作線程。
- 在刪除或卸除驅動程式之前,不保證線程已執行到完成。 如需線程執行同步處理的詳細資訊,請參閱驅動程式模塊架構 (DMF) 專案中與 DMF_Thread 模組相關聯的程式代碼 - https://github.com/Microsoft/DMF。
- 使用單一驅動程式來管理不同但相互依賴的裝置,以及使用全域變數來共用資訊。
記憶體
- 盡可能不要將被動執行程式代碼標示為PAGEABLE。 分頁驅動程式程式代碼可以減少驅動程式程式代碼使用量的大小,從而釋放系統空間以供其他用途使用。 請謹慎標記可設定代碼頁,以引發 IRQL >= DISPATCH_LEVEL,或在引發 IRQL 時呼叫。 請參閱 何時應該讓程式代碼和數據可 分頁,並 讓驅動程式可分頁 並 偵測可分頁的程序代碼。
- 在堆疊上宣告大型結構,應該使用堆積/poolinstead。 請參閱 使用 KernelStack 和 配置系統空間記憶體。
- 不必要的零 WDF 對象內容。 這表示記憶體何時會自動清零時,表示缺乏明確性。
一般驅動程式指導方針
- 混合 WDM 和 WDF 基本類型。 使用可使用 WDF 基本類型的 WDM 基本類型。 使用 WDF 基本類型可保護您免於 gotchas、改善偵錯,更重要的是讓您的驅動程式可移植到 usermode。
- 視需要命名 FDO 並建立符號連結。 請參閱 管理驅動程式訪問控制。
- 從範例驅動程式複製貼上和使用 GUID 和其他常數值。
- 請考慮在驅動程式專案中使用驅動程式模組架構 (DMF) 開放原始碼 程式代碼。 DMF 是 WDF 的延伸模組,可為 WDF 驅動程式開發人員啟用額外的功能。 請參閱 驅動程式模組架構簡介。
- 使用登錄做為進程間通知機制或信箱。 如需替代方案,請參閱 DMF 專案中可用的DMF_NotifyUserWithEvent 和 DMF_NotifyUserWithRequest 模組 - https://github.com/Microsoft/DMF。
- 假設登錄的所有部分在系統早期開機階段都可供存取。
- 相依於另一個驅動程式或服務的載入順序。 由於載入順序可以在驅動程式的控制之外變更,這可能會導致一開始運作的驅動程式,但稍後會以無法預測的模式失敗。
- 重新建立已可用的驅動程序連結庫,例如 WDF 提供 PnP,如支援驅動程式中的 PnP 和電源管理中所述,或在總線介面中提供的連結庫,如使用適用於驅動程式對驅動程式通訊的總線介面一文中所述。
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 指標的 ASSERTING。 如果您嘗試使用 NULL 指標來存取記憶體 Windows,將會檢查錯誤。 錯誤檢查的參數將提供修正 Null 指標的必要資訊。 加班時,當許多不需要的 ASSERT 語句新增至程式代碼時,它們會耗用記憶體並讓系統變慢。
- 對象內容指標上的 ASSERTING。 驅動程式架構保證物件一律會以內容配置。
追蹤
- 未定義 WPP 自定義類型,並在追蹤呼叫中使用它來取得人類可讀取的追蹤訊息。 請參閱 將 WPP 軟體追蹤新增至 Windows 驅動程式。
- 不使用 IFR 追蹤。 請參閱 在 KMDF 和 UMDF 2 驅動程式中使用 Inflight Trace Recorder (IFR)。
- 在 WPP 追蹤呼叫中呼叫函式名稱。 WPP 已經追蹤函式名稱和行號。
- 不使用 ETW 事件來測量影響事件的效能和其他重要用戶體驗。 請參閱 將事件追蹤新增至內核模式驅動程式。
- 未在事件記錄檔中報告重大錯誤,並正常標記裝置無法運作。
驗證
- 在開發和測試期間,未同時執行標準和進階設定的驅動程序驗證器。 請參閱 驅動程式驗證器。 在進階設定中,建議啟用所有規則,但與低資源模擬相關的規則除外。 最好以隔離方式執行低資源模擬測試,以便更輕鬆地偵錯問題。
- 在驅動程式或驅動程序類別上未執行 DevFund 測試,驅動程式是啟用進階驗證程式設定的一部分。 請參閱 如何透過命令行執行DevFund測試。
- 未驗證驅動程式是否符合 HVCI 規範。 請參閱 實作 HVCI 相容性程式代碼。
- 在開發及測試使用者模式驅動程序期間,未在WUDFhost.exe上執行 AppVerifier。 請參閱 應用程式驗證器。
- 在運行時間不使用 !wdfpoolusage 調試程序擴充功能檢查記憶體使用量,以確保不會放棄 WDF 物件。 記憶體、要求和工作專案是這些問題的共同受害者。
- 不使用 !wdfkd 調試程式延伸模組來檢查物件樹狀結構,以確保對象已正確父代,並檢查主要對象的屬性,例如 WDFDRIVER、WDFDEVICE、IO。