Windows

多线程创建&同步

创建线程

PsCreateSystemThread 是一个用于创建内核模式线程的函数。

1
2
3
4
5
6
7
8
9
NTSTATUS PsCreateSystemThread(
OUT PHANDLE ThreadHandle,
IN ULONG DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
IN HANDLE ProcessHandle OPTIONAL,
OUT PCLIENT_ID ClientId OPTIONAL,
IN PKSTART_ROUTINE StartRoutine,
IN PVOID StartContext
);

**ThreadHandle (OUT PHANDLE)**:

  • 类型:输出参数,指向一个句柄变量。
  • 描述:当线程创建成功时,此参数将返回线程的句柄。你可以使用此句柄来操作线程,例如关闭线程句柄或获取线程状态。

**DesiredAccess (IN ULONG)**:

  • 类型:输入参数。
  • 描述:指定所需的访问权限。这通常是一个访问掩码,例如 THREAD_ALL_ACCESS。这个参数决定了调用者对线程的访问权限。

**ObjectAttributes (IN POBJECT_ATTRIBUTES OPTIONAL)**:

  • 类型:可选输入参数。
  • 描述:指向一个 OBJECT_ATTRIBUTES 结构,它指定线程对象的属性。通常为 NULL,除非你需要设置特定的对象属性。

**ProcessHandle (IN HANDLE OPTIONAL)**:

  • 类型:可选输入参数。
  • 描述:指定创建线程的进程句柄。通常为 NULL,表示线程将被创建在系统进程上下文中。如果指定了某个进程句柄,则线程将被创建在该进程中。

**ClientId (OUT PCLIENT_ID OPTIONAL)**:

  • 类型:可选输出参数。
  • 描述:如果不为 NULL,则此参数将返回线程的客户端 ID,包括进程 ID 和线程 ID。

**StartRoutine (IN PKSTART_ROUTINE)**:

  • 类型:输入参数。
  • 描述:指向线程入口函数的指针。当线程开始执行时,将调用此函数。入口函数的原型应为 VOID (*PKSTART_ROUTINE)(PVOID StartContext)

**StartContext (IN PVOID)**:

  • 类型:输入参数。
  • 描述:传递给 StartRoutine 的上下文参数。可以为任意类型的指针,在线程入口函数中使用。
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
#include <header.h>
VOID ThreadEntryRoutine(PVOID Context)
{
UNREFERENCED_PARAMETER(Context);

// 线程执行的任务
DbgPrint("System thread is running\n");

// 模拟一些工作
LARGE_INTEGER interval;
interval.QuadPart = -10 * 1000 * 1000; // 等待 1 秒 1秒 = 1000毫秒 = 1,000,000微秒 = 10,000,000纳秒。
KeDelayExecutionThread(KernelMode, FALSE, &interval);

// 线程结束
DbgPrint("System thread is exiting\n");
PsTerminateSystemThread(STATUS_SUCCESS);
}
NTSTATUS CreateSystemThreadExample(PHANDLE pThreadHandle)
{
NTSTATUS status;

// 创建系统线程
status = PsCreateSystemThread(
pThreadHandle, // 返回的线程句柄
THREAD_ALL_ACCESS, // 线程访问权限
NULL, // 对象属性
NULL, // 进程句柄
NULL, // 线程 ID
ThreadEntryRoutine, // 线程入口点
NULL // 传递给线程的上下文参数
);

if (NT_SUCCESS(status))
{
DbgPrint("System thread created successfully\n");
}
else
{
DbgPrint("Failed to create system thread\n");
}
return status;
}

HANDLE g_ThreadHandle = NULL;

NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
NTSTATUS status;

// 驱动程序卸载例程
DriverObject->DriverUnload = DriverUnloadRoutine;

// 创建系统线程
status = CreateSystemThreadExample(&g_ThreadHandle);
if (!NT_SUCCESS(status))
{
DbgPrint("Failed to create thread in DriverEntry\n");
return status;
}

return STATUS_SUCCESS;
}

VOID DriverUnloadRoutine(IN PDRIVER_OBJECT DriverObject)
{
UNREFERENCED_PARAMETER(DriverObject);

if (g_ThreadHandle)
{
// 终止线程(如果线程尚未退出)
NTSTATUS status = ZwWaitForSingleObject(g_ThreadHandle, FALSE, NULL);
if (status == STATUS_SUCCESS)
{
DbgPrint("System thread has exited\n");
}
else
{
DbgPrint("Failed to wait for system thread to exit\n");
}

// 关闭线程句柄
ZwClose(g_ThreadHandle);
g_ThreadHandle = NULL;
}

DbgPrint("Driver unloaded\n");
}

KeDelayExecutionThread 是 Windows 内核模式中的一个函数,用于使当前线程进入等待状态一段指定的时间。这个函数在内核模式下运行,主要用于实现线程的延迟或睡眠操作。

