DriverEntry
每个驱动程序必须具有 DriverEntry 例程,用于初始化驱动程序范围的数据结构和资源。
在支持即插即用 (PnP) 的驱动程序中,与所有驱动程序一样,DriverEntry 例程负责驱动程序初始化,而 AddDevice 例程负责设备初始化。 驱动程序初始化包括导出驱动程序的其他入口点、初始化驱动程序使用的某些对象以及设置各种每个驱动程序的系统资源。
DriverEntry 例程在 IRQL = PASSIVE_LEVEL 的系统线程上下文中调用,DriverEntry 例程可以分页,并且应位于 INIT 段中,因此将被换出内存。
必要工作流程
下面的流程是大部分驱动必须完成的:
1. 为驱动程序的标准例程提供入口点。
驱动程序将许多标准例程的入口点存储在驱动程序对象或驱动程序扩展中。 此类入口点包括驱动程序的 AddDevice 例程、调度例程、 StartIo 例程和 卸载 例程的入口点。 例如,驱动程序会使用如下语句为其 AddDevice、 DispatchPnP 和 DispatchPower 例程设置入口点:
typedef struct _DRIVER_OBJECT
{CSHORT Type; // 类型CSHORT Size; // 结构大小PDEVICE_OBJECT DeviceObject; // 指向驱动创建的第一个设备对象// 当驱动程序成功调用 IoCreateDevice 时,此成员会自动更新ULONG Plags; // PVOID DriverSize; // PVOID DriverSection; // PDRIVER_EXTENSION DriverExtension; // 驱动对象的扩展UNICODE_STRING DriverName; // PUNICODE_STRING HardwareDatabase; // 注册表中硬件配置信息PFAST_IO_DISPATCH FastIoDispatch; // 仅由 FSD 和网络传输驱动程序PDRIVER_INITIALIZE DriverInit; // DriverEntry例程的指针,系统已设置;PDRIVER_STARTIO DriverStartIo; // StartIo 例程指针PDRIVER_UNLOAD DriverUnload; // Unload 例程指针PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1]; // 由驱动程序的 DispatchXxx 例程的指针数组组成的调度表
}DRIVER_OBJECT;
typedef struct _DRIVER_OBJECT *PDRIVER_OBJECT;
2. 创建和/或初始化驱动程序使用的各种驱动程序范围对象、类型或资源。
请注意,大多数标准例程基于每个设备使用对象,因此驱动程序应在其 AddDevice 例程中或在收到 IRP_MN_START_DEVICE 请求后设置此类对象。
3. 如果驱动程序具有设备专用线程或等待任何内核定义的调度程序对象, DriverEntry 例程可能会初始化 内核调度程序对象。 (根据驱动程序如何使用对象,它可能会改为在其 AddDevice 例程中或在收到 IRP_MN_START_DEVICE 请求后执行此任务。)
4. 释放已分配且不再需要的任何内存。
5. 返回 NTSTATUS,指示驱动程序是否已成功加载,并且可以接受和处理来自 PnP 管理器的请求,以配置、添加和启动其设备。
可选工作流程
根据特定驱动程序在分层驱动程序链中的位置、基础设备的性质, DriverEntry 例程也可能负责以下事项:
1. 如果驱动程序需要存储驱动程序范围的数据,则调用 IoAllocateDriverObjectExtension 来创建和初始化驱动程序对象扩展, 驱动程序对象扩展是特定于驱动程序的数据结构。
2.如果驱动程序是使用此类线程的最高级别的驱动程序 ((例如文件系统驱动程序) ),则调用 PsCreateSystemThread 来创建执行工作线程。 在这种情况下,驱动程序还必须具有WORKER_THREAD_ROUTINE类型的回调例程,该例程采用单个输入 PVOID 参数。
3. 注册 重新初始化 例程;
4. 处理不同于此处讨论的特定于类的初始化要求:例如与端口或类驱动程序协同工作的特定于设备的微型端口或微型类驱动程序可能具有的要求。
5. 为系统资源提供存储区域:
每个设备的对象应在 AddDevice 例程或处理 PnP IRP_MN_START_DEVICE请求的 Dispatch 例程中分配,而不是在 DriverEntry 中分配。
但是,驱动程序可能需要为其他驱动程序范围的使用分配额外的系统空间内存。 如果是这样, DriverEntry 例程可以调用以下例程的一 个或多个 :
A.IoAllocateDriverObjectExtension,用于创建与驱动程序对象关联的上下文区域;
B.ExAllocatePoolWithTag分配分页或非分页系统空间内存;
C.适用于缓存对齐的非分页系统空间内存的 MmAllocateNonCachedMemory 或 MmAllocateContiguousMemory (用于 I/O 缓冲区)
每个 DriverEntry 例程在 IRQL = PASSIVE_LEVEL 的系统线程上下文中运行。 因此,使用 ExAllocatePoolWithTag 分配的、在初始化期间以独占方式使用的任何内存都可以来自分页池,前提是驱动程序不控制保存系统页文件的设备。 在 DriverEntry 返回控制权之前,必须使用 ExFreePool 释放分配的内存。
但是如果设置 Reinitialize 例程的驱动程序,那么可以在调用 IoRegisterDriverReinitialization 时传递指向此内存的指针,从而使驱动程序的 Reinitialize 例程负责释放内存分配。
6. 声明硬件资源
较旧的非 PnP 驱动程序从注册表中声明资源。 另一方面,PnP 驱动程序既不从中声明设备资源,也不直接将资源要求写入注册表。 相反,这些驱动程序会报告要求以响应某些 PnP IRP,作为 PnP 管理器枚举过程的一部分。 PnP 驱动程序在 PnP IRP_MN_START_DEVICE 请求中接收其分配的资源。
7. 使用注册表
DriverEntry 例程可能使用注册表来获取初始化驱动程序所需的一些信息,也可能在注册表中设置信息以供其他驱动程序或受保护的子系统使用。 信息的性质取决于设备类型。 驱动程序可以使用 ZwXxx 和 RtlXxx 例程访问注册表。 DriverEntry 例程的 RegistryPath 参数指向一个计数的 Unicode字符串,该字符串指定驱动程序的注册表项 \Registry\Machine\System\CurrentControlSet\Services\*DriverName 的路径。例程应保存字符串的副本,而不是指针本身,因为DriverEntry 返回后指针不再有效。
AddDevice
设备对象和驱动对象是两个概念,驱动文件可以为N个设备所加载,但是它们在内核中始终只有一份拷贝,可以理解为C++中类和对象的关系,故如果N个设备连接到系统总线上,则它们每个对应的是自己的设备对象,共享的是驱动文件对象。
AddDevice例程就是在新设备插入的时候,系统自动调用的例程。在一个设备第一次插入系统时,如果之前没有插入过,那么系统会先查找有无VID和PID匹配的驱动,windows发布的时候,理论上所有已知类别的驱动都自带了的;如果没有,那么会尝试联网搜索Update;如果也没有,那么就报错,要求用户自行安装;如果找到了匹配的驱动,那么会先调用DriverEntry例程,随后调用AddDevice例程;当同类型的设备再次插入系统时候,就直接调用AddDevice例程,这里将其想象为DLL,就能明白怎么回事了。
编写 AddDevice 例程时,考虑以下设计准则:
1. 如果Filter驱动程序确定为其不需要服务的设备调用了 AddDevice 例程,则Filter驱动程序必须返回STATUS_SUCCESS,以允许为设备加载设备堆栈的其余部分。Filter驱动程序不会创建设备对象,也不会将其附加到设备堆栈; Filter驱动程序的AddDeivce仅返回成功,并允许将其余驱动程序添加到堆栈。
2. 驱动程序必须为它使用的任何内核定义对象和执行旋转锁提供存储,,通常位于设备对象的设备扩展中。 可能会为驱动程序的需求分配额外的系统空间内存,例如用于长期 I/O 缓冲区或List列表。
3. 如果驱动程序具有设备专用线程或等待任何内核定义的调度线程对象, 则 AddDevice 例程可能会初始化内核调度线程对象。
4. 如果驱动程序使用任何执行旋转锁或为中断旋转锁提供存储, 则 AddDevice 例程可能会初始化这些旋转锁。
5. 在调用 IoCreateDevice 时加强文件打开安全性。指定调用 IoCreateDevice 时FILE_DEVICE_SECURE_OPEN特征。 Windows NT 4.0 SP5 及更高版本支持此特征。如果驱动程序在调用 IoCreateDevice 时设置了FILE_DEVICE_SECURE_OPEN特征,则 I/O 管理器会将设备对象的安全描述符应用于任何相对打开或尾随文件名打开。
功能/Filter驱动的AddDevice
功能或Filter驱动程序中的 AddDevice 例程应执行以下步骤:
1. 调用 IoCreateDevice 以 (FDO 或Filter DO) 为要添加的设备创建功能或Filter设备对象:
不要为设备对象指定 DeviceName ,因为这样做会绕过 PnP 管理器的安全性。 如果用户模式组件需要指向设备的符号链接,请注册设备接口。 如果内核模式组件需要旧设备名称,驱动程序必须命名设备对象,但不建议命名。
在 DeviceCharacteristics 参数中包含FILE_DEVICE_SECURE_OPEN。 此特征指示 I/O 管理器针对所有打开的请求(包括相对打开和尾随文件名打开)的设备对象执行安全检查。
2. [可选]创建一个或多个指向设备的符号链接:
调用 IoRegisterDeviceInterface 来注册设备功能,并创建应用程序或系统组件可用于打开设备的符号链接。 驱动程序应在处理IRP_MN_START_DEVICE请求时通过调用 IoSetDeviceInterfaceState 来启用 接口 。 有关详细信息,请参阅 设备接口类。
3. 将指向设备 PDO 的指针存储在设备扩展中:
PnP 管理器提供指向 PDO 的指针,作为 AddDevice 的 PhysicalDeviceObject 参数。 驱动程序在调用 IoGetDeviceProperty 等例程时使用 PDO 指针。
4. 在设备扩展中定义标志以跟踪设备的某些 PnP 状态,例如设备暂停、删除和意外删除:
此外,在设备扩展中分配 IO_REMOVE_LOCK 结构,并调用 IoInitializeRemoveLock 来初始化此结构。
5. 在设备对象中设置DO_BUFFERED_IO或DO_DIRECT_IO标志位,以指定 I/O 管理器用于发送到设备堆栈的 I/O 请求的缓冲类型。 较高级别的驱动程序使用“或”来设置此成员,其值与堆栈中下一个较低级别的驱动程序相同,但可能最高级别驱动程序除外。
6. 如有必要,请设置电源管理的DO_POWER_INRUSH或DO_POWER_PAGABLE标志。 可分页的驱动程序必须设置DO_POWER_PAGABLE标志。 设备对象标志通常由总线驱动程序在为设备创建 PDO 时设置。 但是,较高级别的驱动程序在创建 FDO 或Filter DO 时,有时可能需要在其 AddDevice 例程中更改这些标志的值。
7. 创建和/或初始化驱动程序用于管理此设备的任何其他软件资源,例如事件、旋转锁或其他对象。 (硬件资源(如 I/O 端口)稍后配置,以响应 IRP_MN_START_DEVICE 请求。)
由于 AddDevice 例程在 IRQL = PASSIVE_LEVEL 的系统线程上下文中运行,因此,只要驱动程序不控制保存系统页文件的设备,使用 ExAllocatePoolWithTag 在初始化期间专门使用的任何内存都可以来自分页池。 在 AddDevice 返回控制权之前,必须使用 ExFreePool 释放此类内存分配。
8. 将设备对象附加到设备堆栈 (IoAttachDeviceToDeviceStack) :
在 TargetDevice 参数中指定指向设备的 PDO 的指针。
存储 IoAttachDeviceToDeviceStack 返回的指针。 此指针指向设备下一低级驱动程序的设备对象,是向设备堆栈传递 IRP 时 IoCallDriver 和 PoCallDriver 的必需参数。
9. 使用如下所示的语句清除 FDO 或Filter DO 中的DO_DEVICE_INITIALIZING标志:
FunctionalDeviceObject->Flags &= ~DO_DEVICE_INITIALIZING;
10. 准备好处理设备 (的 PnP IRP,例如 IRP_MN_QUERY_RESOURCE_REQUIREMENTS 和 IRP_MN_START_DEVICE) 。
注意: 驱动程序在收到包含 PnP 管理器分配给设备的硬件资源列表 的IRP_MN_START_DEVICE 之前,不得开始控制设备。
总线驱动的AddDevice例程
PnP 总线驱动程序有一个AddDevice例程,但当总线驱动程序充当其控制器或适配器的功能驱动程序时,会调用它。例如,PnP 管理器调用 USB 总线驱动程序的AddDevice例程来添加Hub设备。但是总线驱动程序的AddDevice例程不会为子设备(插入总线的设备)调用。
和我们想象不太一样,总线驱动的AddDevice不会引发子设备的的AddDevice调用,在系统启动时,总线驱动的AddDevice会被调用,在它调用返回之后,才会调用枚举总线设备的例程,依次枚举总线上的设备,为它们调用Entry和AddDevice。
此处有一个歧义,我们会称 "USB总线"和"PCI总线",在实际上,这两个总线如下图:
从PCI总线那里看,USB总线是它的功能驱动;从USB设备上看,USB总线是它的总线驱动,我们可以将驱动栈中位于特定驱动之下的驱动认为是总线驱动。
Dispatch例程
在应用层和驱动通讯的时候,我们往往会调用DeviceIoControl函数来向驱动获取我们需要的服务,其中有一个函数参数是Ioctl,它往往代表了驱动能提供的服务有哪些,这些Ioctl到驱动之后,都会被统一交给Dispatch例程处理,该例程一般是一个巨大的switch语言,每条case字句对应一个调度例程;
每个具体的调度例程所需的功能会有所不同,具体取决于它处理的 I/O 函数代码、单个驱动程序在驱动程序链中的位置以及基础物理设备的类型。
大多数调度例程处理传入的 I/O 请求数据包额方式,如下所示:
1. 检查 IRP 中驱动程序的 I/O 堆栈位置,以确定要执行的操作,并检查参数(如果有)的有效性。
驱动程序是否必须检查其 I/O 堆栈位置来确定要执行的操作和检查参数取决于给定IRP_MJ_XXX,以及该驱动程序是否为驱动程序处理的每个IRP_MJ_XXX 设置单独的 Dispatch 例程。
2.满足请求并完成 IRP;否则,请将其传递,以供较低级别的驱动程序或其他设备驱动程序例程进一步处理。
驱动程序是否必须传递 IRP 进行进一步处理取决于参数的有效性,以及 IRP_MJ_XXX 以及分层驱动程序链中驱动程序的级别。
必须实现的派发例程
驱动程序必须处理以下 Dispatch 例程:
DispatchPnP: IRP_MJ_PNP 指示涉及 PnP 设备识别、硬件配置或资源分配的请求。 此类请求通常从 PnP 管理器或紧密耦合的更高级别的驱动程序发送到设备驱动程序。
DispatchPower: IRP_MJ_POWER 指示与设备或系统的电源状态相关的请求。 此类请求由电源管理器或紧密耦合的更高级别的驱动程序发送到设备驱动程序。
DispatchCreate: IRP_MJ_CREATE 指示用户模式保护子系统(可能代表应用程序或特定于子系统的驱动程序)已请求与目标设备对象关联的文件对象的句柄,或者更高级别驱动程序正在将其设备对象连接或附加到目标设备对象。
DispatchClose: IRP_MJ_CLOSE 指示已关闭并释放与目标设备对象关联的文件对象的最后一个句柄。 所有 I/O 请求都已完成或取消,因此没有对文件对象指针的未完成引用。
DispatchRead: IRP_MJ_READ 指示将数据从基础物理设备传输到系统的 I/O 请求。
DispatchWrite: IRP_MJ_WRITE 指示将数据从系统传输到基础物理设备的 I/O 请求。
DispatchDeviceControl: IRP_MJ_DEVICE_CONTROL 指示包含系统定义的特定于设备类型的 I/O 控制代码的请求,该代码指定特定于设备类型的操作。 较高级别的驱动程序将这些 IRP 传递给其基础设备驱动程序,后者通常通过访问设备来处理请求。
DispatchInternalDeviceControl: IRP_MJ_INTERNAL_DEVICE_CONTROL 指示发送到设备驱动程序的请求,在大多数情况下是从紧密耦合的较高级别驱动程序发送的,通常带有专用定义的、特定于驱动程序和设备类型的或特定于设备的 I/O 控制代码,这些代码请求特定于设备类型或特定于设备的操作。
仅需要某些类型的驱动程序来处理系统定义的内部设备 I/O 控制请求,包括某些 SCSI 驱动程序、键盘或鼠标设备驱动程序,以及与系统提供的驱动程序互操作的并行驱动程序。
DispatchSystemControl: IRP_MJ_SYSTEM_CONTROL 用于指定对驱动程序的 WMI 请求。
可选的驱动例程
驱动程序可能包括以下调度例程:
DispatchCleanup:IRP_MJ_CLEANUP 指示正在关闭与目标设备对象关联的文件对象的最后一个句柄。 文件对象的未完成 I/O 请求可能仍然存在。 驱动程序可以实现 DispatchCleanup 例程,以执行不特定于任何特定文件句柄的清理。 驱动程序还可以将其 DispatchClose 例程用于同一目的。
DispatchQueryInformation、 DispatchSetInformation:某些最高级别的驱动程序可能需要处理 IRP_MJ_QUERY_INFORMATION 和 IRP_MJ_SET_INFORMATION IRP。 此类请求指示用户模式应用程序、内核模式组件或驱动程序已请求有关文件对象长度的信息, (表示驱动程序的设备对象) 用户模式请求者具有句柄,或者用户模式请求者正在尝试在该文件对象上设置文件结尾。
DispatchFlushBuffers:在设备中缓存数据或在驱动程序分配的内存内部缓冲数据的驱动程序可能会收到 IRP_MJ_FLUSH_BUFFERS。 收到此请求指示驱动程序应写入其缓冲数据或将缓存数据刷新到设备,或者应放弃从设备读取的缓冲或缓存数据。
DispatchShutdown:在系统关闭之前可能调用的任何驱动程序都必须处理 IRP_MJ_SHUTDOWN。 在电源管理器发送系统设置电源 IRP 以关闭系统之前, DispatchShutdown 例程应执行驱动程序确定所需的任何清理。 驱动程序可以调用 IoRegisterShutdownNotification 或 IoRegisterLastChanceShutdownNotification 来注册关闭通知。
在内部缓存数据的大容量存储设备的驱动程序必须提供 DispatchShutdown 和 DispatchFlushBuffers 例程。 如果大容量存储驱动程序在内存中缓冲数据,但其设备没有内部缓存,则它还必须提供 DispatchShutdown 和 DispatchFlushBuffers 例程。
处理 IRP_MJ_FLUSH_BUFFERS 和 IRP_MJ_SHUTDOWN 请求的驱动程序之上的任何中间驱动程序也提供 DispatchShutdown 和 DispatchFlushBuffers 例程。
Unload例程
和DriverEntry不太一样的是,Unload例程和该驱动是否是PNP类型的有关,这并不难理解,当最后一个PNP设备从系统重移除后,对应驱动也会随之卸载;如果非PNP设备,那么意外移除意味着灾难,如果带电的情况下拔插显卡,很可能带来意外的因素。
支持PNP的驱动的Unload例程
PnP 驱动程序必须具有 Unload 例程,该例程可删除由 DriverEntry 例程创建的任何特定于驱动程序的资源,例如内存、线程和事件。 如果没有特定于驱动程序的资源要删除,驱动程序仍必须具有 Unload 例程,但它只能返回。
删除驱动程序的所有设备后,可以随时调用驱动程序的 Unload 例程。 PnP 管理器在 IRQL = PASSIVE_LEVEL 的系统线程上下文中调用驱动程序的 Unload 例程。
PnP 驱动程序释放特定于设备的资源和设备对象,以响应 PnP 设备删除 IRP。 PnP 管理器代表它枚举的每个 PnP 设备以及驱动程序使用 IoReportDetectedDevice 报告的任何根枚举旧设备发送这些 IRP。
因此,PnP 驱动程序的 Unload 例程通常很简单,通常仅由 返回 语句组成。 但是,如果驱动程序在其 DriverEntry 例程中分配了任何驱动程序范围的资源,则必须在其 Unload 例程中解除分配这些资源,除非它已经这样做。 通常,卸载 PnP 驱动程序的过程是同步操作。
I/O 管理器释放驱动程序对象以及驱动程序使用 IoAllocateDriverObjectExtension 分配的任何驱动程序对象扩展。
不支持Pnp的驱动的Unload
不处理 PnP 设备删除请求的早期驱动程序和高级文件系统驱动程序必须在其 Unload 例程中释放资源、删除设备对象以及从设备堆栈分离。
如果尚未执行此操作,旧设备驱动程序在其 Unload 例程中应做的第一件事是禁用来自设备的中断。 否则,当 Unload 例程释放 ISR 处理中断所需的设备扩展中的资源时,可能会调用其 ISR 来处理设备中断。 即使 ISR 在这些情况下成功运行,ISR 排队的 DpcForIsr 或 CustomDpc 例程,以及可能以 IRQL >= DISPATCH_LEVEL 运行的其他驱动程序例程,也会在 Unload 例程重新获得控制权之前执行,从而增加 Unload 例程删除另一个驱动程序例程引用的资源的可能性。
禁用中断后,文件系统和旧版驱动程序必须释放资源和对象。