驱动入口函数原型
驱动程序入口函数原型
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
第一个参数为 PDRIVER_OBJECT 类型,指向一个驱动对象,一个驱动文件 (sys) 运行后,操作系统在内存中为该驱动分配一个类型为 DRIVER_OBJECT 的数据结构,用于记录详细信息。
DRIVER_OBJECT
先来看看 DRIVER_OBJECT 的定义

字段说明
Type
指定驱动程序对象类型标识符,I/O 管理器在分配驱动程序对象时设置此字段,驱动程序不应使用或修改此字段。以下为类型标识符可能的值
#define IO_TYPE_ADAPTER 0x00000001
#define IO_TYPE_CONTROLLER 0x00000002
#define IO_TYPE_DEVICE 0x00000003
#define IO_TYPE_DRIVER 0x00000004
#define IO_TYPE_FILE 0x00000005
#define IO_TYPE_IRP 0x00000006
#define IO_TYPE_MASTER_ADAPTER 0x00000007
#define IO_TYPE_OPEN_PACKET 0x00000008
#define IO_TYPE_TIMER 0x00000009
#define IO_TYPE_VPB 0x0000000a
#define IO_TYPE_ERROR_LOG 0x0000000b
#define IO_TYPE_ERROR_MESSAGE 0x0000000c
#define IO_TYPE_DEVICE_OBJECT_EXTENSION 0x0000000d
#define IO_TYPE_IORING 0x0000000e
Size
指定驱动程序对象结构的大小 (以字节为单位),I/O 管理器在分配驱动程序对象时设置此字段,驱动程序不应使用或修改此字段。
DeviceObject
DEVICE_OBJECT 类型,指向驱动程序创建的设备对象链表中第一个设备对象的指针,此字段将单个驱动程序创建的所有设备链接到一个列表中。当驱动程序成功调用 IoCreateDevice 时,I/O 管理器会自动更新此成员。驱动程序可以使用此成员和 DEVICE_OBJECT 的 NextDevice 成员来遍历驱动程序创建的所有设备对象。这在驱动程序卸载期间尤其有用,可以确保所有设备对象都被正确清理。
Flags
包含系统定义的标志,用于描述驱动程序的各种属性和状态,此字段为驱动程序对象提供了一个可扩展的标志位置。这些标志由 I/O 管理器和其他系统组件设置和维护,驱动程序不应直接修改此字段。可能的值以 DRVO_ 开头的宏表示
#define DRVO_REINIT_REGISTERED 0x00000008
#define DRVO_INITIALIZED 0x00000010
#define DRVO_BOOTREINIT_REGISTERED 0x00000020
#define DRVO_LEGACY_RESOURCES 0x00000040
WDK 只暴露了部分,WDK 里的 DRVO_ 不等于内核里的全部 DRVO_,另外在上述宏定义的上方有 DO_ 开头的宏定义,此为 DEVICE_OBJECT 结构中 Flags 字段可能的值,两者不一样,注意区分
#define DO_BUFFERED_IO 0x00000004
#define DO_EXCLUSIVE 0x00000008
#define DO_DIRECT_IO 0x00000010
#define DO_MAP_IO_BUFFER 0x00000020
#define DO_DEVICE_HAS_NAME 0x00000040
#define DO_DEVICE_INITIALIZING 0x00000080
#define DO_SYSTEM_BOOT_PARTITION 0x00000100
#define DO_LONG_TERM_REQUESTS 0x00000200
#define DO_NEVER_LAST_DEVICE 0x00000400
#define DO_SHUTDOWN_REGISTERED 0x00000800
#define DO_BUS_ENUMERATED_DEVICE 0x00001000
#define DO_POWER_PAGABLE 0x00002000
#define DO_POWER_INRUSH 0x00004000
#define DO_LOW_PRIORITY_FILESYSTEM 0x00010000
#define DO_SUPPORTS_TRANSACTIONS 0x00040000
#define DO_FORCE_NEITHER_IO 0x00080000
#define DO_VOLUME_DEVICE_OBJECT 0x00100000
#define DO_SYSTEM_SYSTEM_PARTITION 0x00200000
#define DO_SYSTEM_CRITICAL_PARTITION 0x00400000
#define DO_DISALLOW_EXECUTE 0x00800000
#define DO_DEVICE_TO_BE_RESET 0x04000000
#define DO_DEVICE_IRP_REQUIRES_EXTENSION 0x08000000
#define DO_DAX_VOLUME 0x10000000
#define DO_BOOT_CRITICAL 0x20000000
DriverStart
指向驱动程序映像加载到系统内存中的虚拟基地址。该地址表示内核地址空间中驱动程序代码段的起始位置,I/O 管理器在加载驱动程序时设置此值。