LARGE_INTEGERQuadPart 成员设置为负数是为了指定相对时间,使线程从当前时间起等待指定的时间长度。如果你希望线程等待绝对时间点到达,则应将 QuadPart 设置为正值。相对时间通常更直观和常用,因为它表示从当前时间起的等待时长。

内核模式下创建的线程必须使用PsTerminateSystemThread强制结束线程,否则该线程无法自动退出

对应三环的内核同步

同步对象的等待

应用层:WaitForSingleObject和WaitForMultipleObjects
内核层:KeWaitForSingleObject和KeWaitForMultipleObjects

同步对象的状态通常有两种:激发态(Signaled)和非激发态(Non-signaled)。

  • 激发态:同步对象在激发态时,等待这个对象的线程会被唤醒。比如,一个事件对象在被设置为激发态时,所有等待这个事件的线程都会继续执行。
  • 非激发态:同步对象在非激发态时,等待这个对象的线程会进入等待状态,直到对象变为激发态或超时。

KeWaitForSingleObject如果等待的同步对象变为激发态,这个函数会退出睡眠状态,并返回STATUS_SUCCESS
如果这个函数因为超时而退出,则返回STATUS_TIMEOUT

KeWaitForMultipleObjects:如果这个函数因为超时而退出,返回STATUS_TIMEOUT
如果数组中其中一个同步对象变为激发态,这个函数返回的状态码减去STATUS_WAIT_0就是激发的同步对象在数组中的索引号

事件对象Event

应用层:CreateEvent
内核层:KeInitializeEvent

注意:
如果创建的事件对象是通知事件,当事件对象变为激发态时,程序员需手动将其改为未激发态。
如果创建的事件对象是同步事件,当事件对象变为激发态时(遇到KeWaitForXXX等内核函数),事件对象自动变回未激发态

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
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
UNREFERENCED_PARAMETER(DriverObject);

// 初始化事件对象为非激发态
KeInitializeEvent(&Event, NotificationEvent, FALSE);

// 创建线程A
NTSTATUS status = PsCreateSystemThread(&ThreadHandleA, THREAD_ALL_ACCESS, NULL, NULL, NULL, ThreadA, NULL);
if (!NT_SUCCESS(status))
{
DbgPrint("Failed to create Thread A\n");
return status;
}

// 创建线程B
status = PsCreateSystemThread(&ThreadHandleB, THREAD_ALL_ACCESS, NULL, NULL, NULL, ThreadB, NULL);
if (!NT_SUCCESS(status))
{
DbgPrint("Failed to create Thread B\n");
ZwClose(ThreadHandleA);
return status;
}

// 等待线程完成
ZwClose(ThreadHandleA);
ZwClose(ThreadHandleB);

return STATUS_SUCCESS;
}


VOID ThreadA(PVOID Context)
{
UNREFERENCED_PARAMETER(Context);

// 初始化资源(这里用一个简单的延时模拟初始化过程)
DbgPrint("Thread A: Initializing resource...\n");
LARGE_INTEGER interval;
interval.QuadPart = -10 * 1000 * 1000; // 1 second in 100-nanosecond intervals
KeDelayExecutionThread(KernelMode, FALSE, &interval);

// 设置事件对象为激发态,通知线程B资源已初始化
KeSetEvent(&Event, IO_NO_INCREMENT, FALSE);
DbgPrint("Thread A: Resource initialized, event set.\n");

PsTerminateSystemThread(STATUS_SUCCESS);
}


VOID ThreadB(PVOID Context)
{
UNREFERENCED_PARAMETER(Context);

// 等待事件对象变为激发态
DbgPrint("Thread B: Waiting for resource initialization...\n");
NTSTATUS status = KeWaitForSingleObject(&Event, Executive, KernelMode, FALSE, NULL);
if (status == STATUS_SUCCESS)
{
DbgPrint("Thread B: Resource initialized, proceeding with execution.\n");
// 使用已初始化的资源
}

PsTerminateSystemThread(STATUS_SUCCESS);
}

应用程序和驱动如何使用同一个事件对象

事件对象在用户模式使用句柄代表,在驱动下使用KEVENT数据结构代表

在用户模式创建一个同步事件
然后使用DeviceIoControl将同步事件的句柄传递给驱动
驱动程序在通过DeviceIoContol的IRP派遣函数获得同步事件的句柄
驱动程序调用ObReferenceObjectByHandle将句柄转化为实际的事件对象指针(此时该事件对象内部维护的一个引用计数将加1)
驱动程序使用此事件对象指针进行同步等自定义操作
使用完毕后,调用ObDereferenceObject函数(将这个事件对象内部维护的引用计数减1)

