文章目录
windows内核虽然用c实现,但也有面向对象的思想,下面是3个重要概念:
- 驱动对象
- 设备对象
- IRP(Interrupted Request Packet)
3. 驱动对象
驱动程序就是一个.sys模块,驱动对象则是.sys被加载到内核中的实例化出来的对象,用于表示这个驱动模块,并作为参数传给DriverEntry(可看作this指针).
结构体
typedef struct _DRIVER_OBJECT {
CSHORT Type;
CSHORT Size;
PDEVICE_OBJECT DeviceObject; //设备链
ULONG Flags;
PVOID DriverStart;
ULONG DriverSize;
PVOID DriverSection;
PDRIVER_EXTENSION DriverExtension; //驱动扩展对WDM程序很重要
UNICODE_STRING DriverName;
// This is a pointer
// to the path to the hardware information in the registry
PUNICODE_STRING HardwareDatabase;
// 快速IO请求函数地址(数组)
PFAST_IO_DISPATCH FastIoDispatch;
PDRIVER_INITIALIZE DriverInit; // 驱动对象初始化函数指针,此函数指针被I/O管理器初始化,指向 DriverEntry 函数
PDRIVER_STARTIO DriverStartIo; //派遣函数地址,可以置为NULL
PDRIVER_UNLOAD DriverUnload; // 必须赋值,否则卸载时蓝屏
// 为了支持拓展,派遣函数表在最后。
// 这个数组内的函数指针都可以设置为NULL
PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];
} DRIVER_OBJECT;
最后一个函数数组,这些函数都是可选的,当用户层或内核层通过设备的符号链接操作设备(打开,读,写,关闭)时,这些函数就会被调用。也就是说,以前所使用的用户层API :CreateFile, GetFileSize , ReadFile, WriteFile, CloseHandle
这些API操作的是一个设备对象,在内核中,文件属于一个设备对象.
同一个函数,能够操作不同的对象,这就是内核中通过C语言实现的多态了.
第5节会详细整理这个数组以及传参原理。
4. 设备对象
设备对象一般是由驱动对象构建出来的(非即插即用驱动)。
驱动对象能够保存各种派遣函数,但是,这些派遣函数的一般是由I/O管理器所调用。在调用派遣函数时,I/O 管理器会将附加的信息打包到一个结构体,
并传递给派遣函数。一般这个结构体被称为 IRP 结构.
只有设备对象才能接收到I/O管理器的I/O请求(IRP)
什么是I/O请求?比如,当一个文件的设备对象被打开( CreateFile )之后,对此文件的设备对象的读( ReadFile )和写( WriteFile )就是 I/O 请求。
但设备对象不能独立存在,设备对象能够接收 I/O 请求,但是却没有处理 I/O 请求的派遣函数,处理 I/O 请求的派遣函数保存在驱动对象中。
因此,在一个驱动项目中,要有两种对象存在:
- 驱动对象,能够保存处理 I/O 请求的派遣函数.
- 设备对象,能够接收到 I/O 请求.
驱动对象无法被用户层代码所访问到,但是设备对象可以(通过符号链接)。
就像程序和窗口一样。
设备对象虽然在内核层,但是创建了DOS下的符号链接之后,在用户层中就可以通过CreateFile
来打开设备对象。并能够通过ReadFile , WriteFile , DeviceIOControl , GetFileSize
等函数来间接地调用保存在驱动对象中的派遣函数。
4.0 创建和销毁
IoCreateDevice(), IoDeleteDevice()
创建时,命名以/Device
开头。
4.1 结构体
typedef struct DECLSPEC_ALIGN(MEMORY_ALLOCATION_ALIGNMENT) _DEVICE_OBJECT {
CSHORT Type;
USHORT Size;
LONG ReferenceCount;
struct _DRIVER_OBJECT *DriverObject;
struct _DEVICE_OBJECT *NextDevice;
struct _DEVICE_OBJECT *AttachedDevice;
struct _IRP *CurrentIrp;
PIO_TIMER Timer;
ULONG Flags;
ULONG Characteristics;
__volatile PVPB Vpb;
PVOID DeviceExtension;
DEVICE_TYPE DeviceType;
CCHAR StackSize;
union {
LIST_ENTRY ListEntry;
WAIT_CONTEXT_BLOCK Wcb;
} Queue;
ULONG AlignmentRequirement;
KDEVICE_QUEUE DeviceQueue;
KDPC Dpc;
ULONG ActiveThreadCount;
PSECURITY_DESCRIPTOR SecurityDescriptor;
KEVENT DeviceLock;
USHORT SectorSize;
USHORT Spare1;
struct _DEVOBJ_EXTENSION *DeviceObjectExtension;
PVOID Reserved;
} DEVICE_OBJECT;
4.2 通讯
4.2.0 设备对象的2种通讯方式
用户层提供的缓冲区地址时用户层的,不能再内核态随意使用。
通讯无非就是读写数据,这里3种读写方式:
- 缓冲区设备读写:将缓冲区拷贝到内核态,用完再拷会用户态;
- 直接读写:将用户态地址重新映射到内存空间;
- 其它
前两种其实就是通过pDevice->Flags与下面的值或运算实现:
DO_BUFFERED_IO
,通过pIrp->AssociatedIrp.SystemBuffer
访问缓冲区;DO_DIRECT_IO
,通过MmGetSystemAddressForMdlSafe(pIrp->MdlAddress, NormalPagePriority)
获取缓冲区。
如果Flags==0,就会通过pIrp->UserBuffer
访问用户层地址,一旦进程切换就会地址错误,极度不安全。
4.2.1 读写方式通讯
4.2.2 控制码方式通讯
4.3 符号链接
符号链接就是一个名字,\DosDevices\D:\\
是一个盘符,但其实也可以视为一个符号链接名.
设备名称不暴露给用户层,需要创建符号链接,命名/DosDevices/
开头,全局则以/DosDevices/Global
开头。
4.3.0 作用
能够让用户层的API发出IO请求,并能够在发出IO请求时指定一个设备来处理此IO请求。
当用户层的应用程序发出一个IO请求时,对象管理器通过此符号链接名称来找到对应的设备,对象管理器能够解析符号链接的名称,以确定IO请求的目的地。
符号链接是给设备对象使用的,设备对象默认没有符号链接,为设备对象创建符号链接之后才可以被用户层访问。
4.3.1 分类
- NT设备名——设备名一般格式为
"\Device\自定义设备名"
,此格式的名字一般是用于传递函
数 IoCreateDevice所要求给出的设备名。这个设备名可以在内核下使用,但是用户层程序无法使用 - DOS设备名——设备名一般格式为:
"\DosDevices\自定义设备名"
,此格式的名字一般用户传递给函数IoCreateSymbolLinkName的参数,后面这个函数的功能是为一个NT设备名创建一个用户层能够使用的符号链接名。
4.3.2 创建与销毁
IoCreateSymbolicLink
——为一个NT设备名链接到一个DOS设备名,供用户层程序使用。
IoDeleteSymbolicLink
删除一个DOS设备名.
5. IRP和派遣函数
5.0 IRP头部
PDRIVER_OBJECT->MajorFunction
数组存储了许多派遣函数,也就是当设备接收到了IO请求之后被调用来处理IO请求的函数。
typedef
NTSTATUS
DRIVER_DISPATCH (
_In_ struct _DEVICE_OBJECT *DeviceObject,
_Inout_ struct _IRP *Irp
);
比如用户层调用CreateFileW()
,那么就会打开一个设备对象,PDRIVER_OBJECT->MajorFunction[IRP_MJ_CREATE]
就会调用。系统会将CreateFileW()
参数传递给派遣函数,保存在了 IRP
和 IO_STACK_LOCALTION
两个非常大的结构中。
IRP由IRP头部和IRP栈组成。
IRP的头部:
typedef struct DECLSPEC_ALIGN(MEMORY_ALLOCATION_ALIGNMENT) _IRP {
CSHORT Type;
USHORT Size;
//
// 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;
ULONG Flags;
union {
struct _IRP *MasterIrp;
__volatile LONG IrpCount;
PVOID SystemBuffer;
} AssociatedIrp;
IO_STATUS_BLOCK IoStatus;
KPROCESSOR_MODE RequestorMode;
/*
...
*/
PIO_STATUS_BLOCK UserIosb;
PKEVENT UserEvent;
union {
//...
} Overlay;
PVOID UserBuffer;
union {
struct {
union {
//...
struct {
//
// The following are available to the driver to use in
// whatever manner is desired, while the driver owns the
// packet.
//
PVOID DriverContext[4];
} ;
} ;
//...
struct {
//...
union {
//...
struct _IO_STACK_LOCATION *CurrentStackLocation;
};
};
//...
} Overlay;
} Tail;
} IRP;
IRP实际就是一个用户保存用户层传递进来的参数. 这些参数有多种 , 而且, 对于不同的IO请求,会有不同的参数, 而无论什么IO请求,多少个参数,都只能通过此结构体来保存, 因此这个结构体比较庞大.
IO管理器创建一个IRP代表一个IO操作,并将IRP传给正确的驱动程序,驱动程序执行该IRP指定操作,再传回给IO管理器,告诉管理器已经完成操作或者传给另一个驱动。
IRP的接收者是设备对象,处理者是驱动对象。
MSDN介绍了不同的IO请求下,参数保存在IRP结构体中哪个字段。
5.1 IO_STACK_LOCATION
上面的大结构体只是IO请求包IRP的头部,后面还有IO_STACK_LOCATION
结构体数组,类似重定位表TypeOffset,与IRP同时创建。
数组中的每个堆栈单元都对应一个将处理该 IRP 的驱动程序。IRP的头部有一个当
前IO_STACK_LOCATION
的数组索引(从1开始),同时也有一个指向该IO_STACK_LOCATION
的指针。
通过IoGetCurrentIrpStackLocation()
就能过获取到当前设备的IO栈.
typedef struct _IO_STACK_LOCATION {
UCHAR MajorFunction;
UCHAR MinorFunction;
UCHAR Flags;
UCHAR Control;
//
// The following user parameters are based on the service that is being
// invoked. Drivers and file systems can determine which set to use based
// on the above major and minor function codes.
//
union {
struct {/*...*/} Create;
struct {/*...*/} CreatePipe;
struct{/*...*/} CreateMailslot;
//...
} Parameters;
//...
PVOID Context;
} IO_STACK_LOCATION, *PIO_STACK_LOCATION;
里面最重要的是那个联合体,包含不同类型IRP所携带的参数结构体。
当驱动程序准备向次低层驱动程序传递IRP时可以调用 IoCallDriver 例程,它其中的一个工作是递减当前
IO_STACK_LOCATION
的索引,使之与下一层的驱动程序匹配。但该索引不会设置成0,如果设置成0,系统将会崩溃。就是说,最底层的驱动程序不会调用 IoCallDriver 例程。
5.2 IRQL
Interrupted ReQuest Level.
内核实际就是一个进程ntoskrnl.exe
,里面有很多线程和全局变量。为了解决内核线程同步问题,微软提出了IRQL的概念。
3个级别:
- Dispatch:所有运行在Dispatch级的代码都是会被进行原子操作的,且不能访问分页内存,也就是说操作系统中在一个时间内只能运行一段Dispatch级的代码,且必须将其完全执行完毕后才会发生线程切换
- APC:比 Dispatch 低的一个级别,可以访问分页内存
- Passive:最低的优先级,大多数代码所运行的级别