Win32编程

一,消息机制

创建窗口的五个步骤:

  • 注册窗口:RegisterClass/RegisterClassEx

    • 用户窗口:多种多样的,我们需要提前去注册,填充一个数据结构(窗口的清单)
    • 系统窗口
  • 创建窗口:CreateWindow/CreateWindowEx

  • 显示刷新窗口:ShowWindow/UpdateWindow

  • 消息循环:GetMessage/PeekMessage

  • 消息处理:LRESULT CALLBACK WinProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lparam)

对照着例子进行讲解:

首先建立一个窗口空项目:

创建完毕以后他给我们生成了一个将近180多行的代码:如下:

运行该项目,如下:

上述就是一个已写好框架的窗口项目。


接下来我们对照着上面他给出的代码创建一个空项目进行一下,简单的解释:

注册窗口:

可以ctrl+鼠标左键进入这个函数看看需要传入什么参数:如下

我们可以再跟进这个参数的数据构造看看其内容:如下:

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct tagWNDCLASSW {
UINT style;//风格
WNDPROC lpfnWndProc;//回调函数
int cbClsExtra;
int cbWndExtra;
HINSTANCE hInstance;
HICON hIcon; //设置窗口的图标
HCURSOR hCursor;//光标
HBRUSH hbrBackground; //背景色
LPCWSTR lpszMenuName; //你这个窗口叫什么名
LPCWSTR lpszClassName;
} WNDCLASSW, *PWNDCLASSW, NEAR *NPWNDCLASSW, FAR *LPWNDCLASSW;

我们也可以直接对RegisterClass();点击F1查看一些使用用例,如下:

将其拷贝出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
WNDCLASS wc; 

// Register the main window class.
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = (WNDPROC) MainWndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hinstance;
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = GetStockObject(WHITE_BRUSH);
wc.lpszMenuName = "MainMenu";
wc.lpszClassName = "MainWindowClass";

我们将我们的空项目源码修改如下:

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

/*
这个是怎么出来的我们可以 ctrl+鼠标左键 “wc.lpfnWndProc”
进入之后再 ctrl+鼠标左键 “WNDPROC”
然后将这个复制出来:
typedef LRESULT (CALLBACK* WNDPROC)(HWND, UINT, WPARAM, LPARAM);

*/
LRESULT CALLBACK WinProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lparam)
{

}

//会发现我们原来的main函数变成了 wWinMain函数,就这么理解就可以了
int APIENTRY wWinMain(_In_ HINSTANCE hInstance, //它代表了我们的一个应用程序
_In_opt_ HINSTANCE hPrevInstance, //已废弃了,代表了上一个应用程序
_In_ LPWSTR lpCmdLine, //命令行参数
_In_ int nCmdShow) //窗口的显示方式,在刚刚生成的框架中,他是显示出来的,我们也可以对这个参数进行设置让其隐藏起来
{
//1.注册窗口
//WNDCLASSW
WNDCLASS wc;
// Register the main window class.
wc.style = CS_HREDRAW | CS_VREDRAW;

//然后将回调函数先给过来上面有定义了
wc.lpfnWndProc = (WNDPROC)WinProc; //回调函数 ,这是我们的一个消息处理的一个函数
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
//wc.hInstance = hinstance; //代表了应用程序
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);

//菜单民什么的可以先设置为空
//wc.lpszMenuName = "MainMenu";


wc.lpszMenuName = NULL;
wc.lpszClassName = "第一个窗口"; //类的名字
RegisterClass(&wc);



}

当我们api生成之后,就会生成第一个窗口这么一个数据清单:

创建窗口:

CreateWindow()

F1 可以看看官方的一些示例

1
2
3
4
5
6
7
8
9
10
11
12
13
HWND CreateWindowA(
[in, optional] LPCSTR lpClassName, //很显然这个参数是类名,和主键很类似,方便其定位
[in, optional] LPCSTR lpWindowName,
[in] DWORD dwStyle, //风格,这些就基本不用进行跳转默认即可
[in] int x,
[in] int y,
[in] int nWidth,
[in] int nHeight,
[in, optional] HWND hWndParent, //有无父亲窗口
[in, optional] HMENU hMenu, //有无菜单
[in, optional] HINSTANCE hInstance, //应用程序
[in, optional] LPVOID lpParam //有无携带参数
);

创建窗口如下:

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

/*
这个是怎么出来的我们可以 ctrl+鼠标左键 “wc.lpfnWndProc”
进入之后再 ctrl+鼠标左键 “WNDPROC”
然后将这个复制出来:
typedef LRESULT (CALLBACK* WNDPROC)(HWND, UINT, WPARAM, LPARAM);

*/
LRESULT CALLBACK WinProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lparam)
{

}

//会发现我们原来的main函数变成了 wWinMain函数,就这么理解就可以了
int APIENTRY wWinMain(_In_ HINSTANCE hInstance, //它代表了我们的一个应用程序
_In_opt_ HINSTANCE hPrevInstance, //已废弃了,代表了上一个应用程序
_In_ LPWSTR lpCmdLine, //命令行参数
_In_ int nCmdShow) //窗口的显示方式,在刚刚生成的框架中,他是显示出来的,我们也可以对这个参数进行设置让其隐藏起来
{
//1.注册窗口
//WNDCLASSW
WNDCLASS wc;

// Register the main window class.
wc.style = CS_HREDRAW | CS_VREDRAW;

//然后将回调函数先给过来上面有定义了
wc.lpfnWndProc = (WNDPROC)WinProc; //回调函数 ,这是我们的一个消息处理的一个函数
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
//wc.hInstance = hinstance; //代表了应用程序
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);

//菜单民什么的可以先设置为空
//wc.lpszMenuName = "MainMenu";


wc.lpszMenuName = NULL;
wc.lpszClassName = "第一个窗口"; //类的名字***********类似于id
RegisterClass(&wc);


//2.创建窗口
//这行代码一旦成功窗口并不会显示出来,而是在内存当中有了内存和数据

//createwindow函数会返回一个句柄,可以理解成将指针再封装,有了这个句柄,就可以找到这个数据缓冲区
//拿到HWND 就能拿到代表窗口的内存
HWND hWnd = CreateWindow("第一个窗口", "标题", WS_OVERLAPPEDWINDOW, 50, 50, 500, 500, NULL, NULL, hInstance, NULL);



/*
HWND CreateWindowA(
[in, optional] LPCSTR lpClassName, //很显然这个参数是类名,和主键很类似,方便其定位
[in, optional] LPCSTR lpWindowName,
[in] DWORD dwStyle, //风格,这些就基本不用进行跳转默认即可
[in] int x,
[in] int y,
[in] int nWidth,
[in] int nHeight,
[in, optional] HWND hWndParent, //有无父亲窗口
[in, optional] HMENU hMenu, //有无菜单
[in, optional] HINSTANCE hInstance, //应用程序
[in, optional] LPVOID lpParam //有无携带参数
);
*/


}

其中CreateWindow之后我们会开辟一段数据缓冲区,存放我们的诸多信息,并且会返回一个HWND(句柄),可以理解成封装好的指针。

补充:

这块数据缓冲区里面还封装的有:hInstance也就是我们的应用程序

显示刷新窗口:

如下代码:

1
2
3
//3.显示刷新窗口
ShowWindow(hWnd,SW_SHOW);
UpdateWindow(hWnd);

这一行代码运行之后我们就会生成一个窗口,不过我们并看不到因为,程序一结束,窗口也就结束了,所以需要对其进行一个消息循环如下。

消息循环:

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
//4.消息循环
//当我们去不同的窗口去产生不同的动作(外设 鼠标,键盘)的时候,他执行的是不同的函数

//当我们在CS窗口的时候按下鼠标的时候,执行是开火功能,而在扫雷窗口按下鼠标,就是扫雷功能

//谁是第一个发生产生动作的(也就是操作系统),他会产生两个进程去监控,鼠标and键盘,去捕捉事件封装成消息

//当事件发生,操作系统就会知道你的事件是属于哪个窗口 还要知道窗口是哪个应用程序的
/*
需要把消息发给CS窗口 CS窗口才能去执行开火函数,
而我们会把消息也就是msg(通过封装的hwnd确定应用程序),
放入其对应的消息队列当中
*/

/*
所以我们自然而然地知道,MSG里面大概需要封装些什么:
事件产生的时间
属于哪个窗口

*/

//下面的GetMessage函数从消息队列中取出消息后,就放入下列的缓冲区内
MSG msg; //消息

//GetMessage
/*
参数:
1.消息
2.拿哪个窗口
0就代表所有窗口
3,4.拿消息的范围
后面三个参数基本都为0
*/
while (GetMessage(&msg,0,0,0)) //代表一直循环做事情 GetMessage //他就是去消息队列里拿消息
{
TranslateMessage(&msg);//翻译键盘大小写

DispatchMessageA(&msg);//会将消息发送到回调函数:派发消息
/*
那么请问他是如何将消息发送到回调函数的位置的参数位置的呢?

我们观察msg的结构体就可以知道,它存储了句柄hwnd
这就可以锁定到WinProc回调函数
*/
/*
验证上述说法:我们先把其注释掉,然后直接执行回调函数来看,
如果功能没变那就说明DispatchMessageA这个函数确实是用来派发消息的。
最后实验证明确实如此。
*/
//WinProc(msg.hwnd, msg.message, msg.wParam, msg.lParam);

}

我们来看看msg这个结构体里面携带了些什么:ctrl+鼠标左键:如下:

1
2
3
4
5
6
7
8
9
10
11
typedef struct tagMSG {
HWND hwnd; //属于哪个窗口(这里还封装了我们的窗口句柄)
UINT message; //消息id,用于确定是鼠标按下了还是键盘按下了,做一个区分
WPARAM wParam; //额外附加:携带参数
LPARAM lParam; //额外附加
DWORD time; //产生时间点
POINT pt; //坐标点
#ifdef _MAC
DWORD lPrivate;
#endif
} MSG, *PMSG, NEAR *NPMSG, FAR *LPMSG;

不过在这里写完以后,仍旧无法运行,我们需要在前面的回调函数WinProc:代码加上如下内容:

1
2
3
4
LRESULT CALLBACK WinProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lparam)
{
return DefWindowProc(hwnd, msg, wParam, lparam);
}

后续增加如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
LRESULT CALLBACK WinProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lparam)
{
//操作系统拿到消息之后并不知道要干什么,比方说开火函数他并不知道怎么做,他就会把这个消息给到我们这里
//消息回调
//我们就可以通过这个msg判断是鼠标还是键盘
switch (msg)
{
//一个窗口创建的一个消息
case WM_CREATE:
MessageBox(0, 0, 0, 0);
break;
}

return DefWindowProc(hwnd, msg, wParam, lparam);
//去执行操作系统默认的函数,
/*
比方,我们最大化窗口也可以
移动窗口也可以
这些我们并没有进行设置,但是他依旧做到了,就说明操作系统已经帮我们执行了这些操作了
如果不加的话窗口创建出来移动都移动不了
*/
}