步骤如下:

  1. 在用户模式创建同步事件
    • 应用程序在用户模式中使用 CreateEvent 函数创建一个同步事件,并获取到事件对象的句柄。
  2. 通过 DeviceIoControl 传递句柄给驱动
    • 应用程序通过 DeviceIoControl 函数将事件对象的句柄传递给驱动程序。这可以通过向驱动程序发送 IOCTL(Input/Output Control)请求来实现。
  3. 驱动程序处理 IOCTL 请求
    • 驱动程序在其 IRP (I/O Request Packet) 派遣函数中处理来自应用程序的 IOCTL 请求,从中获取到事件对象的句柄。
  4. 使用 ObReferenceObjectByHandle 获取事件对象指针
    • 驱动程序使用 ObReferenceObjectByHandle 函数将从用户空间接收到的事件对象句柄转换为实际的 KEVENT 数据结构指针。这个函数调用会增加事件对象的引用计数。
  5. 驱动程序使用事件对象进行操作
    • 驱动程序通过这个指针进行需要的同步操作,比如等待事件的触发 (KeWaitForSingleObject) 或者设置事件为激发状态 (KeSetEvent)。
  6. **使用完毕后调用 ObDereferenceObject**:
    • 驱动程序在不再需要事件对象指针时,使用 ObDereferenceObject 函数将事件对象的引用计数减少,确保正确释放。

效果图:

1721294123749

三环代码:

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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
//#include <iostream>
//#include <windows.h>
//using namespace std;
//

//
//int main()
//{
// // 创建同步事件
// HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, L"MyEvent");
// if (hEvent == NULL)
// {
// std::cerr << "Failed to create event. Error: " << GetLastError() << std::endl;
// system("pause");
// return 1;
// }
//
//
//
// HANDLE hDevice = CreateFile(
// L"\\\\.\\2024_07_17_link", // 设备符号链接名称
// GENERIC_READ | GENERIC_WRITE, // 请求的访问权限
// 0, // 共享模式
// NULL, // 安全属性
// OPEN_EXISTING, // 打开已存在的设备
// 0, // 文件属性和标志
// NULL // 模板文件句柄
// );
// if (hDevice == INVALID_HANDLE_VALUE)
// {
// std::cerr << "Failed to Open Device. Error: " << endl;
// DWORD error = GetLastError();
// std::cerr << "Failed to open device. Error: " << error << std::endl;
//
// // 详细错误检查
// if (error == ERROR_FILE_NOT_FOUND)
// {
// std::cerr << "The system cannot find the file specified." << std::endl;
// }
// else if (error == ERROR_ACCESS_DENIED)
// {
// std::cerr << "Access is denied." << std::endl;
// }
// else if (error == ERROR_INVALID_PARAMETER)
// {
// std::cerr << "The parameter is incorrect." << std::endl;
// }
//
//
// CloseHandle(hEvent);
// system("pause");
// return 1;
// }
//
// // 传递事件句柄给驱动程序
//
//
//
//
//#define IOCTL_CUSTOM_FUNC1 CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)
//#define IOCTL_CUSTOM_FUNC2 CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)
// DWORD bytesReturned;
// if (!DeviceIoControl(hDevice, IOCTL_CUSTOM_FUNC1, &hEvent, sizeof(HANDLE), NULL, 0, &bytesReturned, NULL))
// {
// std::cerr << "DeviceIoControl failed. Error: " << GetLastError() << std::endl;
// CloseHandle(hDevice);
// CloseHandle(hEvent);
// return 1;
// }
//
// while (1)
// {
//
// WaitForSingleObject(hEvent, INFINITE);
// MessageBox(NULL, NULL, NULL, NULL);
// }
//
//
// // 关闭句柄
// CloseHandle(hDevice);
// CloseHandle(hEvent);
//
// system("pause");
//
// return 0;
//
//
//
//}


#include <windows.h>
#include <iostream>
using namespace std;
void PrintErrorMessage(DWORD errorCode)
{
LPVOID lpMsgBuf;

FormatMessageW(
FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL,
errorCode,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // 默认语言
(LPWSTR)&lpMsgBuf,
0,
NULL
);

// 输出错误消息
wprintf(L"Error code %d: %s\n", errorCode, (LPWSTR)lpMsgBuf);

// 释放缓冲区
LocalFree(lpMsgBuf);
}


