windows驱动的简单入门(一)

简介:这是一篇介绍windows驱动的简单入门,包含环境搭建,双机调试以及简单的代码编写。

前言

在开始学习写windows的驱动的时候,有一个很常用的网站,那就是微软官网的学习网站,里面有庞大的学习文档介绍,即便你之前用的更多的是百度,那么这个网站也是在学习windows驱动过程中用的最多的一个,网址:Microsoft

环境准备

配置WDK

开始学习前,需要针对开发驱动做环境准备,需要下载IDE和对应的开发工具包,其他的开发一般叫SDK,驱动的这个它叫WDK(其实都差不多),依然是通过官网下载。 点击查看

注意:里面介绍了非常详细的步骤,而且还是中文介绍,所以按照步骤一步一步执行是没问题的。

  1. 安装vs 2022(咱们学习者就用社区版哈)
注意:在选择安装的组件的时候,要选择c++桌面开发,然后还要额外选择一下几个。

安装 Visual Studio 2022 时,选择使用 C++ 进行桌面开发工作负荷,然后在“单独组件”下添加: • MSVC v143 - VS 2022 C++ ARM64/ARM64EC Spectre 缓解库(最新版本) • MSVC v143 - VS 2022 C++ x64/x86 Spectre 缓解库(最新版本) • 带有 Spectre 缓解库的适用于最新 v143 生成工具的 C++ ATL (ARM64/ARM64EC) • 带有 Spectre 缓解库的适用于最新 v143 生成工具的 C++ ATL (x86 & x64) • 带有 Spectre 缓解库的适用于最新 v143 生成工具的 C++ MFC (ARM64/ARM64EC) • 带有 Spectre 缓解库的适用于最新 v143 生成工具的 C++ MFC (x86 & x64) • Windows 驱动程序工具包 (WDK)

  1. 下载SDK,网站上有链接
  2. 安装WDK

总之,就尽量按照官网的要求来,就不会有问题。当然如果你本身自己的操作系统有装了vs2022,你想装对应版本的WDK,那就需要下载以前版本的WDK,要匹配上对应操作系统才可以。 点击查看以前的版本

如何知道自己操作系统的主版本号

  1. win+R调出运行界面
  2. 输入winver,回车 弹出的对话框上就显示了主版本号 2

配置双机调试环境

除了WDK外,还需要配置一个方便的调试环境,毕竟操作的是驱动,不能直接在自己的电脑上弄,要不然,蓝屏的效果会让你直接崩溃的,而且你也没法把驱动装载进去。

所以我们需要配置一个虚拟机,直接用跟自己一样的操作系统的就行,然后在上文的下载页面里,继续往下看会找到windbg这个调试工具的下载地址,可以直接点进去下载。

当虚拟机准备好之后,需要做以下事情来建立一个可以调试模式打开的启动项。

建立新的调试启动项

windows对于启动项的操作有一些命令行命令,点击具体链接

其中,BCDEdit,就是针对启动项做一些操作的,比如复制,修改等

  1. 复制启动项 首先把自己现在的开机启动项复制下来,以管理员方式启动cmd命令行
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# 当输入bcdedit /copy /?会出现以下命令的提示
bcdedit [/store <filename>] /copy {<id>} /d <description>

    <filename>      指定要使用的存储。如果未指定此选项,则使用系统存储。
                    有关详细信息,请运行 "bcdedit /? store"
    <id>            指定要复制的项的标识符。
                    有关标识符的详细信息,请运行 "bcdedit /? ID"
    <description>   指定要用于新项的描述。

# 示例:

# 下列命令创建指定操作系统启动项的副本:

    bcdedit /copy {cbd971bf-b7b8-4885-951a-fa03044f5d71} /d "Copy of entry"

其中花括号里的是标识符,如果想展示更多当前启动项,可以输入

1
2
bcdedit /enum
# 枚举所有的启动内容

通过观察结果会发现,其中 {cbd971bf-b7b8-4885-951a-fa03044f5d71} 这个内容既出现在标识符字段,也出现在其他字段,但是事实上这个标识符只是字面意思,用来标识的,所以当对每个项进行增删的时候就需要用到这个标识符,咱们一般是就只复制当前用的这个项,所以可以写成current,也就是enum枚举的时候,所列的最后一个。

