下篇较上篇所讲述的内存属性,内容的扫描来说相对简单,也比较好理解

文件内容扫描

首先创建一个txt文档,随便写点内容,比如1234567890

源代码

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
#include <windows.h>
#include <iostream>
using namespace std;

//定义一个OVERLAPPED结构体变量,并且把它的所有成员都初始化为0
OVERLAPPED __Overlapped = { 0 };



void main()
{
unsigned long lpNumber = 0;
char lpBuffer[100] = "";
char IP_path[] = "num.txt";
__Overlapped.Offset = 8;
HANDLE hFile = CreateFile(IP_path,GENERIC_READ,0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0);
if (hFile == INVALID_HANDLE_VALUE) {
MessageBox(NULL, "创建文件句柄出错", "error", MB_OK);
}
int filesucc = ReadFileEx(hFile, lpBuffer, sizeof(lpBuffer), &__Overlapped, NULL);
CloseHandle(hFile);
printf("内容是: %s\n", lpBuffer);
if (filesucc == 0) {
MessageBox(NULL, "读取文件失败", "error", MB_OK);
}
return;
}

逐行讲解

1
OVERLAPPED __Overlapped = { 0 };

OVERLAPPED结构体

这个结构的用处是在异步(或重叠)输入输出(I/O)操作中存储一些信息,比如请求的状态、传输的字节数、文件的位置和事件的句柄。这样可以让你在不阻塞程序的情况下,对文件或其他设备进行读写操作,并在操作完成时得到通知。

参数 类型 说明
Internal ULONG_PTR I/O请求的状态码
InternalHigh ULONG_PTR I/O请求传输的字节数
Offset DWORD 开始I/O请求的文件位置的低位部分
OffsetHigh DWORD 开始I/O请求的文件位置的高位部分
Pointer PVOID 保留给系统使用,初始化为零后不要使用
hEvent HANDLE 一个事件的句柄,当操作完成时,系统会把它设置为有信号状态

InternalInternalHigh是用来存储I/O请求的状态和传输的字节数的。当你发起一个异步(或重叠)I/O请求时,系统会把Internal成员设置为STATUS_PENDING,表示操作还没有开始。当请求完成时,系统会把Internal成员设置为完成请求的状态码。你可以通过调用GetOverlappedResult函数来获取这个状态码。系统也会把InternalHigh成员设置为I/O请求传输的字节数。

Offset和OffsetHigh就是读取文件内容的位置偏移量,比如设置Offset为8,则就从文件内容的第8+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
#include <windows.h>
#include <iostream>
using namespace std;
//定义一个OVERLAPPED结构体变量,并且把它的所有成员都初始化为0
OVERLAPPED __Overlapped = { 0 };
void main()
{
unsigned long lpNumber = 0;
char lpBuffer[100] = "";
char IP_path[] = "num.txt";
__Overlapped.Offset = 8;
HANDLE hFile = CreateFile(IP_path,GENERIC_READ,0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0);
if (hFile == INVALID_HANDLE_VALUE) {
MessageBox(NULL, "创建文件句柄出错", "error", MB_OK);
}
int filesucc = ReadFileEx(hFile, lpBuffer, sizeof(lpBuffer), &__Overlapped, NULL);
CloseHandle(hFile);
printf("内容是: %s\n", lpBuffer);
if (filesucc == 0) {
MessageBox(NULL, "读取文件失败", "error", MB_OK);
}
return;
}
//内容是: 90

其实在windows api中,偏移量和Pointer是在同一片union中定义的

1
2
3
4
5
6
7
union {
struct {
DWORD Offset;
DWORD OffsetHigh;
} DUMMYSTRUCTNAME;
PVOID Pointer;
}

union是一种特殊的数据类型,它可以存储不同类型的数据,但是只能同时存储其中一个成员的值。union中的成员共享同一块内存空间,它们的大小等于最大成员的大小。union可以用来节省内存空间,或者实现不同类型之间的转换。

在OVERLAPPED结构中,union是用来表示文件位置的不同方式。文件位置是一个64位整数,由Offset和OffsetHigh两个32位整数组合而成。但是有些情况下,你可能不需要指定一个具体的文件位置,而是使用一个指针来表示一个内存地址或一个回调函数。这时候,你可以使用Pointer成员来代替Offset和OffsetHigh成员。

