Windows驱动开发学习记录

 一、概念

一个真实物理设备与设备对象可以是一对多的关系,驱动程序或驱动对象与设备对象也可以是一对多的关系。

设备栈

第一种理解:每一个设备对象都可以绑定一个真实硬件设备(也可以不绑定),当一个硬件设备被多个设备对象所绑定时,这组设备对象被称为设备栈(一般最初是至少有一个设备对象的)。

第二种理解:IoAttachDevice函数将设备对象绑定到名字为"xxx"的设备上,一般它会选择栈顶的设备对象进行绑定,因为这样类似后进先出的模式所以称这组设备对象为设备栈。

简而言之,若干个设备对象依次绑定到一起就形成了设备栈。总是最顶的设备对象先收到IRP。设备栈的栈底是物理设备对象(又称PDO,Phsiycal Device Object)。

1
2
3
4
5
NTSTATUS IoAttachDevice(
  [in]  PDEVICE_OBJECT  SourceDevice,
  [in]  PUNICODE_STRING TargetDevice,
  [out] PDEVICE_OBJECT  *AttachedDevice
);

设备通常由多个设备对象表示,一个用于处理设备的 I/O 请求的驱动程序堆栈中的每个驱动程序。 设备的设备对象组织到 设备堆栈中。 每当在设备上执行操作时,系统都会将 IRP 数据结构传递给设备堆栈中顶部设备对象的驱动程序。 每个驱动程序都处理 IRP,或将其传递给与设备堆栈中的下一个设备对象关联的驱动程序。 

用户模式程序通常基于用户模式驱动程序框架(UMDF),是Windows驱动程序框架(WDF)提供的驱动程序模型之一。

在 UMDF 中,驱动程序为用户 DLL,设备对象为实现 IWDFDevice 接口的 COM 对象。UMDF 设备对象中的设备对象为“WDF”(WDF DO)。

下图显示了周边设备和用户类型设备:

 I/O请求数据包

驱动程序之间的通信通过向目标驱动发送 I/O 请求 IRP,将数据包的内容都封装在 IRP 中。

这里的每个连接对象都与驱动程序相关。

将读取或发送请求至设备,我/O管理会然后查找设备的设备将IRP发送至该设备的设备。

IRP通常是由在设备中的驱动程序进行的,它们有各种IRP设备。

参与I/O的驱动程序全部请求的顺序是“驱动程序请求”。

PS: DO 是 Device Object 设备对象。驱动对象是 Driver Object。

运行或组件驱动程序将 IRP 发送到驱动程序,方法是调用 IoCallDriver,它有两个参数:

  1. DEVICE_OBJECT 指向的指针和指向 IRP 的指针。
  2. DEVICE_OBJ 具有指向关联的 DRI_OBJECT 的指针。

 Windows 驱动程序模型 (WDM) 

每个内核模式驱动程序都必须实现名为 DriverEntry 的函数,该函数在加载驱动程序之后会立即得到调用。

DRIVER_OBJECT 结构的 MajorFunction 成员为指向函数数组的指针,这些函数处理 I/O 请求数据包 (IRP)。

通常,驱动程序填充 MajorFunction 数组的某些元素并使剩下的元素设置为 I/O 管理器提供的默认值。

MajorFunction 数组的其余元素包含指向默认调度函数 nt!IopInvalidDeviceRequest 的指针。

为了使驱动程序开发者在开发驱动程序时更方便,Microsoft 创建了多个技术特定的驱动程序模型。 

驱动程序可以拆分为两个部分:一部分负责通用处理,另一部分负责特定于特殊设备的处理。

  • 通用部分由 Microsoft 编写。
  • 特定部分由 Microsoft 或独立硬件供应商编写。

它们之间可以互相组合形成不同的新驱动程序,我们又可以将之称为驱动程序对。

其中,KMDF(作为通用驱动程序对模型)该模型组成:

  • 通用部分(称为“框架”)由 Microsoft 编写。
  • 特定部分(称为“KMDF 驱动程序”)可由 Microsoft 或独立硬件供应商编写。

驱动程序对的“框架”部分执行通用于各种驱动程序的常规任务。 例如,“框架”可以处理 I/O 请求队列、线程同步以及很大一部分的电源管理任务。

“框架”拥有 KMDF 驱动程序的调度表,因此当某驱动程序将 I/O 请求数据包 (IRP) 发送到 KMDF 驱动程序对时,IRP 会转到“框架”部分代码。 如果“框架”可以自行处理 IRP,则不会涉及到 KMDF 驱动程序。 如果“框架”自身无法处理 IRP,则它会通过调用 KMDF 驱动程序实现的事件处理程序来处理。 