1
2
bcdedit /copy {current} /d "启动项的名字"
# 然后再枚举一下,就可以看到你新复制的项

注意:这里要复制一下新生成的项的标识符,方便后面修改启动项的时候用

  1. 修改启动项--启动顺序
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
修改启动顺序用/displayorder 这个命令
输入 bcdedit /displayorder /? 
展示一下帮助内容
此命令设置启动管理器使用的显示顺序。

bcdedit /displayorder <id> [...] [ /addfirst | /addlast | /remove ]

    <id> [...]      指定组成显示顺序的标识符列表。必须至少指定一个标识符,且必须使用
                    空格分隔标识符。有关标识符的详细信息,请运行 "bcdedit /? ID"
    /addfirst       将指定的项标识符添加到显示顺序的顶部。如果已指定此参数,则只能指定
                     一个项标识符。如果列表中已存在指定的标识符,则将其移动到列表顶部。

    /addlast        将指定的项标识符添加到显示顺序的末尾。如果已指定此参数,则只能指定
                     一个项标识符。如果列表中已存在指定的标识符,则将其移动到列表末尾。

    /remove         从显示顺序中删除指定的项标识符。如果已指定此参数,则只能指定
                    一个项标识符。如果该标识符不在列表中,则该操作不起作用。如果删除
                    最后一项,则显示顺序值将会从启动管理器项中删除。

示例:

下列命令设置启动管理器显示顺序中的两个 OS 项以及基于 NTLDR 的 OS 加载器:

    bcdedit /displayorder {802d5e32-0784-11da-bd33-000476eba25f}
        {cbd971bf-b7b8-4885-951a-fa03044f5d71} {ntldr}

下列命令将指定的 OS 项添加到启动管理器显示顺序的末尾:

    bcdedit /displayorder {802d5e32-0784-11da-bd33-000476eba25f} /addlast

所以我们可以尝试将新建的启动项放在首位置 其实是用addfirst还是addlast,字面意思应该是addfirst

1
2
bcdedit /displayorder {刚刚复制下来的标识符} /addfirst
如果重启的时候,没有展示在首位那就是用/addlast,原谅我不想重启测试了。
  1. 修改启动项--设置全局的调试参数
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
这次用/dbgsettings    来设置全局调试程序参数
依然用bcdedit /dbgsettings  /? 一路问过去
此命令设置或显示系统的全局调试程序设置。

此命令不会启用或禁用任何特定启动
项的调试程序。若要启用或禁用特定启动项的调试程序,请使用
"bcdedit /debug < identifier> ON"。有关标识符的信息,请运行
"bcdedit /? ID"
若要设置单个全局调试程序设置,请使用
"bcdedit /set {dbgsettings} <type> <value>"。有关有效
类型的信息,请运行 "bcdedit /? TYPES"
bcdedit /dbgsettings [ <debugtype> [DEBUGPORT:<comport>] [BAUDRATE:<baud>]
                        [CHANNEL:<channel>] [TARGETNAME:<targetname>]
                        [HOSTIP:<ip>] [PORT:<port>] [KEY:<key>] [nodhcp]
                        [newkey] [/start <startpolicy>] [/noumex] ]

    <debugtype>     指定调试程序的类型。<debugtype> 可以是
                    SERIAL、1394、USB、NET 或 LOCAL 之一。

    <comport>       对于 SERIAL 调试,指定要用作
                    调试端口的串行端口。这是可选设置。

    <baud>          对于 SERIAL 调试,指定用于
                    调试的波特率。这是可选设置。

    <channel>       对于 1394 调试,指定用于
                    调试的 1394 通道。

    <targetname>    对于通用串行总线(USB)调试,指定用于调试的 USB
                    目标名称。

    <ip>            对于网络调试,指定
                    主机调试程序的 IPv4 地址。

    <port>          对于网络调试,指定在
                    主机调试程序上要与其通信的端口。应为 49152 或更高。

    <key>           对于网络调试,指定用于
                    加密连接的密钥。仅允许 [0-9][a-z]
    nodhcp          对于网络调试,阻止使用 DHCP 获取
                    目标 IP 地址。

    newkey          对于网络调试,指定应为连接
                    生成新加密密钥。

    /start <startpolicy>   对于所有调试程序类型,此选项会指定调试程序
                    启动策略。<startpolicy> 可以是下列策略之一:
                        ACTIVE
                        AUTOENABLE
                        DISABLE。
                    如果未指定,则默认值为 ACTIVE。

    /noumex         如果指定,这将导致内核调试程序忽略任何
                    用户模式例外。