DriverSize
指定驱动程序映像在内存中的大小 (以字节为单位)。此值表示已加载驱动程序的总内存占用量,包括代码、数据和其他部分,I/O 管理器在加载驱动程序时设置此值。
DriverSection
指向驱动程序节对象,该对象代表内存管理器中的驱动程序映像。这是一个不透明的系统结构,仅供内存管理器和加载程序内部使用,驱动程序不应访问或修改此成员。
DriverExtension
指向驱动程序扩展的指针,驱动程序扩展中唯一可访问的成员是 DriverExtension->AddDevice,驱动程序的 DriverEntry 将 AddDevice 存储到其中。

DriverName
包含驱动程序的 Unicode 字符串名称,错误日志线程使用此字段来确定 I/O 请求绑定到的驱动程序名称,格式通常为 \Driver\DriverName,其中 DriverName 对应于驱动程序在注册表中的服务名称。I/O 管理器会根据驱动程序的注册表配置设置此值。

HardwareDatabase
指向注册表中硬件配置信息的 \Registry\Machine\Hardware 路径的指针。

FastIoDispatch
指向定义驱动程序快速 I/O 入口点的 FAST_IO_DISPATCH 结构的指针,此可选指针指向一个数组,其中包含驱动程序用于快速 I/O 支持的备用入口点。快速 I/O 通过使用单独的参数直接调用驱动程序例程来执行,而不是使用标准的 IRP 调用机制。请注意,这些函数只能用于同步 I/O,并且仅当文件已缓存时才可执行。此成员仅供文件系统驱动程序 (FSD) 和网络传输驱动程序使用。
DriverInit
I/O 管理器在驱动程序加载时会将此字段设置为指向驱动程序的初始化函数。这是驱动程序加载到内存后调用的第一个函数,负责初始化驱动程序对象并设置调度例程。这个跟 DriverStart 不一样,再次强调,DriverStart 指向 DriverEntry 入口

DriverStartIo
驱动程序的 StartIo 例程的入口点,由驱动程序初始化时的 DriverEntry 例程设置。如果驱动程序没有 StartIo 例程,则此成员为 NULL 。
DriverUnload
驱动程序卸载例程的入口点,由驱动程序初始化时的 DriverEntry 例程设置。如果驱动程序没有卸载例程,则此成员为 NULL 。

MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1]
DRIVER_DISPATCH 类型成员数组,其中列出了驱动程序 DispatchXxx 例程的入口点。该数组的索引值是 IRP_MJ_XXX 值,代表每个 IRP 主功能代码。宏定义
#define IRP_MJ_CREATE 0x00
#define IRP_MJ_CREATE_NAMED_PIPE 0x01
#define IRP_MJ_CLOSE 0x02
#define IRP_MJ_READ 0x03
#define IRP_MJ_WRITE 0x04
#define IRP_MJ_QUERY_INFORMATION 0x05
#define IRP_MJ_SET_INFORMATION 0x06
#define IRP_MJ_QUERY_EA 0x07
#define IRP_MJ_SET_EA 0x08
#define IRP_MJ_FLUSH_BUFFERS 0x09
#define IRP_MJ_QUERY_VOLUME_INFORMATION 0x0a
#define IRP_MJ_SET_VOLUME_INFORMATION 0x0b
#define IRP_MJ_DIRECTORY_CONTROL 0x0c
#define IRP_MJ_FILE_SYSTEM_CONTROL 0x0d
#define IRP_MJ_DEVICE_CONTROL 0x0e
#define IRP_MJ_INTERNAL_DEVICE_CONTROL 0x0f
#define IRP_MJ_SHUTDOWN 0x10
#define IRP_MJ_LOCK_CONTROL 0x11
#define IRP_MJ_CLEANUP 0x12
#define IRP_MJ_CREATE_MAILSLOT 0x13
#define IRP_MJ_QUERY_SECURITY 0x14
#define IRP_MJ_SET_SECURITY 0x15
#define IRP_MJ_POWER 0x16
#define IRP_MJ_SYSTEM_CONTROL 0x17
#define IRP_MJ_DEVICE_CHANGE 0x18
#define IRP_MJ_QUERY_QUOTA 0x19
#define IRP_MJ_SET_QUOTA 0x1a
#define IRP_MJ_PNP 0x1b
#define IRP_MJ_PNP_POWER IRP_MJ_PNP // Obsolete....
#define IRP_MJ_MAXIMUM_FUNCTION 0x1b
为了保持对象的可扩展性,此主功能调度表必须是对象的最后一个字段。每个驱动程序都必须在此数组中为其处理的 IRP_MJ_XXX 请求设置入口点。为了帮助验证分析工具,每个 DispatchXxx 例程都应使用 DRIVER_DISPATCH 类型声明
DRIVER_DISPATCH DispatchXxx;
DRIVER_DISPATCH 在 wdm.h 头文件中定义
//
// Define driver dispatch routine type.
// The default is that it can be called <= DISPATCH
// because it might be called from another driver.
// See also below.
//
_Function_class_(DRIVER_DISPATCH)
_IRQL_requires_max_(DISPATCH_LEVEL)
_IRQL_requires_same_
typedef
NTSTATUS
DRIVER_DISPATCH (
_In_ struct _DEVICE_OBJECT *DeviceObject,
_Inout_ struct _IRP *Irp
);
typedef DRIVER_DISPATCH *PDRIVER_DISPATCH;
回调例程的实现方式应如下
_Use_decl_annotations_
NTSTATUS DispatchXxx(struct _DEVICE_OBJECT* DeviceObject, struct _IRP* Irp) {
// ......
}
为了在运行代码分析工具时更准确地识别错误,请务必在函数定义中添加 _Use_decl_annotations 注解。_Use_decl_annotations_ 注解确保使用头文件中应用于 DRIVER_DISPATCH 函数类型的注解。在第一个驱动程序例子中并没有设置,所以显示无效的设备请求,每一项对应上述宏每项