上面的内容可以参考这个示例图来分析,如下:

补充:

所有都设置完毕以后(右键我们的项目——>属性——>配置属性——>链接器——>系统——>子系统——>窗口)记得将这个调整为窗口否则找不到入口点就无法运行,如下设置:

最后我们就成功完成了这个基本的构造完整代码如下:

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

/*
这个是怎么出来的我们可以 ctrl+鼠标左键 “wc.lpfnWndProc”
进入之后再 ctrl+鼠标左键 “WNDPROC”
然后将这个复制出来:
typedef LRESULT (CALLBACK* WNDPROC)(HWND, UINT, WPARAM, LPARAM);

*/

//回调函数 给到我们的位置
LRESULT CALLBACK WinProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lparam)
{
//操作系统拿到消息之后并不知道要干什么,比方说开火函数他并不知道怎么做,他就会把这个消息给到我们这里
//消息回调
//我们就可以通过这个msg判断是鼠标还是键盘
switch (msg)
{
//一个窗口创建的一个消息
case WM_CREATE:
MessageBox(0, 0, 0, 0);
break;
}



return DefWindowProc(hwnd, msg, wParam, lparam);
//去执行操作系统默认的函数,
/*
比方,我们最大化窗口也可以
移动窗口也可以
这些我们并没有进行设置,但是他依旧做到了,就说明操作系统已经帮我们执行了这些操作了
如果不加的话窗口创建出来移动都移动不了
*/
}

//会发现我们原来的main函数变成了 wWinMain函数,就这么理解就可以了
int APIENTRY wWinMain(_In_ HINSTANCE hInstance, //它代表了我们的一个应用程序
_In_opt_ HINSTANCE hPrevInstance, //已废弃了,代表了上一个应用程序
_In_ LPWSTR lpCmdLine, //命令行参数
_In_ int nCmdShow) //窗口的显示方式,在刚刚生成的框架中,他是显示出来的,我们也可以对这个参数进行设置让其隐藏起来
{
//1.注册窗口
//WNDCLASSW
WNDCLASS wc;

// Register the main window class.
wc.style = CS_HREDRAW | CS_VREDRAW;

//然后将回调函数先给过来上面有定义了
wc.lpfnWndProc = (WNDPROC)WinProc; //回调函数 ,这是我们的一个消息处理的一个函数
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
//wc.hInstance = hinstance; //代表了应用程序
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);

//菜单民什么的可以先设置为空
//wc.lpszMenuName = "MainMenu";


wc.lpszMenuName = NULL;
wc.lpszClassName = "第一个窗口"; //类的名字***********类似于id
RegisterClass(&wc);


//2.创建窗口
//这行代码一旦成功窗口并不会显示出来,而是在内存当中有了内存和数据

//createwindow函数会返回一个句柄,可以理解成将指针再封装,有了这个句柄,就可以找到这个数据缓冲区
//拿到HWND 就能拿到代表窗口的内存
HWND hWnd = CreateWindow("第一个窗口", "标题", WS_OVERLAPPEDWINDOW, 50, 50, 500, 500, NULL, NULL, hInstance, NULL);


//3.显示刷新窗口
ShowWindow(hWnd,SW_SHOW);
UpdateWindow(hWnd);

//4.消息循环
//当我们去不同的窗口去产生不同的动作(外设 鼠标,键盘)的时候,他执行的是不同的函数

//当我们在CS窗口的时候按下鼠标的时候,执行是开火功能,而在扫雷窗口按下鼠标,就是扫雷功能

//谁是第一个发生产生动作的(也就是操作系统),他会产生两个进程去监控,鼠标and键盘,去捕捉事件封装成消息

//当事件发生,操作系统就会知道你的事件是属于哪个窗口 还要知道窗口是哪个应用程序的
/*
需要把消息发给CS窗口 CS窗口才能去执行开火函数,
而我们会把消息也就是msg(通过封装的hwnd确定应用程序),
放入其对应的消息队列当中
*/

/*
所以我们自然而然地知道,MSG里面大概需要封装些什么:
事件产生的时间
属于哪个窗口

*/

//下面的GetMessage函数从消息队列中取出消息后,就放入下列的缓冲区内
MSG msg; //消息

//GetMessage
/*
参数:
1.消息
2.拿哪个窗口
0就代表所有窗口
3,4.拿消息的范围
后面三个参数基本都为0
*/
while (GetMessage(&msg,0,0,0)) //代表一直循环做事情 GetMessage //他就是去消息队列里拿消息
{
TranslateMessage(&msg);//翻译键盘大小写

DispatchMessageA(&msg);//会将消息发送到回调函数:派发消息
/*
那么请问他是如何将消息发送到回调函数的位置的参数位置的呢?

我们观察msg的结构体就可以知道,它存储了句柄hwnd
这就可以锁定到WinProc回调函数
*/
/*
验证上述说法:我们先把其注释掉,然后直接执行回调函数来看,
如果功能没变那就说明DispatchMessageA这个函数确实是用来派发消息的。
最后实验证明确实如此。
*/
//WinProc(msg.hwnd, msg.message, msg.wParam, msg.lParam);

}

}

透明窗口的实现:

现在我们要实现一个透视功能:内部绘制/外部绘制

内部绘制:HOOK游戏引擎 D3D绘制函数 帮我们去画方块 与游戏融为一体

外部绘制:去创建一个透明窗口 我们的绘制的方块 全画在了看不见的窗口上

透明窗口就会一直跟随我们的游戏窗口,而且会一直在游戏窗口之上。


上述代码就是一个窗口构造的简单程序现在,我们尝试实现透明窗口:

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
#include <windows.h>
//回调函数
LRESULT CALLBACK WinProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lparam)
{
switch (msg)
{
//一个窗口创建的一个消息
case WM_CREATE:
MessageBox(0, 0, 0, 0);
break;
}
return DefWindowProc(hwnd, msg, wParam, lparam);
}

//会发现我们原来的main函数变成了 wWinMain函数,就这么理解就可以了
int APIENTRY wWinMain(_In_ HINSTANCE hInstance, //它代表了我们的一个应用程序
_In_opt_ HINSTANCE hPrevInstance, //已废弃了,代表了上一个应用程序
_In_ LPWSTR lpCmdLine, //命令行参数
_In_ int nCmdShow) //窗口的显示方式,在刚刚生成的框架中,他是显示出来的,我们也可以对这个参数进行设置让其隐藏起来
{
//1.注册窗口
WNDCLASSEX wc = {0};//数据结构这里也需要加上一个EX,并进行一个初始化

wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = (WNDPROC)WinProc;
wc.hInstance = hInstance;
//将这里的透明色也改成黑色
wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wc.lpszClassName = "第一个窗口";
//还需要在这里加上一个新的属性
wc.cbSize= sizeof(WNDCLASSEX);
RegisterClassEx(&wc);//Ex就代表着是一个扩展风格,透明风格就包括在这里

//2.创建窗口
//查看官方说明,多了第一个参数扩展风格`WS_EX_LAYERED`也就是我们的一个透明风格,并且将第三个参数的风格也进行一个修改`WS_POPUP`,
//并且坐标位置也需要进行调整,尝试让其覆盖到扫雷游戏:这时候我们呢可以调用一个如下接口如下:这里可以看下面的具体操作
HWND hGameHwnd = FindWindow("Minesweeper","扫雷");
//拿到这个游戏之后,我们是不是就相当于能够得到它的所有基本信息:
RECT rect;
GetWindowRect(hGameHwnd,&rect);
//这时候再修改这个坐标
HWND hWnd = CreateWindowEx(WS_EX_LAYERED,"第一个窗口", "标题", WS_POPUP, rect.left, rect.top, rect.right-rect.left, rect.bottom-rect.top, NULL, NULL, hInstance, NULL);
//设置一下分成窗口的属性;在这里可以F1查看一下官方文档
SetLayeredWindowAttributes(hWnd,RGB(0,0,0),0,LWA_COLORKEY);

//3.显示刷新窗口
ShowWindow(hWnd,SW_SHOW);
UpdateWindow(hWnd);

//4.消息循环
MSG msg;
while (GetMessage(&msg,0,0,0))
{
//GetMessage是一个堵塞函数,就是没有事件的时候他就会一直等待
TranslateMessage(&msg);
DispatchMessageA(&msg);
//这里处理的是什么问题呢?
/*
就是我们的扫雷游戏移动时,边框并不会移动
所以得让这个透明窗口跟随啊。
就需要在消息循环里面进行操作了
*/
}

}

运行上述代码就能够直接运行开始就覆盖到扫雷游戏?

如何找到扫雷游戏的标题名and类名呢?

HWND hGameHwnd = FindWindow("扫雷","扫雷")这里我们可以使用工具——>Spy++:如下

然后就会弹出如下窗口,再按照如下操作即可,成功识别出窗口的标题:如下:

GetMessage与PeekMessage的区别:

GetMessagePeekMessage 是 Windows API 中用于消息处理的两个函数,它们有一些重要的区别:

1.GetMessage:

功能GetMessage 从消息队列中检索消息,并且会阻塞直到队列中有消息。

行为:如果队列中没有消息,GetMessage 会阻塞当前线程,直到有新的消息被发送到队列中。

返回值

  • 如果检索到有效的消息,返回值为非零。
  • 如果收到 WM_QUIT 消息,返回值为零,通常用于退出消息循环。

常见用法:通常用于消息循环中,等待并获取消息,然后分派到适当的窗口过程进行处理。

1
2
3
4
5
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
  1. PeekMessage:

功能PeekMessage 检索消息队列中的消息,但不会阻塞线程。它可以检查消息队列是否有消息,甚至可以指定是否要删除队列中的消息。

行为:如果消息队列中没有消息,PeekMessage 立即返回,而不是阻塞等待消息。它允许程序检查是否有消息待处理,或者做一些其他的事情。

返回值

  • 如果成功检索到消息,返回值为非零。
  • 如果没有消息可以检索,返回值为零。

常见用法:适用于需要在空闲时做一些工作或检查消息队列的情况,避免线程被阻塞。可以用来实现非阻塞的消息处理。

1
2
3
4
5
6
7
8
MSG msg;
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
if (msg.message == WM_QUIT) {
break;
}
TranslateMessage(&msg);
DispatchMessage(&msg);
}