示例:

下列命令显示当前的全局调试程序设置:

    bcdedit /dbgsettings

下列命令设置全局调试程序设置在 com1 上以 115,200 波特
进行串行调试:

    bcdedit /dbgsettings SERIAL DEBUGPORT:1 BAUDRATE:115200

下列命令设置全局调试程序设置
使用通道 23 进行 1394 调试:

    bcdedit /dbgsettings 1394 CHANNEL:23

下列命令设置全局调试程序设置
使用目标名称 DEBUGGING 进行 USB 调试:

    bcdedit /dbgsettings USB TARGETNAME:DEBUGGING

下列命令设置全局调试程序设置
通过在端口 50000 上以 192.168.1.2 通信的调试程序主机使用 IPv4 进行网络调试:

    bcdedit /dbgsettings NET HOSTIP:192.168.1.2 PORT:50000

下列命令设置全局调试程序设置
通过在端口 50000 上以 2001:48:d8:2f:5e:c0:42:28:4f5b
通信的调试程序主机使用 IPv6 进行网络调试:

    bcdedit /dbgsettings NET HOSTIPV6:2001:48:d8:2f:5e:c0:42:28:4f5b PORT:50000

下列命令设置全局调试程序设置进行本地调试:

    bcdedit /dbgsettings LOCAL

我们使用串行调试的方式:

1
bcdedit /dbgsettings SERIAL DEBUGPORT:1 BAUDRATE:115200
  1. 修改启动项--开启调试
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
使用这个参数/debug          启用或禁用操作系统项的内核调试。
此命令启用或禁用指定启动项的内核调试程序。

bcdedit /debug [<id>] { ON | OFF }

    <id>         指定要修改的项的标识符。只能指定 Windows 启动加载器项。如果未指定,
                 则使用 {current}。有关标识符的详细信息,请运行 "bcdedit /? ID"
示例:

下列命令启用当前 Windows 操作系统启动项的内核调试:

    bcdedit /debug ON

下列命令禁用指定操作系统项的内核调试:

    bcdedit /debug {cbd971bf-b7b8-4885-951a-fa03044f5d71} OFF

这个参数比较简单

1
bcdedit /debug {刚刚复制下来的标识符} ON
  1. 修改启动项--启动倒计时
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/timeout        设置启动管理器的超时值。
这个值就是启动的时候,进入启动项之前会倒计时。
此命令设置启动管理器选择默认项以前等待的时间,以秒为单位。
有关设置默认项的详细信息,请运行 "bcdedit /? default"
bcdedit /timeout <timeout>

    <timeout>   指定启动管理器选择默认项以前等待的时间,以秒为单位。

示例:

下列命令将启动管理器 <timeout> 设置为 30 秒:

    bcdedit /timeout 30
1
2
咱们设定短一点,10秒即可
bcdedit /timeout 10

接下来重新启动电脑就可以看到开机的时候会多了一个新的启动项,名字就是你在copy的时候赋予的名字(描述),回车进入即可。

配置虚拟机通道

关于使用windbg调试虚拟机里的内核,微软有一篇文档做了一些介绍,点击查看具体内容

启动项配置好就是配置虚拟机了,首先将虚拟机关机

  1. 点开虚拟机设置,看到虚拟机设置页面
  2. 在硬件选项卡中最底部选择添加,硬件类型选择串行端口
  3. 按照上文文档的方式配置串行端口 (使用虚拟 COM 端口手动设置)
1
2
3
选择第三个,使用命名的管道
按照文档的推荐方式:\\.\pipe\PipeName,管道名称随便都可以
\\.\pipe\com_1