int main()
{
//HANDLE hDevice = CreateFile(
// L"\\\\.\\2024_07_17_link2", // 符号链接名称
// GENERIC_READ | GENERIC_WRITE, // 访问权限
// 0, // 共享模式
// NULL, // 安全属性
// OPEN_EXISTING, // 打开已存在的设备
// 0, // 文件属性和标志
// NULL // 模板文件句柄
//);
HANDLE hDevice = CreateFile(L"\\\\.\\2024_07_18_link", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

if (hDevice == INVALID_HANDLE_VALUE)
{
DWORD error = GetLastError();
std::cerr << "Failed to open device. Error: " << error << std::endl;

if (error == ERROR_FILE_NOT_FOUND)
{
std::cerr << "The system cannot find the file specified." << std::endl;
}
else if (error == ERROR_ACCESS_DENIED)
{
std::cerr << "Access is denied." << std::endl;
}
else if (error == ERROR_INVALID_PARAMETER)
{
std::cerr << "The parameter is incorrect." << std::endl;
}
system("pause");
return 1;
}

std::cout << "Device opened successfully" << std::endl;

// 创建同步事件
HANDLE hEvent = CreateEvent(NULL, FALSE, TRUE, L"MyEvent");
if (hEvent == NULL)
{
std::cerr << "Failed to create event. Error: " << GetLastError() << std::endl;
system("pause");
return 1;
}

#define IOCTL_CUSTOM_FUNC1 CTL_CODE(FILE_DEVICE_UNKNOWN, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)
#define IOCTL_CUSTOM_FUNC2 CTL_CODE(FILE_DEVICE_UNKNOWN, 0x801, METHOD_BUFFERED, FILE_ANY_ACCESS)
DWORD bytesReturned;
cout << "事件的句柄是0x" << hex << hEvent << endl;
if (!DeviceIoControl(hDevice, IOCTL_CUSTOM_FUNC1, &hEvent, sizeof(HANDLE), NULL, 0, &bytesReturned, NULL))
{
PrintErrorMessage(GetLastError());
CloseHandle(hDevice);
CloseHandle(hEvent);
return 1;
}

while (1)
{
WaitForSingleObject(hEvent, INFINITE);
cout << "已释放Event" << endl;

DWORD result=MessageBox(NULL, NULL, NULL, NULL);
if (result == IDCANCEL)
{
break;
}
SetEvent(hEvent);
}


// 关闭句柄
CloseHandle(hDevice);
CloseHandle(hEvent);

system("pause");

return 0;
}

驱动代码:

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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
#include "header.h"

PKEVENT g_Event = NULL;
HANDLE hUserEvent = NULL;
HANDLE g_ThreadHandle = NULL;


VOID ThreadEntryRoutine(PVOID Context)
{
UNREFERENCED_PARAMETER(Context);

LARGE_INTEGER interval;
interval.QuadPart = -10 * 1000 * 1000; // 等待 1 秒
KeDelayExecutionThread(KernelMode, FALSE, &interval);
KdBreakPoint();

while (1)
{
KeWaitForSingleObject(g_Event, Executive, KernelMode, FALSE, NULL);
KdPrint(("拿到Event!\n"));
interval.QuadPart = -10 * 1000 * 1000; // 等待 1 秒
KeDelayExecutionThread(KernelMode, FALSE, &interval);
KeSetEvent(g_Event, IO_NO_INCREMENT, FALSE);
}

PsTerminateSystemThread(STATUS_SUCCESS);
}

NTSTATUS CreateSystemThreadExample(PHANDLE pThreadHandle)
{
NTSTATUS status;

status = PsCreateSystemThread(
pThreadHandle, // 返回的线程句柄
THREAD_ALL_ACCESS, // 线程访问权限
NULL, // 对象属性
NULL, // 进程句柄
NULL, // 线程 ID
ThreadEntryRoutine, // 线程入口点
NULL // 传递给线程的上下文参数
);

if (NT_SUCCESS(status))
{
DbgPrint("System thread created successfully\n");
}
else
{
DbgPrint("Failed to create system thread\n");
}
return status;
}

NTSTATUS MyDeviceControlRoutine(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
PIO_STACK_LOCATION irpSp;
PVOID inputBuffer;
ULONG inputBufferLength;
ULONG ioControlCode;
NTSTATUS ntStatus = STATUS_SUCCESS;
LARGE_INTEGER interval;
//KdBreakPoint();

UNREFERENCED_PARAMETER(DeviceObject);

interval.QuadPart = -10 * 1000 * 1000; // 等待 1 秒
KeDelayExecutionThread(KernelMode, FALSE, &interval);

irpSp = IoGetCurrentIrpStackLocation(Irp);
inputBuffer = Irp->AssociatedIrp.SystemBuffer;
inputBufferLength = irpSp->Parameters.DeviceIoControl.InputBufferLength;
ioControlCode = irpSp->Parameters.DeviceIoControl.IoControlCode;

__try
{
if (MmIsAddressValid(inputBuffer))
{
KdPrint(("地址正确!"));
}
else
{
KdPrint(("Invalid user buffer address\n"));
Irp->IoStatus.Status = STATUS_INVALID_USER_BUFFER;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_INVALID_USER_BUFFER;
}
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
KdPrint(("Exception occurred: Access violation\n"));
Irp->IoStatus.Status = GetExceptionCode();
Irp->IoStatus.Information = 0;
return Irp->IoStatus.Status;
}

switch (ioControlCode)
{
case IOCTL_CUSTOM_FUNC1:
{
hUserEvent = *(HANDLE*)inputBuffer;
NTSTATUS status = ObReferenceObjectByHandle(hUserEvent, EVENT_MODIFY_STATE, *ExEventObjectType, KernelMode, (PVOID*)&g_Event, NULL);
if (!NT_SUCCESS(status))
{
DbgPrint("Failed to reference event object from user mode handle. Status: %08x\n", status);
break;
}
else
{
KdPrint(("拿到内核事件对象!"));
}

status = CreateSystemThreadExample(&g_ThreadHandle);
if (!NT_SUCCESS(status))
{
DbgPrint("Failed to create thread in DriverEntry\n");
return status;
}
else
{
KdPrint(("创建线程成功!"));
}

return STATUS_SUCCESS;
}

default:
{
ntStatus = STATUS_INVALID_DEVICE_REQUEST;
break;
}
}

Irp->IoStatus.Status = ntStatus;
IoCompleteRequest(Irp, IO_NO_INCREMENT);

return ntStatus;
}


NTSTATUS CreateDispatch(
IN PDEVICE_OBJECT DeviceObject,
IN PIRP Irp
)
{
DeviceObject;
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}



NTSTATUS DriverEntry(IN PDRIVER_OBJECT DriverObject, IN PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
KdPrint(("Create!"));

// 驱动程序卸载例程&注册例程
DriverObject->DriverUnload = DriverUnloadRoutine;
DriverObject->MajorFunction[IRP_MJ_CREATE] = CreateDispatch;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = MyDeviceControlRoutine;


//创建设备
NTSTATUS status;
PDEVICE_OBJECT DeviceObject = NULL;
UNICODE_STRING DeviceName;
RtlInitUnicodeString(&DeviceName, L"\\Device\\MyDevice");
status = IoCreateDevice(
DriverObject, // 驱动程序对象
0, // 设备扩展大小
&DeviceName, // 设备名称
FILE_DEVICE_UNKNOWN, // 设备类型
0, // 设备特征
FALSE, // 非独占设备
&DeviceObject // 返回的设备对象指针
);

if (!NT_SUCCESS(status))
{
KdPrint(("Failed to create device: %X\n", status));
return status;
}
KdPrint(("Device created successfully\n"));

UNICODE_STRING symbolicLink = RTL_CONSTANT_STRING(L"\\??\\2024_07_18_link");
status = IoCreateSymbolicLink(&symbolicLink, &DeviceName);



if (!NT_SUCCESS(status))
{
KdPrint(("Failed to create device: %X\n", status));
return status;
}
KdPrint(("Device created successfully\n"));


return STATUS_SUCCESS;
}






VOID DriverUnloadRoutine(IN PDRIVER_OBJECT DriverObject)
{
NTSTATUS status = ObDereferenceObject(&g_Event);
if (!NT_SUCCESS(status))
{
DbgPrint("Failed to Deference event object .Status: %08x\n", status);

}
else
{
KdPrint(("对象的引用计数成功减少!"));
}
if (DriverObject->DeviceObject != NULL)
{
UNICODE_STRING symbolicLink = RTL_CONSTANT_STRING(L"\\??\\2024_07_18_link");
IoDeleteSymbolicLink(&symbolicLink);
IoDeleteDevice(DriverObject->DeviceObject);
}
// 完成后解除引用
if (g_Event!=NULL)
{
ObDereferenceObject(g_Event);
g_Event = NULL;
}
DbgPrint("Driver unloaded\n");
}

写这些代码确实遇到不少坑:

1.三环要用DeviceIoControl传递数据,驱动程序不仅要注册IRP_MJ_DEVICE_CONTROL还要注册IRP_MJ_CREATE,否则会报错,呜呜呜,搞好久

2.三环用DeviceIoControl传的句柄貌似只能在IRP_MJ_DEVICE_CONTROL事件处理函数有效??我创了一个子线程,调用ObReferenceObjectByName报错句柄无效,但是实际上句柄值是一样的….又搞了好久…

3.HANDLE hEvent = CreateEvent(NULL, FALSE, TRUE, L”MyEvent”);第三个参数是决定事件Event一开始的状态的,FALSE表示一开始无信号,TRUE表示一开始有信号

参考:驱动程序和驱动程序使用同一个事件对象
场景:驱动程序A的派遣函数需要和驱动程序B的派遣函数进行同步
问题:驱动A如何获取驱动B所创建的事件对象
解决:驱动B创建一个带有名字的事件对象,这样驱动A即可通过名字寻找到事件对象的指针

创建有名事件对象:IoCreateNotificatiionEvent(创建通知事件对象)和IoCreateSynchronizationEvent(创建同步事件对象)

这两个函数得到的是事件对象的句柄,要想进一步得到事件对象指针,还需调用ObReferenceObjectByHandle
内核中不存在指定名称的事件对象,这两个函数会创建;如果已经存在指定名称的事件对象,则会打开

信号灯Semaphore

应用层:CreateSemaphore
内核层:KeInitializeSemaphore对信号对象初始化

Semaphore:需要初始化的内核对象信号灯的指针
Count:初始化时信号灯的计数
Limit:指明信号灯计数的上限值

读取信号灯当前的计数值:KeReadStateSemaphore
释放信号灯,增加计数值:KeReleaseSemaphore(可指定增加计数值的数目)
获得信号灯,减少计数值:KeWaitXXX系列函数(如果能获得信号灯,则计数减1,否则陷入等待)

示例代码:

一共有四个线程,要求同一时间只允许2个线程同时进行

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
VOID ThreadFunction(PVOID Context)
{
UNREFERENCED_PARAMETER(Context);

// 尝试获取信号量
NTSTATUS status = KeWaitForSingleObject(&Semaphore, Executive, KernelMode, FALSE, NULL);
if (status == STATUS_SUCCESS)
{
// 成功获取信号量,执行任务
DbgPrint("Thread %d: Acquired semaphore, performing task...\n", PsGetCurrentThreadId());

// 模拟执行任务的过程
LARGE_INTEGER interval;
interval.QuadPart = -10 * 1000 * 1000; // 1 second in 100-nanosecond intervals
KeDelayExecutionThread(KernelMode, FALSE, &interval);

// 释放信号量
KeReleaseSemaphore(&Semaphore, IO_NO_INCREMENT, 1, FALSE);
DbgPrint("Thread %d: Released semaphore.\n", PsGetCurrentThreadId());
}

PsTerminateSystemThread(STATUS_SUCCESS);
}

VOID ThreadFunction(PVOID Context)
{
UNREFERENCED_PARAMETER(Context);

// 尝试获取信号量
NTSTATUS status = KeWaitForSingleObject(&Semaphore, Executive, KernelMode, FALSE, NULL);
if (status == STATUS_SUCCESS)
{
// 成功获取信号量,执行任务
DbgPrint("Thread %d: Acquired semaphore, performing task...\n", PsGetCurrentThreadId());

// 模拟执行任务的过程
LARGE_INTEGER interval;
interval.QuadPart = -10 * 1000 * 1000; // 1 second in 100-nanosecond intervals
KeDelayExecutionThread(KernelMode, FALSE, &interval);

// 释放信号量
KeReleaseSemaphore(&Semaphore, IO_NO_INCREMENT, 1, FALSE);
DbgPrint("Thread %d: Released semaphore.\n", PsGetCurrentThreadId());
}

PsTerminateSystemThread(STATUS_SUCCESS);
}


NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
UNREFERENCED_PARAMETER(DriverObject);

// 初始化信号量,初始计数为2,最大计数为2
KeInitializeSemaphore(&Semaphore, 2, 2);

// 创建四个线程
for (int i = 0; i < 4; ++i)
{
NTSTATUS status = PsCreateSystemThread(&ThreadHandles[i], THREAD_ALL_ACCESS, NULL, NULL, NULL, ThreadFunction, NULL);
if (!NT_SUCCESS(status))
{
DbgPrint("Failed to create Thread %d\n", i);
// 可能需要清理之前创建成功的线程句柄
for (int j = 0; j < i; ++j)
{
ZwClose(ThreadHandles[j]);
}
return status;
}
}

// 等待线程完成
for (int i = 0; i < 4; ++i)
{
ZwClose(ThreadHandles[i]);
}

return STATUS_SUCCESS;
}

