Windows 驱动开发学习之获取CPU温度

前情提要:

我的台式机经常在打游戏的时候会突然熄屏,然后重启。然后我问了很多人,最后感觉是主板过热,我想要确认一下我的猜测,所以就想着自己开发一个监控软件。

效果可见:https://www.bilibili.com/video/BV1734y1E7mW,目标是想开发一个这样效果的监控软件,所以就需要学习一波Windows的驱动开发了,我的台式机是AMD 5950X,但是不好直接拿我台式机测试,所以就先拿着我的老笔记本进行测试,我笔记本的是Intel的U,所以代码先按Intel的情况来写,之后还需要改成AMD的。

在学习的过程中也踩了很多坑,这里直接写我成功的方案。

第一步,Windows驱动开发环境搭建

参考:https://docs.microsoft.com/en-us/windows-hardware/drivers/download-the-wdk

Visual Studio

首先安装Visual Studio 2019,因为经过我的测试发现,2022在Win11或者Win10上会有一些bug,虽然不影响使用,但是我有强迫症,看不得有错误或者警告信息,所以我换成了2019。

Windows SDK

这里SDK不建议安装文档中写的,单独下载,因为我踩到了坑,就是官方提供最新版的Win SDK和WDK的版本不一样,而安装WDK的前提是需要有相应版本的Win SDK,所以会导致WDK安装失败,这里建议在安装VS 2019的时候,就选上最新版的Win SDK。

Windows Driver Kits

接着下载跟你Win SDK版本相同的WDK,安装上,然后这个安装包还会装上VS开发驱动的插件,在安装好以后,你就能开始驱动开发了。

双机调试

调试本机的虚拟机的驱动,可以使用串口来通信,设置使用串口通信的文章挺多的,这里就不多说了。

我的方案是有两台电脑,所以我得使用网络来进行通信,可以参考文章:https://blog.csdn.net/a4642313/article/details/82414804

但是这篇文章中还是有点问题的,在被调试的机器上,需要安装的应该是:WDK Test Target Setup x64-x64这个程序,因为这个程序会监听本地的50005端口,而VS就是需要跟被调试机器的50005端口进行通信,bcdedit设置的端口是调试器比如windbg监听的端口,然后让被调试的机器主动去访问调试器监听的端口,然后达到通信的目的,所以bcdedit的设置是不会监听本地端口的。

wind0

第一个驱动——HelloWorld

代码如下:

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

NTSTATUS Unload(PDRIVER_OBJECT driver)
{
DbgPrint("Driver unload...\n");
return 0;
}

NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING regPath)
{
driver->DriverUnload = Unload;
DbgPrint("driver test: %ws...\n", regPath->Buffer);
return 0;
}

如果大家创建是一个空的KMDF,直接调试是会出问题的,得修改一下inf文件,如果不知道怎么修改,就创建一个有模板的KMDF项目,然后把inf抄过来就好了,接下来点击调试,就能成功把驱动传送到目标设备,但是调试还是有点问题,不过我目前的需求不需要调试,所以我还没研究怎么调试驱动。

本机驱动部署测试

后来因为在虚拟机里,没办法获取到CPU温度信息,所以后续就考虑在我的笔记本上直接进行测试。

要部署驱动,首先得对驱动进行签名,但是我们又不是要发布驱动,写的驱动也是自己用,所以这种签名对我来说过于麻烦了,查找了一波资料,发现只能用测试签名来安装驱动,这需要使用bcdedit命令来开启允许测试签名,命令为:bcdedit /set TESTSIGNING ON,然后重启计算机,就行了。

接着可以使用sc命令对驱动进行安装,启动,停止,卸载:

1
2
3
4
5
6
7
8
# 安装驱动
$ sc create <ServerName> binPath=<sys file path> type=kernel
# 启动驱动
$ sc start <ServerName>
# 停止驱动
$ sc stop <ServerName>
# 卸载驱动
$ sc delete <ServerName>