依次选择,该端是服务器,另一端是应用程序,也就是windbg会连接到这个通道对应的虚拟机里。

配置windbg启动项

这一步操作也简单,还是按照文档方式,为了方便每次打开windbg,不用每次都用命令行,我们新建一个windbg的快捷方式,然后编辑快捷方式的启动命令

具体用执行什么命令,点击链接,参考这个文档

我们配置快捷方式里的执行命令:执行参数可以设置如下

1
2
3
4
选用这条
windbg [-y SymbolPath] -k com:port=ComPort,baud=BaudRate

windbg.exe -k com:port=//./pipe/com_1,baud=115200,pipe

然后打开windbg,并开启虚拟机 就能看到以下链接页面 3

尝试按一下中间的暂停按钮,如果看到下面的页面,就代表你已经成功用windbg跟虚拟机操作系统的内核建立的调试关系。这时候虚拟机里的操作系统应该是静止的状况。 4

在命令行输入g(也就是go),回车。操作系统就又继续运行了。

第一个驱动程序

当上述的准备工作做好之后,就是可以开始敲代码的过程了,首先,打开安装好的Visual Studio 2022,然后新建一个项目,在模版搜索框中搜索WDM,然后会出现空WDM模版的选项,直接选择这个方式建立一个空项目。选择好位置以及项目名称确定即可。

新建并生成程序

在sourcefiles右键,新建一个main.c文件,注意文件后缀要修改为.c而不是.cpp 输入如下代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <ntifs.h>

void DriverUnload(PDRIVER_OBJECT pDriverObject)
{

}


NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegPath)
{
    DbgPrint("DriverEntry hello world");
    pDriverObject->DriverUnload = DriverUnload;
    return STATUS_SUCCESS;
}

事实上,每个驱动程序必须具有 DriverEntry 例程,用于初始化驱动程序范围的数据结构和资源,在微软的文档中也有关于这个入口程序的写法,可以参考这个链接

但是在编译之前,首先要把一个文件删除掉。在driver files文件夹里有一个inf后缀的文件,先删除掉再编译。 步骤:在解决方案下面的项目名称上,右侧点击-生成,

注意:首次生成总会出现一些问题

  1. 如果显示是缓解库的问题,可以直接百度 比如下面这个问题:

MSB8040 此项目需要缓解了 Spectre 漏洞的库。从 Visual studio 安装程序(单个组件选项卡)为正在使用的任何工 信息: https://aka.ms/Ofhn4c 集和体系结构安装它们。 了解详细

可以直接用Visual Studio installer安装工具,点击当前vs2022卡片上的修改按钮,搜索弹框上方的单个组件选项卡,搜索spectre,拉倒最后面把最新的那个MSVCv143-VS 2022 C++ x64/x86 Spectre 缓解库(最新)勾选上,点击右下角的修改,安装好就可以了,注意,安装需要关掉已经打开的Visual Studio窗口。参考链接

  1. 如果报错是“以下警告被视为错误”,这个一般其实就是警告,我们可以将该告警降级,因为驱动的写法会比较严格,所以有的可以忽视的普通告警也会被定义为错误而无法成功编译。

依然是项目名称右键,属性,打开属性窗口。 左侧选项栏中点击c/c++,展开后点击所有选项,在右侧选项框中搜索错误(类似的关键字),能够找到如下内容。把将警告视为错误修改为否即可。 5

解决完上述问题,生成之后,就会在项目目录文件中找到一个驱动文件,扩展后缀是.sys这个就是我们生成的驱动文件。

演示并测试程序

要知道,驱动程序没法像一般的PE程序一样可以直接执行,需要一个注册并执行的步骤,所以这里引入几个外部工具,辅助驱动注册的一个程序,直接浏览器搜索A1SysTest即可,这个就要感谢一些前辈了。 然后是观察驱动在内存打印的内容,可以用dbgview,这个在微软的官网能找到,地址如下:https://learn.microsoft.com/zh-cn/sysinternals/downloads/debugview

