C++、Rust、および Windows Kernel: 安全性が役立ち、境界が依然として厳しい場所
導入
Windows カーネルは、ホワイトボードで有罪判決を受けた人々が現実の借りを発見するために行く場所です。通常のアプリケーション作業では、チームは、問題が発生した理由についてあいまいな説明ができる場合があります。カーネル作業では、あいまいな説明がバグ チェック、ブルー スクリーン、怒っているオペレーター、デバッグ セッションに発展する傾向があり、あたかもマシンが自分の生い立ちに個人的に失望しているかのように感じさせます。
だからこそ、Windows カーネルに関する C++ と Rust の会話が重要なのです。一方がノスタルジックでもう一方が啓蒙されているからではなく、低レベルの Windows 作業では、IOCTL 境界、IRQL ルール、DMA 仮定、同期、生涯規律、およびたとえアーキテクチャ デッキが大人として振る舞わなかったとしても、大人のように振る舞うことを依然として期待しているツールとの接触を生き延びるために、すべてのクレームが生き残ることを強制されているからです。
Rust はここでその勢いに値します。メモリの安全性は偽物ではありません。明確な所有権は偽物ではありません。より明確な破壊面は偽物ではありません。特権に近いシステム コードを記述していて、その言語が作りやすいバグのカテゴリ全体を削除できる場合、それは表面的なものではありません。これはエンジニアリング上の重大な利点であり、C チームと C++ チームは歴史的に、規律、レビュー、そしてある程度の軽い被害妄想を伴って再現しなければならなかった利点です。
しかし、Windows カーネルは善意に賞品を与えるわけではありません。 WDK の制約、C ベースのインターフェイス、レガシー ドライバー、既存のコードベース、WinDbg ワークフロー、カーネル オブジェクトの有効期間ルール、DMA と同期の現実、そして運用環境で何かが失敗したときにデバッグとロールアウトのチェーン全体が理解可能なままであるかどうかという非常に重要な問題など、実際に存在するエコシステム内で運用できるチームに報酬が与えられます。
最後の部分では、多くのファッショナブルな会話が奇妙に静かになります。カーネルの作業は、コンパイルされるコードを書くことだけではありません。それは、顧客のマシン上で、エンタープライズ イメージ内で、敵対的なサードパーティ ソフトウェアのそばで、またはチームの誰も真夜中にデバッグしたくない更新シーケンスの後でドライバーが誤動作したときに診断できるドライバーを用意することです。ここでは、言語の意味論と同じくらい配信の現実性が重要です。
したがって、有益な質問は「Rust または C++?」ではありません。有益な質問は次のとおりです。Rust はどこで真の利点を生み出し、C++ は実質的なデフォルトのままであり、システムが単なる自己満足ではなくより安全になるように境界をどのように設計すればよいでしょうか?
Windows Kernel が汎用システム プレイグラウンドではない理由
システム プログラミングについて、あたかもすべての下位レベルのドメインが 1 つの感情的環境を共有しているかのように話す人がよくいます。それは真実ではありません。 Windows カーネルは、単なる「低レベルのコード」ではありません。これは厳格な契約があり、契約の誤解を発見するのに非常に費用がかかる運用環境です。
IRQLは存在します。ディスパッチパスが存在します。ページングの制約が存在します。デバイススタックが存在します。 IOCTL 契約が存在します。同期ミスは理論的には長くは残りません。クリーンアップ ロジックが不十分であると、単に厄介なプロセス終了が発生するだけではありません。状態を破壊したり、デバイス パスを妨害したり、他の人が機能し続けることを望んでいたマシンをクラッシュさせたりする可能性があります。
これは、カーネルが 2 つの相反する幻想を罰することを意味します。最初の幻想は、すべての低レベルの作業は永遠に C または C++ に留まるべきだということです。なぜなら、世界は常にそのように配線されているからです。 2 番目の幻想は、Rust を使用すると、作業が自動的に道徳的勝利に変わるというものです。どちらも、実際の設計上の問題を回避するための怠惰な方法です。
本当の問題は、最も危険な境界が小さく、測定可能でデバッグ可能で、明確に所有されるようにシステムを形作ることです。場合によっては、ドライバー モデル、既存のコード、ツール、チームの経験がすべてそこを指しているため、C++ が依然として実用的に最適であることを意味します。場合によっては、Rust コンポーネントが真にリスクを軽減し、明確性を高めることを意味します。ほとんどの場合、それは答えが混在していることを意味し、混在した答えに抵抗がないのは大人だけです。
カーネル作業も組織の弱点を増幅させます。チームが不変条件を文書化していない場合、レビューが弱い場合、デバッグの知識が 1 人の人の頭の中に残っている場合、またはリリースの衛生管理がオプションの事務処理として扱われている場合、低レベルのコードはその混乱を急速に拡大します。この言語では、混沌としたエンジニアリング文化からチームを完全に守ることはできません。それは役に立ちます。規律に代わることはできません。
Rust が Windows の低レベルの作業に実際に役立つ場所
Rust は、混乱する権利のないコードから混乱を取り除くときに最も役立ちます。境界解析、ステートマシンの健全性、明示的な所有権、より明確なクリーンアップパターン、そして何がエイリアスになるか、何がより長く存続するかをめぐる厳格な規律はすべて意味のある勝利です。カーネル隣接システムやドライバー負荷の高いシステムでは、低レベルのバグが詩的なものになることはほとんどないため、これは重要です。それらは反復的で、構造的によく知られており、エンジニアリングチームがロマンスの一部であるかのように何十年も費やしてきた点で屈辱的です。
Rust は、インターフェースを明示的に保つことができる境界付きコンポーネントで特に魅力的です。ユーティリティ レイヤー、ヘルパー モジュール、明確に定義されたパーサー、ドライバー用の一部のユーザー モード コンパニオン、内部ツール、および慎重に分離されたカーネルまたはドライバー ロジックの部分は、周囲のエンジニアリング ストーリーがそれらをサポートするのに十分に成熟している場合、言語の制約から恩恵を受けることができます。
文化的にも役立ちます。 Rust を Windows システム環境に導入するチームは、ライフタイム、エイリアシング、クリーンアップ、境界が約束するものについて、より健全な会話を行うことがよくあります。これは、最終的なアーキテクチャがハイブリッドのままの場合でも役立ちます。言語はディスカッションを形成し、より良いディスカッションがすでに実質的な進歩となっている場合もあります。
もう 1 つの実用的な利点があります。Rust により、範囲が限定されたコンポーネントの引き継ぎが容易になります。所有権モデルが可視化され、インターフェイスがより狭い場合、将来のエンジニアは、爆発を避けるためだけに部族の伝承を吸収する必要がなく、コードを変更できる可能性が高くなります。カーネルに隣接した作業では、そのような保守性は学術的なものではありません。これは、チームがハード システムを長期にわたって健全に保つ方法です。
しかし、これはいずれも、Windows カーネルが現在、チームが神学的熱意によって書き換えるべき遊び場であることを意味するものではありません。限定された勝利は依然として限定された勝利です。その違いは、真剣なエンジニアリングがいかにして非常に高価なライフスタイルブランドになることを回避しているかということです。
C++ が依然として現実の地位を維持している場所
C++ は、頑固に実用的であるという理由から、Windows カーネルとドライバーの動作において強力なままです。 C と C++ を中心に構築された既存のドライバー コード、サンプル、パターン、デバッグの知識、ベンダー統合履歴の膨大な量があります。この分野で作業するチームが何もない土地からスタートすることはほとんどありません。コードが半分 C で感情的には 100% 負債である場合でも、ドライバー、フィルター チェーン、デバイス コントラクト、ユーザー モード クライアント、レガシー C++、ビルド前提条件、および既に C++ 形成されている運用習慣を継承しています。
ツールの話も重要です。 WinDbg、WDK サンプル、KMDF および WDM の習慣、ドライバー検証ワークフロー、シンボル解釈、クラッシュダンプ調査、および Windows カーネル作業に関する広範なデバッグ文化はすべて、依然として既存のネイティブ世界に深く根付いています。チームがプレッシャーにさらされているとき、診断の成熟度は飾り的なものではありません。そうすることで、この研究が公の場で行われる考古学の季節になるのを避けることができる。
統合の問題もあります。ドライバーは、多くの場合、古いコード、ユーザーモード ヘルパー、既存のインストーラー ロジック、ベンダー SDKs、または C および C++ の前提条件にすでにバインドされているセキュリティ ツールの横に存在します。 C++ は、抽象的には自動的に優れているわけではありません。多くの場合、周囲のシステムがすでにそれを話すように訓練されているため、すぐには改善されます。
それによって Rust が無効になるわけではありません。それは単に、コンポーネントがどこに位置するかによって立証責任が変わることを意味します。新しい分離されたモジュールは 1 つの引数です。もう 1 つは、長年にわたるネイティブの想定を経てスレッド化されたドライバー スタックです。真剣なチームは、同じ状況であるかのように振る舞うことをやめます。
もう一つの要因は、運用の信頼性です。多くの Windows ドライバー チームは、C++ ファーストの世界でクラッシュ ダンプを読み取り、シンボルを検証し、障害を再現し、ホットフィックスを配布する方法をすでに知っています。この機能的な筋肉は魅力的ではありませんが、交換するには高価です。インシデント対応を弱体化させながらコードの優雅さを向上させる移行は、最終的な勝利とは言えません。
危険な表面は消えません。動きます。
これが会話全体の中で最も重要なポイントです。コードベースの一部が Rust で記述されているため、安全でない動作は消えません。移転するのです。それは、FFI エッジ、バッファー境界、同期シーム、割り当てパス、デバイス コントラクト、およびオペレーティング モデルが単一言語の機能ではなく Windows 自体によって依然として定義されている場所に集まります。
だからこそ、チームが言語を祝うのが早すぎたり、境界線を祝ったりするのが遅すぎると、悪い取引をする可能性があります。 Rust モジュールがレガシー ドライバー コードにずさんに混入すると、依然として古い混乱が引き継がれ、さらに新しい統合税が課せられる可能性があります。危険な表面を分離し、その不変条件を文書化し、IOCTL セマンティクスを退屈に保ち、深くテスト可能な状態を維持する C++ ドライバーは、その長所を非常に自信を持って語りながら境界を広げたよりファッショナブルなアーキテクチャよりも、まったくの驚きを生み出すことが少ないかもしれません。
したがって、大人向けのデザインの質問はより小さく、よりシャープになります。どのモジュールを分離できますか?どのインターフェースが安定した状態を維持できるでしょうか?言語の違いが実行時の混乱にならないように十分に明確に記述できる所有権ルールはどれですか?システムがすでに起動していて、誰も哲学的なニュアンスを理解する気がない場合でも、どのデバッグ パスが機能するでしょうか?
これらの質問は Rust に反対するものではありません。彼らは生存推進派だ。
彼らは協力推進派でもあります。ドライバの境界が十分に文書化されているため、複数のエンジニアが推論できるため、英雄的税金が軽減されます。コードレビューが強化されます。これにより、監査があまり演劇的ではなくなります。これにより、将来の修正はメモリへの依存度が低くなり、明示的なエンジニアリングの真実に依存するようになります。これはどのシステムでも重要ですが、特権のあるソフトウェアでは特に重要です。
見た目の良さ
優れた Windows カーネル エンジニアリングは、英雄的とは思えません。穏やかな響きですね。
危険な道は既知です。 IOCTL 契約は明示的です。同時実行の話は良い意味で退屈です。所有権の仮定は文書化されます。クラッシュダンプ解析が可能です。展開計画は大胆なものではありません。ドライバーの境界は非常に狭いため、元の実装チーム以外の誰かでも、何を変更しても安全で、何を放っておいても安全なのかを理解できます。
Rust が使用されている場合、その理由は明らかです。スライドには「未来」と強い文字で書かれているので、そこには存在しないはずだ。定義されたコンポーネントは言語の制約から真に恩恵を受け、チームはその選択から生じるデバッグ、ビルド、運用ストーリーをサポートできるため、このコンポーネントが存在する必要があります。
C++ がクリティカル パスに残っている場合、それを運命として擁護すべきではありません。この問題は、ツールの成熟度、統合コスト、ドライバーの制約、チームの経験、コンポーネントを移動した場合に実際に不安定性が生じる場所についての測定されたビューなどの証拠によって擁護される必要があります。 C++ は、システムが疲れすぎて議論できないからではなく、議席を獲得したという理由で設計に含めるべきです。
最強のカーネル チームは、技術的な平穏がデリバリー ヘルスの一部であることも知っています。コード、ツール、契約書が読み取れるようになると、アドレナリンに頼って作業が止まります。これにより、解釈パニック下で行われる変更が少なくなるため、長期的にはシステムがより安全になります。
最初に解決する価値のある実際的なケース
IOCTL境界のクリーンアップ
ドライバーを多用する多くのシステムは、その最も巧妙なコードによって危険にさらされることはなく、ずさんな契約境界によって危険にさらされることはありません。 IOCTL の処理、検証、構造のバージョン管理、およびユーザーからカーネルへの前提条件をクリーンアップすると、多くの場合、野心的な書き換えよりも早く、より安全な結果が得られます。
それは、IOCTL の間違いは単独の間違いではないからです。これらは、ユーザー モードとカーネル モードの間の信頼関係全体を汚染します。境界があいまいな場合、下流でのあらゆる決定を推論することが難しくなります。
ナロードライバーコア硬化
通常、明示的な不変条件とより優れた所有権規律を備えた小さなドライバー コアは、大規模な理論上の移行よりも価値があります。 C++ ではその強化が発生することがあります。場合によっては、将来の Rust コンポーネントが可能になります。いずれにせよ、その見返りは本物です。
測定も可能です。クラッシュ シグネチャがよりクリーンになります。レビューが容易になります。確認の会話が短くなります。このような観点から進歩が実証できれば、チームは感情的な終結のためだけに劇的な書き換えを追い求める誘惑が少なくなります。
ユーザーモードコンパニオンとツール
これは、Rust がドラマなしで輝くことが多い場所です。システムの準備が整う前に、最も脆弱なカーネル パスを新しい統合宗教に引きずり込むことなく、診断ツール、再生ユーティリティ、構成バリデータ、キャプチャ アナライザー、または制御されたヘルパー プロセスをより明確かつ安全にすることができます。
これは、組織が最初に最も実際的な価値を回収することが多い場所でもあります。優れたツールにより、今後のあらゆる調査、展開、インシデント後のあらゆる分析が向上するためです。周囲のツールが強化されると、中核となるエンジニアリング チームがより健全になります。これは、たとえ会議のベンチマークには現れなかったとしても、実際のシステムの成果となります。
ハンズオン ラボ: Windows IOCTL を退屈な方法でデコードする
Windows カーネルは、制御コードを装飾的な整数のように扱うチームを罰します。 IOCTL 値をデコードする小さなユーティリティを構築して、境界が曖昧でなくなるようにしましょう。
重要なのは算術トリックそのものではありません。重要なのは、暗黙的な構造を明示的なエンジニアリング言語に変換する練習をすることです。低レベルの問題の多くは、チームがエンコードされた値を、あたかも誰もがその意味を自然に理解しているかのように受け渡していることから発生します。
main.cpp
#include <cstdint>
#include <iomanip>
#include <iostream>
struct IoctlParts {
std::uint32_t device_type;
std::uint32_t access;
std::uint32_t function;
std::uint32_t method;
};
IoctlParts decode_ioctl(std::uint32_t code) {
return IoctlParts{
(code >> 16) & 0xFFFFu,
(code >> 14) & 0x3u,
(code >> 2) & 0x0FFFu,
code & 0x3u
};
}
int main() {
constexpr std::uint32_t ioctl = 0x222004;
const auto parts = decode_ioctl(ioctl);
std::cout << "IOCTL 0x" << std::hex << std::uppercase << ioctl << "\n";
std::cout << "device_type=0x" << parts.device_type << "\n";
std::cout << "access=0x" << parts.access << "\n";
std::cout << "function=0x" << parts.function << "\n";
std::cout << "method=0x" << parts.method << "\n";
}
建てる
MSVC を使用した Windows の場合:
cl /O2 /std:c++20 main.cpp
.\main.exe
クロスプラットフォーム コンパイラを使用する Linux または macOS の場合:
g++ -O2 -std=c++20 -o ioctl_decode main.cpp
./ioctl_decode
これが教えてくれること
重要なのは算数ではありません。重要なのは、隠れた構造が魔法のように扱われなくなると、低レベルの Windows 作業が容易になるということです。境界を解読し、フィールドに名前を付け、コントラクトを表示すると、デバッグに関する会話が突然短くなり、宗教的ではなくなります。
ツールを拡張して一般的なメソッドとアクセス モードにラベルを付けると、うまく一般化する習慣の構築も始まります。すべての不透明なカーネル コントラクトは、チームの残りのメンバーが検査できる退屈で明示的な用語でレンダリングされると、危険性が低くなります。
愛好家向けのテストタスク
- Rust で同じデコーダを再作成し、コードの長さだけでなく、ドライバー ツールチェーンの残りの部分に公開する境界の明確さを比較します。
- デコーダを拡張して、
METHOD_BUFFERED、METHOD_IN_DIRECT、および関連する値の人間が判読できる名前を出力します。 - テキスト ファイルから IOCTL コードのリストのパーサーを追加し、デバイス タイプと機能別にソートします。
- ランダムな IOCTL 値の小さなファズ入力セットを構築し、デコーダーが安定していて退屈であることを確認します。
- 意図的にずさんな境界仮定を 1 つ追加し、「無害な」ショートカットがツール全体をどれだけ早く嘘つきに変えるかを検証します。
まとめ
Rust は、混乱を減らし、所有権を明確にし、回避可能なバグの特定のカテゴリを縮小するために使用される場合、Windows の低レベル エンジニアリングの真の改善です。 C++ は、作業が既存のドライバー、既存のツール、既存のデバッグ文化、および依然としてネイティブなエコシステムに存在する運用パスに関連付けられている場合、現実的であり、多くの場合正当化されるデフォルトのままです。
本当の仕事は道徳的な勝者を選ぶことではありません。本当の仕事は、システムがすでにプレッシャーにさらされているときにも理解可能な境界を設計することです。カーネル作業では、それがエンジニアリングと楽観主義の違いです。
これをうまく扱うチームは、現代性と成熟性を混同しません。彼らは、システム全体をより管理しやすくする言語、ツール、運用習慣を使用します。これは、大規模な説教に比べて劇的な結果ではありませんが、実際のマシンとの接触でも生き残る傾向がある結果でもあります。
参考文献
- Windows ドライバーのドキュメント: Windows
- I/O 制御コードの定義: https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/defining-i-o-control-codes
- ハードウェアの優先順位と IRQL の管理: https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/managing-hardware-priorities
- WDF ドライバー開発の概要: https://learn.microsoft.com/en-us/windows-hardware/drivers/wdf/
- Windows デバッグ ツールのドキュメント: Windows
- 安全ではありません Rust: Rust