选择驱动程序类型

有三种类型的 WDM 驱动程序:总线驱动程序、函数驱动程序和筛选器驱动程序。

  • 总线驱动程序驱动单个 I/O 总线设备,并提供与设备无关的单槽功能。 总线驱动程序还检测并报告连接到总线的子设备。
  • 函数驱动程序驱动单个设备。
  • 筛选器驱动程序筛选设备的 I/O 请求、设备类或总线。

驱动程序类型有:

  • 设备函数驱动程序
  • 设备筛选器驱动程序
  • 软件驱动程序
  • 文件系统筛选器驱动程序
  • 文件系统驱动程序

设备函数驱动程序选择驱动程序模型,也就是微型驱动程序模型有(处理设备特定任务的驱动程序):

  • 显示器微型端口驱动程序
  • 音频微型端口驱动程序
  • 电池微型类驱动程序
  • 蓝牙协议驱动程序
  • HID 微型驱动程序
  • WIA 微型驱动程序
  • NDIS 微型端口驱动程序
  • 存储器微型端口驱动程序
  • 流微型驱动程序

设备筛选器驱动程序选择驱动程序模型:

首先考虑使用 UMDF 作为驱动程序模型,其次考虑使用 KMDF 作为驱动程序模型。

PS:内核模式驱动程序框架 (KMDF) 。

 

软件驱动程序选择驱动程序模型(未与设备关联的驱动程序称为“软件驱动程序” ):

对于软件驱动程序,可以使用两个选项:KMDF,以及传统的 Windows NT 驱动程序模型。

 二、驱动编程

基础前引

NT式驱动:一种比较老式的驱动程序模型,但适用于现有的Windows系统。程序开发者可以编写一个完全不支持硬件工作的驱动程序,却可以将代码运行在内核模式中。

WDM驱动:WDM式驱动程序在NT式驱动程序的基础上支持各种物理设备管理。

NT式驱动需要导入头文件"ntddk.h",WPM式驱动需要导入头文件"wdm.h"。

在驱动程序中用到的变量或函数都需要指定分配在分页或非分页内存中,分页内存在物理内存不够的情况下可能会被交换出去,对于一些高IRQL的例程绝对不能被交换出页面,因此它们必须被定义为非分页内存。

从Windows 8.0版本开始,WDK中不再提供单独的编译工具(之前的可以用wdk编译),开发者需要使用vs的MSBuild.exe来进行编译。wdk低版本开发的驱动可以在对应更高版本的Windows系统上运行,比如 WDK 7600 可以支持Windows XP 至 Windows 10 系统上。

内核驱动是作为Windows系统服务存在的,分为用户态服务和内核态服务。无论哪种服务在系统上安装后,都会在注册表上记录服务信息。(HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\服务名xxx)。

PS:我的理解是驱动是作为内核的一个模块,但是通过服务(由svchost.exe来运行的)的方式来实现。

内核对象

Windows操作系统把系统内的一切都当作对象来管理,进程是对象,线程是对象,事件是对象。在内核中产生的对象称为内核对象,在内核中销毁,由内核维护。内核对象的地址在内核中,为了能在用户态访问到所以产生了句柄这个概念。每个进程都有一个表,该表中的每一项都存放着内核对象的信息,句柄就是这个表的索引,通过这个索引可以访问到具体的内核对象。

WDK中关于句柄的定义为:

1
typedef HANDLE *PHANDLE;

每个内核对象都存在两个计数,句柄计数和指针计数。句柄计数是指这个内核句柄对象被多少个句柄所指向。一般情况下,它们两个值是相等的。句柄计数的 增减会影响指针计数,但是指针计数可以独立增减而不影响句柄计数。此外,当指针计数归0时,内核对象就会被释放。

PS:使用InitializeObjectAttributes函数来初始化对象属性。

通过句柄 获取对象指针:

NTSTATUS ObReferenceObjectByHandle(
  [in]            HANDLE                     Handle,
  [in]            ACCESS_MASK                DesiredAccess,
  [in, optional]  POBJECT_TYPE               ObjectType,
  [in]            KPROCESSOR_MODE            AccessMode,
  [out]           PVOID                      *Object,
  [out, optional] POBJECT_HANDLE_INFORMATION HandleInformation
);

注册表