以下执行过程配置好的虚拟机里执行: 打开测试程序(A1SysTest),出现了一个界面。然后将上文生成的驱动文件直接拖动到界面上即可。 点击安装,然后点击启动,这个时候如果没有出现什么错误信息,那你的驱动文件就已经加载到了系统了。但是如果验证呢。这就要用到刚刚说的dbgview工具了,或者使用上文配置的windbg都可以。

如果你已经提前都打开了windbg,但是在驱动启动的时候并没有看到代码里写的DbgPrint("DriverEntry hello world");这句要打印的内容,那需要修改一个注册表以使得打印的内容更多。

具体可以参考这篇文章写的很清楚:https://learn.microsoft.com/zh-cn/windows-hardware/drivers/debugger/reading-and-filtering-debugging-messages

简单将其实就是,首先能够打印的消息的级别是确定的,有下面几种

1
2
3
4
5
#define   DPFLTR_ERROR_LEVEL     0
#define   DPFLTR_WARNING_LEVEL   1
#define   DPFLTR_TRACE_LEVEL     2
#define   DPFLTR_INFO_LEVEL      3
#define   DPFLTR_MASK   0x80000000

其次,注册表中可以自己新建一个组件筛选器掩码,

1
2
3
位置如下:
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Debug Print Filter 
新建一个值,类型为DWORD,name为Default

最后,这个值要足够大,因为最终是否会打印,取决于这个值和默认打印级别的AND运算,结果为1就会打印。所以不如直接写0XFFFFFFFF(哈哈哈哈)

最后重新注册和启动驱动就可以看到如下打印内容了。 6

代码的含义

当第一个驱动能够成功加载并运行的之后,我们就该详细了解下这个段代码的具体含义了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <ntifs.h>

void DriverUnload(PDRIVER_OBJECT pDriverObject)
{

}


NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject, PUNICODE_STRING pRegPath)
{
    DbgPrint("DriverEntry hello world");
    pDriverObject->DriverUnload = DriverUnload;
    return STATUS_SUCCESS;
}

UNICODE_STRING

从上文提到过的一个微软的DriverEntry的链接中能知道,DriverEntry是驱动程序的首个入口点,也就类似于我们在应用程序里常说的main函数,这个函数有两个变量,第一个是PUNICODE_STRING格式,也就是UNICODE_STRING的指针变量,那么这个UNICODE_STRING到底是一个什么格式呢。

以下是UNICODE_STRING结构体的定义

1
2
3
4
5
typedef struct _UNICODE_STRING {
  USHORT Length;
  USHORT MaximumLength;
  PWSTR  Buffer;
} UNICODE_STRING, *PUNICODE_STRING;

这个结构体是在驱动里用的最多的字符串类的结构,其中,buffer就是字符串的实际开始地址,字符串是用宽字符表示(通俗理解就是两个字节表示一个字符),其次第一个元素length是字符的实际长度,第二个元素MaximumLength,是buffer最初分配的大小,也就是当前buffer最大能到多大。

  • UNICODE_STRING的创建
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
//方法1,直接赋值
UNICODE_STRING str1 = { 0 };
WCHAR wstr1[50] = L"hello driver";
str1.Length = wcslen(wstr1)*sizeof(WCHAR);
str1.MaximumLength = 50;
str1.Buffer = wstr1;

//方法2,用宏来创建,有两个宏方法来创建
DECLARE_CONST_UNICODE_STRING(ustr2,L"DECLARE_CONST_UNICODE_STRING");
//或者
UNICODE_STRING str2 = RTL_CONSTANT_STRING(L"hello driver");

方法3,用函数
UNICODE_STRING str4 = { 0 };
RtlInitUnicodeString(&str4,L"hello driver");
  • 字符串的拷贝与拼接
1
2
3
4
5
6
7
UNICODE_STRING str5 = { 0 };
WCHAR wcBuffer[256];
RtlInitEmptyUnicodeString(&str5, (PWCHAR)&wcBuffer, 256 * sizeof(WCHAR));
RtlCopyUnicodeString(&str5, &str0);

// 把后面的字符串拼到前面的字符串中
RtlAppendUnicodeStringToString(&usStr5, &usStr0);
  • 宽字符和单字符的相关转换
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 把单字符转换成Unicodestring
NTSYSAPI NTSTATUS RtlAnsiStringToUnicodeString(
  [in, out] PUNICODE_STRING DestinationString,
  [in]      PCANSI_STRING   SourceString,
  [in]      BOOLEAN         AllocateDestinationString
////最后元素含义:指定此例程是否应为目标字符串分配缓冲区空间。
);
 