互斥体Mutex

应用层:CreateMutex
内核层:KeInitializeMutex

Mutex:需要初始化的互斥体
Level:保留值,一般为0

获得互斥体:KeWaitXXX系列函数
释放互斥体:KeReleaseMutex函数

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
VOID ThreadFunction(PVOID Context)
{
UNREFERENCED_PARAMETER(Context);

// 尝试获取互斥体
NTSTATUS status = KeWaitForMutexObject(&Mutex, Executive, KernelMode, FALSE, NULL);
if (status == STATUS_SUCCESS)
{
// 成功获取互斥体,访问共享资源
DbgPrint("Thread %d: Acquired mutex, accessing shared resource...\n", PsGetCurrentThreadId());

// 模拟访问共享资源的过程
SharedResource++;
DbgPrint("Thread %d: Shared resource updated to %lu.\n", PsGetCurrentThreadId(), SharedResource);

// 释放互斥体
KeReleaseMutex(&Mutex, FALSE);
DbgPrint("Thread %d: Released mutex.\n", PsGetCurrentThreadId());
}

PsTerminateSystemThread(STATUS_SUCCESS);
}
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
UNREFERENCED_PARAMETER(RegistryPath);
UNREFERENCED_PARAMETER(DriverObject);
// 初始化互斥体
KeInitializeMutex(&Mutex, 0);
// 创建两个线程,尝试访问共享资源
HANDLE ThreadHandleA, ThreadHandleB;
NTSTATUS status;
status = PsCreateSystemThread(&ThreadHandleA, THREAD_ALL_ACCESS, NULL, NULL, NULL, ThreadFunction, NULL);
if (!NT_SUCCESS(status))
{
DbgPrint("Failed to create Thread A\n");
return status;
}