这样设计的原因可能是为了兼容不同的I/O操作和设备,或者为了保留一些未来的扩展性。你可以根据你的需要选择使用union中的哪个成员。

1
HANDLE hFile = CreateFile(IP_path,GENERIC_READ,0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,0);

CreateFile函数是用来创建或打开一个文件或I/O设备的。你必须指定文件或设备的名称,创建方式,以及其他属性。

函数返回一个句柄,你可以用这个句柄来对文件进行操作

1
2
3
4
5
6
7
8
9
10
11
12
HANDLE CreateFileA(
LPCSTR lpFileName,//要创建或打开的文件或设备名称
DWORD dwDesiredAccess,//请求对文件或设备的访问权限,可以是读、写、两者或0表示无
DWORD dwShareMode,//请求对文件或设备的共享方式,可以是读、写、两者、删除、所有这些或者都不是

LPSECURITY_ATTRIBUTES lpSecurityAttributes,/*一个指向SECURITY_ATTRIBUTES结构的指针,它包含了安全描述符和继承标志。如果为NULL,文件或设备会得到默认的安全描述符,并且句柄不能被子进程继承*/

DWORD dwCreationDisposition,/*指定如果文件已经存在时要采取什么动作,称为创建方式。例如,一个应用程序可以调用CreateFile并把dwCreationDisposition设置为CREATE_ALWAYS来总是创建一个新文件,即使同名文件已经存在(从而覆盖原来的文件)。是否成功取决于一些因素,比如原来文件的属性和安全设置*/

DWORD dwFlagsAndAttributes,//指定文件或设备的标志和属性。这些标志和属性只有在创建新文件时才有效。
HANDLE hTemplateFile/*一个有效的文件句柄,它包含了一些预定义的属性(比如安全描述符)来创建新文件。如果为NULL,这些属性会从父目录继承*/
);

补充

dwDesiredAccess的预定义常量如下:

1
2
3
4
5
6
7
8
9
//
// These are the generic rights.
//

#define GENERIC_READ (0x80000000L)
#define GENERIC_WRITE (0x40000000L)
#define GENERIC_EXECUTE (0x20000000L)
#define GENERIC_ALL (0x10000000L)

对dwShareMode的”共享模式”的解释:

dwShareMode参数是用来指定对文件或设备的共享模式的,也就是说,你是否允许其他进程同时打开同一个文件或设备,并进行读、写、删除等操作。你可以使用下表中的值来指定不同的共享模式:

含义
0 不允许其他进程共享文件或设备。如果文件已经被其他进程打开,CreateFile会失败。这也称为独占访问。
FILE_SHARE_READ 允许其他进程打开文件或设备进行读取操作。如果没有指定这个值,任何请求读取的进程都会被拒绝,即使调用进程也没有请求读取访问。
FILE_SHARE_WRITE 允许其他进程打开文件或设备进行写入操作。如果没有指定这个值,任何请求写入的进程都会被拒绝,即使调用进程也没有请求写入访问。
FILE_SHARE_DELETE 允许其他进程打开文件或设备进行删除操作。如果没有指定这个值,任何请求删除的进程都会被拒绝,即使调用进程也没有请求删除访问。

lpSecurityAttributes:

SECURITY_ATTRIBUTES结构体是用来包含一个对象的安全描述符,并指定通过这个结构体获取的句柄是否可以被继承的

1
2
3
4
5
typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength;
LPVOID lpSecurityDescriptor;
BOOL bInheritHandle;
} SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES, *LPSECURITY_ATTRIBUTES;

其中,各个成员的含义是:

  • nLength:这个结构体的大小,以字节为单位。设置这个值为SECURITY_ATTRIBUTES结构体的大小。
  • lpSecurityDescriptor:一个指向SECURITY_DESCRIPTOR结构体的指针,它控制了对对象的访问。如果这个成员的值为NULL,对象会被分配调用进程的访问令牌关联的默认安全描述符。
  • bInheritHandle:一个布尔值,指定当创建一个新进程时,返回的句柄是否可以被继承。如果这个成员为TRUE,新进程继承句柄。