UNICODE_STRING ustr = {0};
RtlAnsiStringToUnicodeString(&ustr, "Hello World", TRUE);

// 把单字节转换成双字节字符的
ANSI_STRING ustr2 = { 0 };
RtlUnicodeStringToAnsiString(&ustr2, &ustr, TRUE);

除此之外还有很多很多,一般我们都是用到的时候或者有一些想要处理的目标才会去寻找是否有更加方便的系统函数以实现,当然,在结合AI的方式会让使用的效率更加高。更多内容可以结合文档来查看https://learn.microsoft.com/zh-cn/windows-hardware/drivers/ddi/wdm/nf-wdm-rtlansistringtounicodestring

内存分配与操作

  • 内存分配与操作
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 官网文档函数定义
PVOID ExAllocatePool(
  [in] __drv_strictTypeMatch(__drv_typeExpr)POOL_TYPE PoolType,
  [in] SIZE_T                                         NumberOfBytes
);

// 其中,PoolType是分配内存池的类型

typedef enum _POOL_TYPE {
    NonPagedPool,
    NonPagedPoolExecute = NonPagedPool,
    PagedPool,
    NonPagedPoolMustSucceed = NonPagedPool + 2,
    DontUseThisType,
    NonPagedPoolCacheAligned = NonPagedPool + 4,
    PagedPoolCacheAligned,
    NonPagedPoolCacheAlignedMustS = NonPagedPool + 6,
    MaxPoolType,
    NonPagedPoolBase = 0,
    NonPagedPoolBaseMustSucceed = NonPagedPoolBase + 2,
    NonPagedPoolBaseCacheAligned = NonPagedPoolBase + 4,
    NonPagedPoolBaseCacheAlignedMustS = NonPagedPoolBase + 6,
    NonPagedPoolSession = 32,
    PagedPoolSession = NonPagedPoolSession + 1,
    NonPagedPoolMustSucceedSession = PagedPoolSession + 1,
    DontUseThisTypeSession = NonPagedPoolMustSucceedSession + 1,
    NonPagedPoolCacheAlignedSession = DontUseThisTypeSession + 1,
    PagedPoolCacheAlignedSession = NonPagedPoolCacheAlignedSession + 1,
    NonPagedPoolCacheAlignedMustSSession = PagedPoolCacheAlignedSession + 1,
    NonPagedPoolNx = 512,
    NonPagedPoolNxCacheAligned = NonPagedPoolNx + 4,
    NonPagedPoolSessionNx = NonPagedPoolNx + 32,

} POOL_TYPE;

// 一般用NonPagedPool很多,也就是非分页内存,就是希望这个逻辑空间,不要跨物理空间的物理页,跨页一般会涉及到内存调度,CPU读取切换,所以效率会比较低。

// 第二个参数就是分配的大小
  • ExAllocatePoolWithTag
1
2
3
4
5
6
// 这个函数和上面这个函数是一样的,唯一区别是有一个tag,标签,用作一些标记。
PVOID ExAllocatePoolWithTag(
  [in] __drv_strictTypeMatch(__drv_typeExpr)POOL_TYPE PoolType,
  [in] SIZE_T                                         NumberOfBytes,
  [in] ULONG                                          Tag
);

注意:以上两个函数的方法文档中有一个提示,就是这个函数其实已经废弃了,对应请使用ExAllocatePool2和ExAllocatePool3,定义如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
DECLSPEC_RESTRICT PVOID ExAllocatePool2(
  POOL_FLAGS Flags,
  SIZE_T     NumberOfBytes,
  ULONG      Tag
);

DECLSPEC_RESTRICT PVOID ExAllocatePool3(
  POOL_FLAGS                Flags,
  SIZE_T                    NumberOfBytes,
  ULONG                     Tag,
  PCPOOL_EXTENDED_PARAMETER ExtendedParameters,
  ULONG                     ExtendedParametersCount
);
  • ExFreePool、ExFreePool2、ExFreePoolWithTag