主要区别:

  • 阻塞行为

    • GetMessage 会阻塞当前线程,直到消息队列中有消息。
    • PeekMessage 不会阻塞,可以立即返回,允许线程做其他事情(例如执行其他任务或继续处理消息)。
  • 消息处理方式

    • GetMessage 会自动从队列中删除消息。
    • PeekMessage 需要通过 PM_REMOVE 标志手动决定是否从队列中删除消息。若不使用 PM_REMOVE,则消息不会被移除。

总结:

  • 如果你希望一个线程在没有消息时能够继续执行其他任务,可以使用 PeekMessage
  • 如果你希望线程等待直到有消息可处理,则可以使用 GetMessage

我们可以来试试如下代码:更直观的感受一下它们二者的区别:

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
#include <windows.h>
LRESULT CALLBACK WinProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lparam)
{
switch (msg)
{
case WM_CREATE:
MessageBox(0, 0, 0, 0);
break;
}
return DefWindowProc(hwnd, msg, wParam, lparam);
}

int APIENTRY wWinMain(_In_ HINSTANCE hInstance, //它代表了我们的一个应用程序
_In_opt_ HINSTANCE hPrevInstance, //已废弃了,代表了上一个应用程序
_In_ LPWSTR lpCmdLine, //命令行参数
_In_ int nCmdShow) //窗口的显示方式,在刚刚生成的框架中,他是显示出来的,我们也可以对这个参数进行设置让其隐藏起来
{
//1.注册窗口
WNDCLASSEX wc = {0};//数据结构这里也需要加上一个EX,并进行一个初始化

wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = (WNDPROC)WinProc;
wc.hInstance = hInstance;
//将这里的透明色也改成黑色
wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wc.lpszClassName = "第一个窗口";
//还需要在这里加上一个新的属性
wc.cbSize= sizeof(WNDCLASSEX);
RegisterClassEx(&wc);//Ex就代表着是一个扩展风格,透明风格就包括在这里

//2.创建窗口
HWND hGameHwnd = FindWindow("Minesweeper","扫雷");
//拿到这个游戏之后,我们是不是就相当于能够得到它的所有基本信息:
RECT rect;
GetWindowRect(hGameHwnd,&rect);
//这时候再修改这个坐标
HWND hWnd = CreateWindowEx(WS_EX_LAYERED,"第一个窗口", "标题", WS_POPUP, rect.left, rect.top, rect.right-rect.left, rect.bottom-rect.top, NULL, NULL, hInstance, NULL);
//设置一下分成窗口的属性;在这里可以F1查看一下官方文档
SetLayeredWindowAttributes(hWnd,RGB(0,0,0),0,LWA_COLORKEY);

//3.显示刷新窗口
ShowWindow(hWnd,SW_SHOW);
UpdateWindow(hWnd);

//在这里做修改来看看GetMessage消息处理特性。
AllocConsole();
char buf[256];
//4.消息循环
MSG msg;
while (GetMessage(&msg,0,0,0))
{

TranslateMessage(&msg);
DispatchMessageA(&msg);
//这里处理的是什么问题呢?
/*
就是我们的扫雷游戏移动时,边框并不会移动
所以得让这个透明窗口跟随啊。
就需要在消息循环里面进行操作了
*/
WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE),"空闲处理",strlen("空闲处理"),0,0);
}

}

我们会发现只有当我们的鼠标在白框里面移动或点击时,它的console才会有动静:如下:

我们随便移动扫雷窗口它的console都没有任何反应。这就是GetMessage他会阻塞这个线程(它处理完这个消息以后他就会直接移除,最后导致消息队列里没有消息了就会阻塞),直到其有消息。

可以看到用GetMessage接口的话会遇到一个什么问题,就是不同步的问题,这时候我们就可以看看另一个API了PeekMessage来帮我们解决这个问题

PeekMessage的话你就可以把它理解成一个侦察兵,他只是看看这个消息队列里有没有消息,如果有消息或者没有消息它都会直接返回,处不处理这个消息呢主要看最后一个参数(是否移除)

然后我们来看看如下代码让大家更直观感受一下:如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//在上面的基础上只对这里进行修改
while (1)
{
if(!PeekMessage(&msg,0,0,0,PM_NOREMOVE))//派这个侦察兵看看有没有消息,如果有消息,并且我们不移除这个消息,是不是证明了这个消息队列里还有这个消息啊
{
//其他时间就进入这里来运行
WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE),"空闲处理",strlen("空闲处理"),0,0);
}else{
//判断有消息后直接进入else中,进入这个判断的前置条件就是其中一定有消息,所以GetMessage一定能够捕获这个消息(100%)
if(GetMessage(&msg,0,0,0)){
//然后正常的翻译,派发消息
TranslateMessage(&msg);
DispatchMessageA(&msg);
}
}

}

他就会有如下效果:

那么经过一番处理我们决定二者配合来捕获消息。

继续对源码进行修改:

达到如下需求:

透明窗口能够同步跟随我们的扫雷窗口

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
#include <windows.h>
//回调函数
LRESULT CALLBACK WinProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lparam)
{
switch (msg)
{
//一个窗口创建的一个消息
case WM_CREATE:
MessageBox(0, 0, 0, 0);
break;
}
return DefWindowProc(hwnd, msg, wParam, lparam);
}

int APIENTRY wWinMain(_In_ HINSTANCE hInstance, //它代表了我们的一个应用程序
_In_opt_ HINSTANCE hPrevInstance, //已废弃了,代表了上一个应用程序
_In_ LPWSTR lpCmdLine, //命令行参数
_In_ int nCmdShow) //窗口的显示方式,在刚刚生成的框架中,他是显示出来的,我们也可以对这个参数进行设置让其隐藏起来
{
//1.注册窗口
WNDCLASSEX wc = { 0 };//数据结构这里也需要加上一个EX,并进行一个初始化

wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = (WNDPROC)WinProc;
wc.hInstance = hInstance;

wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wc.lpszClassName = "第一个窗口";
wc.cbSize = sizeof(WNDCLASSEX);
RegisterClassEx(&wc);//Ex就代表着是一个扩展风格,透明风格就包括在这里

//2.创建窗口
HWND hGameHwnd = FindWindow("Minesweeper", "扫雷");
//拿到这个游戏之后,我们是不是就相当于能够得到它的所有基本信息:
RECT rect;
GetWindowRect(hGameHwnd, &rect);
HWND hWnd = CreateWindowEx(WS_EX_LAYERED, "第一个窗口", "标题", WS_POPUP, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, NULL, NULL, hInstance, NULL);

SetLayeredWindowAttributes(hWnd, RGB(0, 0, 0), 0, LWA_COLORKEY);

//3.显示刷新窗口
ShowWindow(hWnd, SW_SHOW);
UpdateWindow(hWnd);


//AllocConsole();
//char buf[256];
//4.消息循环
MSG msg;
while (1)
{
RECT GameRect;
//不断获取扫雷窗口位置:
GetWindowRect(hGameHwnd, &GameRect);
//然后同步修改透明窗口位置:
//HWND_TOPMOST让其覆盖在扫雷窗口上方
//SWP_SHOWWINDOW显示跟踪,这些参数都可以F1去官方文档看
SetWindowPos(hWnd, HWND_TOPMOST, GameRect.left, GameRect.top, GameRect.right - GameRect.left, GameRect.bottom - GameRect.top, SWP_SHOWWINDOW);
if (!PeekMessage(&msg, 0, 0, 0, PM_NOREMOVE))
{
//WriteConsole(GetStdHandle(STD_OUTPUT_HANDLE), "空闲处理", strlen("空闲处理"), 0, 0);
//GetForegroundWindow()获取最顶层窗口的一个句柄
if (GetForegroundWindow() == hGameHwnd) {
//内存画家HDC:GetDC获取画家
HDC hdc = GetDC(hWnd);
//然后我们去获取这个操作系统的这个画笔:`BLACK_BRUSH`
HBRUSH hbrush = (HBRUSH)GetStockObject(WHITE_BRUSH);
//拿到白色画刷之后需要给我们的画家,并且返回一个原来的画刷
HBRUSH oldBursh = (HBRUSH)SelectObject(hdc, hbrush);
//接下来就要画这个矩形,这个矩形多大我们要知道从(0,0)点开始画尺寸大小
RECT DrawRect = { 0,0,rect.right - rect.left, rect.bottom - rect.top };
//画矩形:
FillRect(hdc, &DrawRect, hbrush);
//销毁画刷
SelectObject(hdc, oldBursh);
DeleteObject(hbrush);
//画完之后需要释放内存,要给画家给释放回去,因为别的人可能还要用
ReleaseDC(hWnd, hdc);

//是游戏窗口的话,我们就需要去移动跟踪它
//注意画矩形和移动矩形位置是不一样的,移动的画是要根据游戏窗口去移动
MoveWindow(hWnd, GameRect.left, GameRect.top, GameRect.right - GameRect.left, GameRect.bottom - GameRect.top, TRUE);
}

}
else {
if (GetMessage(&msg, 0, 0, 0)) {
TranslateMessage(&msg);
DispatchMessageA(&msg);
}
}

}
}

运行效果如下:我们会发现其一直跟随我们的游戏窗口,我们可以将其画刷换成BLACK_BRUSH也就是透明的了,能时刻跟随。

我们可以将其画刷换成BLACK_BRUSH也就是透明的了,能时刻跟随,有如下效果:

二,文件与目录

学习一些文件和目录的API的操作:

文件操作:

CreateWindow()

打开文件和创建文件都是用这个API

基本操作如下:创建一个文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
#include <Windows.h>

int main()
{
//最常用的API:F1查看相关介绍
HANDLE hFile = CreateFile(
"D:\\abc.ini",//文件名字
GENERIC_READ | GENERIC_WRITE,//访问方式:可读可写
NULL,//独占:当我在操作该文件的时候其他进程是无法打开这个文件的,无法读无法写(是否独占)
NULL,//安全属性描述符:是否继承
//OPEN_EXISTING,//这是默认情况:文件存在打开它
CREATE_NEW,//不存在则自动创建
FILE_ATTRIBUTE_NORMAL,
NULL
);
if (INVALID_HANDLE_VALUE == hFile)
{
printf("文件创建失败");
}

}

运行:

F10:

这时候我们来看看是否创建成功了这个文件:如下

可见成功创建。这是将参数CREATE_NEW修改为OPEN_EXISTING

WriteFile():

基本操作如下:写文件

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
#include <iostream>
#include <Windows.h>

