Surface Team ドライバー開発ベスト プラクティス
はじめに
これらのドライバー開発ガイドラインは、Microsoft のドライバー開発者が長年かけて作成したものです。 時間が経つうちに、ドライバーの不適切な行動や学んだ事柄が積み重なり、このガイダンスへと発展していったのです。 これらのベスト プラクティスは、Microsoft Surface ハードウェア チームが独自の Surface ハードウェア エクスペリエンスをサポートするデバイス ドライバー コードの開発と管理に使用しています。
どのガイドラインにも言えることですが、正当な例外や同じように有効な代替アプローチが存在します。 これらのガイドラインを開発標準に組み込むか、またはそれらを使用して、開発環境や独自の要件に対応したドメイン固有のガイドラインを作成することを検討してください。
ドライバー開発者が陥りやすい一般的な間違い
I/O の処理
- 長さを検証せずに IOCTL から取得されたバッファーにアクセスする。 バッファーのサイズのチェックの失敗を参照してください。
- ユーザー スレッドまたはランダム スレッド コンテキストのコンテキストでブロッキング I/O を実行する。 「カーネル ディスパッチャー オブジェクト の概要」を参照してください。
- タイムアウトなしで別のドライバーに同期 I/O を送信します。 「I/O 要求を同期的に送信する」を参照してください。
- セキュリティへの影響を理解せずにどちらの IOCTL も使用する。 「バッファー付き I/O もダイレクト I/O も使用しない」を参照してください 。
- WdfRequestForwardToIoQueue のリターン状態をチェックしていないか、失敗を正しく処理していないため、WDFREQUEST が破棄されます。
- キューの外部にある WDFREQUEST をキャンセル不可能な状態に保ちます。 「I/O キューの管理」、「I/O 要求の完了」、および「 I/O 要求のキャンセル」を参照してください。
- IoQueues を使用するのではなく、Mark/UnmarkCancelable 関数を使用して取り消しを管理しようとしています。 「フレームワーク キュー オブジェクト」を参照してください 。
- ファイル ハンドルのクリーンアップ操作と閉じる操作の違いが分かりません。 「クリーンアップ操作と閉じる操作の処理のエラー」を参照してください 。
- I/O 完了を伴う潜在的な再帰を見落とし、完了ルーチンから再送信する。
- WDFQUEUES の電源管理属性について明示されていません。 電源管理の選択を明確に文書化していません。 これは、WDF ドライバーでのバグ チェック: 0x9F DRIVER_POWER_STATE_FAILURE の主な原因です。 デバイスが削除されると、フレームワークは、削除プロセスのさまざまな段階で、電源管理キューと非電源管理キューから IO を消去します。 非電源管理キューは、最後の IRP_MN_REMOVE_DEVICE を受信すると消去されます。 そのため、非電源管理キューで I/O を保持している場合は、デッドロックを回避するために、EvtDeviceSelfManagedIoFlush のコンテキストで I/O を明示的に消去することをお勧めします。
- IRP の取り扱いルールに従っていません。 「クリーンアップ操作と閉じる操作の処理のエラー」を参照してください 。
Synchronization
- 保護を必要としないコードのロックを保持します。 少数の操作のみを保護する必要がある場合は、関数全体のロックを保持しないでください。
- ロックが保持されているドライバーの呼び出し。 これがデッドロックの主な原因です。
- ミューテックス、セマフォ、スピンロックなどの適切なシステム提供のロック プリミティブを使用する代わりに、インターロック プリミティブを使用してロック スキームを作成します。 「ミューテックス オブジェクトの概要」、「セマフォ オブジェクト」、および「スピン ロックの概要」を参照してください。
- 何らかの種類のパッシブ ロックの方が適切な場合は、スピンロックを使用します。 「高速ミューテックスと保護されたミューテックス」と「イベント オブジェクト」を参照してください。 ロックに関するその他の観点については、OSR に関する記事 「同期の状態」を参照してください。
- 影響を完全に理解することなく、WDF 同期と実行レベル モデルを選択します。 「フレームワーク ロックの使用」を参照してください 。 ドライバーがモノリシック最上位ドライバーであり、そしてハードウェアと直接やり取りしている場合を除き、WDF 同期を選択することは避けてください。これは、再帰によるデッドロックにつながる可能性があるためです。
- クリティカル領域に入ることなく、複数のスレッドのコンテキストで KEVENT、セマフォ、ERESOURCE、UnsafeFastMutex を取得します。 この操作を行うと、これらのロックのいずれかを保持しているスレッドが中断される可能性があるため、DOS 攻撃につながる場合があります。 「カーネル ディスパッチャー オブジェクト の概要」を参照してください。
- スレッド スタックに KEVENT を割り当て、EVENT がまだ使用されている間に呼び出し元に戻ります。 通常、IoBuildSyncronousFsdRequest または IoBuildDeviceIoControlRequest と共に使用する場合に実行されます。 これらの呼び出しの呼び出し元は、IRP が完了したときに I/O マネージャーが イベントを通知するまで、スタックからアンワインドしないようにする必要があります。
- ディスパッチ ルーチンで無期限に待機しています。 一般的に、ディスパッチ ルーチンで待機するのは良くない方法です。
- オブジェクトを削除する前に、オブジェクトの有効性を不適切な方法でチェックします (blah == NULL の場合)。 この場合は、通常、作成者がオブジェクトの有効期間を制御するコードを完全に理解していないことを意味します。
オブジェクト管理
- WDF オブジェクトを明示的に親にするのではありません。 「フレームワーク オブジェクトの概要」を参照してください。
- WDF オブジェクトをオブジェクトにペアレンティングする代わりに、 WDFDRIVER にペアレンティングすることで、より優れた有効期間管理を提供し、メモリ使用量が最適化されます。 たとえば、WDFREQUEST を IOTARGET の代わりに WDFDEVICE にペアレンティングします。 「一般的なフレームワーク オブジェクトの使用」、「フレームワーク オブジェクトのライフ サイクル」、および「フレームワーク オブジェクトの概要」を参照してください。
- ドライバー間でアクセスされる共有メモリ リソースのランダウン保護を行っていません。 「ExInitializeRundownProtection 関数」を参照してください。
- 前の作業項目が既にキューに入っているか、既に実行されている間に、同じ作業項目を誤ってキューに入れます。 これは、クライアントがキューに登録されているすべての作業項目が実行されると想定している場合に問題となります。 「フレームワーク 作業項目の使用」を参照してください。 作業項目のキューイングの詳細については、Driver Module Framework (DMF) プロジェクトの DMF_QueuedWorkitem モジュールを参照してください。https://github.com/Microsoft/DMF
- メッセージを投稿する前にタイマーをキューに入れ、タイマーが処理すると予想されます。 「タイマーの使用」を参照してください。
- 完了するまでに無期限にブロックされたり、または時間がかかる可能性がある作業項目で操作を実行する。
- 大量の作業項目がキューに登録されるソリューションを設計する。 悪人がアクションを制御できる場合は、応答しないシステムや DOS 攻撃につながる可能性があります (たとえば、すべての I/O の新しい作業項目をキューに入れるドライバーに I/O を送り込むなど)。 「フレームワーク 作業項目の使用」を参照してください。
- オブジェクトを削除する前、作業項目 DPC コールバックが完了するまで実行されません。 「DPC ルーチンの記述に関するガイドライン」および 「WdfDpcCancel 関数」を参照してください。
- 短時間またはポーリング以外のタスクで作業項目を使用する代わりにスレッドを作成します。 「システム ワーカー スレッド」を参照してください。
- ドライバーを削除またはアンロードする前に、スレッドが完了するまで実行されたことを確認していません。 スレッドランダウン同期の詳細については、Driver Module Framework (DMF) プロジェクトの DMF_Thread モジュールに関連付けられているコードを参照してください。https://github.com/Microsoft/DMF
- 単一のドライバーを使用して、異なるが相互に依存しているデバイスを管理し、グローバル変数を使用して情報を共有します。
メモリ
- 可能な場合は、パッシブ実行コードを PAGEABLE としてマークしません。 ドライバー コードをページングすると、ドライバーのコード フットプリントのサイズが小さくなるため、システム領域を他の用途に解放することができます。 IRQL> = DISPATCH_LEVEL を発生させる、または発生した IRQL で呼び出すことができるページング可能なコードにマークすることには注意が必要です。 「コードとデータをページング可能にするタイミング」、「ドライバーをページング可能にする」、「ページング可能なコードを検出する」を参照してください。
- スタックで大きな構造体を宣言する場合は、代わりにヒープ/プールを使用する必要があります。 「KernelStack の使用」と「システム空間メモリの割り当て」を参照してください。
- WDF オブジェクト コンテキストを不必要にゼロにします。 これは、メモリが自動的にゼロになるタイミングが明確でないことを示しています。
ドライバーの一般的なガイドライン
- WDM プリミティブと WDF プリミティブの混在。 WDF プリミティブを使用できる場所で WDM プリミティブを使用。 WDF プリミティブを使用すると、問題からユーザーを保護し、デバッグを改善し、さらに重要なのは、ドライバーを usermode に移植できるようにします。
- 必要でない場合は、FDO に名前を付け、シンボリック リンクを作成します。 「ドライバーのアクセス制御の管理」を参照してください。
- サンプル ドライバーから GUID やその他の定数値をコピーして貼り付け、使用します。
- ドライバー プロジェクトで Driver Module Framework (DMF) オープンソース コードを使用することを検討してください。 DMFは、WDF ドライバー開発者の追加機能を有効にする WDF の拡張機能です。 「Driver Module Framework の概要」を参照してください。
- プロセス間通知メカニズムまたはメールボックスとしてレジストリを使用する。 別の方法については、DMF プロジェクトで使用可能な DMF_NotifyUserWithEvent モジュールと DMF_NotifyUserWithRequest モジュールを参照してください。https://github.com/Microsoft/DMF
- システムの初期ブート フェーズ中に、レジストリのすべての部分がアクセス可能になると仮定します。
- 別のドライバーまたはサービスの読み込み順序に依存しています。 読み込み順序はドライバーの制御外で変更できるため、ドライバーが最初に動作する可能性がありますが、後で予期しないパターンで失敗することがあります。
- 既に使用可能なドライバー ライブラリの再作成します。たとえば、WDF が PnP のために提供するものは、ドライバーでの PnP と電源管理のサポートで説明されていますし、 または OSR のドライバー間通信にバス インターフェイスを使用する で説明されているように、バス インターフェイスで提供されているものもあります。
PnP/Power
- pnp 以外のフレンドリな方法で別のドライバーとやり取りする - pnp デバイス変更通知に登録しない。 「デバイス インターフェイス変更の通知登録」を参照してください。
- ACPI ノードを作成してデバイスを列挙し、バス ドライバーやシステム提供のソフトウェアデバイス作成インタフェースを使用する代わりに、デバイス間の電源依存関係を作成して、PNP と電源依存関係をエレガントな方法で実現します。 「ファンクション ドライバーでの PnP と電源管理のサポート」を参照してください 。
- デバイスを無効にできないというマークを付ける - ドライバーの更新時に強制的に再起動します。
- デバイス マネージャーでデバイスを非表示にします。 「デバイス マネージャーからのデバイスの非表示」を参照してください。
- ドライバーがデバイスの 1 つのインスタンスにのみ使用されることを前提としています。
- ドライバーがアンロードされることは決してないことを前提にしています。 「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」を参照してください。
コーディングのガイドライン
- 安全な文字列関数と整数関数を使用していません。 「セーフ文字列関数の使用」と「セーフ整数関数の使用」を参照してください。
- 定数を定義するために typedef を使用しません。
- グローバル変数と静的変数の使用。 デバイス コンテキストごとにグローバルに格納しないようにします。 グローバルは、デバイスの複数のインスタンス間で情報を共有するためのものです。 別の方法として、デバイスの複数のインスタンス間で情報を共有するために WDFDRIVER オブジェクト コンテキストを使用することを検討してください。
- 変数にわかりやすい名前を使用しません。
- 変数名に一貫性がない - 大文字と小文字の整合性。 既存のコードを更新するときに、既存のスタイルのコーディングに従わない。 たとえば、異なる関数の共通構造に異なる変数名を使用します。
- 電源管理、ロック、状態管理、作業項目の使用、DPC、タイマー、グローバル リソースの使用状況、リソースの事前割り当て、複雑な式/条件ステートメントなど、重要な設計上の選択肢をコメントしません。
- 呼び出される API の名前から明らかなことについてのコメント。 コメントを関数名と同じような英語表記にします (WdfDeviceCreate を呼び出すときに"Create the Device Object" というコメントを書くなど)。
- リターン呼び出しがあるマクロは作成しないでください。 「Functions (C++)」を参照してください。
- ソース コード注釈 (SAL) が存在しないか、または不完全です。 「Windows ドライバーの SAL 2.0 注釈」を参照してください。
- インライン関数の代わりにマクロを使用します。
- C++ の使用時に constexpr の代わりに定数のマクロを使用する
- 厳密な型のチェックを行うために、C++ コンパイラではなく C コンパイラを使用してドライバーをコンパイルします。
エラー処理
- 致命的なドライバー エラーを報告せず、デバイスが正常に機能していないとマークします。
- 意味のある WIN32 エラー状態に変換される適切な NT エラー状態が返されません。 「NTSTATUS値 の使用」を参照してください。
- システム関数の返された状態をチェックするために、NTSTATUS マクロを使用しません。
- 必要に応じて、状態変数またはフラグをアサートしません。
- 競合状態を回避するために、ポインターにアクセスする前にポインターが有効かどうかを確認します。
- NULL ポインターに対するアサート。 NULL ポインターを使用してメモリにアクセスしようとすると、Windows はバグチェックを行います。 バグ チェックのパラメーターは、null ポインターを修正するために必要な情報を提供します。 時間をかけすぎた場合、不要なアサート ステートメントが多数コードに追加されると、メモリが消費され、システムの速度を低下させます。
- オブジェクト コンテキスト ポインターの アサート。 ドライバー フレームワークは、オブジェクトが常にコンテキストと共に割り当てられることを保証します。
トレース
- WPP カスタム型を定義せず、トレース呼び出しで使用して人間が判読できるトレース メッセージを取得します。 「Windows ドライバーへの WPP ソフトウェア トレースの追加」を参照してください。
- IFR トレースを使用していません。 「KMDF ドライバーと UMDF 2 ドライバー でのインフライト トレース レコーダー (IFR) の使用」を参照してください。
- WPP トレース呼び出しで関数名を呼び出す。 WPP では、関数名と行番号が既に追跡されています。
- イベントに影響を与えるパフォーマンスやその他の重要なユーザー エクスペリエンスを測定するために ETW イベントを使用しない。 「カーネル モード ドライバーにイベント トレーシングを追加する」を参照してください。
- イベントログで致命的なエラーを報告せず、デバイスが正常に機能していないとマークします。
検証
- 開発およびテスト中に、標準設定と詳細設定の両方でドライバー検証ツールを実行していません。 「ドライバーの検証ツール」を参照してください。 詳細設定では、リソースの少ないシミュレーションに関連するルールを除き、すべてのルールを有効にすることをお勧めします。 問題のデバッグを容易にするために、リソースの少ないシミュレーション テストを単独で実行することをお勧めします。
- 高度な検証設定を有効にして、ドライバーまたはドライバーが含まれるデバイス クラスで、DevFund テストを実行していません 。 「コマンド ライン から DevFund テストを実行する方法」を参照してください。
- ドライバーが HVCI に準拠していることを確認していません。 「HVCI 互換性コードの実装」を参照してください。
- ユーザー モード ドライバーの開発とテスト中に、WUDFhost.exe で AppVerifier を実行していません。 「アプリケーション検証ツール」を参照してください。
- オブジェクトが破棄されていないことを確認するために、実行時に !wdfpoolusage デバッガー拡張機能を使用してメモリの使用量をチェックしていません。 メモリ、要求、および作業項目は、これらの問題の一般的な被害者です。
- !wdfkd デバッガー拡張機能を使用してオブジェクト ツリーを検査し、オブジェクトが正しく親となっているかを確認し、WDFDRIVER、WDFDEVICE、IO などの主要オブジェクトの属性をチェックしていません。