驱动程序和应用程序不一样的,由于其直接运行于windows r0级,故对于开发有更多和更严格的标准,一般会有以下一些常见的设计目标:
安全性、可移植性、可配置性、 可被中断、多处理器安全、可重用 IRP、 支持异步 I/O这些是基本目标。
1. 安全性:
驱动程序是足够安全的,它在系统运行的任何时候,都可以执行安装、卸载、禁用、启用等操作,而不引起蓝屏(BlueScreen)、系统运行缓慢等异常问题。
保障安全性方法包括但不限于:
A. 正确的处理编译器的任何警告和错误;和应用层不一样,任何警告都可能包含着错误,在WinDDK 7600中,甚至专门给出了对于警告和错误的工具链,检测并给出如何修改的建议,在新版本中集成到IDE中的,但感觉WinDDK还是很好用的
B. 良好的编码习惯和风格: 每一种标准风格的背后,都有一大堆的经验教训,例如下面的风格:
BOOLEAN
FunctionName (IN PVOID DeviceExtension, IN PMOUSE_INPUT_DATA CurrentInput, OUT POUTPUT_PACKET CurrentOutput,IN UCHAR StatusByte,IN PUCHAR DataByte,OUT PBOOLEAN ContinueProcessing,OUT PMOUSE_STATE MouseState,OUT PMOUSE_RESET_SUBSTATE ResetSubState
);// 这种风格中,每一行都有特定的功能,并用IN和OUT表明参数是输入参数还是输出参数;作为对比我们看看
// 下面的风格BOOLEAN FunctionName (PVOID DeviceExtension, PMOUSE_INPUT_DATA CurrentInput, POUTPUT_PACKET CurrentOutput, UCHAR StatusByte, PUCHAR DataByte, PBOOLEAN ContinueProcessing, PMOUSE_STATE MouseState, PMOUSE_RESET_SUBSTATE ResetSubState);// 对比发现上面风格非常清晰明确;
C. 注重对异常的检查和处理;根据实践的情况,和应用层是相反的,驱动层最好不要抛出异常,毕竟r9级别的异常可能带来非常多的问题,例如,如果我们使用物理地址拷贝数据时候,这时候越界很可能将毫不相关的进程写崩溃掉,此时系统很可能已经无法检测到的;
所有的异常最好是在当前函数中处理掉,故检测输入参数有效和确保输出参数有效非常有必要;
在没有必要的情况下,不要在内核中做一些骚操作,例如跑一个复杂的算法;
非常慎重对使用的内存进行处理;
大量的专业测试和问题调试;所有驱动的代码都需要经过HLK测试和认证,这可以避免一些异常;同时驱动尽可能留下日志,因为驱动有问题等同于系统有问题。
2. 可移植性
驱动程序应该支持所有 Windows 支持的硬件平台移植。 要实现跨平台可移植性,驱动程序开发中应该注意以下几点:
使用C语言开发:内核模式驱动程序都应使用 C 编写,以便它们可以使用系统兼容的 C 编译器重新编译、重新链接并在不同的 Windows 平台上运行,而无需重写或替换任何代码。不能在内核模式驱动程序中使用许多 C++ 语言构造,因此使用C++要仔细评估。
驱动程序不应依赖于任何特定系统兼容的 C 编译器或 C 支持库的功能,代码应符合 ANSI C 标准,最好避免:
依赖于大小或布局因平台而异的数据类型。
调用维护状态的任何标准 C 运行时库函数。
调用操作系统为其提供替代支持例程的任何标准 C 运行时库函数。
使用 WDK 编程接口:
每个Windows NT执行组件导出驱动程序和所有其他内核模式组件调用的一组内核模式驱动程序支持例程。 WDK 提供一组头文件,用于定义特定于系统的数据类型和常量,驱动程序需要保持从一个平台到另一个平台的可移植性。 所有内核模式驱动程序都包含一个主 WDK 内核模式头文件 Wdm.h 或 Ntddk.h。 在使用相应的编译器指令编译驱动程序时,主头文件不仅会引入系统提供的用于定义基本内核模式类型的宏,还会从任何特定于处理器体系结构的宏中拉取适当的选择。
如果驱动程序需要依赖于平台的定义,最好在 #ifdef 语句中隔离这些定义,以便针对相应的硬件平台编译和链接每个驱动程序。
在目前为止常见的架构是: x86、x64、IA64、Arm x86、Arm x64,我们最常用的还是x64架构;
3. 可中断/抢占
windows操作系统本身是可抢占的,它并非实时操作系统,可抢占意味着中断会按照一定的优先级来抢占,发生抢占时,低优先级的中断被挂起,待高优先级中断运行完成后,在恢复运行;
可中断设计的目标是最大限度地提高系统性能。 任何线程都可以被优先级较高的线程抢占,并且任何驱动程序的中断服务例程 (ISR) 都可以被以更高的中断请求级别运行的例程中断 (IRQL) 。
内核组件根据以下优先级条件之一确定代码序列的运行时间:
线程的内核定义的运行时优先级方案:
系统中的每个线程都有关联的优先级属性。 通常,大多数线程具有 可变 优先级属性:它们始终是抢占的,并计划与当前处于同一优先级的所有其他线程一起运行轮循机制。 某些线程具有 实时 优先级属性:这些时间关键型线程将运行到完成,除非它们被具有更高实时优先级属性的线程抢占。 Microsoft Windows 体系结构不提供固有的实时系统。
无论其优先级属性如何,在发生硬件中断和某些类型的软件中断时,系统中的任何线程都可以被抢占。
内核定义的 中断请求级别 (IRQL):
内核确定硬件和软件中断的优先级,以便某些内核模式代码在更高的 IRQL 下运行,从而使其具有高于系统中其他线程的计划优先级。 执行内核模式驱动程序代码的特定 IRQL 由其基础设备 的硬件优先级确定。
内核模式代码始终是可中断的:具有较高 IRQL 值的中断随时可能发生,从而导致具有更高系统分配 IRQL 的另一段内核模式代码立即在该处理器上运行。 但是,当一段代码在给定 IRQL 中运行时,内核会屏蔽处理器上 IRQL 值较小或相等的所有中断向量。
最低 IRQL 级别称为 PASSIVE_LEVEL。 在此级别,不会屏蔽任何中断向量。 线程通常以 IRQL=PASSIVE_LEVEL 运行。
软件中断中下一个更高的 IRQL 级别适用于软件中断。 这些级别包括APC_LEVEL、DISPATCH_LEVEL或内核调试WAKE_LEVEL。
硬件中断中设备中断的 IRQL 值仍然较高。 内核保留系统关键中断(例如来自系统时钟或总线错误)的最高 IRQL 值。
驱动程序中的每个例程都是可中断的。 这包括以高于 PASSIVE_LEVEL 的 IRQL 运行的任何例程。 仅在运行某个特定 IRQL 时未发生更高 IRQL 中断的情况下,在特定 IRQL 上运行的任何例程才保留对处理器的控制。
在 Windows 中,所有线程都具有线程上下文。 此上下文包含标识拥有线程的进程的信息以及其他特征,例如线程的访问权限。
通常,在请求驱动程序的当前 I/O 操作的线程上下文中,仅调用最高级别驱动程序。 中间级别或最低级别驱动程序永远不能假定它在请求其当前 I/O 操作的线程的上下文中执行。
因此,驱动程序例程通常在 任意线程上下文中执行 -- 调用标准驱动程序例程时,任何线程的上下文都是最新的。 出于性能原因(避免上下文切换),很少有驱动程序会设置自己的线程。
4. 多处理器
基于 Microsoft Windows NT 的操作系统设计为在单处理器和对称多处理器 (SMP) 平台上统一运行,内核模式驱动程序应设计为同样地运行。
在任何 Windows 多处理器平台中,都存在以下条件:
所有 CPU 都是相同的,所有或所有处理器都必须具有相同的协处理器。
所有 CPU 共享内存,并统一访问内存。
在 对称 平台中,每个 CPU 都可以访问内存、中断和访问 I/O 控制寄存器。 (相比之下,在 非对称 多处理器计算机中,一个 CPU 会接受一组从属 CPU 的所有中断。)
若要在 SMP 平台上安全运行,操作系统必须确保在一个处理器上执行的代码不会同时访问和修改另一个处理器正在访问和修改的数据。
此外,在单处理器计算机中序列化的驱动程序的 I/O 操作可以在 SMP 计算机中重叠。 也就是说,处理传入 I/O 请求的驱动程序例程可以在一个处理器上执行,而与设备通信的另一个例程在另一个处理器上并发执行。 无论内核模式驱动程序是在单处理器还是对称多处理器计算机上执行,它们都必须同步对驱动程序例程之间共享的任何驱动程序定义数据或系统提供资源的访问,并同步对物理设备的访问。
Windows NT内核使用称为自旋锁的同步机制,驱动程序可以使用该机制保护共享数据 (或设备寄存器) ,避免在对称多处理器平台上并发运行的一个或多个例程同时访问。 内核强制实施两个有关使用旋转锁的策略:
在任何给定时刻,只有一个例程可以持有特定的旋转锁。 在访问共享数据之前,必须引用数据的每个例程必须首先尝试获取数据的旋转锁。 若要访问相同的数据,另一个例程必须获取旋转锁,但在当前持有者释放旋转锁之前,无法获取旋转锁。
内核将 IRQL 值分配给系统中的每个旋转锁。 内核模式例程仅当在旋转锁的分配 IRQL 上运行该例程时,才能获取特定的旋转锁。
这些策略阻止通常以较低 IRQL 运行但当前持有旋转锁的驱动程序例程被尝试获取相同旋转锁的较高优先级驱动程序例程抢占。 因此,可以避免死锁。
分配给旋转锁的 IRQL 通常是可以获取旋转锁的最高 IRQL 例程的 IRQL。
5. IRP可重用
IRP是驱动程序工作的核心,驱动程序的一切工作基本都是围绕IRP进行,应用层往往使用IRP来控制设备的正常工作,同时I/O 管理器、PNP管理器和电源管理器也会使用 I/O 请求数据包 (IRP) 与内核模式驱动程序通信,同时windows允许驱动程序之间相互通信(基于设备树的结构保证了驱动程序可以从任何一个叶子结点遍历整个内核中所有设备,在这里设备是一个抽象概念,不仅仅包含实际硬件设备)。
IRP创建之初就考虑可重用,本身IRP就是从应用层切换到内核层时候封装的上层请求,故IRP会在不同的驱动和应用程序之间共同使用,它们看起来像下面这样:
IRP可以由系统服务函数或者内核驱动创建,它被传递给驱动程序,驱动程序可以自行决定如何处理它,同时IRP也属于I/O管理器的重要部分,I/O 管理器通过IRP来管理应用程序和设备驱动程序之间的通信;
一个IRP通常会有下面几种处理方式:
完成这个IRP;
创建新的IRP,并向下传递以完成这个IRP;
复用当前IRP,并向下传递IRP;此时我们可以在IRP上挂载一个IRP完成例程,这样当IRP在下层被完成时,我们的驱动也会得到通知;
6. 支持异步I/O请求
异步I/O对于系统性能的提升非常巨大,非常建议驱动程序提供异步 I/O 支持,以便 I/O 请求的发起方可以继续执行,而不是等待其 I/O 请求完成。 异步 I/O 支持可提高发出 I/O 请求的系统吞吐量和性能。
使用异步I/O主要是带来了系统的性能提升,和应用层不一样,驱动程序考虑系统性能的影响非常必要且重要,异步I/O意味着:
驱动程序不一定按照发送的相同顺序处理 I/O 请求,驱动程序可以在收到 I/O 请求时重新排序;
驱动程序可以将一个数据传输请求拆分为N个传输请求;
驱动程序也可以重叠 I/O 请求处理,尤其是在对称多处理器平台中,这样对性能提高非常有效;
内核模式驱动程序对单个 I/O 请求的处理不一定是序列化的,当驱动程序在开始处理下一个传入 I/O 请求之前,不一定处理每个 IRP 以完成;
驱动程序可以在设备对象的设备扩展中维护有关其当前 I/O 操作的状态信息;
和应用程序不一样的是,驱动程序只有“好”和“更好”两种状态,当代码在R0级运行时,任何细小的错误都会影响整个系统,所以如果仅仅是可以用,那么这个驱动程序很可能带来灾难。