status = PsCreateSystemThread(&ThreadHandleB, THREAD_ALL_ACCESS, NULL, NULL, NULL, ThreadFunction, NULL);
if (!NT_SUCCESS(status))
{
DbgPrint("Failed to create Thread B\n");
ZwClose(ThreadHandleA);
return status;
}
// 等待线程完成
ZwClose(ThreadHandleA);
ZwClose(ThreadHandleB);
return STATUS_SUCCESS;
}

使用自旋锁进行同步

驱动程序常使用自旋锁作为一种有效的同步机制
举例:应用程序打开一个设备后,使用多线程对设备进行读取,此时IRP_MJ_READ的派遣函数会并发进行,然而绝大部分设备没有响应并发请求的能力,此时需要使用自旋锁进行同步

内核特有的同步对象

驱动如果很慢,会导致整个系统变卡变慢,因此驱动程序对比三环程序很讲究效率,所以微软给内核多加了几种同步对象

自旋锁(Spin Lock)

特点

  1. 忙等待:自旋锁是一种忙等待的锁,线程在获取锁时会不断地检查锁的状态,直到成功获取锁。不会主动让出 CPU 时间片。
  2. 低开销:适合用于保护短时间内的临界区,因为自旋锁避免了线程上下文切换的开销。
  3. 非阻塞:因为自旋锁不会阻塞线程,所以不会发生线程切换。
  4. 高优先级:通常用于高优先级的线程中,以避免线程上下文切换的开销。