dwFlagsAndAttributes:

标志和属性可以分为两类:文件标志和文件属性。文件标志控制或影响系统如何缓存与句柄相关的数据,比如是否使用缓冲区、是否优化随机或顺序访问等。文件属性指定了文件的一些元数据,比如是否只读、隐藏、系统、压缩等。

hTemplateFile和函数返回的句柄的区别:

hTemplateFile参数是一个有效的文件句柄,它包含了一些预定义的属性(比如安全描述符)来创建新文件。如果为NULL,这些属性会从父目录继承。这个参数只有在创建新文件时才有效。

CreateFile函数返回的句柄是一个用来访问文件或设备的标识符,它可以用于不同类型的I/O操作,取决于文件或设备和指定的标志和属性。这个句柄在关闭之前一直有效,可以被继承或不被继承。

hTemplateFile参数和CreateFile函数返回的句柄的区别是:

  • hTemplateFile参数是一个输入参数,用来提供新文件的一些属性。CreateFile函数返回的句柄是一个输出参数,用来标识打开或创建的文件或设备。
  • hTemplateFile参数只有在创建新文件时才有效,而CreateFile函数返回的句柄可以用于打开或创建文件或设备。
  • hTemplateFile参数只包含一些预定义的属性,而CreateFile函数返回的句柄可以用于读取或写入文件或设备的数据和元数据。
1
int filesucc = ReadFileEx(hFile, lpBuffer, sizeof(lpBuffer), &__Overlapped, NULL);

ReadFileEx函数定义:

1
2
3
4
5
6
7
ReadFileEx(
_In_ HANDLE hFile,//指向文件或IO的句柄
_Out_writes_bytes_opt_(nNumberOfBytesToRead) __out_data_source(FILE) LPVOID lpBuffer,//指向接受读取数据的缓冲区
_In_ DWORD nNumberOfBytesToRead,//要读取的字节数
_Inout_ LPOVERLAPPED lpOverlapped,//一个指向OVERLAPPED结构体的指针
_In_ LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine/*一个指向在读取操作完成时要调用的完成例程*/
);

对于 lpCompletionRoutine的通俗解释:

lpCompletionRoutine参数是一个指向一个函数的指针,这个函数是你自己写的,它的作用是在ReadFileEx函数读取文件或设备的数据完成后,告诉你读取操作的结果,比如成功还是失败,读取了多少字节,以及一些其他信息。这个函数只有在你的程序在等待其他事情的时候才会被调用,这样就不会浪费时间。你可以在这个函数里面做一些后续的处理,比如显示数据,保存数据,或者继续读取其他数据。但是你不能在这个函数里面做一些可能会影响程序运行的事情,比如退出程序,或者再次调用ReadFileEx函数。

举个例子,

当你调用ReadFileEx函数。这个函数会把你的读取请求发送给系统,并且立即返回。这样你的程序就不会被阻塞,而是可以继续执行其他代码。但是系统并没有忘记你的请求,它会在后台处理你的读取操作,并且在完成后通知你。

为了接收系统的通知,你需要让你的程序进入一个可以接收异步通知的状态,这个状态就叫做可警告等待状态(alertable wait state)。有一些函数可以让你的程序进入这个状态,比如SleepEx, SignalObjectAndWait, MsgWaitForMultipleObjectsEx, WaitForMultipleObjectsEx, 或 WaitForSingleObjectEx。这些函数都有一个参数叫做bAlertable,如果你把它设置为TRUE,就表示你想让你的程序进入可警告等待状态。如果你把它设置为FALSE,就表示你不想让你的程序进入可警告等待状态。

当你的程序进入可警告等待状态时,它就可以接收到系统发送的异步通知,并且调用你指定的lpCompletionRoutine函数来处理结果。这个函数会接收到读取操作的结果,比如成功还是失败,读取了多少字节,以及一些其他信息。然后你可以在这个函数里面做一些后续的处理,比如显示数据,保存数据,或者继续读取其他数据。但是你不能在这个函数里面做一些可能会影响程序运行的事情,比如退出程序,或者再次调用ReadFileEx函数。

当你的程序不再需要等待其他事情时,它就会退出可警告等待状态,并且不会再接收到任何通知。这样你就完成了一个异步读取操作,并且利用了系统提供的通知机制。