int main()
{
//最常用的API:F1查看相关介绍
HANDLE hFile = CreateFile(
"D:\\abc.ini",
GENERIC_READ | GENERIC_WRITE,
NULL,
NULL,
OPEN_EXISTING,//这是默认情况:文件存在打开它
FILE_ATTRIBUTE_NORMAL,
NULL
);
if (INVALID_HANDLE_VALUE == hFile)
{
printf("文件创建失败");
}

//写操作:WriteFile():F1查看官网
PCHAR szBuffer[MAX_PATH] = { 0 };
memcpy(szBuffer, "Hello World", strlen("Hello World"));
DWORD dwRet;
BOOL bRet = WriteFile(hFile,
szBuffer,//缓冲区
strlen("Hello World"),//写入长度
&dwRet,//返回写入多少字节
NULL
);
//判断是否写入成功:
if (!bRet)
{
printf("写入失败");
}
else
{
printf("dwRet:%lu", dwRet);
}
}

我来大概解释一下上面的写文件操作:

1.定义缓冲区

PCHAR szBuffer[MAX_PATH] = { 0 };

  • PCHARchar的别名
  • 这里定义了一个字符数组 szBuffer,最大长度为 MAX_PATH,通常是 260 字符(这取决于平台)。初始化时,数组的每个元素都被设为 0,即数组的每个字符都为 '\0',这是一个空的字符串。

2. 拷贝数据到缓冲区

memcpy(szBuffer, "Hello World", strlen("Hello World"));

  • memcpy 是一个内存复制函数,它把 "Hello World" 字符串的数据拷贝到 szBuffer 中。strlen("Hello World") 计算 "Hello World" 的长度(即 11 个字符),然后 memcpy 将这 11 个字符拷贝到 szBuffer 中。

3.定义返回值变量

DWORD dwRet;

  • dwRet 是一个 DWORD 类型的变量,通常用于存储函数的返回值,这里它存储的是 WriteFile 成功写入的字节数。

4. 调用 WriteFile 写入文件

1
2
3
4
5
6
BOOL bRet = WriteFile(hFile,
szBuffer,//缓冲区
strlen("Hello World"),//写入长度
&dwRet,
NULL
);

WriteFile 是 Windows API 函数,用于向文件中写入数据。各参数含义如下:

  • hFile:文件句柄,指向你已经打开的文件。如果你已经通过 CreateFile 等函数获取了文件句柄,那么这里传入这个句柄。
  • szBuffer:数据缓冲区,存储要写入的数据,当前是 "Hello World"
  • strlen("Hello World"):写入的字节数,这里是 11 字节,因为 "Hello World" 包含 11 个字符。
  • &dwRet:指向一个 DWORD 变量的指针,用来接收实际写入的字节数。如果写入成功,dwRet 将是成功写入的字节数。
  • NULL:这是一个指向 OVERLAPPED 结构体的指针,如果文件是以异步方式打开的,这个参数才需要使用。对于同步写入,可以传入 NULL

WriteFile 函数执行完后,会返回一个布尔值 TRUEFALSE,表示写入操作是否成功。

最后就是如下效果:

成功写入内容。

GetFileSizeEx():

这个是获取文件大小,并将其确定存入哪块内存中

ReadFile()

读取文件

基本操作如下:读文件

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
#include <iostream>
#include <Windows.h>

int main()
{
//打开文件
HANDLE hFile = CreateFile(
"D:\\abc.ini",
GENERIC_READ | GENERIC_WRITE,
NULL,
NULL,
OPEN_EXISTING,//文件存在打开它
FILE_ATTRIBUTE_NORMAL,
NULL
);
if (INVALID_HANDLE_VALUE == hFile)
{
printf("文件创建失败");
}

//读文件
LARGE_INTEGER fileSize;
//第二个参数是一个结构体类型:LARGE_INTEGER
GetFileSizeEx(hFile,&fileSize);
//之后他会将这个文件大小存入这个结构体当中,这个结构体又分为高32位和低32位
/*
typedef union _LARGE_INTEGER {
struct {
DWORD LowPart;//低32位
LONG HighPart;//高32位
} DUMMYSTRUCTNAME;
struct {
DWORD LowPart;
LONG HighPart;
} u;
LONGLONG QuadPart;
} LARGE_INTEGER;
*/
//很显然低32位就够存了
//获取到文件大小之后,我们就需要申请一段缓冲区,因为他是字符串大小会有个`\0`故加1
PCHAR pBuffer = new CHAR[fileSize.LowPart + 1];
//进行一个清0对这段申请的缓冲区
memset(pBuffer, 0, fileSize.LowPart + 1);

//返回读取字符数
DWORD dwRet;
BOOL readEnd = ReadFile(hFile,pBuffer,fileSize.LowPart,&dwRet,NULL);

if (!readEnd)
{
printf("读取文件失败");
}
else
{
printf("dwRet:%lu\r\n", dwRet);
printf("文件存储了如下内容:\n%s", pBuffer);
}
}

最后运行结果,如下:

SetFilePointer()

移动文件指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//读文件
LARGE_INTEGER fileSize;
GetFileSizeEx(hFile,&fileSize);
//移动文件指针:SetFilePointer
SetFilePointer(hFile, 2, NULL,FILE_BEGIN);

PCHAR pBuffer = new CHAR[fileSize.LowPart + 1];
memset(pBuffer, 0, fileSize.LowPart + 1);

//返回读取字符数
DWORD dwRet;
BOOL readEnd = ReadFile(hFile,pBuffer,fileSize.LowPart,&dwRet,NULL);

if (!readEnd)
{
printf("读取文件失败");
}
else
{
printf("dwRet:%lu\r\n", dwRet);
printf("文件存储了如下内容:\n%s", pBuffer);
}

运行达到如下效果:

基本的文件操作就这样。

最后别忘了来一这个将引用次数减减:

1
2
//将这个文件对象引用次数减减
CloseHandle(hFile);

目录操作:

CreateDirectory()

创建目录

如下代码:

1
CreateDirectory("D:\\123",NULL);

RemoveDirectory()

删除目录

如下代码:

1
2
3
4
RemoveDirectory("D:\\123")
//不过它只能删除空目录

//所以我们还是要学会遍历

FindFirstFile()

它会把文件的一些信息存到一个结构体WIN32_FIND_DATAA当中

ctrl+鼠标左键

我们可以看看这个结构体WIN32_FIND_DATAA中有存储什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//可以直接F1进官方介绍里面看看对应参数具体的值
typedef struct _WIN32_FIND_DATAA {
DWORD dwFileAttributes; //文件的属性,通过这个属性可以判断出来其是一个文件夹,还是一个目录
FILETIME ftCreationTime;
FILETIME ftLastAccessTime;
FILETIME ftLastWriteTime;//这三个是一些写入时间啊,访问时间等等
DWORD nFileSizeHigh;
DWORD nFileSizeLow;//文件的大小
DWORD dwReserved0;
DWORD dwReserved1;//保留字段
_Field_z_ CHAR cFileName[ MAX_PATH ];
_Field_z_ CHAR cAlternateFileName[ 14 ];//第一个文件或目录的完整名,第二个文件的备用文件名
#ifdef _MAC
DWORD dwFileType;
DWORD dwCreatorType;
WORD wFinderFlags;
#endif
} WIN32_FIND_DATAA, *PWIN32_FIND_DATAA, *LPWIN32_FIND_DATAA;

我们利用其做到一个简单的遍历所有文件:至于如何递归遍历后续再研究。

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

int main()
{

WIN32_FIND_DATAA fileInfo;
//最后其会返回一个文件句柄
HANDLE hFile = FindFirstFile("D:\\123\\*", &fileInfo);
//然后通过这个文件句柄继续向下做一个遍历:
do
{
printf("%s\r\n",fileInfo.cFileName);
} while (FindNextFile(hFile,&fileInfo));
}

上面就是一些常用的相关操作的API的更多的可以看看官方文档。

三,进程与线程

进程(Process)线程(Thread) 是操作系统和并发编程中的两个重要概念。它们在程序执行和资源管理上扮演着不同的角色。让我们分别详细解释一下它们的定义、区别和作用。

1. 进程(Process)

定义:

  • 进程 是计算机中正在执行的一个程序的实例。它是操作系统分配资源的基本单位。
  • 每个进程都有自己独立的内存空间(包括代码、数据、堆栈等)、系统资源(如文件句柄、输入输出资源等)、以及一个执行状态(如运行、就绪、等待等)。

特性:

  • 独立性:每个进程运行在自己的独立空间中,相互之间无法直接访问。
  • 资源隔离:不同进程之间不会共享内存和资源,除非通过操作系统提供的进程间通信(IPC)机制。
  • 开销较大:创建进程、上下文切换等操作相对较为复杂和耗时。

进程的组成:

  • 地址空间:每个进程都有自己的虚拟地址空间。操作系统将每个进程的虚拟地址映射到物理内存中。
  • 进程控制块(PCB):存储进程的基本信息,如进程状态、程序计数器、CPU 寄存器、内存管理信息等。

举例:

  • 启动一个程序时,操作系统为该程序创建一个进程。例如,打开一个网页浏览器(如 Chrome)时,操作系统会为该浏览器程序分配一个进程。

2. 线程(Thread)

定义:

  • 线程 是进程中的一个执行单元,是操作系统调度的基本单位。每个进程至少有一个线程(称为主线程),而一个进程可以包含多个线程(称为子线程或工作线程)。
  • 线程是进程的组成部分,多个线程共享进程的资源,如内存、文件描述符等。

特性:

  • 轻量级:线程是比进程更小的执行单元,创建和销毁的开销比进程小。
  • 共享资源:同一个进程内的多个线程共享进程的内存空间和资源,因此它们之间的数据共享比较容易,但也容易出现同步问题(如数据竞争)。
  • 并发性:多个线程可以同时执行不同的任务,提高程序的执行效率。尤其在多核处理器上,线程可以在多个 CPU 核心上并行执行。

线程的组成:

  • 线程控制块(TCB):包含线程的状态、程序计数器、堆栈指针、寄存器等信息。
  • 共享资源:线程共享进程的虚拟地址空间,但每个线程都有自己的栈和寄存器。

举例:

  • 在一个视频播放器程序中,主线程负责播放视频流,另一个子线程可能负责解码视频,另一个线程负责处理用户的输入。多个线程共同工作,增强了程序的性能和响应速度。

3. 进程与线程的区别

特性 进程 线程
定义 一个程序的实例,是操作系统调度的基本单位。 进程内的一个执行单元,是操作系统调度的最小单位。
资源 每个进程有独立的内存空间和资源。 同一进程中的多个线程共享进程的内存空间和资源。
独立性 进程之间相互独立,不共享资源。 同一进程内的线程共享资源,容易引发竞争条件。
开销 创建和销毁进程的开销较大,进程之间切换较慢。 线程创建和销毁的开销较小,线程之间的切换较快。
调度单位 操作系统以进程为单位进行调度。 操作系统以线程为单位进行调度。
通信方式 进程间通信较为复杂,需要使用 IPC(如管道、消息队列等)。 线程间通信简单,因为它们共享进程的内存空间。
状态 进程有多个状态:就绪、运行、等待等。 线程有多个状态:就绪、运行、阻塞等。

