关于二进制安全技能树
【二进制安全学习技能树】
关于多线程
5.【Windows API程序设计】为什么使用多线程?
关于线程和进程函数
4.【Windows API程序设计】线程与进程的相关API函数
在学习了这么半个月二进制,感觉基础知识很枯燥但是很重要,虽然我们最后的目的不是写出一个操作系统或者一个编译器,我们可以很严格要求自己,总之,不想在某个关键时刻忘记某个参数或者某个函数。
1x0 线程的通信
三种方式:
- 全局变量
- 自定义消息
- 事件对象
1x1 全局变量
在Counter()例子中就有用到全局变量g_nOption,在多线程的解释时,有提到这个通信方式,没有看过的可以看一下5.【Windows API程序设计】为什么使用多线程?
弊端:多个工作线程使用同一个全局变量时,由于每个工作线程都可以修改全局变量,可能会引起同步问题。在后文会探讨这个问题。
1x2 自定义消息
-
主线程向工作线程发送自定义消息
如果主线程要向工作线程发送自定义消息,那么工作线程就要维护一个消息循环,如果工作线程创建了窗口,那么就要有一个窗口过程,但是这违背了多线程设计原则。即主线程负责用户界面,工作线程负责耗时的后台处理。如果工作线程开始处理了用户窗口,那么使用多线程就失去了原本设计的意义。
-
工作线程向主线程发送自定义消息
工作线程向主线程发送自定义消息,比较简单,调用
SendMessage
或PostMessage
函数即可。
一个例子:
#include <windows.h>
#include "resource.h"
#pragma comment(linker,"\"/manifestdependency:type='win32' \
name='Microsoft.Windows.Common-Controls' version='6.0.0.0' \
processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")
// 自定义消息,用于计数线程向显示线程发送消息报告工作进度(这两个都是工作线程)
#define WM_WORKPROGRESS (WM_APP + 1)
// 自定义消息,计数线程发送消息给主线程告知工作已完成
#define WM_CALCOVER (WM_APP + 2)
// 全局变量
HWND g_hwndDlg;
BOOL g_bRuning; // 计数线程没有消息循环,主线程通过把这个标志设为FALSE通知其终止线程
// 函数声明
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
// 线程函数声明
DWORD WINAPI ThreadProcShow(LPVOID lpParameter); // 把数值显示到编辑控件中
DWORD WINAPI ThreadProcCalc(LPVOID lpParameter); // 模拟执行一项任务,定时把一个数加1
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_MAIN), NULL, DialogProc, NULL);
return 0;
}
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
static HANDLE hThreadShow, hThreadCalc;
static DWORD dwThreadIdShow;
switch (uMsg)
{
case WM_INITDIALOG:
g_hwndDlg = hwndDlg;
// 禁用停止按钮
EnableWindow(GetDlgItem(hwndDlg, IDC_BTN_STOP), FALSE);
return TRUE;
case WM_COMMAND:
switch (LOWORD(wParam))
{
case IDC_BTN_START:
g_bRuning = TRUE;
// 创建显示线程和计数线程
hThreadShow = CreateThread(NULL, 0, ThreadProcShow, NULL, 0, &dwThreadIdShow);
hThreadCalc = CreateThread(NULL, 0, ThreadProcCalc, (LPVOID)dwThreadIdShow, 0, NULL);
EnableWindow(GetDlgItem(hwndDlg, IDC_BTN_START), FALSE);
EnableWindow(GetDlgItem(hwndDlg, IDC_BTN_STOP), TRUE);
break;
case IDC_BTN_STOP:
// 通知计数线程退出
g_bRuning = FALSE;
// 通知显示线程退出
PostThreadMessage(dwThreadIdShow, WM_QUIT, 0, 0);
if (hThreadShow != NULL)
{
CloseHandle(hThreadShow);
hThreadShow = NULL;
}
if (hThreadCalc != NULL)
{
CloseHandle(hThreadCalc);
hThreadCalc = NULL;
}
EnableWindow(GetDlgItem(hwndDlg, IDC_BTN_START), TRUE);
EnableWindow(GetDlgItem(hwndDlg, IDC_BTN_STOP), FALSE);
break;
case IDCANCEL:
EndDialog(hwndDlg, 0);
break;
}
return TRUE;
case WM_CALCOVER:
if (hThreadShow != NULL)
{
CloseHandle(hThreadShow);
hThreadShow = NULL;
}
if (hThreadCalc != NULL)
{
CloseHandle(hThreadCalc);
hThreadCalc = NULL;
}
EnableWindow(GetDlgItem(hwndDlg, IDC_BTN_START), TRUE);
EnableWindow(GetDlgItem(hwndDlg, IDC_BTN_STOP), FALSE);
MessageBox(hwndDlg, TEXT("计数线程工作已完成"), TEXT("提示"), MB_OK);
return TRUE;
}
return FALSE;
}
DWORD WINAPI ThreadProcShow(LPVOID lpParameter)
{
MSG msg;
while (GetMessage(&msg, NULL, 0, 0) != 0)
{
switch (msg.message)
{
case WM_WORKPROGRESS:
SetDlgItemInt(g_hwndDlg, IDC_EDIT_COUNT, (UINT)msg.wParam, FALSE);
break;
}
}
return msg.wParam;
}
DWORD WINAPI ThreadProcCalc(LPVOID lpParameter)
{
// lpParameter参数是传递过来的显示线程ID
DWORD dwThreadIdShow = (DWORD)lpParameter;
int nCount = 0;
while (g_bRuning)
{
PostThreadMessage(dwThreadIdShow, WM_WORKPROGRESS, nCount++, NULL);
Sleep(50);
// nCount到达100,说明工作完成
if (nCount > 100)
{
// 通知显示线程退出
PostThreadMessage(dwThreadIdShow, WM_QUIT, 0, 0);
// 发送消息给主线程告知工作已完成
PostMessage(g_hwndDlg, WM_CALCOVER, 0, 0);
// 本计数线程也退出
g_bRuning = FALSE;
break;
}
}
return 0;
}
运行结果:
程序说明:
CustomMSG程序有“开始”和“停止”两个按钮。
用户按下“开始”按钮,创建显示线程和计数线程,计数线程模拟执行一项任务,每50ms计数加1。
创建计数线程时需要将显示线程的ID作为线程函数参数,以便计数线程定时通过 PostThreadMessage 函数向显示线程发送自定义消息WM_WORKPROGRESS报告工作进度,显示线程获取到WM_WORKPROGRESS消息后将工作进度显示在程序的编辑控件中。
如果计数线程的计数已经达到100,则说明工作已经完成,向显示线程发送WM_QUIT 消息通知其终止线程,向主线程发送自定义消息 WM_CALCOVER 告知工作已完成,主线程获取到WM_CALCOVER消息后会关闭两个线程句柄,启用/禁用相关按钮,然后显示一个消息框。
在计数线程工作过程中,用户随时可以按下“停止”按钮,主线程将全局变量g_bRuning设置为FALSE告知计数线程终止线程,调用PostThreadMessage函数向显示线程发送WM_QUIT消息告知其终止线程,然后关闭两个线程句柄,启用/禁用相关按钮。
1x3 事件对象
先看一个例子,也就是Couter程序里的那个线程函数。
如下所示:
DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
int n = 0;
while (!(g_nOption & F_STOP))
{
if (g_nOption & F_START)
SetDlgItemInt(g_hwndDlg, IDC_EDIT_COUNT, n++, FALSE);
}
return 0;
}
当调用这个线程函数之后,会进入while
循环,为了判断条件,需要时刻检测F_STOP
标志的值,以确保程序及时终止,但是这样会使CPU开销越来越大,所以需要一个解决方案。
在学习事件对象之前,可以使用SuspendThread
函数和ResumeThread
函数。既然工作线程不知道停止以节省CPU开销,那么就让系统介入,让系统主动挂起工作线程。
优点:可以节省CPU开销。
缺点:主线程并不知道工作线程应该在哪里停止,如果此时工作线程在取堆栈的值,挂起线程之后,堆栈将被锁定,而其他需要调用堆栈数据的线程就在等待状态,从而导致死锁。
而事件对象可以解决以上问题。
事件对象创建函数:
HANDLE WINAPI CreateEvent(
_In_opt_ LPSECURITY_ATTRIBUTES lpEventAttributes,// 指向事件对象安全属性结构的指针
_In_ BOOL bManualReset, // 手动重置还是自动重置,TRUE或FALSE
_In_ BOOL bInitialState, // 事件对象的初始状态,TRUE或FALSE
_In_opt_ LPCTSTR lpName); // 事件对象的名称字符串,区分大小写
事件对象是内核对象,内核对象受系统管理,供同一个进程的任何线程进行使用。但是另一个进程的线程却不能使用,调用创建内核对象的函数后,会返回一个句柄值,这个句柄值与进程相关联。
-
如何允许其他进程使用这个内核对象呢?
使用CreateEvent函数或OpenEvent函数并指定"lpName"参数即可。例如:创建一个命名事件对象"MyEventObject",那么其他进程调用
hEvent = CreateEvent(NULL, TRUE, FALSE, TEXT("MyEventObject"));
即可使用此命名事件对象,其获得的句柄值不一定与其他进程的相同,但是指向同一个对象。此时调用GetLastError会返回ERROR_ALREADY_EXEISTS。
如果其他内核对象已经被命名为"MyEventObject",调用GetLastError就会返回ERROR_INVALID_HANDLE,即无效的句柄值。
-
事件对象的有无信号是什么意思?
有信号:事件已经发生或条件满足,所有等待该事件的线程会被力立即唤醒。调用SetEvent(hEvent)
函数将事件状态设置为有信号。
无信号:事件还未发生或条件不满足,所有等待该事件的线程被阻塞(挂起)。调用ResetEvent(hEvent)
函数将事件状态设置为无信号。 -
事件对象的手动重置和自动重置?
手动重置:有信号状态,唤醒所有等待线程,保持有信号状态,直到手动重置(需调用ResetEvent)。
自动重置:有信号状态,唤醒一个等待线程,并自动重置为无信号。 -
如何检查事件状态?
调用WaitForSingleObject(hEvent,INFINITE)
,此函数只能检测一个事件对象,如果要同时检测多个对象,要使用WaitForSingleObjects
,用法网上自查,这里不再赘述。
若有信号,线程继续执行,若无信号,线程挂起等待。
INFINITE(0xFFFFFFFF秒)指函数一直等待直到指定的对象变为有信号状态才返回,但是这个秒数可以自己把握。
在调用事件对象后,其引用计数会增加1,所以在使用完事件对象后,要调用CloseHandle
函数来关闭句柄。
有了事件对象对线程的控制,那样就可以轻松解决CPU开销高居不下的问题了。
改良完的Counter.cpp
#include <windows.h>
#include "resource.h"
#pragma comment(linker,"\"/manifestdependency:type='win32' \
name='Microsoft.Windows.Common-Controls' version='6.0.0.0' \
processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")
// 全局变量
HWND g_hwndDlg;
HANDLE g_hEventStart; // 事件对象句柄,作为开始标志
HANDLE g_hEventStop; // 事件对象句柄,作为停止标志
// 函数声明
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
DWORD WINAPI ThreadProc(LPVOID lpParameter);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_MAIN), NULL, DialogProc, NULL);
return 0;
}
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
static HWND hwndBtnStart, hwndBtnStop, hwndBtnPause, hwndBtnContinue;
HANDLE hThread = NULL;
switch (uMsg)
{
case WM_INITDIALOG:
g_hwndDlg = hwndDlg;
hwndBtnStart = GetDlgItem(hwndDlg, IDC_BTN_START);
hwndBtnStop = GetDlgItem(hwndDlg, IDC_BTN_STOP);
hwndBtnPause = GetDlgItem(hwndDlg, IDC_BTN_PAUSE);
hwndBtnContinue = GetDlgItem(hwndDlg, IDC_BTN_CONTINUE);
// 禁用停止、暂停、继续按钮
EnableWindow(hwndBtnStop, FALSE);
EnableWindow(hwndBtnPause, FALSE);
EnableWindow(hwndBtnContinue, FALSE);
// 创建事件对象
g_hEventStart = CreateEvent(NULL, TRUE, FALSE, NULL);
g_hEventStop = CreateEvent(NULL, TRUE, FALSE, NULL);
return TRUE;
case WM_COMMAND:
switch (LOWORD(wParam))
{
case IDC_BTN_START:
hThread = CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
if (hThread != NULL)
{
CloseHandle(hThread);
hThread = NULL;
}
SetEvent(g_hEventStart); // 设置开始标志
ResetEvent(g_hEventStop); // 清除停止标志
EnableWindow(hwndBtnStart, FALSE);
EnableWindow(hwndBtnStop, TRUE);
EnableWindow(hwndBtnPause, TRUE);
break;
case IDC_BTN_STOP:
SetEvent(g_hEventStop); // 设置停止标志
EnableWindow(hwndBtnStart, TRUE);
EnableWindow(hwndBtnStop, FALSE);
EnableWindow(hwndBtnPause, FALSE);
EnableWindow(hwndBtnContinue, FALSE);
break;
case IDC_BTN_PAUSE:
ResetEvent(g_hEventStart); // 清除开始标志
EnableWindow(hwndBtnStart, FALSE);
EnableWindow(hwndBtnStop, TRUE);
EnableWindow(hwndBtnPause, FALSE);
EnableWindow(hwndBtnContinue, TRUE);
break;
case IDC_BTN_CONTINUE:
SetEvent(g_hEventStart); // 设置开始标志
EnableWindow(hwndBtnStart, FALSE);
EnableWindow(hwndBtnStop, TRUE);
EnableWindow(hwndBtnPause, TRUE);
EnableWindow(hwndBtnContinue, FALSE);
break;
case IDCANCEL:
// 关闭事件对象句柄
CloseHandle(g_hEventStart);
CloseHandle(g_hEventStop);
EndDialog(hwndDlg, 0);
break;
}
return TRUE;
}
return FALSE;
}
DWORD WINAPI ThreadProc(LPVOID lpParameter)
{
int n = 0;
while (WaitForSingleObject(g_hEventStop, 0) != WAIT_OBJECT_0) // 是否设置了停止标志
{
if (WaitForSingleObject(g_hEventStart, 100) == WAIT_OBJECT_0) // 是否设置了开始标志
SetDlgItemInt(g_hwndDlg, IDC_EDIT_COUNT, n++, FALSE);
}
return 0;
}
运行之后会发现当按下"开始"时,CPU占用率不断增加,当按下"暂停"时,CPU占用率直线下降。
1x4 总结
在线程之间的通信中,有三个主要的通信方法,分别是全局变量、自定义消息和事件对象。
-
全局变量的优点是一次定义,全局使用,但缺点却很明显,所有线程均可修改,容易出现问题。
-
自定义消息最常用于子线程向主线程通信,但是也有局限性。
-
事件对象很好解决了前面两个的问题,只是频繁调用函数和内核对象,对于引用计数的部分要格外关注,通常
CreateEvent
函数和CloseHandle
一起使用。