可以使用DebugView,微软官方提供的工具,来查看DbgPrint函数输出的信息,最开始,我用DebugView没办法获取到调试信息,后续发现可能是以下原因的一种:

  1. DbgPrint没有回车(是这个原因的概率比较低)
  2. 编译成了x86的在x64的机器上跑,VS默认好像就是编译X86版本的
  3. winsdk/wdk版本不对(概率也比较低)

用户代码与驱动交互

驱动代码:

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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#include <ntddk.h>
#include <ntdef.h>

#define DEVICENAME L"\\Device\\DriverTest"
#define SYMBOLLINK L"\\??\\DriverTest"


NTSTATUS TodoSymLink(UNICODE_STRING *DeviceName)
{
NTSTATUS status = 0;
UNICODE_STRING symLinkName = RTL_CONSTANT_STRING(SYMBOLLINK);
if (DeviceName)
{
DbgPrint("[driverTest] IoCreateSymbolicLink...\n");
if (status = IoCreateSymbolicLink(&symLinkName, DeviceName))
{
DbgPrint("[driverTest] IoCreateSymbolicLink failed...\n");
return status;
}
}
else
{
DbgPrint("[driverTest] IoDeleteSymbolicLink...\n");
status = IoDeleteSymbolicLink(&symLinkName);
}
return status;
}

NTSTATUS Unload(PDRIVER_OBJECT driver)
{
NTSTATUS status = 0;
UNREFERENCED_PARAMETER(driver);
DbgPrint("[driverTest] driver unload......\n");
if (driver->DeviceObject)
{
DbgPrint("[driverTest] driver delete device object...\n");
IoDeleteDevice(driver->DeviceObject);
status = TodoSymLink(NULL);
}
return 0;
}

NTSTATUS DriverTestCreate(PDEVICE_OBJECT driver, PIRP pirp)
{
NTSTATUS status = 0;
pirp->IoStatus.Status = 0;
pirp->IoStatus.Information = 0;
IoCompleteRequest(pirp, IO_NO_INCREMENT);
DbgPrint("[driverTest] driver create success...\n");
return status;
}

NTSTATUS DriverTestClose(PDEVICE_OBJECT driver, PIRP pirp)
{
NTSTATUS status = 0;
pirp->IoStatus.Status = 0;
pirp->IoStatus.Information = 0;
IoCompleteRequest(pirp, IO_NO_INCREMENT);
DbgPrint("[driverTest] driver close success...\n");
return status;
}

NTSTATUS DriverTestRead(PDEVICE_OBJECT driver, PIRP pirp)
{
NTSTATUS status = 0;
DbgPrint("[driverTest] driver start read...\n");
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pirp);
ULONG readLength = stack->Parameters.Read.Length;
pirp->IoStatus.Status = 0;
pirp->IoStatus.Information = readLength - 0x80;
memset(pirp->AssociatedIrp.SystemBuffer, 0x41, readLength);
IoCompleteRequest(pirp, IO_NO_INCREMENT);
DbgPrint("[driverTest] driver read success...\n");
return status;
}

NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING regPath)
{
NTSTATUS status = 0;
PDEVICE_OBJECT deviceobj = NULL;

DbgPrint("[driverTest] start entry.\n");

// set unload function
driver->DriverUnload = Unload;
// create device
UNICODE_STRING DeviceName = RTL_CONSTANT_STRING(DEVICENAME);
if (status = IoCreateDevice(driver, 0, &DeviceName, FILE_DEVICE_UNKNOWN, 0, TRUE, &deviceobj))
{
DbgPrint("[driverTest] IoCreateDevice Error....\n");
return status;
}
// create symbol link, windows user mod CreateFile use symbol link to operate driver.
if (status = TodoSymLink(&DeviceName))
{
IoDeleteDevice(deviceobj);
return status;
}
// set flag
deviceobj->Flags |= DO_BUFFERED_IO;

// create callback
driver->MajorFunction[IRP_MJ_READ] = DriverTestRead;
driver->MajorFunction[IRP_MJ_CREATE] = DriverTestCreate;
driver->MajorFunction[IRP_MJ_CLOSE] = DriverTestClose;

DbgPrint("[driverTest] Driver Entry success...\n");
return status;
}