4. 进程和线程的关系

  • 线程依赖于进程:线程是进程的一个组成部分。一个进程至少有一个线程(主线程),但是可以创建多个子线程来执行不同的任务。
  • 共享进程资源:同一个进程中的所有线程共享进程的内存空间和文件描述符等资源。但每个线程都有自己独立的寄存器、堆栈和程序计数器等。

5. 进程与线程的实际应用

  • 进程的应用
    • 在不同的应用程序之间保持隔离。例如,浏览器、文本编辑器、视频播放器等是各自独立的进程。
    • 用于管理大型程序或服务,确保它们的独立运行和资源分配。
  • 线程的应用
    • 在同一个应用程序内部,多个任务可以并发执行。例如,视频播放器程序使用一个线程播放视频,另一个线程处理用户输入,另一个线程进行网络请求。
    • 在多核处理器上,线程可以并行执行,提高程序效率。

6. 总结

  • 进程 是程序执行的实例,是操作系统分配资源和管理的基本单位。
  • 线程 是进程内的一个执行单元,多个线程共享进程的资源,适合并发执行不同的任务。

理解进程和线程的区别,对于优化程序的性能、实现并发编程、资源管理等方面都非常重要。在多核系统中,利用多个线程可以显著提高应用程序的并发性和响应速度,而合理地使用多个进程可以提高程序的稳定性和安全性。


进程与线程相关的API:

CreateProcess()

创建进程

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
#include <Windows.h>
#include <iostream>

int main()
{
//创建进程
STARTUPINFOA si = {sizeof(STARTUPINFOA)};
//会把进程的一些信息放入这里
PROCESS_INFORMATION pi;
BOOL bRet = CreateProcess("D:\\tool\\x64dbg\\release\\x32\\x32dbg.exe",
NULL,//要执行的命令行
NULL,//安全属性描述符
NULL,//安全属性描述符
FALSE,//是否可继承父进程的句柄
NULL,//标志位
NULL,//环境相关
NULL,
&si,
&pi
);

if (!bRet)
{
std::cout << "CreateProcess Error Code" << std::endl;
}
else
{
printf("%d %d %d %d\r\n", pi.hProcess, pi.hThread, pi.dwProcessId, pi.dwThreadId);
}

system("pause");
return 0;
}

如图,进程句柄线程句柄进程pid线程pid我们都能获得到:

TerminateProcess()

终止进程。

这里可以在上面创建的进程的源码上打断点一步一步观察

1
2
3
	//终止进程
TerminateProcess(pi.hProcess, 0);
//传入进程句柄

TerminateThread()

终止主线程。

1
2
//想要终止进程也可以直接终止主线程,主线程一死,所有的子进程也就gg了
TerminateThread(pi.hThread, 0);

OpenProcess()

获取运行中的进程的进程句柄。

比方说我们要获取关闭如下进程x64dbg.exe,然后就需要获得其进程句柄:

就可以利用如下代码:

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

int main()
{
//获取进程句柄
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, 6988);
//终止进程
TerminateProcess(hProcess, 0);
//想要终止进程也可以直接终止主线程,主线程一死,所有的子进程也就gg了
//TerminateThread(pi.hThread, 0);

system("pause");
return 0;
}

然后运行我们发现成功关闭了:如下

CreateToolhelp32Snapshot()

获取指定进程以及这些进程使用的堆、模块和线程的快照。(遍历进程)

F1查看官方文档使用需要导入库tlhelp32.h

相关参数:

返回值:

现在又这样一个场景,我们并不能手动查看任务管理器中的PID,从而无法获得进程句柄,这时候我们该如何做?

如下代码:

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
#include <Windows.h>
#include <iostream>
#include <TlHelp32.h>

int main()
{
//遍历进程:可以理解为在这一瞬间,他像任务管理器似的拍了一张快照,图片,然后我们把他的每一条给他遍历出来就好了
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);

//该结构体用于接受Process32First它还给我们的进程信息
PROCESSENTRY32 pe32;
//通过查看官方文档可以知道这里是必须的
//pe32.dwSize = sizeof(PROCESSENTRY32);
pe32.dwSize = sizeof(pe32);
/*
我们可以看看这个结构里面有什么
typedef struct tagPROCESSENTRY32
{
DWORD dwSize;
DWORD cntUsage;
DWORD th32ProcessID; //PID
ULONG_PTR th32DefaultHeapID;
DWORD th32ModuleID; //模块id
DWORD cntThreads;
DWORD th32ParentProcessID; // this process's parent process
LONG pcPriClassBase; // Base priority of process's threads
DWORD dwFlags;
CHAR szExeFile[MAX_PATH]; //可执行文件的路径
} PROCESSENTRY32;

*/

//通过Process32First去遍历第一个进程,F1查看官方介绍
BOOL Ret = Process32First(hSnap, &pe32);

while (Ret)
{
printf("可执行文件路径:%s\r\nPID:%d\r\n", pe32.szExeFile, pe32.th32ProcessID);
//检索有关系统快照中记录的下一个进程的信息,并且将信息防止到缓冲区pe32中
Ret = Process32Next(hSnap, &pe32);
}
return 0;
}

最后运行他就会将所有的信息都给遍历到:如下:

CreateThread()

创建在调用进程的虚拟地址空间内执行的线程。

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <Windows.h>
#include <iostream>
#include <TlHelp32.h>

//这里就是我们新线程的一个入口
DWORD WINAPI ThreadProc(
_In_ LPVOID lpParameter
)
{
int i = 0;
while (1)
{
printf("ThreadMain %d\r\n",i++);
}
}
int main()
{
//这里各个参数,F1看官方文档
CreateThread(NULL, NULL, ThreadProc, NULL, 0, NULL);

//然后主线程呢别死掉就行
system("pause");
return 0;
}

如下效果:他就会无限打印循环

还有其他的API可以看看官方文档里面介绍的很多,一定要学会看文档介绍。

四,线程同步机制

通过一段代码来理解为什么要有线程同步这个东西

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
#include <Windows.h>
#include <iostream>
#include <TlHelp32.h>

//定义一个全局变量
DWORD g_value = 0;

//线程的一个回调
DWORD WINAPI ThreadMain(
_In_ LPVOID lpParameter
)
{
for (int i = 0;i<1000000;i++)
{
g_value++;

//我们可以来看看这段的汇编代码
/*
g_value++;
01011783 mov eax,dword ptr [g_value (0101A138h)] // eax = g_value
01011788 add eax,1 // eax = eax + 1 ======= eax = 1 它在这一步的时候直接转到了另外一个线程,值还没有存入
0101178B mov dword ptr [g_value (0101A138h)],eax // g_value = eax
*/
}
return 0;
}


//再来一个线程
DWORD WINAPI ThreadMain1(
_In_ LPVOID lpParameter
)
{
for (int i = 0; i < 1000000; i++)
{
g_value++;
/*
g_value++;
01011783 mov eax,dword ptr [g_value (0101A138h)] // eax = g_value
01011788 add eax,1 // eax = eax + 1
0101178B mov dword ptr [g_value (0101A138h)],eax // g_value = eax g_value = 1
*/
}
return 0;
}

/*
当线程A执行g_value++的时候,
如果线程切换时间正好是线程A将原来的值保存到当前线程A执行g_value之前

当线程B继续去执行g_value++
当线程A再次被切换回来之后,会将原来线程A保存的值存到g_value里,
那么线程B的加法操作就会被覆盖一次

故最后g_value的结果会小于2000000
*/
int main()
{
//通过一段代码来理解为什么要有线程同步这个东西


HANDLE hThread[2];
hThread[0] = CreateThread(NULL, NULL, ThreadMain, NULL, 0, NULL);
hThread[1] = CreateThread(NULL, NULL, ThreadMain1, NULL, 0, NULL);

//windows专门准备了一个API是专门用来等待这些内核对象的
//等两个线程都结束后我们的程序才退出
WaitForMultipleObjects(2,//等待的个数
hThread,//等待的句柄
TRUE,//等待都结束时,返回
INFINITE//等待时间,一直等
);//等多个
//等一个:WaitForSingleObject();

printf("%d", g_value);

return 0;

}

最后通过运行会发现g_value的值最后并不等于2000000,不过有的情况下会等于。

通过观察其汇编代码我们能够分析出其中的一些问题:

1
2
3
4
;g_value++;
01011783 mov eax,dword ptr [g_value (0101A138h)] // eax = g_value
01011788 add eax,1 // eax = eax + 1
0101178B mov dword ptr [g_value (0101A138h)],eax // g_value = eax g_value = 1

原因:

  • 当线程A执行g_value++的时候,
    如果线程切换时间正好是线程A将原来的值保存到当前线程A执行g_value之前

  • 当线程B继续去执行g_value++
    当线程A再次被切换回来之后,会将原来线程A保存的值存到g_value里,
    那么线程B的加法操作就会被覆盖一次

  • 故最后g_value的结果会小于2000000

那么我们该如何解决这个问题?

  • 原子操作
  • 运算操作 ++--+-*/

将这些基本运算操作全部变成原子操作:

保证这个变量的内存在一个瞬间只有一个线程去访问。

如上述代码中我们可以使用如下:

InterlockedIncrement()

Windows 操作系统提供的一个用于原子操作的函数,它属于 Windows API 中的互斥(同步)机制。该函数主要用于 增加 一个变量的值,并保证操作的 原子性。也就是说,它可以防止多线程环境中对同一个变量进行并发访问时出现竞争条件(race condition)。

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 <iostream>
#include <TlHelp32.h>

//定义一个全局变量
DWORD g_value = 0;

//线程的一个回调
DWORD WINAPI ThreadMain(
LPVOID lpParameter
)
{
for (int i = 0;i<1000000;i++)
{
InterlockedIncrement(&g_value);
}
return 0;
}


//再来一个线程
DWORD WINAPI ThreadMain1(
LPVOID lpParameter
)
{
for (int i = 0; i < 1000000; i++)
{
InterlockedIncrement(&g_value);
}
return 0;
}

int main()
{

HANDLE hThread[2];
hThread[0] = CreateThread(NULL, NULL, ThreadMain, NULL, 0, NULL);
hThread[1] = CreateThread(NULL, NULL, ThreadMain1, NULL, 0, NULL);

WaitForMultipleObjects(2,//等待的个数
hThread,//等待的句柄
TRUE,//等待都结束时,返回
INFINITE//等待时间,一直等
);//等多个
printf("%d", g_value);

return 0;
}

最后结果就等于2000000.

那么如果我们是想对一段代码进行一个类似的操作又该如何做呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
DWORD WINAPI ThreadMain1(
LPVOID lpParameter
)
{
//需要从外界获得一把钥匙才能打开
//---------加锁------------
for (int i = 0; i < 1000000; i++)
{
printf("Hello World");
}
//执行完毕以后
//----------再把钥匙还回去----------
return 0;
}