适用场景

  • 保护临界区时间非常短的代码段。
  • 中断上下文中使用,防止在中断处理过程中被其他中断打断。

什么是自旋?

“自旋”可以理解为“自我旋转”,这里的“旋转”指“循环”,比如 while 循环或者 for 循环。“自旋”就是自己在这里不停地循环,直到目标达成。而不像普通的锁那样,如果获取不到锁就进入阻塞。

对比自旋和非自旋的获取锁的流程

下面我们用这样一张流程图来对比一下自旋锁和非自旋锁的获取锁的过程

1721315305700

自旋锁并不会放弃 CPU 时间片,而是通过自旋等待锁的释放,也就是说,它会不停地再次地尝试获取锁,如果失败就再次尝试,直到成功为止。

我们再来看下非自旋锁,非自旋锁和自旋锁是完全不一样的,如果它发现此时获取不到锁,它就把自己的线程切换状态,让线程休眠,然后 CPU 就可以在这段时间去做很多其他的事情,直到之前持有这把锁的线程释放了锁,于是 CPU 再把之前的线程恢复回来,让这个线程再去尝试获取这把锁。如果再次失败,就再次让线程休眠,如果成功,一样可以成功获取到同步资源的锁。

可以看出,非自旋锁和自旋锁最大的区别,就是如果它遇到拿不到锁的情况,它会把线程阻塞,直到被唤醒。而自旋锁会不停地尝试。

首先,阻塞和唤醒线程都是需要高昂的开销的,如果同步代码块中的内容不复杂,那么可能转换线程带来的开销比实际业务代码执行的开销还要大。

在很多场景下,可能我们的同步代码块的内容并不多,所以需要的执行时间也很短,如果我们仅仅为了这点时间就去切换线程状态,那么其实不如让线程不切换状态,而是让它自旋地尝试获取锁,等待其他线程释放锁,有时我只需要稍等一下,就可以避免上下文切换等开销,提高了效率。

用一句话总结自旋锁的好处,那就是自旋锁用循环去不停地尝试获取锁,让线程始终处于 Runnable 状态,节省了线程状态切换带来的开销。

1
2
3
4
5
6
7
8
9
10
11
KSPIN_LOCK mySpinLock;
KeInitializeSpinLock(&mySpinLock);

// Acquire the spin lock
KIRQL oldIrql;
KeAcquireSpinLock(&mySpinLock, &oldIrql);

// Critical section

// Release the spin lock
KeReleaseSpinLock(&mySpinLock, oldIrql);

总之这个线程没结束,系统不放的,适合代码量少的,频繁的同步

资源锁(Resource Lock)

特点

  1. 阻塞等待:当一个线程尝试获取资源锁时,如果锁已经被其他线程持有,该线程会进入休眠状态,直到锁被释放。这种方式避免了忙等待,节省了CPU资源。
  2. 适用于长临界区:由于线程在等待时会进入休眠状态,因此资源锁适用于那些持锁时间较长的临界区代码。
  3. 上下文切换:当锁被持有时,线程会进入休眠状态,导致上下文切换。