剩下的就是打印读取到缓冲区的内容了,然后关闭文件句柄

注册表监控

前置知识

Windows 注册表是一个分层数据库,其中包含对 Windows 操作系统和运行在 Windows 上的应用程序和服务的配置数据。注册表的数据以树状结构组织,树中的每个节点称为,每个键可以包含数据

每个键(节点)可以有多个值和数据

值和数据可以存储各种类型的信息,例如字符串、数字、二进制数据等。

注册表有五个主要的键,分别是:

  • HKEY_CLASSES_ROOT:一个注册表的根键,它主要是用来记录不同类型的文件和程序之间的关系。比如说,你有一个 .txt 的文本文件,你想用记事本打开它,那么就需要在 HKEY_CLASSES_ROOT 里面找到 .txt 的扩展名,然后看它对应的是什么程序,然后就可以用那个程序打开它。
  • HKEY_CURRENT_USER:包含当前登录用户的配置信息,例如桌面设置、历史记录、收藏夹等。
  • HKEY_LOCAL_MACHINE:包含本地计算机的配置信息,例如硬件设备、系统服务、软件安装等。
  • HKEY_USERS:包含所有用户的配置信息,每个用户都有一个子键,其名称是用户的安全标识符(SID)。
  • HKEY_CURRENT_CONFIG:包含当前硬件配置的信息,例如显示器分辨率、字体、颜色等。

源代码

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
#include <Windows.h>
#include <iostream>
#include <string>
#include <tchar.h>
using namespace std;
bool RegeditNotifyChanged(HKEY hKey_, char* path_)
{

HANDLE hNotify = CreateEvent(NULL, FALSE, FALSE, "RegeditNotifyChanged");
if (hNotify == INVALID_HANDLE_VALUE)
{
cout << "监控事件创建失败" << endl;
CloseHandle(hNotify);
return false;
}
//2. 打开注册表对应位置
HKEY hRegKey;
if (RegOpenKeyEx(hKey_, path_, 0, KEY_NOTIFY, &hRegKey) != ERROR_SUCCESS)
{
cout << "打开注册表失败" << endl;
CloseHandle(hNotify);
RegCloseKey(hRegKey);
return false;
}
//3.使用RegNotifyChangeKeyValue进行监控
if (RegNotifyChangeKeyValue(hRegKey, TRUE, REG_NOTIFY_CHANGE_NAME | REG_NOTIFY_CHANGE_ATTRIBUTES | REG_NOTIFY_CHANGE_LAST_SET, hNotify, TRUE) != ERROR_SUCCESS)
{
cout << "监控失败" << endl;
CloseHandle(hNotify);
RegCloseKey(hRegKey);
return false;
}
if (WaitForSingleObject(hNotify, INFINITE) != WAIT_FAILED)
{
cout << "监控项发生改变" << endl;
//知道谁改的话 RegQueryValue 路径
CloseHandle(hNotify);
RegCloseKey(hRegKey);
return true;
}
CloseHandle(hNotify);
RegCloseKey(hRegKey);
return false;
}
void main(void)
{
//根键、子键名称和到子键的句柄
HKEY hRoot = HKEY_CURRENT_USER;
char* szSubKey = (char*)"Software\\Microsoft\\Windows\\CurrentVersion\\Run";
RegeditNotifyChanged(hRoot, szSubKey);
}

逐行讲解

1
HANDLE hNotify = CreateEvent(NULL, FALSE, TRUE, "RegeditNotifyChanged");

这个函数是用来创建或打开一个命名或无名的事件对象,并返回一个对象的句柄。

事件对象是一种同步对象,可以用来通知一个或多个等待的线程发生了某个事件

CreateEvent函数定义

1
2
3
4
5
6
7
CreateEventA(
_In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,//一个指向 SECURITY_ATTRIBUTES 结构的指针。如果这个参数是 NULL,那么句柄不能被子进程继承。

_In_ BOOL bManualReset,//决定事件对象类型
_In_ BOOL bInitialState,//如果这个参数是 TRUE,那么事件对象的初始状态是信号状态;否则,它是非信号状态。
_In_opt_ LPCSTR lpName//自定义事件对象的名字
);