这时候有一个概念产生了互斥体

你可以把互斥体理解成上面所说的那把钥匙

可以使用这个API:

CreateMutex()

创建或打开命名或未命名的互斥体对象。

releaseMutex()

释放指定互斥对象的所有权

示例代码如下:

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
#include <Windows.h>
#include <iostream>
#include <TlHelp32.h>

DWORD g_value = 0;
//钥匙
HANDLE hMutex;
DWORD WINAPI ThreadMain(
LPVOID lpParameter
)
{
for (int i = 0;i<1000000;i++)
{
//找互斥体要钥匙,第二个参数就是要不到就一直等待
WaitForSingleObject(hMutex, INFINITE);
printf("----------\r\n");
//代码执行完毕以后归还钥匙,标志又可用了
ReleaseMutex(hMutex);
}
return 0;
}

DWORD WINAPI ThreadMain1(
LPVOID lpParameter
)
{
for (int i = 0; i < 1000000; i++)
{
//查看钥匙是否存在
WaitForSingleObject(hMutex, INFINITE);
printf("**********\r\n");
ReleaseMutex(hMutex);
}
return 0;
}

int main()
{
//这样我们的这把钥匙就出来了
hMutex = CreateMutex(NULL,//内核对象
FALSE,//是否有信号:最开始的时候你是有信号还是没信号,就是这把钥匙它在不在,FALSE就说明在的
NULL//这个参数就是判断其是否能够跨进程同步
);

HANDLE hThread[2];
hThread[0] = CreateThread(NULL, NULL, ThreadMain, NULL, 0, NULL);
hThread[1] = CreateThread(NULL, NULL, ThreadMain1, NULL, 0, NULL);

WaitForMultipleObjects(2,//等待的个数
hThread,//等待的句柄
TRUE,//等待都结束时,返回
INFINITE//等待时间,一直等
);//等多个

return 0;

}

观察可以发现它有规律的交替出现了,而不是随机打印,如下打印效果:

在上面这些代码当中我们发现了两个这个相关函数,具体了解一下:

WaitForMultipleObjects()

WaitForMultipleObjects() 是 Windows API 用于等待多个同步对象(如互斥量、事件、线程等)的一种机制。它的作用是让当前线程等待一个或多个同步对象变为可用,可以用于协调多个线程的执行或等待多个条件满足。

函数原型:

1
2
3
4
5
6
7
DWORD WaitForMultipleObjects(
DWORD nCount, // 等待的对象数量
const HANDLE *lpHandles, // 指向句柄数组的指针
BOOL bWaitAll, // 是否等待所有对象,TRUE为等待所有,FALSE为等待任意一个
DWORD dwMilliseconds // 等待的时间(以毫秒为单位),INFINITE 表示无限等待
);

参数:

  1. **nCount**:
    • 表示要等待的对象数量,通常是句柄数组中对象的数量。
  2. **lpHandles**:
    • 这是一个指向句柄数组的指针,数组中包含要等待的多个同步对象句柄。每个句柄通常是由函数(如 CreateMutex()CreateEvent() 等)返回的。
  3. **bWaitAll**:
    • 如果为 **TRUE**,则表示当前线程必须等待 所有 同步对象都变为可用,才会继续执行。
    • 如果为 **FALSE**,则表示当前线程只要等待 任意一个 同步对象变为可用,便会继续执行。
  4. **dwMilliseconds**:
    • 表示等待的时间(单位:毫秒)。如果设置为 **INFINITE**,则线程会一直等待,直到所有对象(如果 bWaitAllTRUE)或任意一个对象(如果 bWaitAllFALSE)变为可用。

返回值:

WaitForMultipleObjects() 的返回值是一个 DWORD 类型,表示等待的结果:

  • **WAIT_OBJECT_0WAIT_OBJECT_0 + nCount - 1**:表示对应的同步对象已变为可用,线程继续执行。
  • **WAIT_TIMEOUT**:表示等待超时,线程没有在指定的时间内获得信号。
  • **WAIT_FAILED**:表示发生了错误,函数调用失败。

主要作用:

WaitForMultipleObjects() 允许一个线程等待多个同步对象的状态变化。它常用于以下场景:

  1. 等待多个线程完成:比如主线程等待多个工作线程完成。
  2. 等待多个事件或信号量:多个事件或信号量发生时,线程才继续执行。
  3. 协调多个任务:多个任务或资源的协调处理。

WaitForSingleObject()

WaitForSingleObject() 是 Windows API 中用于线程同步的一个函数。它的作用是让当前线程等待一个特定的对象变为可用,通常用于等待信号量、互斥量(mutex)、事件(event)等同步对象。其常见用法是在多线程程序中使线程等待某个资源或条件满足后再继续执行。

函数原型:

1
2
3
4
DWORD WaitForSingleObject(
HANDLE hObject, // 要等待的对象句柄
DWORD dwMilliseconds // 等待的时间(以毫秒为单位)
);

参数

  1. hObject
    • 这是一个句柄,指向一个同步对象(如互斥量、事件、信号量等)。
    • 该句柄通常由 CreateMutex()CreateEvent() 或其他同步对象创建函数返回。
  2. dwMilliseconds
    • 指定线程等待的时间。单位是毫秒。
    • 如果设置为 INFINITE,则表示线程会一直等待,直到同步对象被释放或信号变为有效。
    • 如果指定了非 INFINITE 的值,线程会等待指定的时间后自动返回,如果在此时间内对象的状态没有改变,函数会返回超时错误。

返回值

  • 返回值是一个 DWORD 类型的常量,表示等待的结果。

    可能的返回值:

    • WAIT_OBJECT_0:表示同步对象已经变为可用,线程继续执行。
    • WAIT_TIMEOUT:表示等待超时,线程没有在指定的时间内获得信号。
    • WAIT_FAILED:表示发生了错误,函数调用失败。

主要作用

WaitForSingleObject() 的主要作用是 阻塞当前线程,直到指定的同步对象被信号释放或满足某个条件。在多线程程序中,常常使用它来等待一些共享资源或条件的满足,确保线程之间按照预定的顺序执行。

补:

至于其他的信号量,事件等都和互斥量差不多,很类似就不说了。

五,静态库与动态库

静态库

场景:当我们只想让别人使用这个功能,而不想让其看到我们的源代码的时候

  • 运行的时候它不存在,静态库的源代码会被连接到调用程序当中
  • 步骤:
    • 创建静态库 添加源文件 封装库函数
    • pragma comment(lib,"lib路径")
    • C++库

示例代码如下:

StaticLib1.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// StaticLib1.cpp : 定义静态库的函数。
//
#include "pch.h"
#include "framework.h"

// TODO: 这是一个库函数示例
int add(int a,int b)
{
return a + b;
}

int sub(int a, int b)
{
return a - b;
}

main.cpp:

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 <iostream>
#include <Windows.h>

//静态库与动态库

/*
静态库:
运行的时候它不存在,静态库的源代码会被连接到调用程序当中
步骤:
1.创建静态库 添加源文件 封装库函数
2.pragma comment(lib,"lib路径")
C++库

*/

#pragma comment(lib,"StaticLib1.lib")

//我们呢要在此声明一下这两个函数,因为一开始其在下方找不到
int add(int a, int b);
int sub(int a, int b);

int main()
{
printf("add结果:%d\r\n", add(10, 20));
printf("sub结果:%d\r\n", sub(10, 20));
system("pause");
return 0;
}

首先我们直接生成一个静态库文件项目:

然后在静态库中编写一些函数:如下:

然后我们让其重新生成连接,如下所示:其会生成一个lib文件,这个呢就是我们的一个静态库

然后我们将其放入到我们的其他的项目文件中,如下:

这时,我们调用此函数:

但是只有声明没有实现啊,这时就需要我们,将静态库链接进来:如下:

最后一运行,成功导入:结果如下:


动态库

动态链接库代码:

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
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
//需要将两个函数导出
__declspec(dllexport) int add(int a, int b);
__declspec(dllexport) int sub(int a, int b);


int add(int a, int b)
{
return a + b;
}

int sub(int a, int b)
{
return a - b;
}

BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH://进程加载时
case DLL_THREAD_ATTACH://线程加载时
case DLL_THREAD_DETACH://进程退出
case DLL_PROCESS_DETACH://线程退出
break;
}
return TRUE;
}

隐式调用main.cpp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <Windows.h>

/*
动态库:
运行时候是独立存在 自己是有内存的
和静态库不同它是有自己的一个独立的4GB内存的
*/

#pragma comment(lib,"Dll1.lib")

//导入我们要用的函数
__declspec(dllimport) int add(int a, int b);
__declspec(dllimport) int sub(int a, int b);



int main()
{
printf("add结果:%d\r\n", add(10, 20));
printf("sub结果:%d\r\n", sub(10, 20));
system("pause");
return 0;
}

显式调用main.cpp:

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
#include <iostream>
#include <Windows.h>
//定义一个函数指针类型,用于接收返回值
typedef int(*pAdd)(int a, int b);

int main()
{
/*
手动将我们的库(Dll1.dll)链接进来,加载到内存当中
然后利用hMoudule接收其内存首地址
*/
HMODULE hModule = LoadLibrary("Dll1.dll");
/*题外话2:
加载过程:
1.加载 DLL 文件:当 LoadLibrary() 被调用时,Windows 会在指定的路径中查找 "Dll1.dll" 文件,并将其加载到内存中。
2.映射到进程地址空间:一旦 DLL 被加载,操作系统将它的代码和数据映射到当前进程的虚拟地址空间,使得程序可以访问 DLL 中的功能。
3.返回句柄:成功加载 DLL 后,LoadLibrary() 会返回一个句柄,这个句柄可以用来访问 DLL 的导出函数或进一步操作 DLL。
*/
/*
然后有了这个Dll1内存的首地址之后,
我们就可以通过这个内存找到这个函数的地址,
然后利用GetProcAddress,这个函数会返回一个函数指针类型
*/



pAdd add = (pAdd)GetProcAddress(
hModule, //下面的函数名字我们可以通过工具CFF Explorer找到,
"?add@@YAHHH@Z"
);
pAdd sub = (pAdd)GetProcAddress(
hModule, //下面的函数名字我们可以通过工具CFF Explorer找到
"?sub@@YAHHH@Z"
);

//通过官方文档也可以知道,可以通过传入序号来调用
/*
pAdd pFunc = (pAdd)GetProcAddress(
hModule, //下面的函数名字我们可以通过工具CFF Explorer找到,
(char*)1
);
*/
printf("add结果:%d\r\n", add(10, 20));
printf("sub结果:%d\r\n", sub(10, 20));
system("pause");
return 0;
}

