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; 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> LRESULT CALLBACK WinProc (HWND hwnd, UINT msg, WPARAM wParam, LPARAM lparam) {} int APIENTRY wWinMain (_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nCmdShow) { WNDCLASS wc; wc.style = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = (WNDPROC)WinProc; wc.cbClsExtra = 0 ; wc.cbWndExtra = 0 ; wc.hInstance = hInstance; wc.hIcon = LoadIcon (NULL , IDI_APPLICATION); wc.hCursor = LoadCursor (NULL , IDC_ARROW); wc.hbrBackground = (HBRUSH)GetStockObject (WHITE_BRUSH); 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> LRESULT CALLBACK WinProc (HWND hwnd, UINT msg, WPARAM wParam, LPARAM lparam) {} int APIENTRY wWinMain (_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nCmdShow) { WNDCLASS wc; wc.style = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = (WNDPROC)WinProc; wc.cbClsExtra = 0 ; wc.cbWndExtra = 0 ; wc.hInstance = hInstance; wc.hIcon = LoadIcon (NULL , IDI_APPLICATION); wc.hCursor = LoadCursor (NULL , IDC_ARROW); wc.hbrBackground = (HBRUSH)GetStockObject (WHITE_BRUSH); wc.lpszMenuName = NULL ; wc.lpszClassName = "第一个窗口" ; RegisterClass (&wc); HWND hWnd = CreateWindow ("第一个窗口" , "标题" , WS_OVERLAPPEDWINDOW, 50 , 50 , 500 , 500 , NULL , NULL , hInstance, NULL ); }
其中CreateWindow
之后我们会开辟一段数据缓冲区,存放我们的诸多信息,并且会返回一个HWND
(句柄 ),可以理解成封装好的指针。
补充:
这块数据缓冲区里面还封装的有:hInstance
也就是我们的应用程序
显示刷新窗口: 如下代码:
1 2 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 MSG msg; while (GetMessage (&msg,0 ,0 ,0 )) { TranslateMessage (&msg); DispatchMessageA (&msg); }
我们来看看msg这个结构体里面携带了些什么:ctrl+鼠标左键
:如下:
1 2 3 4 5 6 7 8 9 10 11 typedef struct tagMSG { HWND hwnd; UINT message; 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) { 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> 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) { WNDCLASS wc; wc.style = CS_HREDRAW | CS_VREDRAW; wc.lpfnWndProc = (WNDPROC)WinProc; wc.cbClsExtra = 0 ; wc.cbWndExtra = 0 ; wc.hInstance = hInstance; wc.hIcon = LoadIcon (NULL , IDI_APPLICATION); wc.hCursor = LoadCursor (NULL , IDC_ARROW); wc.hbrBackground = (HBRUSH)GetStockObject (WHITE_BRUSH); wc.lpszMenuName = NULL ; wc.lpszClassName = "第一个窗口" ; RegisterClass (&wc); HWND hWnd = CreateWindow ("第一个窗口" , "标题" , WS_OVERLAPPEDWINDOW, 50 , 50 , 500 , 500 , NULL , NULL , hInstance, NULL ); ShowWindow (hWnd,SW_SHOW); UpdateWindow (hWnd); MSG msg; while (GetMessage (&msg,0 ,0 ,0 )) { TranslateMessage (&msg); DispatchMessageA (&msg); } }
透明窗口的实现: 现在我们要实现一个透视功能:内部绘制/外部绘制
内部绘制 :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); } int APIENTRY wWinMain (_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPWSTR lpCmdLine, _In_ int nCmdShow) { WNDCLASSEX wc = {0 }; 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); 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); ShowWindow (hWnd,SW_SHOW); UpdateWindow (hWnd); MSG msg; while (GetMessage (&msg,0 ,0 ,0 )) { TranslateMessage (&msg); DispatchMessageA (&msg); } }
运行上述代码就能够直接运行开始就覆盖到扫雷游戏?
如何找到扫雷游戏的标题名and类名呢? HWND hGameHwnd = FindWindow("扫雷","扫雷")
这里我们可以使用工具——>Spy++
:如下
然后就会弹出如下窗口,再按照如下操作即可,成功识别出窗口的标题
和类
:如下:
GetMessage与PeekMessage的区别: GetMessage
和 PeekMessage
是 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); }
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) { WNDCLASSEX wc = {0 }; 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); 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); ShowWindow (hWnd,SW_SHOW); UpdateWindow (hWnd); AllocConsole (); char buf[256 ]; 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 { 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) { WNDCLASSEX wc = { 0 }; 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); 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); ShowWindow (hWnd, SW_SHOW); UpdateWindow (hWnd); MSG msg; while (1 ) { RECT GameRect; GetWindowRect (hGameHwnd, &GameRect); 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)) { if (GetForegroundWindow () == hGameHwnd) { HDC hdc = GetDC (hWnd); HBRUSH hbrush = (HBRUSH)GetStockObject (WHITE_BRUSH); HBRUSH oldBursh = (HBRUSH)SelectObject (hdc, hbrush); 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 () { HANDLE hFile = CreateFile ( "D:\\abc.ini" , GENERIC_READ | GENERIC_WRITE, NULL , NULL , 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 () { HANDLE hFile = CreateFile ( "D:\\abc.ini" , GENERIC_READ | GENERIC_WRITE, NULL , NULL , OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL ); if (INVALID_HANDLE_VALUE == hFile) { printf ("文件创建失败" ); } 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 };
这里定义了一个字符数组 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
函数执行完后,会返回一个布尔值 TRUE
或 FALSE
,表示写入操作是否成功。
最后就是如下效果:
成功写入内容。
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; GetFileSizeEx (hFile,&fileSize); 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); } }
最后运行结果,如下:
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 (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); }
运行达到如下效果:
基本的文件操作就这样。
最后别忘了来一这个将引用次数减减:
目录操作: 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 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 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 ); system ("pause" ); return 0 ; }
然后运行我们发现成功关闭了:如下
获取指定进程以及这些进程使用的堆、模块和线程的快照。(遍历进程)
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 ); PROCESSENTRY32 pe32; pe32. dwSize = sizeof (pe32); BOOL Ret = Process32First (hSnap, &pe32); while (Ret) { printf ("可执行文件路径:%s\r\nPID:%d\r\n" , pe32. szExeFile, pe32. th32ProcessID); 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 () { 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++; } return 0 ; } DWORD WINAPI ThreadMain1 ( _In_ LPVOID lpParameter ) { for (int i = 0 ; i < 1000000 ; i++) { 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 ; }
最后通过运行会发现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
原因:
那么我们该如何解决这个问题?
将这些基本运算操作全部变成原子操作:
保证这个变量的内存在一个瞬间只有一个线程去访问。
如上述代码中我们可以使用如下:
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, 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()
是 Windows API 用于等待多个同步对象(如互斥量、事件、线程等)的一种机制。它的作用是让当前线程等待一个或多个同步对象变为可用 ,可以用于协调多个线程的执行或等待多个条件满足。
函数原型:
1 2 3 4 5 6 7 DWORD WaitForMultipleObjects ( DWORD nCount, const HANDLE *lpHandles, BOOL bWaitAll, DWORD dwMilliseconds ) ;
参数:
**nCount
**:
表示要等待的对象数量,通常是句柄数组中对象的数量。
**lpHandles
**:
这是一个指向句柄数组的指针,数组中包含要等待的多个同步对象句柄。每个句柄通常是由函数(如 CreateMutex()
、CreateEvent()
等)返回的。
**bWaitAll
**:
如果为 **TRUE
**,则表示当前线程必须等待 所有 同步对象都变为可用,才会继续执行。
如果为 **FALSE
**,则表示当前线程只要等待 任意一个 同步对象变为可用,便会继续执行。
**dwMilliseconds
**:
表示等待的时间(单位:毫秒)。如果设置为 **INFINITE
**,则线程会一直等待,直到所有对象(如果 bWaitAll
为 TRUE
)或任意一个对象(如果 bWaitAll
为 FALSE
)变为可用。
返回值:
WaitForMultipleObjects()
的返回值是一个 DWORD
类型,表示等待的结果:
**WAIT_OBJECT_0
到 WAIT_OBJECT_0 + nCount - 1
**:表示对应的同步对象已变为可用,线程继续执行。
**WAIT_TIMEOUT
**:表示等待超时,线程没有在指定的时间内获得信号。
**WAIT_FAILED
**:表示发生了错误,函数调用失败。
主要作用:
WaitForMultipleObjects()
允许一个线程等待多个同步对象的状态变化。它常用于以下场景:
等待多个线程完成 :比如主线程等待多个工作线程完成。
等待多个事件或信号量 :多个事件或信号量发生时,线程才继续执行。
协调多个任务 :多个任务或资源的协调处理。
WaitForSingleObject()
:WaitForSingleObject()
是 Windows API 中用于线程同步的一个函数。它的作用是让当前线程等待一个特定的对象变为可用,通常用于等待信号量、互斥量(mutex)、事件(event)等同步对象 。其常见用法是在多线程程序中使线程等待某个资源或条件满足后再继续执行。
函数原型:
1 2 3 4 DWORD WaitForSingleObject ( HANDLE hObject, DWORD dwMilliseconds ) ;
参数 :
hObject
这是一个句柄,指向一个同步对象(如互斥量、事件、信号量等)。
该句柄通常由 CreateMutex()
、CreateEvent()
或其他同步对象创建函数返回。
dwMilliseconds
指定线程等待的时间。单位是毫秒。
如果设置为 INFINITE
,则表示线程会一直等待,直到同步对象被释放或信号变为有效。
如果指定了非 INFINITE
的值,线程会等待指定的时间后自动返回,如果在此时间内对象的状态没有改变,函数会返回超时错误。
返回值 :
主要作用 :
WaitForSingleObject()
的主要作用是 阻塞当前线程,直到指定的同步对象被信号释放或满足某个条件 。在多线程程序中,常常使用它来等待一些共享资源或条件的满足,确保线程之间按照预定的顺序执行。
补:
至于其他的信号量,事件等都和互斥量差不多,很类似就不说了。
五,静态库与动态库 静态库 场景:当我们只想让别人使用这个功能,而不想让其看到我们的源代码的时候
运行的时候它不存在,静态库的源代码会被连接到调用程序当中
步骤:
创建静态库 添加源文件 封装库函数
pragma comment(lib,"lib路径")
C++库
示例代码如下:
StaticLib1.cpp:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include "pch.h" #include "framework.h" 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> #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 #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> #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 () { HMODULE hModule = LoadLibrary ("Dll1.dll" ); pAdd add = (pAdd)GetProcAddress ( hModule, "?add@@YAHHH@Z" ); pAdd sub = (pAdd)GetProcAddress ( hModule, "?sub@@YAHHH@Z" ); printf ("add结果:%d\r\n" , add (10 , 20 )); printf ("sub结果:%d\r\n" , sub (10 , 20 )); system ("pause" ); return 0 ; }
操作过程如下:
创建一个动态链接库项目,如下:
然后动态库需要我们这么写:如下:
写完以后并生成。
然后将Dll1.dll
和Dll1.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> 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); return 0 ; }
最后运行结果如下:(这里整错了:这里是十进制的10223616)
CreateFileMapping()
:申请共享内存。
为指定文件创建或打开命名或未命名的文件映射对象。
CreateFileMapping
创建共享内存区域时,这块内存区域是由操作系统在 虚拟内存空间 中为进程A分配的。具体地说,这块共享内存并不直接位于进程A的地址空间内,而是位于操作系统的 内核空间 或者操作系统管理的 虚拟内存区域 中。
进程A中的共享内存位置:
共享内存在虚拟地址空间中的位置
在调用 CreateFileMapping
后,进程A通过 MapViewOfFile
将该共享内存映射到它的 虚拟地址空间 中。此时,进程A通过 pBuff
等指针访问的内存区域是 映射到进程A的虚拟内存地址空间中的一部分 。
共享内存的“位置”实际上是由操作系统分配给进程A的虚拟内存的一部分,不是进程A内存中的常规堆栈、堆或全局变量内存。
内存映射(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, [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> int main () { HANDLE hFileMapping = CreateFileMapping (INVALID_HANDLE_VALUE, NULL , PAGE_EXECUTE_READWRITE, 0 , 0x1000 , "共享内存" ); 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 #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> int main () { __try { int a = 0 ; int b = 0 ; int c = a / b; } __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 #include <iostream> #include <Windows.h> int main () { __try { int a = 0 ; int b = 0 ; int c = a / b; } __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 ; } __finally { 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; } __finally { 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 通过向操作系统注册向量化的异常处理程序 ,这些处理程序会在异常发生时被调用。它的工作流程大致如下:
注册异常处理程序 :开发者通过 AddVectoredExceptionHandler
函数来注册多个异常处理程序。这些处理程序会被放入一个向量表中,按照注册的顺序依次执行。
异常发生时的处理 :当异常发生时,系统会按照注册顺序查找并调用相关的异常处理程序。异常处理程序可以获取异常的详细信息,并决定是否处理异常、是否继续执行其他处理程序等。
异常类型 :VEH 能处理硬件异常、访问违规等低级别的异常,也能够与 SEH 配合使用。
如何注册和使用 VEH? 这就需要介绍这个接口了:
AddVectoredExceptionHandler()
:在 Windows 中,使用 AddVectoredExceptionHandler
函数来注册异常处理程序。该函数的原型如下:
1 2 3 4 PVOID AddVectoredExceptionHandler ( ULONG FirstHandler, PVECTORED_EXCEPTION_HANDLER 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 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> LONG NTAPI Handle ( struct _EXCEPTION_POINTERS *ExceptionInfo ) { DWORD dwCode = ExceptionInfo->ExceptionRecord->ExceptionCode; if (dwCode == EXCEPTION_BREAKPOINT) { printf ("接收到断点异常" ); system ("pause" ); return EXCEPTION_CONTINUE_EXECUTION; } } int main () { AddVectoredExceptionHandler (0 , Handle ); __asm int 3 ; printf ("异常已经处理" ); }
我们会发现它陷入了死循环,无法结束,如下:
它会一直卡在__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; DWORD EFlags; 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; if (dwCode == EXCEPTION_BREAKPOINT) { printf ("接收到断点异常" ); system ("pause" ); 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> VOID NTAPI MyCallBack ( PVOID DllHandle, DWORD Reason, PVOID Reserved ) { ::MessageBox (0 , L"TLS CALL BACK" , 0 , 0 ); } #pragma comment(linker,"/INCLUDE:__tls_used" ) #pragma data_seg(".CRT$XLX" ) PIMAGE_TLS_CALLBACK pTlsArry[] = { MyCallBack , 0 }; #pragma data_seg() int 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" ); } }