注册表本质上是一组文件,存储着Windows的核心配置信息,位于C:\Windows\System32\config目录,以HIVE的方式组织起来。一个HIVE文件是由一个header以及多个hbin记录所组成的,而每个hbin记录是由cell记录和list记录组成。

1
2
3
打开注册表:ZwCreateKey
修改注册表:ZwSetValueKey
读取注册表:ZwQueryValueKey

文件操作

内核态的文件操作与用户态的基本相同,只是对IRQL有严格的要求。

打开文件:ZwCreateFile/ZwOpenFile
写入文件:ZwWriteFile  
读取文件:ZwReadFile

PS:Windows下的最长路径长度是260字符。但是Windows下的很多API支持扩展长度的路径名,支持最长为32767的路径。这些路径要以\\?\作为前缀,如果是UNC格式,那么就以\\?\UNC\为前缀。

线程

在驱动中生成线程一般是系统线程,系统线程所在的进程为“System"。

复制代码
NTSTATUS PsCreateSystemThread(
  [out]           PHANDLE            ThreadHandle,
  [in]            ULONG              DesiredAccess,
  [in, optional]  POBJECT_ATTRIBUTES ObjectAttributes,
  [in, optional]  HANDLE             ProcessHandle,
  [out, optional] PCLIENT_ID         ClientId,
  [in]            PKSTART_ROUTINE    StartRoutine,
  [in, optional]  PVOID              StartContext
);
复制代码

线程处理函数定义:

void KstartRoutine(
  [in] PVOID StartContext
)
{
  ...
  PsTerminateSystemThread(STATUS_SUCCESS);
}

线程处理完任务后,应该在线程中自己调用PsTerminateSystemThread来完成线程销毁,最后在线程外调用ZwClose函数关闭句柄(关闭句柄并不会 销毁线程)。

IRQL中断请求级别

内核中的API的IRQL一共有三个级别:PASSIVE_LEVEL、 APC_LEVEL ,和 DISPATCH_LEVEL。

从左往右级别越高,在cpu中处理的优先级越高,使用限制越大。

PASSIVE_LEVEL最常见,限制最小,不限制调用所有内核API,可以访问非分页内存和分页内存。

Rtl系列API

Windows内核都是使用Unicode 编码的。定义一个_UNICODE_STRING变量之后,它的buffer还没有分配空间。Unicode字符串不是靠 '\0' 来表示字符串结束的,而是依靠UNICODE_STRING的Length来确定。

typedef struct _UNICODE_STRING {
    USHORT Length;
    USHORT MaximumLength;
    PWSTR  Buffer;
} UNICODE_STRING, *PUNICODE_STRING;

微软提供了Rtl系列API,初始化 Unicode 字符串:

RtlInitUnicodeString(&str,L"xxx");
//
Rtl_CONSTANT_STRING(L"xxx");

其他API,如下:

RtlCopyUnicodeString          --    字符串拷贝
RtlCompareUnicodeString    --    字符串比较
RtlUpcaseUnicodeString       --    字符串转大写
RtlStringCbPrintfW                --    输出字符串
RtlUnicodeStringToInteger     --    字符串转字符串
RttlIntegerToUnicodeString--    转转字符串        

 链表

链表作为内核开发中常见的数据结构,主要分为单向链表和双向链表。wdk中,单向链表可以看作是双向链表的子集。

wdk中对链表的定义:

typedef struct _LIST_ENTRY {
  struct _LIST_ENTRY *Flink;
  struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY, PRLIST_ENTRY;

我们要使用链表时可以自己定义一个结构体,把_LIST_ENTRY作为成员变量,如:

typedef struct _MY_NODE {
    int aa;
    int bb
  _LIST_ENTRY myListEntry;
    int cc;
} MY_NODE, *PMY_NODE;

_LIST_ENTRY 展开后即是:

typedef struct _MY_NODE {
    int aa;
    int bb
  _LIST_ENTRY* Flink;
  _LIST_ENTRY* Blink;
    int cc;
} MY_NODE, *PMY_NODE;

我们知道Flink的地址,也就知道如aa等其他成员的地址了。

自旋锁

用于多线程处理情况下,高IRQL,使用如下:

KSPIN_LOCK myLock;
KIRQL irql;
KeInitializeSpinLock(&myLock);
KeAcquireSpinLock(&myLock,&irql);//加锁
......
KeReleaseSpinLock(&myLock,&irql);//解锁

升级版--队列自旋锁

用于多cpu协作情景,采用先到先服务原则

KSPIN_LOCK          myLock;
PKLOCK_QUEUE_HANDLE LockHandle;
KeInitializeSpinLock(&myLock);
KeAcquireInStackQueuedSpinLock(&myLock,&LockHandle);//加锁
......
KeReleaseInStackQueuedSpinLock(&LockHandle);//解锁

线程同步--事件驱动

一个线程需要等待另一个线程完成任务后才能继续进行。

其数据结构是KEVENT,事件初始化如下:

void KeInitializeEvent(
  [out] PRKEVENT   Event,
  [in]  EVENT_TYPE Type,
  [in]  BOOLEAN    State
);

通过设置事件使用,KeSetEvent;通过 KeWaitForSingleObject 或 KeWaitForMultipleObjects 函数等待事件,该函数会自动让事件数减1。

LONG KeSetEvent(
  [in, out] PRKEVENT  Event,
  [in]      KPRIORITY Increment,
  [in]      BOOLEAN   Wait
);

Increment用于要提升的优先级,一般为 0 。如果设置事件导致等待得到满足,则指定要应用的优先级增量。

如果 Wait 为 TRUE,则 KeSetEvent 调用之后必须调用 KeWaitForMultipleObjects、KeWaitForMutexObject 或 KeWaitForSingleObject 等待事件。

内存分配

内核中有一种称为“池”的概念,开发者从pool中申请内存。

复制代码

PVOID ExAllocatePoolWithTag(
[in] __drv_strictTypeMatch(__drv_typeExpr)POOL_TYPE PoolType,
[in] SIZE_T NumberOfBytes,
[in] ULONG Tag
);

void ExFreePoolWithTag(
[in] PVOID P,
[in] ULONG Tag
);

复制代码

Tag参数是一个4个字节的标志,用于标志一块内存的使用者。Tag中的每个 ASCII 字符必须是 0x20(空格)到 0x7E(波浪号)范围内的值。

ExAllocatePoolWithTag(NonPagedPool/PagedPool,NumberOfBytes,Tag);
ExFreePoolWithTag(pBuf,Tag);

 

发布符号链接

我们在DriverEntry中设备的创建,IoCreateDevice函数来创建设备对象后开始创建设备对象。需要在内核层中看到,如果用户的应用程序知道它的存在,就可以向用户层发布一个符号链接,这个真正指向真正的盘的符号链接。 例如,它们在内核中的设备对象是"\DosDevices\C:" 和 "\DosDevices\C:"。 IoRegisterDeviceInterface 函数用于注册设备接口。

PS:在内核中,符号链接“\??\”或者是\DosDevices\”,而用户模式下是“以剧情下的”\\.\“。

> 与用户态下的dll通过导出函数来提供其功能接口不同,驱动程序通过注册回调函数来向外提供功能。当用户态程序或其他驱动程序请求一个服务时,这些回调函数就会被调用。

> 用户态程序使用系统调用的流程如下:

 

> 设置符号文件或使用在线Windows符号服务器,方式:File->Symbol File Path,设置“SRV*d:\Code\symbols*https://msdl.microsoft.com/download/symbols”。

 

 > 在运行内核调试后,可以使用Ctrl + Alt + V开启/关闭详细信息输出功能(或view->verbose Output),每次内核模块载入时得到通知。

驱动和应用程序通信

驱动要和用户态的应用程序通信,首先需要生成一个设备对象(DeviceObject)。设备对象是内核驱动故意暴露给用户态的,应用程序可以像文件一样操作它。

使用 IoCreateDevice 函数生成设备对象。Exclusive是指是否设置为独占设备,独占设备是指该设备同一时间只能被打开一个句柄。

1
2
3
4
5
6
7
8
9
NTSTATUS IoCreateDevice(
  [in]           PDRIVER_OBJECT  DriverObject,
  [in]           ULONG           DeviceExtensionSize,
  [in, optional] PUNICODE_STRING DeviceName,
  [in]           DEVICE_TYPE     DeviceType,
  [in]           ULONG           DeviceCharacteristics,
  [in]           BOOLEAN         Exclusive,
  [out]          PDEVICE_OBJECT  *DeviceObject
);

使用 IoCreateDevice 函数生成的设备采用默认的安全属性,导致只有具有管理员权限的应用程序才能打开该设备的句柄。

我们还可以调用 WdmlibIoCreateDeviceSecure 函数 (或 IoCreateDeviceSecure) 创建设备对象,将DefaultSDDLString设置为 RTL_CONSTANT_STRING(L"D:P(A;;GA;;;WD)")可以让任何用户态的进程打开该设备的句柄。

1
2
3
4
5
6
7
8
9
10
11
NTSTATUS WdmlibIoCreateDeviceSecure(
  [in]           PDRIVER_OBJECT   DriverObject,
  [in]           ULONG            DeviceExtensionSize,
  [in, optional] PUNICODE_STRING  DeviceName,
  [in]           DEVICE_TYPE      DeviceType,
  [in]           ULONG            DeviceCharacteristics,
  [in]           BOOLEAN          Exclusive,
  [in]           PCUNICODE_STRING DefaultSDDLString,
  [in, optional] LPCGUID          DeviceClassGuid,
                 PDEVICE_OBJECT   *DeviceObject
);

普通设备可以没有名字,但是要与用户态通信的设备是需要名字的。设备的名字在上面两个函数中都可以设置,但是用户态的应用程序无法通过设备名字来打开对象,而是要通过符号链接。

符号链接就是记录一个字符串对应到另一个字符串的一种数据结构。

NTSTATUS IoCreateSymbolicLink(
  [in] PUNICODE_STRING SymbolicLinkName,
  [in] PUNICODE_STRING DeviceName
);

用户态应用程序发来的IRP的功能号为 IRP_MJ_DEVICE_CONTROL 

应用程序与驱动通信

打开设备句柄,通过文件操作打开即可。

1
2
3
HANDLE device = NULL;
device = CreateFile("\\\\.\\test_device123",GENERIC_READ|GENERIC_WRITE,0,0,OPEN_EXISTING,FILE_ATTRIBUTE_SYSTEM,0);
CloseHandle(device);

 使用这个 CTL_CODE 宏来创建一个自己的设备请求功能号。第一个参数为设备类型,如果该设备与硬件无关,可以定义为未知类型FILE_DEVICE_UNKNOWN。第二个参数是生成这个功能号的核心数字,这个数字直接用来和其他参数合成功能号,0x0~0x7ff被微软预留,同时这个数字不能大于0xfff。

void CTL_CODE(
   DeviceType,
   Function,
   Method,
   Access
);

METHOD_BUFFERED:IRP 提供一个指向Irp->AssociatedIrp.SystemBuffer的缓冲区的指针。

对于输入数据,缓冲区大小由驱动程序的IO_STACK_LOCATION结构中的Parameters.DeviceIoControl.InputBufferLength指定。驱动程序将数据从该缓冲区传出,然后传入该缓冲区。

METHOD_IN_DIRECT 或 METHOD_OUT_DIRECT:

  • 如果处理 IRP 的驱动程序在调用时接收到缓冲区中的数据,则指定 METHOD_IN_DIRECT。MDL 描述了一个输入缓冲区,并指定 METHOD_IN_DIRECT 确保执行线程具有对缓冲区的读取访问权限。
  • 如果处理 IRP 的驱动程序将在完成 IRP 之前将数据写入缓冲区,则指定 METHOD_OUT_DIRECT。MDL 描述了一个输出缓冲区,并指定 METHOD_OUT_DIRECT 确保执行线程对缓冲区具有写访问权限。

  IRP 在Irp->AssociatedIrp.SystemBuffer处提供指向缓冲区的指针。缓冲区大小由驱动程序的IO_STACK_LOCATION结构中的Parameters.DeviceIoControl.InputBufferLength指定。

  对于这两种传输类型,Parameters.DeviceIoControl.OutputBufferLength 指定 MDL 描述的缓冲区大小。

  IRP 在Irp->MdlAddress 处提供指向 MDL 的指针。

METHOD_NEITHER:I/O 管理器不提供任何系统缓冲区或 MDL。

复制代码
#define CWK_DVC_SEND_STR \
    (ULONG)CTL_CODE( \
    FILE_DEVICE_UNKNOWN, \
    0X211,METHOD_BUFFERED,\
    FILE_WRITE_DATA)

......
char *msg = "xxx"; DeviceIoControl(divce,CWK_DVC_SEND_STR,msg,strlen(msg)
+1,NULL,0,&retLen,0);
复制代码

通过 DeviceIoControl 函数来直接向指定的设备驱动程序发送控制代码。

复制代码
BOOL DeviceIoControl(
  [in]                HANDLE       hDevice,
  [in]                DWORD        dwIoControlCode,
  [in, optional]      LPVOID       lpInBuffer,
  [in]                DWORD        nInBufferSize,
  [out, optional]     LPVOID       lpOutBuffer,
  [in]                DWORD        nOutBufferSize,
  [out, optional]     LPDWORD      lpBytesReturned,
  [in, out, optional] LPOVERLAPPED lpOverlapped
);
复制代码

分发函数

 分发函数就是一组用来处理设备对象接受到的 IRP 的函数。分发函数是设置在驱动对象(Driver Object)上的。每个驱动都有一组自己的分发函数。

Windows的IO管理器在收到IRP时,会根据IRP的目标,也就是一个设备对象,来调用这个设备对象所从属的驱动对象上对应的分发函数。

 标准的分发函数定义如下:

1
2
3
NTSTATUS  cwkDispatch(
            IN PDEVICE_OBJECT dev,
            IN PIRP irp);

 cwkDispatch 函数中的参数 dev 为该 irp 本应该发送给哪个设备处理,所以在 cwkDispatch 函数中开始处理irp前判断一下是否是给当前设备处理的。处理IRP的第一步是获得IRP当前栈空间,通过IoGetCurrentIrpStackLocation 函数取得。IoCompleteRequest 函数用于结束这个请求。

设备对象绑定

设备对象绑定另一个设备对象的方法有三种:

  1. IoAttachDeivce
  2. IoAttachDeviceToDeviceStack
  3. IoAttachDeviceToDeviceStackSafe

IoGetDeviceObjectPointer 函数通过名字获取设备对象。

加载驱动

 驱动的入口函数是DriverEntry函数,驱动初始化的工作在其中完成,也就是DriverEntry函数返回 STATUS_SUCCESS。

微软规定,驱动必须经过数字签名后,才可以运行在64位系统上。解决方法:

  1. 把操作系统配置成调试模式,默认允许未签名的驱动运行;
  2. 临时关闭系统驱动签名校验:开机时修改启动参数,F8,选择禁用驱动程序签名机制。

安装驱动:以管理员权限运行cmd,执行命令“sc create 驱动名_xxx binPath= "路径_xxx" type= kernel start= demand ”安装,接着执行命令“sc start 驱动名_xxx”。停止服务命令“sc stop 驱动名_xxx ”。

Windows提供了关于服务管理相关的API,如:

OpenSCManager、CreateService创建、OpenService打开、StartService启动、ControlService停止,以及DeleteService删除。

卸载驱动例程

驱动停止服务的本质就是把驱动模块对应在内核地址空间中的代码以及数据清除,此外之前设置的DriverObject->DriverUnload指向的函数会被回调。但是DriverUnload函数是可选的,如果没有定义该函数则会导致驱动一旦启动就无法停止。驱动初始化失败不会触发回调,也就是DriverEntry函数返回 STATUS_SUCCESS 以外的值。

首先删除符号链接,使用 RtlInitUnicodeString 函数初始化UNICODE字符串,然后调用IoDeleteSymbolicLink函数来删除符号链接,然后调用IoDeleteDevice函数删除设备对象。

1
2
3
4
5
6
7
NTSTATUS IoDeleteSymbolicLink(
  [in] PUNICODE_STRING SymbolicLinkName
);
 
void IoDeleteDevice(
  [in] PDEVICE_OBJECT DeviceObject
);

 三、用例实现

1.过滤

过滤是在不影响上层和下层接口的情况下,在Windows内核中加入新的中间层,从而加入新功能。

一个真实设备可能对应多个设备对象,而一个设备对象对应一个驱动程序。当真实设备绑定有多个设备对象,就会有设备栈这个概念。我们向真实设备绑定一个新的设备对象,则该设备对象位于栈顶,也就是最先接收到该真实设备的IRP,可以决定和怎么处理每一个IRP,进而实现过滤功能。而真实设备其实在操作系统中也是用一个设备对象表示,所以所谓绑定真实设备就是一个设备对象绑定另一个设备对象(或称之为绑定设备栈中作为栈顶的设备对象)。

首先,生成一个过滤设备:IoCreateDevice

  1. DriverObject变量是驱动对象,系统提供,从DriverEntry中传入;
  2. DeviceName:过滤设备不需要名称,传NULL就行;
  3. DeviceType:设备类型,保持和被绑定设备对象的类型一致即可。

其次,需要将设备对象绑定到真实设备上,有两种方法:

  • 通过设备的符号链接来绑定真实设备:IoAttachDevice
  • 通过设备对象的指针来绑定真实设备:IoAttachDeviceToDeviceStackSafe

PS:我们获取设备对象的指针,可以通过IoGetDeviceObjectPointer函数。

 

posted @   An2i  阅读(176)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示