补充

bManualReset:

bManualReset 这个参数是用来决定事件对象的类型的。事件对象有两种类型:手动重置事件对象和自动重置事件对象。

手动重置事件对象是这样的:当你创建它时,你可以指定它的初始状态是信号状态还是非信号状态。然后,你可以用 SetEvent 函数来将它的状态改变为信号状态,或者用 ResetEvent 函数来将它的状态改变为非信号状态。 这些操作都需要你手动去做,系统不会自动帮你做。

自动重置事件对象是这样的:当你创建它时,你也可以指定它的初始状态是信号状态还是非信号状态。然后,你可以用 SetEvent 函数来将它的状态改变为信号状态,但是你不能用 ResetEvent 函数来将它的状态改变为非信号状态。 因为系统会在每次有一个等待线程被释放后,自动将事件对象的状态重置为非信号状态。 这就是为什么叫做自动重置事件对象。

所以,bManualReset 这个参数就是用来控制你想要创建哪种类型的事件对象的。如果你想要创建一个手动重置事件对象,那么你就把这个参数设为 TRUE;如果你想要创建一个自动重置事件对象,那么你就把这个参数设为 FALSE。

lpName:

虽然我们在后续的代码中只需要用到事件句柄,但这个事件名的作用是可以在不同的进程中共享这个事件对象,或者打开一个已经存在的命名事件对象。

1
if (RegOpenKeyEx(hKey_, path_, 0, KEY_NOTIFY, &hRegKey) != ERROR_SUCCESS)
1
2
3
4
5
6
7
8
//函数定义
RegOpenKeyExA(
_In_ HKEY hKey,
_In_opt_ LPCSTR lpSubKey,
_In_opt_ DWORD ulOptions,
_In_ REGSAM samDesired,
_Out_ PHKEY phkResult
);

RegOpenKeyExA用于打开注册表的键

hKey:

一个已经打开的注册表键的句柄,可以是由RegCreateKeyEx或RegOpenKeyEx函数返回的句柄,也可以是一些预定义的键,如HKEY_CLASSES_ROOT, HKEY_CURRENT_USER等。

lpSubKey:

要打开的子键的名称,不区分大小写。如果这个参数为NULL或空字符串,那么函数会将phkResult指向的键设置为hKey相同

ulOptions:

指定打开键时要应用的选项,目前只有一个可用的选项,就是REG_OPTION_OPEN_LINK,表示要打开的键是一个符号链接。符号链接是一种特殊的键,它指向另一个键,可以用于跨不同根键或不同机器访问注册表信息。

samDesired:

指定要打开键时所需的访问权限掩码,这个掩码决定了对该键可以进行哪些操作,如查询、设置、枚举、删除等。如果该键的安全描述符不允许调用进程拥有所需的访问权限,那么函数会失败,并返回ERROR_ACCESS_DENIED错误码。

phkResult:

一个指向句柄的指针,用于接收打开的子键的句柄。如果打开的子键不是预定义的键,那么在使用完毕后应该调用RegCloseKey函数来关闭该句柄。

1
if (RegNotifyChangeKeyValue(hRegKey, TRUE, REG_NOTIFY_CHANGE_NAME | REG_NOTIFY_CHANGE_ATTRIBUTES | REG_NOTIFY_CHANGE_LAST_SET, hNotify, TRUE) != ERROR_SUCCESS)

RegNotifyChangeKeyValue是一个用于监控注册表键变化的函数。

RegNotifyChangeKeyValue函数定义:

1
2
3
4
5
6
7
RegNotifyChangeKeyValue(
_In_ HKEY hKey,//已经打开的键句柄(这个键必须已经用KEY_NOTIFY访问权限打开,这样才能监控它的变化)
_In_ BOOL bWatchSubtree,//一个布尔值,表示是否要监控指定键及其子键的变化。如果这个参数为TRUE,那么函数会报告指定键和其 //所有子键的变化。如果这个参数为FALSE,那么函数只会报告指定键本身的变化
_In_ DWORD dwNotifyFilter,//一个整数值,表示要报告哪些类型的变化
_In_opt_ HANDLE hEvent,//一个事件对象的句柄
_In_ BOOL fAsynchronous//一个布尔值,表示是否要异步地监控变化。如果这个参数为TRUE,那么函数会立即返回,并通过信号化 //hEvent来报告变化。如果这个参数为FALSE,那么函数不会返回,直到发生了变化。如果hEvent不指定一 //个有效的事件对象,那么fAsynchronous不能为TRUE
);

