Windows内核 基本数据结构
驱动对象:
每个驱动程序都会有唯一的驱动对象与之对应,并且这个驱动对象是在驱动加载时被内核中的对象管理程序所创建的。驱动对象用DRIVER_OBJECT数据结构表示,它作为驱动的一个实例被内核加载,并且内核对一个驱动只加载一个实例。确切地说,是由内核中的I/O管理器负责加载的,驱动程序需要在DriverEntry中初始化。驱动对象的结构定义如下(wdm.h):
typedef struct _DRIVER_OBJECT {
//结构的类型和大小
CSHORT Type;
CSHORT Size;
//每个驱动程序会有一个或多个设备对象,其中,每个设备对象都有一个指针指向下一个驱动对象
//最后一个设备对象指向空。DeviceObject指向驱动对象的第一个设备对象。通过DeviceObject,就
//可以遍历驱动对象中的所有设备对象了。
PDEVICE_OBJECT DeviceObject;
ULONG Flags;
// The following section describes where the driver is loaded. The count
// field is used to count the number of times the driver has had its
// registered reinitialization routine invoked.
PVOID DriverStart;
ULONG DriverSize;
PVOID DriverSection;
PDRIVER_EXTENSION DriverExtension;
// 记录驱动设备的名字,用UNICODE字符串记录,该字符串一般/Driver/[驱动程序名称]
UNICODE_STRING DriverName;
//设备的硬件数据库键名,也是UNICODE字符串记录。一般为
// /REGISTRY/MACHINE/HADRWARE/DESCRIPTION/SYSTEM
PUNICODE_STRING HardwareDatabase;
//文件驱动中用到的派遣函数
PFAST_IO_DISPATCH FastIoDispatch;
// The following section describes the entry points to this particular
// driver. Note that the major function dispatch table must be the last
// field in the object so that it remains extensible.
PDRIVER_INITIALIZE DriverInit;
//记录StartIO例程的函数地址,用于串行化操作
PDRIVER_STARTIO DriverStartIo;
//指定驱动卸载时所用的回调函数地址
PDRIVER_UNLOAD DriverUnload;
//MajorFunction域记录的是一个函数指针数组,也就是MajorFunction是一个数组,数组中的每个
//成员记录着一个指针,每一个指针指向的是一个函数。这个函数是处理IRP的派遣函数
PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];
} DRIVER_OBJECT;
typedef struct _DRIVER_OBJECT *PDRIVER_OBJECT;
实际上如果写一个驱动程序,或者说编写一个内核模块,要在Windows中加载,就必须填写上面的结构,来告诉Windows程序提供的功能。注意内核模块并不生成一个进程,它只是写一组回调函数让Windows调用,且这组回调函数必须符合Windows内核规定的格式。上面代码中的“快速IO分发函数”FastIoDispatch和“普通分发函数”MajorFunction就是这样一种回调函数。这些函数用来处理发送给这个内核模块的请求。
Windows中很多组件都拥有自己的DRIVER_OBJECT,例如:所有的硬件驱动程序、所有的类驱动(Disk、Cdrom…)、文件系统(NTFS和FastFat,有各自的DRIVER_OBJECT),以及许多其他的内核组件。我们可以使用一个软件WinObj来查看所有的内核对象。
设备对象:
每个驱动程序会创建一个或多个设备对象,用DEVICE_OBJECT数据结构表示。每个设备对象都会有一个指针指向下一个设备对象,因此就形成一个设备链。设备链的第一个设备是由DRIVER_OBJECT结构体中指明的。
设备对象是内核中的重要对象,其重要性不亚于Windows GUI编程中的窗口。窗口是唯一可以接收消息的对象,任何消息都是发送到一个窗口中的;而在内核编程中,大部分“消息”是以请求IRP的方式传递的。而设备对象(DEVICE_OBJECT)是唯一可以接收请求的实体,任何一个请求IRP都是发送给某个设备对象的。
设备对象的结构是DEVICE_OBJECT,常常被简称为DO。一个DO可以代表很多不同的东西,例如一个实际的硬盘、或实现一个类似管道的功能等等。我们总是在内核程序中生成一个DO,而一个内核程序是用一个驱动对象表示的,因此,一个设备对象总是属于一个驱动对象。
在WDK的wdm.h文件中DO的定义如下:
typedef struct DECLSPEC_ALIGN(MEMORY_ALLOCATION_ALIGNMENT)
_DEVICE_OBJECT {
//结构的类型和大小
CSHORT Type;
USHORT Size;
//引用计数
LONG ReferenceCount;
//指向驱动程序中的驱动对象,同属于一个驱动程序的驱动对象指向的是同一驱动对象
struct _DRIVER_OBJECT *DriverObject;
//下一个设备对象。
//这里指的下一个设备对象是同属于一个驱动对象的设备,也就是同一个驱动程序创建的若干设备
//对象,每个设备对象根据NextDevice域形成链表,从而可以枚举每个设备对象
struct _DEVICE_OBJECT *NextDevice;
//指向下一个设备对象,这里指的是,如果有更高一层的驱动附加到这个驱动的时候
//AttachedDevice指向的就是那个更高一层的驱动
struct _DEVICE_OBJECT *AttachedDevice;
//在使用StartIO例程的时候,此域指向的是当前IRP结构
struct _IRP *CurrentIrp;
PIO_TIMER Timer;
//此域是一个32位的无符号整型,每一位有具体的含义
//DO_BUFFERED_IO---读写操作使用缓冲方式(系统复制缓冲区)访问用户模式数据
//DO_EXCLUSIVE---一次只允许一个线程打开设备句柄
//DO_DIRECT_IO---读写操作使用直接方式(内存描述符表)访问用户模式数据
//DO_DEVICE_INITIALIZING---设备对象正在初始化
//DO_POWER_PAGABLE---必须在PASSIVE_LEVEL级上处理IRP_MJ_PNP请求
//DO_POWER_INRUSH---设备上电期间需要大电流
ULONG Flags;
ULONG Characteristics;
__volatile PVPB Vpb;
//指向设备扩展对象,每个设备都会指定一个设备扩展对象,设备扩展对象记录的是设备自己
//特殊定义的结构体,即程序员自己定义的结构体。另外,在驱动开发中,应该尽量避免全局变量的
//使用,因为全局变量涉及不容易同步问题。解决的方法是:将全局变量存在设备扩展中
PVOID DeviceExtension;
//设备类型,当制作虚拟设备时,应选择FILE_DEVICE_UNKNOWN类型的设备
DEVICE_TYPE DeviceType;
//IRP栈大小。在多层驱动情况下,驱动与驱动之间会形成类似堆栈的结构,IRP会依次从
//最高层传递到最底层
CCHAR StackSize;
union {
LIST_ENTRY ListEntry;
WAIT_CONTEXT_BLOCK Wcb;
} Queue;
//设备在大容量传输时,需要内存对齐,以保证传输速度
ULONG AlignmentRequirement;
KDEVICE_QUEUE DeviceQueue;
KDPC Dpc;
// The following field is for exclusive use by the filesystem to keep
// track of the number of Fsp threads currently using the device
ULONG ActiveThreadCount;
PSECURITY_DESCRIPTOR SecurityDescriptor;
KEVENT DeviceLock;
USHORT SectorSize;
USHORT Spare1;
struct _DEVOBJ_EXTENSION *DeviceObjectExtension;
PVOID Reserved;
} DEVICE_OBJECT;
typedef struct _DEVICE_OBJECT *PDEVICE_OBJECT;
一个驱动对象可以生成多个设备对象,而Windows向设备对象发送请求时,这些请求是被驱动对象的分发函数所捕获的,即当Windows内核向一个设备发送一个请求时,驱动对象的分发函数中的某一个会被调用,分发函数原型如下:
//参数device是请求的目标设备;参数irp是请求的指针
NTSTATUE ASCEDispatch(PDEVICE_OBJECT device, PIRP irp);
附:设备扩展:
设备对象记录“通用”设备的信息,而另外一些“特殊”信息记录在设备扩展里。各个设备扩展由程序员自己定义,每个设备的设备扩展不尽相同。设备扩展是由程序员指定内容和大小,由I/O管理器创建的,并保存在非分页内存中。
在驱动程序中,尽量避免使用全局函数,因为全局函数往往导致函数的不可重入性。重入性指的是在多线程程序中,多个函数并行运行,函数的运行结果不会根据函数的调用先后顺序而导致不同。解决的办法是,将全局变量以设备扩展的形式存储,并加以适当的同步保护措施。除此之外,在设备扩展中还会记录下列一些内容:
设备对象的反向指针;
设备状态或驱动环境变量;
中断对象指针;
控制器对象指针。
由于设备扩展是驱动程序专用的,它的结构必须在驱动程序的头文件中定义。
请求IRP:
内核中大部分请求以IRP的形式发送。IRP是一个内核数据结构,比较复杂,因为它要表示无数种实际请求。在WDK的wdm.h中可看到IRP的结构:
// I/O Request Packet (IRP) definition
typedef struct DECLSPEC_ALIGN(MEMORY_ALLOCATION_ALIGNMENT) _IRP {
//结构的类型和大小
CSHORT Type;
USHORT Size;
// Define the common fields used to control the IRP.
// Define a pointer to the Memory Descriptor List (MDL) for this I/O
// request. This field is only used if the I/O is "direct I/O".
PMDL MdlAddress;
// Flags word - used to remember various flags.
ULONG Flags;
// The following union is used for one of three purposes:
//
// 1. This IRP is an associated IRP. The field is a pointer to a master
// IRP.
//
// 2. This is the master IRP. The field is the count of the number of
// IRPs which must complete (associated IRPs) before the master can
// complete.
//
// 3. This operation is being buffered and the field is the address of
// the system space buffer.
union {
struct _IRP *MasterIrp;
__volatile LONG IrpCount;
PVOID SystemBuffer;
} AssociatedIrp;
// Thread list entry - allows queueing the IRP to the thread pending I/O
// request packet list.
LIST_ENTRY ThreadListEntry;
// I/O status - final status of operation.
IO_STATUS_BLOCK IoStatus;
// Requestor mode - mode of the original requestor of this operation.
KPROCESSOR_MODE RequestorMode;
// Pending returned - TRUE if pending was initially returned as the
// status for this packet.
BOOLEAN PendingReturned;
// Stack state information.
CHAR StackCount; //IPR栈空间大小
CHAR CurrentLocation; //IRP当前栈空间
// Cancel - packet has been canceled.
BOOLEAN Cancel;
// Cancel Irql - Irql at which the cancel spinlock was acquired.
KIRQL CancelIrql;
// ApcEnvironment - Used to save the APC environment at the time that the
// packet was initialized.
CCHAR ApcEnvironment;
// Allocation control flags.
UCHAR AllocationFlags;
// User parameters.
PIO_STATUS_BLOCK UserIosb;
PKEVENT UserEvent;
union {
struct {
union {
PIO_APC_ROUTINE UserApcRoutine;
PVOID IssuingProcess;
};
PVOID UserApcContext;
} AsynchronousParameters;
LARGE_INTEGER AllocationSize;
} Overlay;
// CancelRoutine - Used to contain the address of a cancel routine supplied
// by a device driver when the IRP is in a cancelable state.
__volatile PDRIVER_CANCEL CancelRoutine;
// Note that the UserBuffer parameter is outside of the stack so that I/O
// completion can copy data back into the user's address space without
// having to know exactly which service was being invoked. The length
// of the copy is stored in the second half of the I/O status block. If
// the UserBuffer field is NULL, then no copy is performed.
PVOID UserBuffer;
// Kernel structures
// The following section contains kernel structures which the IRP needs
// in order to place various work information in kernel controller system
// queues. Because the size and alignment cannot be controlled, they are
// placed here at the end so they just hang off and do not affect the
// alignment of other fields in the IRP.
union {
struct {
union {
// DeviceQueueEntry - The device queue entry field is used to
// queue the IRP to the device driver device queue.
KDEVICE_QUEUE_ENTRY DeviceQueueEntry;
struct {
// The following are available to the driver to use in
// whatever manner is desired, while the driver owns the
// packet.
PVOID DriverContext[4];
} ;
} ;
// Thread - pointer to caller's Thread Control Block.
PETHREAD Thread;
// Auxiliary buffer - pointer to any auxiliary buffer that is
// required to pass information to a driver that is not contained
// in a normal buffer.
PCHAR AuxiliaryBuffer;
// The following unnamed structure must be exactly identical
// to the unnamed structure used in the minipacket header used
// for completion queue entries.
struct {
// List entry - used to queue the packet to completion queue, among
// others.
LIST_ENTRY ListEntry;
union {
// Current stack location - contains a pointer to the current
// IO_STACK_LOCATION structure in the IRP stack. This field
// should never be directly accessed by drivers. They should
// use the standard functions.
struct _IO_STACK_LOCATION *CurrentStackLocation;
// Minipacket type.
ULONG PacketType;
};
};
// Original file object - pointer to the original file object
// that was used to open the file. This field is owned by the
// I/O system and should not be used by any other drivers.
PFILE_OBJECT OriginalFileObject;
} Overlay;
// APC - This APC control block is used for the special kernel APC as
// well as for the caller's APC, if one was specified in the original
// argument list. If so, then the APC is reused for the normal APC for
// whatever mode the caller was in and the "special" routine that is
// invoked before the APC gets control simply deallocates the IRP.
KAPC Apc;
// CompletionKey - This is the key that is used to distinguish
// individual I/O operations initiated on a single file handle.
PVOID CompletionKey;
} Tail;
} IRP;
typedef IRP *PIRP;
上面出现IRP栈空间,是因为一个IRP往往要传递n个设备才能得以完成,而在传递过程中会有一些“中间转换”,导致请求的参数变化。为了保存这些变换,我们给每次中转都留一个“栈空间”,用于保存中间参数。
常见的请求:
生成请求:主功能号为IRP_MJ_CREATE
查询请求:主功能号为IRP_MJ_QUERY_INFORMATION
设置请求:主功能号为IRP_MJ_SET_INFORMATION
控制请求:主功能号为IRP_MJ_DEVICE_CONTROL或IRP_MJ_INTERNAL_DEVICE_CONTROL
关闭请求:主功能号为IRP_MJ_CLOSE
请求指针:IRP的指针,即PIRP或IRP*