Windows 内核中的 C++ 与 Rust:安全收益与现实边界

Windows 内核中的 C++ 与 Rust:安全收益与现实边界

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 不是通用系统游乐场

人们经常谈论系统编程,就好像每个低级域都有一种情绪氛围一样。 Windows 内核有自己的运行环境。 Windows 内核是一个具有严格契约的操作环境,并且需要非常昂贵的方法才能发现您对它们的误解。

IRQL 存在。存在调度路径。存在分页限制。设备堆栈是存在的。 IOCTL 合同存在。同步错误不会长期存在于理论上。不良的清理逻辑可能会破坏状态、堵塞设备路径或使其他人希望继续运行的机器崩溃。

这意味着内核惩罚了两种相反的幻想。第一个幻想是所有低级工作都应该永远保留在 C 或 C++ 中,因为世界一直都是这样连接的。第二个幻想是使用Rust会自动将工作转化为道德胜利。两者都是避免真正设计问题的懒惰方法。

真正的问题是塑造系统,使最危险的边界变小、可测量、可调试且清晰。有时,这意味着 C++ 仍然是最实际的选择,因为驱动程序模型、现有代码、工具和团队经验都指向那里。有时,这意味着 Rust 组件真正降低了风险并提高了清晰度。大多数时候,这意味着答案是混合的,只有成年人才能接受混合的答案。

核心工作也会放大组织的弱点。如果一个团队没有记录不变量,如果审查很弱,如果调试知识存在于一个人的头脑中,或者如果发布卫生被视为可选的文书工作,那么低级代码将快速放大这种混乱。该语言无法完全保护团队免受混乱的工程文化的影响。它可以提供帮助。它不能取代纪律。

Rust 对 Windows 低级工作的实际帮助

Rust 当它消除了无权混淆的代码中的混淆时,它的帮助最大。边界解析、状态机卫生、显式所有权、更清晰的清理模式以及围绕什么可以别名或比什么更严格的纪律都是有意义的胜利。在内核相邻或驱动程序较多的系统中,这一点很重要,因为低级错误很少是诗意的。它们是重复的,结构上很熟悉,而且工程团队几十年来一直假装自己是浪漫的一部分,这让他们感到羞辱。

Rust 在接口可以保持明确的有界组件中特别有吸引力。如果周围的工程故事足够成熟以支持它们,那么实用程序层、帮助器模块、定义良好的解析器、驱动程序的一些用户模式伴侣、内部工具以及仔细隔离的内核或驱动程序逻辑片段都可以从语言的约束中受益。

它在文化上也有帮助。将 Rust 引入 Windows 系统环境的团队通常会就生命周期、别名、清理以及边界的具体承诺进行更健康的对话。即使最终的架构仍然是混合的,这也是有用的。语言塑造讨论,有时更好的讨论已经是实质性的进步。

还有另一个实际优势:Rust 可以使有限范围的组件更容易移交。如果所有权模型是可见的并且接口更窄,未来的工程师就有更好的机会更改代码,而无需吸收部落知识以避免引爆它。在与内核相邻的工作中,这种可维护性并不是学术性的。这就是团队如何长期保持硬系统健康的方式。但这并不意味着 Windows 内核现在是一个游乐场,团队应该以神学热情重写。有限的胜利仍然是有限的胜利。这种区别在于,严谨的工程设计如何避免成为非常昂贵的生活方式品牌。

C++ 仍然保持真正的基础

C++ 在 Windows 内核和驱动程序工作中仍然保持着强大的实力,原因是顽固的实用性。有大量围绕 C 和 C++ 构建的现有驱动程序代码、示例、模式、调试知识和供应商集成历史。在这个领域工作的团队很少是从空地开始的。他们继承了驱动程序、过滤器链、设备契约、用户模式客户端、遗留Windows、构建假设和已经C++形成的操作习惯,即使代码是半C语言并且情感上是100%的债务。

工具的故事也很重要。 WinDbg、WDK 示例、KMDF 和 WDM 习惯、驱动程序验证程序工作流程、符号解释、故障转储调查以及围绕 Windows 内核工作的更广泛的调试文化都仍然深深植根于现有的本机世界。当团队面临压力时,成熟的诊断并不是一种装饰性的好处。这就是为什么这项工作避免成为公开进行的考古季。