驱动代码没啥好说的,网上一堆,就是最简单的实现了驱动的打开,关闭和读取。可以类比为Linux下的open, close, read。

主要是还是用户怎么与驱动交互,也就是读取驱动数据。下面是用户的代码:

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
#include <windows.h>
#include <stdio.h>

void down(HANDLE handle)
{
CloseHandle(handle);
}

int main(int argc, char* argv[])
{
HANDLE driverHandle;
wchar_t * driverName;
size_t lent, converted = 0;
DWORD readnum;
char buf[0x200] = {0, };

if (argc <= 1)
{
printf("Please enter driver Name.\n");
return -1;
}

driverHandle = CreateFileA(driverName, GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (driverHandle == INVALID_HANDLE_VALUE)
{
printf("CreateFile %ws ERROR.\nThe error code: %d\n", driverName, GetLastError());
return -3;
}

if (!ReadFile(driverHandle, buf, 0x200 - 1, &readnum, NULL))
{
printf("ReadFile call failed.\n");
down(driverHandle);
return -4;
}
printf("Get buf length: %d\nbuf: %s\nbuf real length: %lld\n", readnum, buf, strlen(buf));
for (int i = 0; i < 0x200; i++)
{
printf("0x%02X: 0x%02X\n", i, buf[i]);
}
for (int i = 0; i < 0x200; i++)
{
printf("%02X", buf[i]);
}
printf("\n");
down(driverHandle);
return 0;
}

其中CreateFileA可以类比成Linux的open函数,ReadFile就是Linux下的read函数。代码挺简单的,没啥需要过多解释的,也不会有啥坑。

内核读取MSR寄存器

在Intel的CPU中,可以使用rdmsr指令,从msr寄存器中获取到CPU的温度信息,该指令只能在内核态进行调用,也就是在驱动中调用。但是在x64下,VS不允许内敛汇编了,不过Windows封装了一个__readmsr函数,可以让我们调用rdmsr指令。然后我在驱动的DeviceIoControl中实现执行rdmsr指令的代码。这个DeviceIoControl大家可以类比成Linux下的ioctl函数功能。

现在的内核代码如下:

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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
#include <ntddk.h>
#include <ntdef.h>

#define DEVICENAME L"\\Device\\DriverTest"
#define SYMBOLLINK L"\\??\\DriverTest"

#define MSR 0x01
#define IOCTL_RDMSR CTL_CODE(FILE_DEVICE_UNKNOWN,MSR,METHOD_BUFFERED,FILE_ANY_ACCESS)

NTSTATUS TodoSymLink(UNICODE_STRING *DeviceName)
{
NTSTATUS status = 0;
UNICODE_STRING symLinkName = RTL_CONSTANT_STRING(SYMBOLLINK);
if (DeviceName)
{
DbgPrint("[driverTest] IoCreateSymbolicLink...\n");
if (status = IoCreateSymbolicLink(&symLinkName, DeviceName))
{
DbgPrint("[driverTest] IoCreateSymbolicLink failed...\n");
return status;
}
}
else
{
DbgPrint("[driverTest] IoDeleteSymbolicLink...\n");
status = IoDeleteSymbolicLink(&symLinkName);
}
return status;
}

NTSTATUS Unload(PDRIVER_OBJECT driver)
{
NTSTATUS status = 0;
UNREFERENCED_PARAMETER(driver);
DbgPrint("[driverTest] driver unload......\n");
if (driver->DeviceObject)
{
DbgPrint("[driverTest] driver delete device object...\n");
IoDeleteDevice(driver->DeviceObject);
status = TodoSymLink(NULL);
}
return 0;
}

NTSTATUS DriverTestCreate(PDEVICE_OBJECT driver, PIRP pirp)
{
NTSTATUS status = 0;
pirp->IoStatus.Status = 0;
pirp->IoStatus.Information = 0;
IoCompleteRequest(pirp, IO_NO_INCREMENT);
DbgPrint("[driverTest] driver create success...\n");
return status;
}

NTSTATUS DriverTestClose(PDEVICE_OBJECT driver, PIRP pirp)
{
NTSTATUS status = 0;
pirp->IoStatus.Status = 0;
pirp->IoStatus.Information = 0;
IoCompleteRequest(pirp, IO_NO_INCREMENT);
DbgPrint("[driverTest] driver close success...\n");
return status;
}

NTSTATUS DriverTestRead(PDEVICE_OBJECT driver, PIRP pirp)
{
NTSTATUS status = 0;
DbgPrint("[driverTest] driver start read...\n");
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pirp);
ULONG readLength = stack->Parameters.Read.Length;
pirp->IoStatus.Status = 0;
pirp->IoStatus.Information = readLength - 0x80;
memset(pirp->AssociatedIrp.SystemBuffer, 0x41, readLength);
IoCompleteRequest(pirp, IO_NO_INCREMENT);
DbgPrint("[driverTest] driver read success...\n");
return status;
}