1
2
3
4
5
6
7
8
9
10
11
12
13
ERESOURCE myResource;
ExInitializeResourceLite(&myResource);

// Acquire the resource lock in exclusive mode
ExAcquireResourceExclusiveLite(&myResource, TRUE);

// Critical section

// Release the resource lock
ExReleaseResourceLite(&myResource);

// Delete the resource when done
ExDeleteResourceLite(&myResource);

IRQL(中断请求级别)

IRQL(Interrupt Request Level,中断请求级别)是Windows操作系统内核中的一个关键概念,用于管理和控制中断和线程调度。IRQL决定了当前代码的执行优先级,影响了哪些中断和线程可以被调度和处理。

定义:IRQL是一个整数值,表示当前处理器在特定时间点的中断请求级别。

作用:IRQL用于控制中断的处理顺序和线程的调度,确保高优先级的中断能够及时处理,防止低优先级的任务干扰高优先级任务的执行。

3. IRQL的作用

  • 中断处理:IRQL控制中断的优先级,确保高优先级的中断能够抢占低优先级的中断处理。例如,硬盘中断通常具有高优先级,以确保数据的快速读写。
  • 线程调度:操作系统根据线程的优先级和当前IRQL来决定线程的调度顺序。高优先级的线程可以在高IRQL下运行,以确保关键任务的及时完成。
  • 同步机制:IRQL用于实现内核同步机制,例如自旋锁。提高IRQL可以防止当前代码被中断,从而保护临界区。

4. IRQL的管理

  • 提升IRQL:在某些情况下,内核代码需要提升IRQL以防止被中断。例如,在处理关键数据结构时,代码可能会将IRQL提升到DISPATCH_LEVEL以防止上下文切换。
  • 降低IRQL:代码在完成关键任务后需要恢复之前的IRQL,以允许其他中断和线程执行。
  • IRQL检查:内核提供了一些API用于检查和操作当前的IRQL,如KeGetCurrentIrqlKeRaiseIrqlKeLowerIrql

微软设计了 0~31级别,在程序内,可以用代码把自己的级别提高或者降低。那么有没有可能软件的级别比硬件高?

微软设计了软件只能是02级,而硬件有331级。这样即使软件权限提到最高,也不至于影响到硬件操作

1721356067682

例如硬件上的电源级别就是POWER_LEVEL 30,几乎是最高的了,那么代表软件在运行的时候,如果按下了电源键,不会管代码有没有运行完,就执行关机的指令了

软件的三个级别,分别是

1
2
3
#define PASSIVE_LEVEL 0             // Passive release level
#define APC_LEVEL 1 // APC interrupt level
#define DISPATCH_LEVEL 2 // Dispatcher level

在DISPATCH_LEVEL运行的代码不能处理PASSIVE_LEVEL的异常。DISPATCH_LEVEL是一个较高的中断请求级别,用于调度和处理高优先级的任务。此时,许多系统功能是受限的,比如内存分页,操作文件和大多数的阻塞操作。(级别太高了,不能切换线程去拿,例如文件,要调硬件的驱动,但是线程切不过去了)。在DISPATCH_LEVEL,内核代码不应执行会导致线程阻塞或需要较长时间完成的操作。 级别为1的APC_LEVEL 不能操作文件

在MSDN查API也会有显示,需要满足的IRQL

1721357796201

级别不对找Bug有点难受,毕竟不是参数问题,不是权限问题

所以如何知道自己的代码在什么级别?用一个API KeGetCurrentIrql();

提升,降低等级

1
2
3
4
5
6
7
8
9
10
11
12
13
void DispatchLevelFunction()
{
KIRQL oldIrql;
KeRaiseIrql(DISPATCH_LEVEL, &oldIrql);

// 在DISPATCH_LEVEL运行的代码
// 必须避免任何会阻塞的操作
// 例如:
// KeWaitForSingleObject 等待操作是不可行的
// IoAllocateIrp 分页内存分配是不可行的

KeLowerIrql(oldIrql);
}

一般在写驱动程序的时候,在程序入口会写一个

1
PAGED_CODE()

PAGED_CODE() 是一个内核宏,用于确保某段代码只在 IRQL 为 PASSIVE_LEVEL 时运行。 是一个断言宏,用来检测级别对不对

内核程序中,写下这个代码就会蓝屏:

1
2
3
KIRQL oldIrql;
KeRaiseIrql(DISPATCH_LEVEL, &oldIrql);
return STATUS_UNSUCCESSFUL;

因为提升到DISPATCH_LEVEL这个级别,就和I/O管理器一个级别了,返回的不能被处理

提升到高 IRQL 后立即返回会导致系统在高 IRQL 上运行不允许分页或等待的代码,从而引发蓝屏。