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 コンポーネントが真にリスクを軽減し、明確性を高めることを意味します。ほとんどの場合、それは答えが混在していることを意味し、混在した答えに抵抗がないのは大人だけです。
Rust が Windows の低レベルの作業に実際に役立つ場所
Rust は、混乱する権利のないコードから混乱を取り除くときに最も役立ちます。境界解析、ステートマシンの健全性、明示的な所有権、より明確なクリーンアップパターン、そして何がエイリアスになるか、何がより長く存続するかをめぐる厳格な規律はすべて意味のある勝利です。カーネル隣接システムやドライバー負荷の高いシステムでは、低レベルのバグが詩的なものになることはほとんどないため、これは重要です。それらは反復的で、構造的によく知られており、エンジニアリングチームがロマンスの一部であるかのように何十年も費やしてきた点で屈辱的です。
Rust は、インターフェースを明示的に保つことができる境界付きコンポーネントで特に魅力的です。ユーティリティ レイヤー、ヘルパー モジュール、明確に定義されたパーサー、ドライバー用の一部のユーザー モード コンパニオン、内部ツール、および慎重に分離されたカーネルまたはドライバー ロジックの部分は、周囲のエンジニアリング ストーリーがそれらをサポートするのに十分に成熟している場合、言語の制約から恩恵を受けることができます。
文化的にも役立ちます。 Rust を Windows システム環境に導入するチームは、ライフタイム、エイリアシング、クリーンアップ、境界が約束するものについて、より健全な会話を行うことがよくあります。これは、最終的なアーキテクチャがハイブリッドのままの場合でも役立ちます。言語はディスカッションを形成し、より良いディスカッションがすでに実質的な進歩となっている場合もあります。
しかし、これはいずれも、Windows カーネルが現在、チームが神学的熱意によって書き換えるべき遊び場であることを意味するものではありません。限定された勝利は依然として限定された勝利です。その違いは、真剣なエンジニアリングがいかにして非常に高価なライフスタイルブランドになることを回避しているかということです。
C++ が依然として現実の地位を維持している場所
C++ は、頑固に実用的であるという理由から、Windows カーネルとドライバーの動作において強力なままです。 C と C++ を中心に構築された既存のドライバー コード、サンプル、パターン、デバッグの知識、ベンダー統合履歴の膨大な量があります。この分野で作業するチームが何もない土地からスタートすることはほとんどありません。コードが半分 C で感情的には 100% 負債である場合でも、ドライバー、フィルター チェーン、デバイス コントラクト、ユーザー モード クライアント、レガシー C++、ビルド前提条件、および既に C++ 形成されている運用習慣を継承しています。
ツールの話も重要です。 WinDbg、WDK サンプル、KMDF および WDM の習慣、ドライバー検証ワークフロー、シンボル解釈、クラッシュダンプ調査、および Windows カーネル作業に関する広範なデバッグ文化はすべて、依然として既存のネイティブ世界に深く根付いています。チームがプレッシャーにさらされているとき、診断の成熟度は飾り的なものではありません。そうすることで、この研究が公の場で行われる考古学の季節になるのを避けることができる。
統合の問題もあります。ドライバーは、多くの場合、古いコード、ユーザーモード ヘルパー、既存のインストーラー ロジック、ベンダー SDKs、または C および C++ の前提条件にすでにバインドされているセキュリティ ツールの横に存在します。 C++ は、抽象的には自動的に優れているわけではありません。多くの場合、周囲のシステムがすでにそれを話すように訓練されているため、すぐには改善されます。
それによって Rust が無効になるわけではありません。それは単に、コンポーネントがどこに位置するかによって立証責任が変わることを意味します。新しい分離されたモジュールは 1 つの引数です。もう 1 つは、長年にわたるネイティブの想定を経てスレッド化されたドライバー スタックです。真剣なチームは、同じ状況であるかのように振る舞うことをやめます。
危険な表面は消えません。動きます。
これが会話全体の中で最も重要なポイントです。コードベースの一部が Rust で記述されているため、安全でない動作は消えません。移転するのです。それは、FFI エッジ、バッファー境界、同期シーム、割り当てパス、デバイス コントラクト、およびオペレーティング モデルが単一言語の機能ではなく Windows 自体によって依然として定義されている場所に集まります。
だからこそ、チームが言語を祝うのが早すぎたり、境界線を祝ったりするのが遅すぎると、悪い取引をする可能性があります。 Rust モジュールがレガシー ドライバー コードにずさんに混入すると、依然として古い混乱が引き継がれ、さらに新しい統合税が課せられる可能性があります。危険な表面を分離し、その不変条件を文書化し、IOCTL セマンティクスを退屈に保ち、深くテスト可能な状態を維持する C++ ドライバーは、その長所を非常に自信を持って語りながら境界を広げたよりファッショナブルなアーキテクチャよりも、まったくの驚きを生み出すことが少ないかもしれません。
したがって、大人向けのデザインの質問はより小さく、よりシャープになります。どのモジュールを分離できますか?どのインターフェースが安定した状態を維持できるでしょうか?言語の違いが実行時の混乱にならないように十分に明確に記述できる所有権ルールはどれですか?システムがすでに起動していて、誰も哲学的なニュアンスを理解する気がない場合でも、どのデバッグ パスが機能するでしょうか?
これらの質問は Rust に反対するものではありません。彼らは生存推進派だ。
見た目の良さ
優れた Windows カーネル エンジニアリングは、英雄的とは思えません。穏やかな響きですね。
危険な道は既知です。 IOCTL 契約は明示的です。同時実行の話は良い意味で退屈です。所有権の仮定は文書化されます。クラッシュダンプ解析が可能です。展開計画は大胆なものではありません。ドライバーの境界は非常に狭いため、元の実装チーム以外の誰かでも、何を変更しても安全で、何を放っておいても安全なのかを理解できます。
Rust が使用されている場合、その理由は明らかです。スライドには「未来」と強い文字で書かれているので、そこには存在しないはずだ。定義されたコンポーネントは言語の制約から真に恩恵を受け、チームはその選択から生じるデバッグ、ビルド、運用ストーリーをサポートできるため、このコンポーネントが存在する必要があります。
C++ がクリティカル パスに残っている場合、それを運命として擁護すべきではありません。この問題は、ツールの成熟度、統合コスト、ドライバーの制約、チームの経験、コンポーネントを移動した場合に実際に不安定性が生じる場所についての測定されたビューなどの証拠によって擁護される必要があります。 C++ は、システムが疲れすぎて議論できないからではなく、議席を獲得したという理由で設計に含めるべきです。
最初に解決する価値のある実際的なケース
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