操作过程如下:

创建一个动态链接库项目,如下:

然后动态库需要我们这么写:如下:

写完以后并生成。

然后将Dll1.dllDll1.lib放入其他项目当中,如下:

它们两个一个是显示调用一个是隐式调用

隐示调用:

我们先来看隐示调用,其实隐示调用也就是把他的lib给他调用进来:如下:

运行结果如下:

显示调用:

源码如下:

由上图可知我们需要找到我们需要用的函数的名字传参的时候:利用工具CFF Explorer

将生成好的Dll1.dll文件拖入其中:如下

然后即可获得两个函数的名字了。

运行结果如下:

六,内存管理

我们来简单的看看示意图:

介绍几个内存管理的API:

VirtualAlloc()/VirtualAllocEx()

申请内存。保留、提交或更改调用进程的虚拟地址空间中页面区域的状态。 此函数分配的内存会自动初始化为零。

简单示例如下代码:

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
#include <iostream>
#include <Windows.h>

//申请内存的API 只有两个 VirtualAlloc/VirtualAllocEx(私有的)
//Createfilemapping 共享内存
/*
virtualalloc/virtualallocEx:
申请出来的内存只能是A进程单独使用

Createfilemapping:
申请出来的内存可以A进程,B进程共同使用
*/

int main()
{
//内存管理
LPVOID pAddress = VirtualAlloc(NULL, 0x1000, MEM_COMMIT, PAGE_EXECUTE_READWRITE);

//会正常的打印出这块已经申请到的内存地址:(这里申请的是虚拟地址)
printf("%p\r\n", pAddress);

//我们可以像使用指针一样使用它,比方说向其中存值:
*(int*)pAddress = 500;

//打印存入这块内存的值
printf("0x%d:%d\r\n", pAddress, *(int*)pAddress);

/*
LPVOID VirtualAlloc(
[in, optional] LPVOID lpAddress, //你要申请的地址,一般都填空,因为我们并不知道操作系统哪里被使用了哪里没被使用
[in] SIZE_T dwSize, //申请的大小一般来说都是0x1000的倍数
[in] DWORD flAllocationType,//内存分配的类型
//MEM_COMMIT:分配内存并申请物理页 MEM_RESERVE:分配了虚拟内存,但是没有物理页
[in] DWORD flProtect //读写标志,可读可写可执行
);
*/
//system("pause");
return 0;
}

最后运行结果如下:(这里整错了:这里是十进制的10223616)


CreateFileMapping()

申请共享内存。

为指定文件创建或打开命名或未命名的文件映射对象。

CreateFileMapping 创建共享内存区域时,这块内存区域是由操作系统在 虚拟内存空间 中为进程A分配的。具体地说,这块共享内存并不直接位于进程A的地址空间内,而是位于操作系统的 内核空间 或者操作系统管理的 虚拟内存区域 中。

进程A中的共享内存位置:

  1. 共享内存在虚拟地址空间中的位置
    • 在调用 CreateFileMapping 后,进程A通过 MapViewOfFile 将该共享内存映射到它的 虚拟地址空间 中。此时,进程A通过 pBuff 等指针访问的内存区域是 映射到进程A的虚拟内存地址空间中的一部分
    • 共享内存的“位置”实际上是由操作系统分配给进程A的虚拟内存的一部分,不是进程A内存中的常规堆栈、堆或全局变量内存。
  2. 内存映射(Memory Mapping)
    • 当你调用 MapViewOfFile 时,操作系统会将共享内存映射到进程A的虚拟地址空间,但这块内存的 实际物理位置 是由操作系统决定的,可能是物理内存、磁盘中的交换文件或其他地方。
    • 映射后的共享内存相对于进程A来说是 虚拟内存的一部分,即它看起来像进程A的一部分内存区域,但实际上,它可能并没有真正占用进程A的物理内存,直到操作系统将其映射到物理内存中。
1
2
3
4
5
6
7
8
HANDLE CreateFileMappingW(
[in] HANDLE hFile, //第一个参数是文件句柄
[in, optional] LPSECURITY_ATTRIBUTES lpFileMappingAttributes,//安全描述
[in] DWORD flProtect,//读写标志
[in] DWORD dwMaximumSizeHigh,//文件映射的高低位
[in] DWORD dwMaximumSizeLow,
[in, optional] LPCWSTR lpName//取名
);

总结:

  • 当进程A调用 CreateFileMapping 创建共享内存并通过 MapViewOfFile 映射它时,共享内存的区域实际上是在操作系统的虚拟内存空间中,然后映射到进程A的虚拟地址空间。
  • 共享内存并不是进程A的常规内存的一部分,它是由操作系统通过内存映射机制在进程A的虚拟内存空间中分配的一块区域。虽然进程A可以像访问其自己的内存一样访问这块共享内存,但它的 实际物理位置 由操作系统决定,并不固定在进程A的内存中。

MapViewOfFile()

将文件映射的视图映射到调用进程的地址空间。

1
2
3
4
5
6
7
LPVOID MapViewOfFile(
[in] HANDLE hFileMappingObject,//文件映射对象的句柄
[in] DWORD dwDesiredAccess,//选择映射的读写权限
[in] DWORD dwFileOffsetHigh,//视图开始的文件偏移量 DWORD 高阶。
[in] DWORD dwFileOffsetLow,
[in] SIZE_T dwNumberOfBytesToMap//要映射到视图的文件映射的字节数。
);

让我们来看看具体的示例代码:是如何实现进程共享内存的

代码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
#include <iostream>
#include <Windows.h>
//申请内存的API 只有两个 VirtualAlloc/VirtualAllocEx(私有的)
//Createfilemapping 共享内存
/*
VirtualAlloc/VirtualAllocEx:
申请出来的内存只能是A进程单独使用

CreateFileMapping:
申请出来的内存可以A进程,B进程共同使用
*/
int main()
{
HANDLE hFileMapping = CreateFileMapping(INVALID_HANDLE_VALUE, NULL, PAGE_EXECUTE_READWRITE, 0, 0x1000, "共享内存");

/*
他会创建一个内核对象
有了这个内核对象之后后,我们上面这个操作只是申请了一个物理内存,
我们需要将其映射到当前进程的虚拟内存上
*/

//这时候我们要用到第二个API关联到内存
LPVOID pBuff = MapViewOfFile(hFileMapping, FILE_MAP_ALL_ACCESS, 0, 0, 0x1000);
printf("%d\r\n", pBuff);
*(DWORD*)pBuff = 0x12345678;

printf("%p\r\n", pBuff);
return 0;
}

代码2:让我们新开一个项目来访问这段共享内存

1
2
3
4
5
6
7
8
9
10
11
12
13
// Share_Process.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
#include <iostream>
#include <Windows.h>
int main()
{
//对文件映射对象的访问。
HANDLE handle = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, L"共享内存");
//然后一样的映射到虚拟内存
LPVOID pBuff = MapViewOfFile(handle, FILE_MAP_ALL_ACCESS, 0, 0, 0x1000);
printf("%x\r\n", *(DWORD*)pBuff);

std::cout << "Hello World!\n";
}

首先如下我们,下个断点,并运行代码1

接下来我们同样的下个断点运行代码2,运行结果如下:

七,异常处理

windows有两种异常处理机制:
结构化异常:SEH

向量化异常:VEH

结构化异常SEH的语法使用:

代码解析

  • __try:这是一个用于捕获异常的语句块,它包裹了可能抛出异常的代码。
  • __except:在异常发生时,__except 块会被执行,它允许你决定如何处理异常。
  • __finally:这是一个保证执行的语句块,不管是否发生异常,__finally 都会被执行。它常用于执行清理操作。

except:

  • EXCEPTION_CONTINUE_EXECUTION(-1)
    它就会跳转回原来出现异常的代码位置继续执行

  • EXCEPTION_CONTINUE_SEARCH(0)
    无法识别异常。 继续向上搜索堆栈查找处理程序,首先是所在的 try-except 语句,然后是具有下一个最高优先级的处理程序。
    异常有一个异常链表 A—–>B——>C

  • EXCEPTION_EXECUTE_HANDLER(1)
    异常可识别。 通过执行__except复合语句将控制权转移到异常处理程序,然后在 __except 块后继续执行。
    他会跳转到出现异常的下一行代码去执行

示例代码1如下:EXCEPTION_CONTINUE_EXECUTION

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
#include <iostream>
#include <Windows.h>
/*
windows有两种异常处理机制:
结构化异常:SEH

向量化异常:VEH
*/

int main()
{
//结构化异常
__try
{
//将可能出现异常的代码放入这里
int a = 0;
int b = 0;
int c = a / b;//除0异常
//也就是try内代码出现异常他就会进入__except这个位置
}
//出现异常后我们可以用如下关键字去处理它:
__except(EXCEPTION_CONTINUE_EXECUTION) //执行代码
{

}
printf("程序执行完毕");
}

我们会发现其进入循环一直出不来,因为其一直判断异常又跳转回去:如下:

示例代码2如下:EXCEPTION_EXECUTE_HANDLER

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Exception_throw.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。
//

#include <iostream>
#include <Windows.h>

int main()
{
//结构化异常
__try
{
//将可能出现异常的代码放入这里
int a = 0;
int b = 0;
int c = a / b;//除0异常
//也就是try内代码出现异常他就会进入__except这个位置
}
//出现异常后我们可以用如下关键字去处理它:
__except(EXCEPTION_EXECUTE_HANDLER) //执行代码
{
printf("存在异常\r\n");
}
printf("程序执行完毕");
}

运行结果如下:

接下来我们还要哦讲述结构化异常中的finally关键字:

finally

示例代码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
#include <iostream>
#include <Windows.h>

int main()
{
//结构化异常
__try
{
//将可能出现异常的代码放入这里
int a = 0;
int b = 0;
//int c = a / b;//除0异常
}
__finally //执行代码判断try里的代码有没有正常执行完
{
//然后我们可以调用一个API:AbnormalTermination()来判断是否正常
if (AbnormalTermination())
{
printf("异常退出");
}
else
{
printf("正常退出");
}
}
printf("程序执行完毕");
}

如图可知正常退出:代码执行完毕

我们再来看看示例代码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
#include <iostream>
#include <Windows.h>
int main()
{
//结构化异常
__try
{
//将可能出现异常的代码放入这里
int a = 0;
int b = 0;
goto heihei;
//int c = a / b;//除0异常
}
__finally //执行代码判断try里的代码有没有正常执行完
{
//然后我们可以调用一个API:AbnormalTermination()来判断是否正常
if (AbnormalTermination())
{
printf("异常退出\r\n");
}
else
{
printf("正常退出\r\n");
}
}
heihei:
printf("程序执行完毕");
}

