Surface 团队驱动程序开发最佳实践
介绍
这些驱动程序开发指南由 Microsoft 的驱动程序开发人员开发多年。 随着时间的推移,当司机行为不当和吸取教训时,这些教训被捕获并演变成这一套指导。 Microsoft Surface 硬件团队使用这些最佳做法来开发和维护支持独特 Surface 硬件体验的设备驱动程序代码。
与任何一组准则一样,会有合法例外和替代方法同样有效。 请考虑将这些准则合并到开发标准中,或使用它们来启动开发环境和独特要求的域特定准则。
驱动程序开发人员犯的常见错误
处理 I/O
- 访问从 IOCTL 检索到的缓冲区,而无需验证长度。 请参阅 “无法检查缓冲区的大小”。
- 在用户线程或随机线程上下文的上下文中执行阻止 I/O。 请参阅 内核调度程序对象的简介。
- 在不超时的情况下将同步 I/O 发送到另一个驱动程序。 请参阅 同步发送 I/O 请求。
- 在不了解安全隐患的情况下使用两个 io IOCTL。 请参阅 不使用缓冲 I/O 和直接 I/O。
- 不检查 WdfRequestForwardToIoQueue 的返回状态或未正确处理失败并导致放弃的 WDFREQUEST。
- 将 WDFREQUEST 保留在队列之外处于不可取消状态。 请参阅 管理 I/O 队列、 完成 I/O 请求 和 取消 I/O 请求。
- 尝试使用 Mark/UnmarkCancelable 函数而不是使用 IoQueues 来管理取消。 请参阅 框架队列对象。
- 不知道文件句柄清理和关闭操作之间的差异。 请参阅 处理清理和关闭操作中的错误。
- 忽略通过 I/O 完成和从完成例程重新提交的潜在递归。
- 不明确 WDFQUEUE 的电源管理属性。 没有清楚地记录电源管理选择。 这是 Bug 检查0x9F的主要原因 :WDF 驱动程序中的DRIVER_POWER_STATE_FAILURE 。 删除设备后,框架将在删除过程的不同阶段从电源托管队列和非电源托管队列中清除 IO。 收到最终IRP_MN_REMOVE_DEVICE时,将清除非电源托管队列。 因此,如果在非电源托管队列中保存 I/O,最好在 EvtDeviceSelfManagedIoFlush 的上下文中显式清除 I/O,以避免死锁。
- 不遵循处理 IRP 的规则。 请参阅 处理清理和关闭操作中的错误。
同步
- 为不需要保护的代码保留锁。 当只需要保护少量操作时,不要为整个函数保留锁。
- 用锁叫出司机。 这是死锁的主要原因。
- 使用互锁基元创建锁定方案,而不是使用适当的系统提供的锁定基元,如互斥体、信号灯和旋转锁。 请参阅 Mutex 对象简介、 信号灯对象 和 旋转锁简介。
- 使用旋转锁,其中某些类型的被动锁更合适。 请参阅 快速互斥体和受保护的互斥体 和 事件对象。 有关锁的其他视角,请查看 OSR 文章 - 同步状态。
- 选择加入 WDF 同步和执行级别模型,而无需完全了解影响。 请参阅 “使用框架锁”。 除非驱动程序是单一顶级驱动程序直接与硬件交互,否则请避免选择使用 WDF 同步,因为这可能会导致由于递归而导致死锁。
- 在多个线程的上下文中获取 KEVENT、Semaphore、ERESOURCE、UnsafeFastMutex,而无需进入关键区域。 这样做可能会导致 DOS 攻击,因为持有其中一个锁的线程可以挂起。 请参阅 内核调度程序对象的简介。
- 在线程堆栈上分配 KEVENT,并在事件仍在使用期间返回到调用方。 通常在与 IoBuildSyncronousFsdRequest 或 IoBuildDeviceIoControlRequest 一起使用时完成。 这些调用的调用方应确保它们不会从堆栈中展开,直到 I/O 管理器在 IRP 完成后向事件发出信号。
- 无限期地在调度例程中等待。 通常,调度例程中的任何类型的等待都是一种不良做法。
- 在删除对象之前,不恰当地检查对象的有效性(如果 blah == NULL)。 这通常意味着作者对控制对象的生存期的代码没有完全了解。
对象管理
- 不显式为 WDF 对象提供父级。 请参阅 框架对象简介。
- 将 WDF 对象父级设置为 WDFDRIVER,而不是为对象提供更好的生存期管理和优化内存使用情况的父级。 例如,将 WDFREQUEST 父级设置为 WDFDEVICE 而不是 IOTARGET。 请参阅 使用常规框架对象、 框架对象生命周期 和 框架对象的摘要。
- 不对跨驱动程序访问的共享内存资源执行运行保护。 请参阅 ExInitializeRundownProtection 函数。
- 在上一个工作项已在队列中或正在运行时,错误地将同一工作项排队。 如果客户端假设每个排队的工作项都将被执行,则这是一个问题。 请参阅 Using Framework WorkItems。 有关队列 WorkItems 的详细信息,请参阅 驱动程序模块框架 (DMF) 项目中的 DMF_QueuedWorkitem 模块 - https://github.com/Microsoft/DMF。
- 在发布计时器消息之前,队列计时器应处理。 请参阅 “使用计时器”。
- 在工作项中执行操作,可以无限期地阻止或花费很长时间才能完成。
- 设计导致大量工作项排队的解决方案。 如果坏人可以控制操作(例如将 I/O 抽入到将新工作项排入每个 I/O)的驱动程序,则可能会导致系统无响应或 DOS 攻击。 请参阅 使用框架工作项。
- 在删除对象之前,工作项 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 或总线接口中提供的驱动程序库提供,如 OSR 文章使用驱动程序到驱动程序通信的总线接口中所述。
PnP/Power
- 以非 pnp 友好方式与另一个驱动程序交互 - 不注册 pnp 设备更改通知。 请参阅 注册设备接口更改通知。
- 创建 ACPI 节点以枚举设备并在其中创建电源依赖项,而不是使用总线驱动程序或系统以优雅的方式为 PNP 和电源依赖项提供软件设备创建接口。 请参阅 函数驱动程序中的支持 PnP 和电源管理。
- 标记设备不可禁用 - 强制在驱动程序更新时重新启动。
- 将设备隐藏在设备管理器中。 请参阅从设备管理器隐藏设备。
- 假设驱动程序将仅用于设备的一个实例。
- 假设驱动程序永远不会卸载。 请参阅 PnP 驱动程序的 Unload 例程。
- 不处理虚假的接口到达通知。 这种情况可能发生,驱动程序应安全地处理此情况。
- 不实现 S0 空闲电源策略,这对于 DRIPS 约束或子级设备非常重要。 请参阅 支持空闲电源关闭。
- 不检查 WdfDeviceStopIdle 返回状态会导致电源引用泄漏,因为 WdfDeviceStopIdle/ResumeIdle 不平衡,最终导致 9F bug 检查。
- 由于资源重新均衡,不知道 PrepareHardware/ReleaseHardware 可以多次调用。 这些回调应限制为初始化硬件资源。 请参阅 EVT_WDF_DEVICE_PREPARE_HARDWARE。
- 使用 PrepareHardware/ReleaseHardware 分配软件资源。 如果分配与硬件交互所需的资源,则应在 AddDevice 或 SelfManagedIoInit 中将软件资源分配静态到设备。 请参阅EVT_WDF_DEVICE_标准版LF_MANAGED_IO_INIT。
编码指导原则
- 不使用安全字符串和整数函数。 请参阅使用保险箱字符串函数和使用保险箱整数函数。
- 不使用 typedefs 定义常量。
- 使用全局变量和静态变量。 避免将每个设备上下文存储在全局环境中。 全局用于跨多个设备实例共享信息。 或者,请考虑使用 WDFDRIVER 对象上下文跨多个设备实例共享信息。
- 不对变量使用描述性名称。
- 命名变量中不一致 - 大小写一致性。 对现有代码进行更新时,不遵循现有的编码样式。 例如,对不同函数中的常见结构使用不同的变量名称。
- 不注释重要的设计选择 - 电源管理、锁、状态管理、工作项的使用、DPC、计时器、全局资源使用情况、资源预分配、复杂表达式/条件语句。
- 对调用的 API 名称中明显的内容进行注释。 将注释设置为与函数名称等效的英语语言(如在调用 WdfDeviceCreate 时编写注释“创建设备对象” )。
- 不要创建具有返回调用的宏。 请参阅 Functions (C++)。
- 没有或不完整的源代码注释(SAL)。 请参阅 适用于 Windows 驱动程序的 SAL 2.0 批注。
- 使用宏而不是内联函数。
- 使用 C++ 时,对常量使用宏代替 constexpr
- 使用 C 编译器(而不是 C++ 编译器)编译驱动程序,以确保获得强类型检查。
错误处理
- 不报告关键驱动程序错误,并正常标记设备未正常运行。
- 不返回转换为有意义的 WIN32 错误状态的相应 NT 错误状态。 请参阅 使用 NTSTATUS 值。
- 不使用 NTSTATUS 宏检查系统函数的返回状态。
- 不根据需要对状态变量或标志断言。
- 在访问指针以解决争用情况之前检查指针是否有效。
- AS标准版 NULL 指针上的RTING。 如果尝试使用 NULL 指针访问内存 Windows,检查将 bug。 bug 检查的参数将提供修复 null 指针所需的信息。 加班,当许多不需要的 AS标准版RT 语句添加到代码时,它们会消耗内存并降低系统速度。
- AS标准版对象上下文指针上的RTING。 驱动程序框架保证将始终使用上下文分配对象。
跟踪
- 不定义 WPP 自定义类型,并在跟踪调用中使用它来获取人类可读的跟踪消息。 请参阅 向 Windows 驱动程序添加 WPP 软件跟踪。
- 不使用 IFR 跟踪。 请参阅 KMDF 和 UMDF 2 驱动程序中的“使用登录跟踪记录器”(IFR)。
- 在 WPP 跟踪调用中调用函数名称。 WPP 已跟踪函数名称和行号。
- 不使用 ETW 事件来衡量影响事件的性能和其他关键用户体验。 请参阅 将事件跟踪添加到内核模式驱动程序。
- 在事件日志中不报告严重错误,并正常标记设备功能不正常。
验证
- 在开发和测试期间,未运行具有标准和高级设置的驱动程序验证程序。 请参阅 驱动程序验证程序。 在高级设置中,建议启用除与低资源模拟相关的规则之外的所有规则。 最好隔离运行低资源模拟测试,以便更轻松地调试问题。
- 未在驱动程序或设备类上运行 DevFund 测试,驱动程序是启用高级验证程序设置的一部分。 请参阅 如何通过命令行运行 DevFund 测试。
- 不验证驱动程序是否符合 HVCI。 请参阅 实现 HVCI 兼容性代码。
- 在开发和测试用户模式驱动程序期间,在WUDFhost.exe上运行 AppVerifier。 请参阅 应用程序验证程序。
- 不要检查运行时使用 !wdfpoolusage 调试器扩展使用内存,以确保不会放弃 WDF 对象。 内存、请求和工作项是这些问题的常见受害者。
- 不使用 !wdfkd 调试器扩展检查对象树,以确保对象正确父化,并检查主要对象(如 WDFDRIVER、WDFDEVICE、IO)的属性。