补充:

dwNotifyFilter的可选值:

  • REG_NOTIFY_CHANGE_NAME:如果添加或删除了子键,就通知调用者。

  • REG_NOTIFY_CHANGE_ATTRIBUTES:如果改变了键的属性,如安全描述符信息,就通知调用者。

  • REG_NOTIFY_CHANGE_LAST_SET:如果改变了键的值,如添加、删除或修改了值,就通知调用者。

  • REG_NOTIFY_CHANGE_SECURITY:如果改变了键的安全描述符,就通知调用者。

  • REG_NOTIFY_THREAD_AGNOSTIC:表示注册的生命周期不应该与发起RegNotifyChangeKeyValue调用的线程的生命周期绑定。(只支持Windows 8及以上版本)这样做的好处就是即使当前线程结束了,监控函数依然在起作用,直到主动取消它或者程序退出

1
if (WaitForSingleObject(hNotify, INFINITE) != WAIT_FAILED)

WaitForSingleObject函数是一个用于等待某个对象变成信号状态的函数。

信号状态是一种表示对象是否可用或者满足某种条件的状态。不同类型的对象有不同的信号状态的含义,比如:

  • 事件对象:信号状态表示事件已经发生,非信号状态表示事件还没有发生。
  • 互斥对象:信号状态表示互斥没有被占用,非信号状态表示互斥已经被占用。
  • 信号量对象:信号状态表示信号量的计数大于零,非信号状态表示信号量的计数等于零。
  • 定时器对象:信号状态表示定时器已经到期,非信号状态表示定时器还没有到期。

WaitForSingleObject的作用是检查指定的对象是否已经是信号状态。

如果是,那么函数就会立即返回,并告诉您对象已经是信号状态了。

如果不是,那么函数就会让程序等待一段时间,直到对象变成信号状态或者超时了为止。

函数定义:

1
2
3
4
WaitForSingleObject(
_In_ HANDLE hHandle,//一个指定要等待的对象的句柄。
_In_ DWORD dwMilliseconds//一个指定要等待多长时间的值,单位是毫秒。
);

补充:

dwMilliseconds:

如果指定了一个非零的值,那么函数就会在对象变成信号状态或者等待时间到了之后返回。

如果指定了零,那么函数就不会等待,而是直接检查对象是否已经是信号状态,并立即返回。

如果指定了INFINITE,那么函数就会无限期地等待,直到对象变成信号状态为止。

WaitForSingleObject函数有一个返回值,它是一个表示函数执行结果的值。它可以是以下几种值之一:

  • WAIT_OBJECT_0:表示对象已经变成了信号状态。
  • WAIT_ABANDONED:表示对象是一个互斥对象,并且它之前被占用的线程在退出时没有释放它。这种情况下,系统会把互斥的所有权转给调用者,并把互斥设置为非信号状态。但是,由于之前占用互斥的线程可能没有正确地完成它要保护的数据操作,所以需要检查数据是否一致。
  • WAIT_TIMEOUT:表示等待时间已经到了,但是对象还没有变成信号状态。
  • WAIT_FAILED:表示函数执行失败了。可以调用GetLastError函数来获取具体的错误信息。

剩下就是主函数里的提供要监控的根键和子键,以及函数内部的关闭句柄的操作,在这里不再赘述

总结:

主函数->定义要监控的根键,子键->RegeditNotifyChanged监控函数->创建一个事件对象用于监控注册表的变化->打开注册表(获取注册表句柄)->调用RegNotifyChangeKeyValue函数进行监控,结果将通过事件对象的状态来体现->WaitForSingleObject函数监控事件对象的信号状态(阻塞当前线程)->修改注册表对于键->hNotify事件对象的状态变为信号状态->WaitForSingleObject获取到返回值->打印信息

来自Microsoft官方的提示:

此函数不能用于检测使用 RegRestoreKey函数生成的注册表的更改。