NTSTATUS DriverTestControl(PDEVICE_OBJECT driver, PIRP pirp)
{
NTSTATUS status = 0;
size_t inLen, outLen;
PCHAR inBuf, outBuf;

INT32 msrAddr;
INT64 msrValue;

DbgPrint("[driverTest] driver start Control...\n");
PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pirp);
switch (stack->Parameters.DeviceIoControl.IoControlCode)
{
case IOCTL_RDMSR:
inLen = stack->Parameters.DeviceIoControl.InputBufferLength;
inLen = inLen > 4 ? 4 : inLen;
outLen = stack->Parameters.DeviceIoControl.OutputBufferLength;
outLen = outLen > 8 ? 8 : outLen;
inBuf = pirp->AssociatedIrp.SystemBuffer;
outBuf = pirp->AssociatedIrp.SystemBuffer;

RtlCopyBytes(&msrAddr, inBuf, inLen);
__try {
msrValue = __readmsr(msrAddr);
}
__except (EXCEPTION_EXECUTE_HANDLER) {
status = STATUS_NOT_SUPPORTED;
break;
}
RtlCopyMemory(outBuf, &msrValue, outLen);
pirp->IoStatus.Information = outLen;
break;
default:
status = STATUS_NOT_FOUND;
break;
}
pirp->IoStatus.Status = status;
IoCompleteRequest(pirp, IO_NO_INCREMENT);
DbgPrint("[driverTest] driver Control success...\n");
return status;
}

NTSTATUS DriverEntry(PDRIVER_OBJECT driver, PUNICODE_STRING regPath)
{
NTSTATUS status = 0;
PDEVICE_OBJECT deviceobj = NULL;

DbgPrint("[driverTest] start entry.\n");

// set unload function
driver->DriverUnload = Unload;
// create device
UNICODE_STRING DeviceName = RTL_CONSTANT_STRING(DEVICENAME);
if (status = IoCreateDevice(driver, 0, &DeviceName, FILE_DEVICE_UNKNOWN, 0, TRUE, &deviceobj))
{
DbgPrint("[driverTest] IoCreateDevice Error....\n");
return status;
}
// create symbol link, windows user mod CreateFile use symbol link to operate driver.
if (status = TodoSymLink(&DeviceName))
{
IoDeleteDevice(deviceobj);
return status;
}
// set flag
deviceobj->Flags |= DO_BUFFERED_IO;

// create callback
driver->MajorFunction[IRP_MJ_READ] = DriverTestRead;
driver->MajorFunction[IRP_MJ_CREATE] = DriverTestCreate;
driver->MajorFunction[IRP_MJ_CLOSE] = DriverTestClose;
driver->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DriverTestControl;

DbgPrint("[driverTest] Driver Entry success...\n");
return status;
}

这里有一个坑大家需要注意,就是IoControlCode不能随意设置的,需要按照CTL_CODE结构来设置,在我的宏定义里面:CTL_CODE(FILE_DEVICE_UNKNOWN,MSR,METHOD_BUFFERED,FILE_ANY_ACCESS)

第二个MSR,是我可以随意设置的,其他标识位都代表了该操作代码的一些属性,这些属性都是啥意思,就自行网上搜索了。