能够分配内存,也就能够释放回收内存,用的就是以上三个函数,定义如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
void ExFreePool(
  [in] PVOID P
);
// 参数p就是分配的地址

void ExFreePool2(
  PVOID                     P,
  ULONG                     Tag,
  PCPOOL_EXTENDED_PARAMETER ExtendedParameters,
  ULONG                     ExtendedParametersCount
);
// 这个对应的应该是ExAllocatePool3分配的内存,

void ExFreePoolWithTag(
  [in] PVOID P,
  [in] ULONG Tag
);
// Tag就是分配内存的时候写的tag

LIST_ENTRY

在windows驱动内核中,有一个很常用的双链表结构,定义如下:

1
2
3
4
typedef struct _LIST_ENTRY {
  struct _LIST_ENTRY *Flink;
  struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY, PRLIST_ENTRY;

这个结构体的主要作用是把诸多带有实际含义的结构体串联到一起,就想下图这样。

7

  • 初始化函数
1
2
3
4
5
6
// 用以下函数可以初始化链表头
void InitializeListHead(
  [out] PLIST_ENTRY ListHead
);

// 顾名思义,初始化一个链表头
  • 插入元素
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 插入元素用InsertHeadList这个函数
void InsertHeadList(
  [in, out] PLIST_ENTRY                  ListHead,
  [in, out] __drv_aliasesMem PLIST_ENTRY Entry
);
// 顾名思义,参数列表分别是,头结点和插入节点,那么可以推测是用头插法插入链表

// 进一步用vs2022点进函数查看可以验证所推断,函数的完整定义是


FORCEINLINE
VOID
InsertHeadList(
    _Inout_ PLIST_ENTRY ListHead,
    _Out_ __drv_aliasesMem PLIST_ENTRY Entry
    )

{

    PLIST_ENTRY NextEntry; //用来确定头结点之后的第一个数据节点

#if DBG

    RtlpCheckListEntry(ListHead);

#endif

    NextEntry = ListHead->Flink;
    if (NextEntry->Blink != ListHead) {
        FatalListEntryError((PVOID)ListHead,
                            (PVOID)NextEntry,
                            (PVOID)NextEntry->Blink);
    }
    // 头插法标准做法
    Entry->Flink = NextEntry;
    Entry->Blink = ListHead;
    NextEntry->Blink = Entry;
    ListHead->Flink = Entry;
    return;
}
  • CONTAINING_RECORD 在上述链表中,当通过flink或者blink遍历链表的时候,也只能找到LIST_ENTRY的地址,并不能找到该结构体的头部地址,所以可以用这个宏方便的计算出函数的地址
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// 定义如下:
void CONTAINING_RECORD(
   address,
   type,
   field
);

// 举个例子
// 定义包含链表节点的更大的结构体
typedef struct _MY_OBJECT {
  LIST_ENTRY Link; // 链表节点
  // 其他成员...
  int Data; // 假设MY_OBJECT还包含其他数据
} MY_OBJECT, *PMY_OBJECT;

// 假设我们有一个指向链表节点的指针pListEntry
// 我们想要获取包含这个链表节点的MY_OBJECT结构体的指针

// 使用CONTAINING_RECORD宏获取MY_OBJECT结构体的指针
PMY_OBJECT pMyObject = CONTAINING_RECORD(pListEntry, MY_OBJECT, Link);
  • RemoveHeadList、RemoveEntryList、RemoveTailList

以上三个函数是用来删除链表指定节点的函数,具体定义如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 移除头结点后的第一个数据节点
PLIST_ENTRY RemoveHeadList(
  [in, out] PLIST_ENTRY ListHead
);

// 移除指定地址的节点
BOOLEAN RemoveEntryList(
  [in] PLIST_ENTRY Entry
);

// 移除尾节点
PLIST_ENTRY RemoveTailList(
  [in, out] PLIST_ENTRY ListHead
);

感兴趣可以直接在vs2022上点击进去查看具体函数的定义,简单理解就是在一个带头结点的双链表上进行增删操作而已。

updatedupdated2024-12-292024-12-29