还有整合问题。驱动程序通常与旧代码、用户模式帮助程序、现有安装程序逻辑、供应商 C++ 或已绑定到 C 和 C++ 假设的安全工具并存。 C++ 抽象而言并不自动更好。立即执行通常会更好,因为周围的系统已经接受过使用该语言的训练。

这并不会使 Rust 无效。它只是意味着举证责任会根据组件所在的位置而变化。一个新的隔离模块是一个论点。另一个是通过多年的本机假设线程化的驱动程序堆栈。严肃的团队不再假装这些都是同样的情况。

另一个因素是运营信心。许多 Windows 驱动程序团队已经知道如何读取故障转储、验证符号、重现故障以及在 C++ 优先的世界中发布修补程序。这种操作肌肉看起来很普通,而且更换成本很高。提高代码优雅性同时削弱事件响应的迁移并不是最终的胜利。

不安全的表面不会消失。它会移动。

这是整个谈话中最重要的一点。不安全行为不会消失,因为代码库的一部分是用 Rust 编写的。它搬迁了。它聚集在 FFI 边缘、缓冲区边界、同步缝、分配路径、设备契约以及操作模型仍然由 Windows 本身定义的地方,而不是由单一语言的细节定义的地方。

这就是为什么如果团队过早庆祝语言而过晚庆祝边界,他们可能会做出糟糕的交易。草率地跨入遗留驱动程序代码的 Rust 模块仍然可以继承旧的混乱,加上新的集成税。隔离其危险表面、记录其不变量、保持 IOCTL 语义无聊并保持深度可测试的 C++ 驱动程序可能会比更时尚的架构(扩大边界,同时非常自信地讲述其优点)产生更少的意外。

因此,成人设计问题更小、更尖锐。哪些模块可以被隔离?哪个接口可以保持稳定?哪些所有权规则可以足够清楚地表述,以使语言差异不会造成运行时混乱?当系统已经着火并且没有人有兴趣了解哲学上的细微差别时,哪种调试路径仍然有效?

这些问题并不是反Rust。他们是支持生存的。

他们也赞成合作。一个有足够记录的驱动程序边界可供多个工程师推理,可以减少英雄主义税。它使代码审查更加强大。它使审计不再那么戏剧化。它使得未来的修复更少地依赖于记忆,而更多地依赖于明确的工程事实。这对于任何系统都很重要,但对于特权软件尤其重要。

好的是什么样的

好Windows-内核工程听起来并不英雄。听起来很平静。

危险的道路是已知的。 IOCTL 合同是明确的。并发故事非常无聊。所有权假设已记录在案。可以进行故障转储分析。推出计划并不是一个大胆的事情。驱动程序边界足够窄,原始实施团队之外的人员仍然可以了解什么可以安全更改,什么可以安全保留。

如果使用 Rust,原因应该很明显。它不应该在那里,因为“未来”是用粗体字体写在幻灯片上的。它应该在那里,因为定义的组件真正受益于语言的约束,并且因为团队可以支持该选择所产生的调试、构建和操作故事。

如果 C++ 仍处于关键路径中,则不应将其视为命运。应该用证据来捍卫它:工具成熟度、集成成本、驱动程序限制、团队经验,以及如果组件被移动,不稳定实际上来自何处的衡量观点。 C++ 应该出现在设计中,因为它赢得了席位,因为有证据支持它。

最强大的核心团队也知道技术冷静是交付健康的一部分。当代码、工具和合同可读时,工作就会停止,这取决于肾上腺素。从长远来看,这使得系统更安全,因为在解释恐慌下所做的更改更少。

值得优先解决的实际案例

IOCTL边界清理

许多驱动程序繁重的系统受到最聪明的代码的威胁,而不是草率的合同边界。清理 IOCTL 处理、验证、结构版本控制和用户到内核的假设通常比雄心勃勃的重写更快地产生更安全的结果。这是因为 IOCTL 错误不是孤立的错误。它们污染了用户模式和内核模式之间的整个信任关系。当边界模糊时,每个下游决策都变得更难以推理。

狭窄的驱动核心强化

具有明确的不变量和更好的所有权规则的小型驱动程序核心通常比巨大的理论迁移更有价值。有时这种强化会发生在 C++ 中。有时它使未来的 Rust 组件成为可能。无论哪种方式,回报都是真实的。