我踩到的坑是,我没设置CTL_CODE的结构,就直接定义#define IOCTL_RDMSR MSR,结果就是,用户的数据能传递到驱动中,但是驱动没发通过SystemBuffer传递数据到用户态。

上面代码还有一个缺陷,现在的CPU都是多核CPU,每个U都有一个msr寄存器,所以按上面的代码,每次过去到的信息,还不一定是哪个U的,所以需要指定哪个U来执行代码,在Windows中可以使用SetThreadAffinityMask ( GetCurrentThread (), 2 );函数来切换执行代码的CPU。

获取CPU温度信息

接下来就是用户态通过读取msr寄存器信息来获取CPU温度了,代码如下:

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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <locale.h>

#define MSR 0x01
#define IOCTL_RDMSR CTL_CODE(FILE_DEVICE_UNKNOWN,MSR,METHOD_BUFFERED,FILE_ANY_ACCESS)

void down(HANDLE handle)
{
CloseHandle(handle);
}

int readmsr(HANDLE driverHandle, UINT32 msrAddr)
{
UINT64 msrResult;
DWORD res;

if (DeviceIoControl(driverHandle, IOCTL_RDMSR, &msrAddr, sizeof(msrAddr), &msrResult, sizeof(msrResult), &res, NULL))
{
return msrResult;
}
else {
printf("readmsr %d Failed.\n", msrAddr);
return -1;
}
}

NTSTATUS testControl(HANDLE driverHandle)
{
NTSTATUS status = 0;
UINT32 msrAddr;
UINT64 msrResult = 0;
DWORD res;
char buf[0x11];

printf("Please enter msrAddr: ");
res = read(0, buf, 0x10);
buf[res] = 0;
msrAddr = strtol(buf, NULL, 0x10);
status = DeviceIoControl(driverHandle, IOCTL_RDMSR, &msrAddr, sizeof(msrAddr), &msrResult, sizeof(msrResult), &res, NULL);
// printf("do msrAddr: 0x%08X\nstatus: %d, return: %d\nget Result: 0x%016llX\n", msrAddr, status, res, msrResult);
return status;
}

NTSTATUS testRead(HANDLE driverHandle)
{
char buf[0x200] = { 0, };
DWORD readnum;

if (!ReadFile(driverHandle, buf, 0x200 - 1, &readnum, NULL))
{
printf("ReadFile call failed.\n");
down(driverHandle);
return -1;
}
printf("Get buf length: %d\nbuf: %s\nbuf real length: %lld\n", readnum, buf, strlen(buf));
for (int i = 0; i < 0x200; i++)
{
printf("0x%02X: 0x%02X\n", i, buf[i]);
}
for (int i = 0; i < 0x200; i++)
{
printf("%02X", buf[i]);
}
printf("\n");
return 0;
}

NTSTATUS GetTemp(HANDLE driverHandle)
{
NTSTATUS status = 0;

setlocale(LC_ALL, "chinese");
UINT32 maxTemCode = 0x1A2;
UINT32 maxTemMask = 0xFF;
int maxTem;
maxTem = readmsr(driverHandle, maxTemCode);
printf("Debug maxTem: %d\n", maxTem);
maxTem = (maxTem >> 16) & maxTemMask;
wprintf(L"Max Temperature: %d °C\n", maxTem);
UINT32 offTemCode = 0x19C;
UINT32 offTemMask = 0x7F;
int offTem;
offTem = readmsr(driverHandle, offTemCode);
printf("Debug offTem: %d\n", offTem);
offTem = (offTem >> 16) & offTemMask;
wprintf(L"Off Temperature: %d °C\n", offTem);
wprintf(L"CPU Temperature: %d °C\n", maxTem - offTem);
return status;
}

int main(int argc, char* argv[])
{
HANDLE driverHandle;
wchar_t * driverName;
size_t lent, converted = 0;

if (argc <= 1)
{
printf("Please enter driver Name.\n");
return -1;
}
driverName = argv[1];
driverHandle = CreateFileA(driverName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (driverHandle == INVALID_HANDLE_VALUE)
{
printf("CreateFile %ws ERROR.\nThe error code: %d\n", driverName, GetLastError());
return