可以看到是异常退出直接跳转了:如下:

向量化异常:VEH

VEH 的基本概念

VEH 是一种面向向量的异常处理机制,所谓的“向量化”意味着异常处理程序可以像数组中的元素一样被存储和访问,并且按照注册的顺序依次执行。这种机制允许开发者对异常处理进行细粒度的控制,而不仅仅局限于 SEH 中的 __try/__except 语句。

VEH 与 SEH 的关系

  • SEH(结构化异常处理)是 Windows 提供的一个较为传统的异常处理机制,通常用于捕获和处理由应用程序引发的异常。SEH 的异常处理是在异常发生时展开的,异常处理顺序通常是由操作系统决定的。
  • VEH(向量化异常处理)则提供了更高的灵活性,允许开发者注册多个异常处理程序,并且可以指定这些处理程序的执行顺序。VEH 机制通常用于更底层的编程场景,比如操作系统、驱动程序以及一些调试工具。

VEH 的工作原理

VEH 通过向操作系统注册向量化的异常处理程序,这些处理程序会在异常发生时被调用。它的工作流程大致如下:

  1. 注册异常处理程序:开发者通过 AddVectoredExceptionHandler 函数来注册多个异常处理程序。这些处理程序会被放入一个向量表中,按照注册的顺序依次执行。
  2. 异常发生时的处理:当异常发生时,系统会按照注册顺序查找并调用相关的异常处理程序。异常处理程序可以获取异常的详细信息,并决定是否处理异常、是否继续执行其他处理程序等。
  3. 异常类型:VEH 能处理硬件异常、访问违规等低级别的异常,也能够与 SEH 配合使用。

如何注册和使用 VEH?

这就需要介绍这个接口了:

AddVectoredExceptionHandler()

在 Windows 中,使用 AddVectoredExceptionHandler 函数来注册异常处理程序。该函数的原型如下:

1
2
3
4
PVOID AddVectoredExceptionHandler(
ULONG FirstHandler, // 0: 后处理, 1: 前处理
PVECTORED_EXCEPTION_HANDLER Handler // 异常处理程序
);
  • **FirstHandler**:指定异常处理程序的执行顺序。如果设置为 1,则该处理程序会在 SEH 之前执行;如果设置为 0,则该处理程序会在 SEH 之后执行。

  • Handler异常处理程序的回调函数,接收 EXCEPTION_POINTERS 作为参数,EXCEPTION_POINTERS 结构包含了异常的详细信息

下面我通过这个代码来简单的介绍一下这个接口的使用:

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
#include <iostream>
#include <Windows.h>

/*
windows有两种异常处理机制:
结构化异常:SEH
向量化异常:VEH
*/

/*
相关结构体:
这是我们的传入参数结构体
typedef struct _EXCEPTION_POINTERS {
PEXCEPTION_RECORD ExceptionRecord; //这又是一个结构体,是关于异常的结构体,也就是异常信息都存储在此
PCONTEXT ContextRecord; //寄存器的环境,都保存在此
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;

PEXCEPTION_RECORD:
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode; //异常码 断点异常,数据异常,C05异常等都是通过它来判断
DWORD ExceptionFlags; //标志位
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress; //异常地址,比较关键
DWORD NumberParameters; //数组里有多少成员
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];//一些附加信息
} EXCEPTION_RECORD;

typedef EXCEPTION_RECORD *PEXCEPTION_RECORD;
*/

//这个就是我们的异常处理函数等会我们给他注册到AddVectoredExceptionHandler
LONG NTAPI Handle(
struct _EXCEPTION_POINTERS *ExceptionInfo
)
{
DWORD dwCode = ExceptionInfo->ExceptionRecord->ExceptionCode;
//EXCEPTION_BREAKPOINT 遇到断点(c05异常)
if (dwCode == EXCEPTION_BREAKPOINT)
{
printf("接收到断点异常");
system("pause");
//这个和我们刚刚那个`结构化异常处理`意思都是一样的
/*
原地执行,不处理,或者跳过

EXCEPTION_CONTINUE_EXECUTION:
告诉他你继续去执行,刚刚的代码
*/
return EXCEPTION_CONTINUE_EXECUTION;
}
}


int main()
{
/*
向量化异常:
他不是用关键字,而是用API去注册一个异常处理函数,
一旦你发生异常它就会找到你注册的这个异常函数
*/

//注册异常API
AddVectoredExceptionHandler(0,
Handle
);

//__asm int 3; 是一种汇编指令,表示触发一个调试断点异常
//int 3 是一种 CPU 指令,也被称为“断点中断”,其原始功能是触发一个中断(异常)。在程序执行
__asm int 3;

printf("异常已经处理");
/*
当触发异常之后,就会走到上面Handle的位置:
but 异常有很多种,
我们就需要通过这个结构体来判断:在最上方
*/
}

我们会发现它陷入了死循环,无法结束,如下:

它会一直卡在__asm int 3;,这个C05异常

那么我们该如何调整他才能正常处理这个异常呢?

这个时候我们就可以利用结构体当中的第二个参数,寄存器的环境——>PCONTEXT ContextRecord;我们让EIP指向下一行,直接跳过这个断点是不是就可以正常运行了。

修改代码之前我们可以看看这个结构体PCONTEXT ContextRecord;

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
typedef struct _CONTEXT {

DWORD ContextFlags;

DWORD Dr0;
DWORD Dr1;
DWORD Dr2;
DWORD Dr3;
DWORD Dr6;
DWORD Dr7;

FLOATING_SAVE_AREA FloatSave;

DWORD SegGs;
DWORD SegFs;
DWORD SegEs;
DWORD SegDs;

DWORD Edi;
DWORD Esi;
DWORD Ebx;
DWORD Edx;
DWORD Ecx;
DWORD Eax;

DWORD Ebp;
DWORD Eip;
DWORD SegCs; // MUST BE SANITIZED
DWORD EFlags; // MUST BE SANITIZED
DWORD Esp;
DWORD SegSs;

BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];

} CONTEXT;

typedef CONTEXT *PCONTEXT;

我们如下修改:

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
#include <iostream>
#include <Windows.h>

LONG NTAPI Handle(
struct _EXCEPTION_POINTERS *ExceptionInfo
)
{
DWORD dwCode = ExceptionInfo->ExceptionRecord->ExceptionCode;
//EXCEPTION_BREAKPOINT 遇到断点(c05异常)
if (dwCode == EXCEPTION_BREAKPOINT)
{
printf("接收到断点异常");
system("pause");
//eip=eip+1即可跳过
ExceptionInfo->ContextRecord->Eip += 1;
return EXCEPTION_CONTINUE_EXECUTION;
}
}


int main()
{
AddVectoredExceptionHandler(0,
Handle
);

__asm int 3;

printf("异常已经处理");
getchar();
}

这一次只需按一次就成功处理了断点异常,如下:

上述就是结构化异常和向量化异常的简单使用。

八,TLS机制

TLS(Thread Local Storage)机制 是一种允许每个线程拥有自己的独立存储空间的机制,使得多线程应用程序中每个线程都可以拥有自己独立的数据,而不会与其他线程的数据发生冲突。TLS 使得多线程编程变得更加高效和安全,特别是在需要为每个线程存储独立状态(如线程局部变量)的情况下。

TLS 机制的概念

  • 线程局部存储(Thread Local Storage) 允许每个线程访问自己的独立数据,而这个数据对其他线程不可见。每个线程在运行时会有自己的 TLS 数据区域,用于存储线程特有的变量。
  • TLS 的目的是避免多个线程间共享同一变量时出现的竞态条件(race condition),因为每个线程都使用自己的独立存储空间。

如何实现 TLS

TLS 在 Windows 中的实现

在 Windows 操作系统中,TLS 通过一个特殊的机制来支持。Windows 提供了一些 API 来管理线程的局部存储。以下是几个重要的函数:

  • **TlsAlloc**:分配一个线程局部存储(TLS)索引,返回一个 TLS 键。
  • **TlsGetValue**:获取当前线程的 TLS 数据。
  • **TlsSetValue**:设置当前线程的 TLS 数据。
  • **TlsFree**:释放一个已分配的 TLS 键。

我们来演示如下一个示例代码:

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
#include <iostream>
#include <Windows.h>

/*
TLS机制

C库的东西都是全局的,在线程切换的时候容易被切换走。(我们前面讲过类似的问题"线程同步机制")
为了解决这个问题就发明了 TLS 线程局部描述符:
每个线程都有一个类似数组的东西,把这些全局变量存在这个数组当中,只有当前线程可以访问

然后有一些API:
TlsAlloc:申请一段内存
TlsFree:释放这段内存
TlsSet:设置内存当中的一些数据
TlsGet:获取
__beginThred

回调函数 触发机制,运行在Main函数之前 其常用于一些反调试手段

*/


/*
这是其函数指针的声明,我们直接将其拿过来进行改造
typedef VOID
(NTAPI *PIMAGE_TLS_CALLBACK) (
PVOID DllHandle,
DWORD Reason,
PVOID Reserved
);
*/
VOID NTAPI MyCallBack(
PVOID DllHandle,
DWORD Reason,
PVOID Reserved
)
{
//我们先来弹个窗:
::MessageBox(0, L"TLS CALL BACK", 0, 0);
}
//如何用到Tls的回调函数?
//1.你的先告诉连接器
#pragma comment(linker,"/INCLUDE:__tls_used") //告诉连接器linker我要使用tls了

//2.申请一个tls的段
/*
CRT C RUN TIME
X 代表告诉你等会生成的tls段名称是随机的
L 就代表着TLS CALLBACK回调函数
X 随便 B-Y之间随便选个字母
*/
#pragma data_seg(".CRT$XLX")
//这是一个数组,你就可以把这个回调函数放到这个数组当中,数组就是以0结尾结束
PIMAGE_TLS_CALLBACK pTlsArry[] = { MyCallBack , 0 };

#pragma data_seg()

int main()
{
//我们在这里打印一句话看看其是不是跑在main函数之前
printf("Hello World!\r\n");
}

我们来看看其回调函数是不是跑在main函数之前:如下

点击确定,如下:

我们发现其确实跑在main函数之前。

既然如此,那这样我们就可以做一些反调试的手脚,在此我们用到我们前面频繁遇见的一个APICheckRemoteDebuggerPresent();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//修改回调函数
VOID NTAPI MyCallBack(
PVOID DllHandle,
DWORD Reason,
PVOID Reserved
)
{

//我们先来弹个窗:
::MessageBox(0, L"TLS CALL BACK", 0, 0);
//用于检查程序的一个标志位
BOOL bRet;
CheckRemoteDebuggerPresent(GetCurrentProcess(),&bRet);
if (bRet)
{
printf("程序已经被调试了\r\n");
}
}