UNICODE_STRING
入口函数的第二个参数 RegistryPath 为 PUNICODE_STRING 类型,UNICODE_STRING 是个结构体,包含三个字段
typedef struct _UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
#ifdef MIDL_PASS
[size_is(MaximumLength / 2), length_is((Length) / 2) ] USHORT * Buffer;
#else // MIDL_PASS
_Field_size_bytes_part_opt_(MaximumLength, Length) PWCH Buffer;
#endif // MIDL_PASS
} UNICODE_STRING;
Length 表示 Buffer 所指向缓冲区中字符串的长度,单位为字节,需要注意的是,Buffer 指向的字符串,并不要求以 \0 作为结束,在大多数情况下,Buffer 指向的字符串没有以 \0 结尾。MaximumLength 表示 Buffer 所指向缓冲区的总空间大小,一般等于 Buffer 被分配时的内存大小,单位为字节。

例如上述字符串包含 65 个字符,因为为宽字符,所以长度为 130,十六进制为 0x82。
驱动文件 (sys) 要运行,即要加载到内核中,需要以服务的方式进行,首先要把这个驱动文件注册成一个服务,注册成功后,系统会把服务信息写入到注册表中,路径为
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services

DisplayName 为显示的服务名称,ErrorControl 驱动程序加载失败时系统应如何响应,ImagePath 表示驱动程序二进制文件的路径,Start 为启动类型,表示驱动程序何时加载
- boot – 启动
- system – 系统
- automatic – 自动
- demand – 按需
- disabled – 禁用
Type 为服务类型,例如内核驱动程序、文件系统驱动程序等。以服务的名字作为一个注册表的键名,由于我这个是用第三方驱动加载工具,给出的默认服务名为 MyDriver1_Service,所以 RegistryPath 应为
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\MyDriver1_Service
但是这与上面图中路径不相符,这是因为内核态与用户态下对注册表的路径表示方式有差异
| 用户态 | 内核态 |
|---|---|
| HKEY_LOCAL_MACHINE | \Registry\Machine |
| HKEY_USERS | \Registry\User |
| HKEY_CURRENT_USER | \Registry\User\<SID> |
函数体内容
函数体中 DbgPrint 函数用于打印字符串,打印的字符串需在 dbgview 中查看,用法与 C 语言中的 printf 差不多,在判断驱动对象不为 NULL 的情况下为 DriverUnload 成员赋予了卸载函数,何时调用,因为驱动运行是以服务方式进行,当服务停止时,系统把该驱动模块对应在内核地址空间中的代码及数据移除,DriverUnload 成员指定的函数会被调用,开发者可以在这个函数中执行清理相关的工作。
驱动卸载函数非常重要,但也不是必须,它是可选的,如果不指定,驱动运行后就无法停止,该特性被很多安全软件利用,刻意不提供,避免驱动被恶意停止。
返回值
返回值为 NSTATUS 类型,它是一个 LONG 类型,STATUS_SUCCESS 表示初始化成功,如果返回的是该值的以外值,则表示驱动初始化失败,用户态则表现为服务启动失败
#define STATUS_SUCCESS ((NTSTATUS)0x00000000L) // ntsubauth
系统发现驱动初始化失败会移除内核地址空间的驱动代码与数据,这个操作与驱动服务停止类似,但是初始化失败不会去调用驱动卸载函数。