下面是对创建 Windows NT 设备驱动程序的一些提示。 这些提示适用于所有技术。 还可以将这些提示用作解决驱动程序问题的核对清单。
要有效地使用下面提供的信息,您需要具备 Windows NT 体系结构的基本知识和某些设备驱动程序的开发经验。 有关开发设备驱动程序的详细信息,请参见 Windows NT 设备驱动程序工具包 (DDK),该工具包可以通过 MSDN Professional 成员资格获得。
下面是开发人员在使用 Windows NT 设备驱动程序时应当避免的事项列表:
- 一定不要在没有标注 I/O 请求数据包 (IRP) 挂起 (IoMarkIrpPending) 的情况下通过调度例程返回 STATUS_PENDING。
- 一定不要通过中断服务例程 (ISR) 调用 KeSynchronizeExecution。 它会使系统死锁。
- 一定不要将 DeviceObject->Flags 设置为 DO_BUFFERED_IO 和 DO_DIRECT_IO。 它会扰乱系统并最终导致致命错误。 而且,一定不要在 DeviceObject->Flags 中设置 METHOD_BUFFERED、METHOD_NEITHER、METHOD_IN_DIRECT 或 METHOD_OUT_DIRECT,因为这些值只在定义 IOCTL 时使用。
- 一定不要通过页面缓冲池分配调度程序对象。 如果这样做,将会偶尔导致系统故障检测 (Bugcheck)。
- 当运行于 IRQL >= DISPATCH_LEVEL 时,一定不要通过页面缓冲池分配内存,或访问页面缓冲池中的内存。 这是一个致命错误。
- 一定不要在 IRQL >= DISPATCH_LEVEL 上等候核心调度程序对象出现非零间隔。 这是一个致命错误。
- 在 IRQL >= DISPATCH_LEVEL 上执行时,一定不要调用任何导致调用线程发生直接或间接等待的函数。 这是一个致命错误。
- 一定不要把中断请求级别 (IRQL) 降低到低于您的顶级例程被调用的级别。
- 如果没有调用过 KeRaiseIrql(),则一定不要调用 KeLowerIrql()。
- 一定不要使处理器 (KeStallExecutionProcessor) 停止运转的时间超过 50 微秒。
- 一定不要使旋转锁 (Spin Lock) 保持锁定状态的时间超过您的需要。 要使系统获得更好的总体性能,请不要使任何系统范围内有效的旋转锁的锁定时间超过 25 微秒。
- 当 IRQL 大于 DISPATCH_LEVEL 时,一定不要调用 KeAcquireSpinLock 和 KeReleaseSpinLock,或 KeAcquireSpinLockAtDpcLevel 和 KeReleaseSpinLockFromDpcLevel。
- 一定不要通过调用 KeReleaseSpinLockFromDpcLevel 来释放 KeAcquireSpinLock 所获取的旋转锁,因为这会使原始 IRQL 无法被还原。
- 一定不要在 ISR 或 SynchCritSection 例程中调用 KeAcquireSpinLock 和 KeReleaseSpinLock 或者其它任何使用可执行旋转锁的例程。
- 当您在例程中而不是在 DriverEntry 中创建设备对象时,一定不要忘记清除 DO_DEVICE_INITIALIZING 标记。
- 一定不要同时在不同处理器的多个线程中将延时过程调用 (DPC) 对象添加到队列中(使用 KeInsertQueueDpc)。 这会导致致命错误。
- 一定不要通过 CutomerTimerDPC 例程释放周期定时器。 您可以通过 DPC 例程释放非周期定时器。
- 一定不要将相同的 DPC 指针传递给 KeSetTimer,或者 KeSetTimerEx (CustomTimerDpc) 和 KeInsertQueueDpc (CustomDpc),因为这将导致竞争。
- 旋转锁锁定时,一定不要调用 IoStartNextPacket。 这将使系统死锁。
- 旋转锁锁定时,一定不要调用 IoCompleteRequest。 这将使系统死锁。
- 如果您的驱动程序设置了完成例程,那么一定不要在没有把完成例程设置为 NULL 的情况下调用 IoCompleteRequest。
- 调用 IoCompleteRequest 之前,一定不要忘记设置 IRP 中的 I/O 状态区。
- 在将 IRP 添加到队列中或将它发送到另一个驱动程序 (IoCallDriver) 之后,一定不要调用 IoMarkPending。 在驱动程序调用 IoMarkPending 之前,IRP 可能已经完成,由此可能发生故障检测。 对于包含完成例程的驱动程序,如果设置了 Irp->PendingReturned,则完成例程必须调用 IoMarkPending。
- 一定不要在已经对某个 IRP 调用 IoCompleteRequest 之后再去访问该 IRP。
- 一定不要对不属于您的驱动程序的 IRP 调用 IoCancelIrp,除非您知道该 IRP 还没有完成。
- 在您的调度例程返回到调用者之前,一定不要对您的调度例程正在处理的 IRP 调用 IoCancelIrp。
- 一定不要从中间驱动程序调用 IoMakeAssociatedIrp 来为较低的驱动程序创建 IRP。 在中间驱动程序中所获得的 IRP 可能是已被关联的 IRP,而您不能将其它 IRP 关联到已经被关联的 IRP。
- 一定不要对使用缓冲 I/O 而设置的 IRP 调用 IoMakeAssociatedIrp。
- 一定不要简单地将指向设备 I/O 寄存器的虚拟指针解除引用并访问这些指针。 始终使用正确的硬件抽象层 (HAL) 函数来访问设备。
- 如果 IRP 或设备对象可能在 DISPATCH 级别被修改,那么一定不要通过 ISR 来访问 它。 在对称多处理器系统中,这会造成数据损坏。
- 正在高级 IRQL 中运行时,如果数据可能被低级 IROL 代码写入,那么一定不要修改该数据。 应当使用 KeSynchronizeExecution 例程。
- 在获取系统范围的取消旋转锁 (IoAcquireCancelSpinLock) 之前,一定不要在您的 DispatchCleanup 例程中获取驱动程序自己的旋转锁(如果有的话)。 要避免可能出现的死锁,一定要在驱动程序中遵循一致的锁定获取层次结构。
- 一定不要在取消例程中调用 IoAcquireCancelSpinLock,因为该例程被调用时已经获取了系统级的取消旋转锁。
- 在从取消例程返回之前,一定不要忘记调用 IoReleaseCancelSpinLock。
- 一定不要使用基于 IRQL 的同步,因为它只对单处理器系统有效。 提高单处理器上的 IRQL 将不会掩蔽在其它处理器上的中断。
- 一定不要对重叠的内存地址范围使用 RtlCopyMemory。 应当使用 RtlMoveMemory。
- 一定不要假定页面大小是常量,即使是用于给定的 CPU。 为了保持可移植性,应当使用 PAGE_SIZE 以及在头文件中所定义的其它页面相关常量。
- 一定不要从引导/系统初始化阶段加载的驱动程序的 DriverEntry 例程中访问除 Registry/Machine/Hardware 和 Registry/Machine/System 以外的任何注册表项。
- 一定不要为了加载驱动程序而在驱动程序的注册表项 (Registry/Machine/System/CurrentControlSet/Services) 下创建 Enum 项。 系统将动态地创建该项。
- 如果没有先在注册表中申请必需的与总线相关的 I/O 端口、内存范围、中断或直接内存访问 (DMA) 通道/端口等硬件资源,一定不要初始化物理设备。
- 一定不要在您的 DriverEntry 例程调用 IoRegisterDriverReinitialization,除非重初始化例程返回了 STATUS_SUCCESS。
- IRQL 为 PASSIVE_LEVEL 时,一定不要从被页面调度的线程或驱动程序例程中在 Wait 参数被设置为 TRUE 的情况下调用 KeSetEvent。 如果碰巧在调用 KeSetEvent 和 KeWait..Object(s) 之间您的例程被页面调度出去,这类调用就会导致致命的页面错误。
- 与上例相同的条件下,同样不能调用 KeReleaseSemaphore 。
- 与上例相同的条件下,同样不能调用 KeReleaseMutex 。
- 一定不要通过零售的 Windows NT 驱动程序调用 KeBugCheckEx 或 KeBugCheck 来停止系统的运行,除非您遇到的是破坏系统内存并最终导致系统进入故障检测的重要错误。 应当始终巧妙地处理错误条件。
- 一定不要假定 IoTimer 例程将会准确地在一秒边界处被调用,因为任何特定 IoTimer 例程的调用间隔最终取决于系统时钟。
- 一定不要从核心模式的设备驱动程序调用 Win32 应用程序编程接口 (API)。
- 一定不要使用会导致堆栈溢出的递归函数,因为调用线程的核心模式堆栈不能动态增长。
- 在处理多个中断的 ISR 例程中,一定不要使用中断对象指针 (PKINTERRUPT) 来标识中断,因为您在 ISR 中所获得的中断对象地址不会始终与您通过 IoConnectInterrupt 所获得的地址相同。 要想识别当前发生中断的设备,应当仅使用您在 IoConnectInterrupt 中所指定的 ServiceContext 值。
- 如果没有清零 CustomTimerDpc (KeCancelTimer),一定不要卸载驱动程序。 如果在卸载驱动程序后启动 DPC,它可能调用不存在的代码,并导致系统进入故障检测查。
- 如果 IRP 中设置了某个驱动程序的 I/O CompletionRoutine,那么一定要等到所有这些 IRP 完成之后,才能卸载该驱动程序。 如果卸载驱动程序后,IRP 被更低级的驱动程序完成,那么系统会试图执行不存在的代码,并导致系统崩溃。
- 一定要等到驱动程序准备好要处理某个设备中断时,才能启用该设备中断。 应当只在完成驱动程序初始化之后才启用它,执行 ISR 和 DPC 时,系统才能安全的访问设备对象的若干私有成员。
- 在旋转锁锁定时,一定不要调用驱动程序以外的代码,因为这会引起死锁。
- 如果您的驱动程序通过 IoBuildAsynchronousFsdRequest/IoAllocateIrp 创建了一个 IRP,那么,一定不要从您的 I/O CompletionRoutine 为这个 IRP 返回 STATUS_MORE_PROCESSING_REQUIRED 以外的任何状态,因为该 IRP 没有为与完成有关的 I/O 管理器的处理后工作做好准备。 这样的 IRP 应当被驱动程序显式地释放 (IoFreeIrp)。 如果本来没有打算重用 IRP,可以在返回状态 STATUS_MORE_PROCESSING_REQUIRED 之前,在 CompletionRoutine 中将它释放。
- 一定不要在任意的线程上下文中使用 IoBuildSynchronousFsdRequest/IoBuildDeviceIoControlRequest 来分配 IRP,因为该 IRP 依然与该线程保持关联 (Irp->ThreadListEntry),直到它被释放。
- 如果已经使用 IoAllocateIrp 在 ChargeQuota 参数被设置为 TRUE 的情况下分配了某个 IRP,那么一定不要对该 IRP 调用 IoInitializeIrp。 如果在 ChargeQuota 设置为 TRUE 的情况下分配 IRP,则 I/O 管理器将把它为该 IRP 分配内存时所用的缓冲池的相关信息保存在该 IRP 的内部标记中。
如果对这样的 IRP 调用 IoInitializeIrp,那么,当该函数盲目地清零整个 IRP 时,分配池信息将会丢失。 当您释放 IRP 时,这将导致内存被破坏。 同时,一定不要重用来自 IO 管理器的 IRP。 如果要重用 IRP,应当使用 IoAllocateIrp 分配您自己的 IRP。 - 如果在调用线程的堆栈中分配了对象,就一定不要在 KeWaitForSingleObject/KeWaitForMultipleObjects 中将 WaitMode 指定为 UserMode。 这样做的结果是,如果被等候的对象是在函数堆栈中创建的,那么您必须将 WaitMode 指定为 KernelMode 才能防止线程被页面调度出去。
- 在没有对关键节中的代码加以保护的情况下,一定不要在用户模式线程的上下文中获取诸如 ERESOURCES 和 FastMutex(Unsafe) 这类资源。
因为获取这些资源不会使 IRQL 提高到 APC_LEVEL,所以,如果线程在已获取资源后被挂起(通过将 APC 加入队列实现),它可能导致死锁,并使系统安全性降低。 因此,应当通过显式地将 IRQL 提高到 APC_LEVEL,或者调用 KeEnterCriticalRegion 来进入关键段,然后可以获取这些资源。