也是可以衡量的。崩溃签名变得更清晰。审查变得更容易。验证对话变得更短。当能够以这些方式证明进展时,团队就不会为了情感上的结束而追求戏剧性的重写。

用户模式同伴和工具

这就是 Rust 经常在没有戏剧性的情况下大放异彩的地方。诊断工具、重放实用程序、配置验证器、捕获分析器或受控帮助程序进程可以变得更加清晰和安全,而无需在系统准备就绪之前将最脆弱的内核路径拖入新的集成宗教。

这也是组织通常首先恢复最实用价值的地方,因为更好的工具可以改进每一次未来的调查、每一次部署和每一次事件后分析。更强大的周边工具使核心工程团队更健康,这是一个真正的系统结果,即使它从未出现在会议基准中。

动手实验室:以无聊的方式解码 Windows IOCTL

Windows 内核惩罚那些将控制代码视为装饰性整数的团队。让我们构建一个小型实用程序来解码 IOCTL 值,这样边界就不再神秘了。

重点不在于算术技巧本身。重点是练习将隐式结构转变为显式工程语言。许多低级痛苦来自于团队传递编码值,就好像每个人自然都知道它们的含义一样。

C++```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";

}



在 Windows 上使用 MSVC:```powershell
cl /O2 /std:c++20 main.cpp
.\main.exe
```在带有跨平台编译器的 Linux 或 macOS 上:```bash
g++ -O2 -std=c++20 -o ioctl_decode main.cpp
./ioctl_decode
```### 这教给你什么

重点不是算术。关键是,当隐藏结构不再被视为魔法时,低级 Windows 工作就会变得更加容易。解码边界、命名字段、使合约可见,突然间调试对话变得更短、更少宗教性。

如果你扩展该工具来标记通用方法和访问模式,你也开始养成一个通用的习惯:每个不透明的内核合约一旦以团队其他成员可以检查的无聊、明确的术语呈现,就会变得不那么危险。

## 爱好者测试任务

1. 在 Rust 中重新创建相同的解码器,并将代码长度与您将向驱动程序工具链的其余部分公开的边界的清晰度进行比较。
2. 扩展解码器以打印 Windows、Rust 和相关值的人类可读名称。
3. 为文本文件中的 IOCTL 代码列表添加解析器,并按设备类型和功能对它们进行排序。
4. 构建一个随机 IOCTL 值的微小模糊输入集,并验证您的解码器是否保持稳定和无聊。
5. 添加一个故意草率的边界假设,然后检查“无害”的快捷方式将整个工具变成骗子的速度有多快。

## 总结

Rust 是对 Windows 低级工程的真正改进,它用于缩小混乱、澄清所有权并缩小某些类别的可避免错误。当工作与现有驱动程序、现有工具、现有调试文化和仍存在于高度原生生态系统中的操作路径相关联时,C++ 仍然是真实且通常合理的默认设置。

真正的工作不是选出道德上的胜利者。真正的工作是设计当系统已经处于压力下时仍然可以理解的边界。在核心工作中,这就是工程学和乐观主义之间的区别。

能够很好地处理这一点的团队不会将现代性与成熟性混为一谈。他们使用的语言、工具和操作习惯使整个系统更易于治理。与一场全面的布道相比,这是一种不那么戏剧性的结果,但它也是一种在与真实机器接触后仍能保存下来的结果。

## 参考文献

1. Windows 驱动程序文档:C++
2. 定义I/O控制码:[https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/defining-i-o-control-codes](https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/defining-i-o-control-codes)
3. 管理硬件优先级和 IRQL:[https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/managing-hardware-priorities](https://learn.microsoft.com/en-us/windows-hardware/drivers/kernel/managing-hardware-priorities)
4. WDF驱动开发概述:[https://learn.microsoft.com/en-us/windows-hardware/drivers/wdf/](https://learn.microsoft.com/en-us/windows-hardware/drivers/wdf/)
5. Windows调试工具文档:[https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/](https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/)
6. 不安全 Rust:[https://doc.rust-lang.org/book/ch20-01-unsafe-rust.html](https://doc.rust-lang.org/book/ch20-01-unsafe-rust.html)
Philip P.

Philip P., CTO

返回博客

联系方式

开始对话

几条清晰的线条就足够了。描述一下系统、压力、被阻碍的决策。或者直接写信给midgard@stofu.io。

0 / 10000
未选择文件