• CreateThread 线程操作与 _beginthreadex 线程安全(Windows核心编程)


    0x01 线程的创建

    • 线程不同于进程,Windows 中的进程是拥有 ‘惰性’ 的,本身并不执行任何代码,而执行代码的任务转交给主线程,列如使用 CreateProcess 创建一个进程打开 Cmd 程序,实际上是这个进程的主线程去执行打开 Cmd 程序的任务,也就是说创建一个进程必然有一个主线程与之对应
    • 当然 Windows 下也可以也使用 CreateThread 创建属于当前进程或者线程的额外线程,返回值是一个线程句柄,示例程序如下图所示
    #include <Windows.h>
    #include <iostream>
    #include <stdio.h>
    #include <process.h>
    using namespace std;
    
    // 用于线程调用的函数
    DWORD WINAPI ThreadFun1(PVOID pvParam);
    
    int main(int argc, char *argv[])
    {
    	DWORD ThreadId = NULL;
    	HANDLE MyThread1 = CreateThread(
    		NULL,					// 第一个参数表示传入的用于设置安全标志的结构体对象
    		NULL,					// 第二个参数用于设置线程运行时申请多大的堆栈空间
    		ThreadFun1,				// 第三个参数是线程调用的函数名称
    		NULL,					// 第四个参数是传递给调用函数的参数
    		NULL,					// 第五个参数是设置如何执行线程
    		&ThreadId				// 第六个参数返回线程 ID
    	);
    	// 等待线程
    	WaitForSingleObject(MyThread1, INFINITE);
    	return 0;
    }
    
    DWORD WINAPI ThreadFun1(PVOID pvParam)
    {
    	cout << "ThreadFun1 is Start" << endl;
    	return 0;
    }
    
    • 值得注意的是 CreateThread 的第二个参数会设置线程运行时堆栈的大小(因为每一个线程都有独立的空间供线程使用),如果无需特殊设置,可以将第二个参数设置为 NULL,表示默认分配 1MB 大小的堆栈空间,如果在运行过程中超出了 1MB 大小,系统也会分配额外的空间,所以不必担心内存泄漏的问题
    • 第三个参数就是线程的调用函数,且函数的类型必须是 DWORD WINAPI ThreadFun1(PVOID pvParam),WINAPI 就等于 __stdcall,为什么一定要以这种方式调用呢,原因是 CreateThread 内部维护了一个函数指针指向了 ThreadFun1
    DWORD (__stdcall * PTHREAD_START_ROUTINE) (LPVOID pvParam)
    

    提示:__stdcall 是函数的一种调用方式,用于规定函数调用前后如何平衡堆栈,大多数的 Windows 内核函数都会使用 __stdcall,也就是 WINAPI

    • 第五个参数表示如何执行线程,如果设置为 NULL,则立刻执行
    	DWORD ThreadId = NULL;
    	HANDLE ThreadId = CreateThread(NULL, NULL, ThreadFun1, NULL, NULL, &ThreadId);
    
    • 如果设置为 CREATE_SUSPENDED,表示先将线程挂起,之后调用 ResumeThread 函数执行,这也是挂起线程的通常做法
    	DWORD ThreadId = NULL;
    	HANDLE ThreadId = CreateThread(NULL, NULL, ThreadFun1, NULL, CREATE_SUSPENDED, &ThreadId);
    	ResumeThread(MyThread1);
    

    提示:CREATE_SUSPENDED 的最终目的就是将线程的内核计数 +1,而 ResumeThread 则是将线程的内核计数 -1,这样的话就相当于线程挂起后执行。也可以多次挂起,再多次调用 ResumeThread

    • 至于为什么在最后调用 WaitForSingleObject 来等待,是因为线程不同于进程,虽然说线程有独立的空间,但是线程的所有空间都是在进程地址空间上分配的,属于相对独立而已。如果线程在 main 函数之前没有执行完那么线程的所有空间都会被回收,相当于判处线程死刑且立即执行,而用 WaitForSingleObject 的好处是保证线程能够完整的执行后再从 main 函数返回

    0x02 线程终止

    • 假如正在运行的线程由于 IO 操作时间过长或者由于某种未知原因导致死锁,那么可以选择将线程终止,一般不提倡这种做法,因为会导致内存泄漏,比如线程中的类构造函数动态申请了堆空间,但是由于终止了线程导致类析构函数无法执行,这样堆空间得不到及时释放,给后面的利用埋下了安全隐患
    • 终止线程非常简单,通常利用两个函数即可完成,ExitThread 和 TerminateThread 函数,两者的不同在于 ExitThread 只可以终止本线程(除了返回错误代码以外没有其他参数),而 TerminateThread 可以终止任何线程
    #include <Windows.h>
    #include <iostream>
    #include <stdio.h>
    #include <process.h>
    using namespace std;
    
    // 用于线程调用的函数
    DWORD WINAPI ThreadFun1(PVOID pvParam);
    
    int main(int argc, char *argv[])
    {
    	DWORD ThreadId = NULL;
    	HANDLE MyThread1 = CreateThread(NULL, NULL, ThreadFun1, NULL, CREATE_SUSPENDED, &ThreadId);
    	ResumeThread(MyThread1);
    	// 终止线程
    	TerminateThread(MyThread1, NULL);
    	// 等待线程
    	WaitForSingleObject(MyThread1, INFINITE);
    	return 0;
    }
    
    DWORD WINAPI ThreadFun1(PVOID pvParam)
    {
    	// 终止线程
    	ExitThread(NULL); // 示例程序而已
    	cout << "ThreadFun1 is Start" << endl;
    	return 0;
    }
    
    • 所以为了保证程序发生不必要的 Bug,尽量不要使用这两个函数终止线程

    0x03 查询线程存活状态

    #include <Windows.h>
    #include <iostream>
    #include <stdio.h>
    #include <process.h>
    #include <strsafe.h>
    using namespace std;
    
    DWORD WINAPI ThreadFun1(PVOID pvParam);
    DWORD WINAPI ThreadFun2(PVOID pvParam);
    // 全局句柄,用于线程间共享
    HANDLE MyThread1; HANDLE MyThread2; 
    // 全局变量,表示某一个线程的运行状态
    DWORD WaitFile;
    // 将错误代码打印为错误信息,这个函数非常有用
    VOID WINAPI ErrorCodeTransformation(DWORD ErrorCode);
    
    int main(int argc, char *argv[])
    {
    	// 创建两个线程
    	MyThread1 = CreateThread(NULL, NULL, ThreadFun1, NULL, NULL, NULL);
    	MyThread2 = CreateThread(NULL, NULL, ThreadFun2, NULL, NULL, NULL);
    	// 等待两个线程执行完毕
    	HANDLE Threads[2] = { 0 };
    	Threads[0] = MyThread1; Threads[1] = MyThread2;
    	WaitForMultipleObjects(2, Threads, TRUE, INFINITE);
    	// 关闭句柄
    	CloseHandle(MyThread1); CloseHandle(MyThread2);
    	return 0;
    }
    
    DWORD WINAPI ThreadFun1(PVOID pvParam)
    {
    	while (true)
    	{
    		// 每隔 200 毫秒,调用 GetExitCodeThread 显示函数运行状态
    		Sleep(200);
    		if (GetExitCodeThread(MyThread2, &WaitFile))
    		{
    			// STILL_ACTIVE 表示线程尚未终止
    			if (WaitFile == STILL_ACTIVE)
    			{
    				cout << "程序尚未终止" << endl;
    			}
    			else
    			{
    				// 进程终止就结束 while 循环
    				cout << "线程已经终止" << endl;
    				break;
    			}
    		}
    		else
    		{
    			// GetExitCodeThread 调用失败就打印具体错误信息
    			DWORD res = GetLastError();
    			ErrorCodeTransformation(res);
    		}
    	}
    	return TRUE;
    }
    DWORD WINAPI ThreadFun2(PVOID pvParam)
    {
    	// 随眠 3 秒
    	Sleep(3000);
    	return TRUE;
    }
    
    // 如果返回错误,可调用此函数打印详细错误信息
    VOID WINAPI ErrorCodeTransformation(DWORD ErrorCode)
    {
    	LPVOID lpMsgBuf; LPVOID lpDisplayBuf; DWORD dw = ErrorCode;
    	// 将错误代码转换为错误信息
    	FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
    		NULL, dw, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPTSTR)&lpMsgBuf, 0, NULL
    	);
    	lpDisplayBuf = (LPVOID)LocalAlloc(LMEM_ZEROINIT, (lstrlen((LPCTSTR)lpMsgBuf) + 40) * sizeof(TCHAR));
    	StringCchPrintf((LPTSTR)lpDisplayBuf, LocalSize(lpDisplayBuf), TEXT("错误代码 %d :  %s"), dw, lpMsgBuf);
    	// 弹窗显示错误信息
    	MessageBox(NULL, (LPCTSTR)lpDisplayBuf, TEXT("Error"), MB_OK);
    	LocalFree(lpMsgBuf); LocalFree(lpDisplayBuf); ExitProcess(dw);
    }
    
    
    • 运行起来的效果就像这样
      在这里插入图片描述

    0x03 关于线程安全

    • 因为 C 运行库函数最初不是为了多线程设计的,所以在使用一些 C 运行库全局变量时应该注意任何线程都可以修改全局变量(比如 errno),在单线程情况下肯定没有问题,但是多线程就会出现混乱,比如一个线程前脚刚设置了 errno 准备查看,后脚就被另外一个线程改了
    • 这个就是 C/C++ 线程安全的由来,解决的方法就是为每个线程都分配一个独立的 C 运行库全局变量空间,当然这么复杂的工作并不需要我们来做,使用线程安全函数 _beginthreadex 就可以,真的是很方便,这个函数在内部会自动分配 C 运行库全局变量,分配完之后再调用 CreateThread 创建线程,所以以后创建线程只需要用 _beginthreadex 就足够了
    • 示例如下,包含的头文件为 process.h,除了参数类型不一样,其他的包括参数类别和参数顺序与使用 CreateThread 是一模一样的,美中不足的是将线程调用函数的返回值由 DWORD 变成了 unsigned
    #include <Windows.h>
    #include <iostream>
    #include <process.h>
    using namespace std;
    
    unsigned WINAPI ThreadFun1(PVOID pvParam);
    
    int main(int argc, char *argv[])
    {
    	// 用于接收线程的 ID
    	DWORD ThreadId;
    	HANDLE MyThread = (HANDLE)_beginthreadex(NULL, NULL, ThreadFun1, NULL, CREATE_SUSPENDED, (unsigned *)&ThreadId);
    	ResumeThread(MyThread);
    		
    	//_endthreadex(); // 如果需要终止线程使用 _endthreadex 即可,该函数内部会释放申请的 C 运行库全局变量空间
    	
    	// 等待线程执行完毕
    	WaitForSingleObject(MyThread, INFINITE);
    	// 关闭句柄
    	CloseHandle(MyThread);
    	return 0;
    }
    
    unsigned WINAPI ThreadFun1(PVOID pvParam)
    {
    	cout << "ThreadFun1 is start" << endl;
    	return TRUE;
    }
    
    • 其实 _beginthreadex 和 _endthreadex 函数还有一个相对简单的版本叫 _beginthread 和 _endthread;区别是 _beginthread 函数不可以返回线程 ID,也不可以设置安全标志(如继承等),_endthread 不可以返回线程退出代码,总之差别很大,示例如下
    #include <Windows.h>
    #include <iostream>
    #include <process.h>
    using namespace std;
    
    void __cdecl ThreadFun1(PVOID pvParam);
    
    int main(int argc, char *argv[])
    {
    	// 用于接收线程的 ID
    	DWORD ThreadId;
    	HANDLE MyThread = (HANDLE)_beginthread(ThreadFun1, NULL, NULL);
    	ResumeThread(MyThread);
    		
    	//_endthread(); // 如果需要终止线程使用 _endthread 即可
    	
    	// 等待线程执行完毕
    	WaitForSingleObject(MyThread, INFINITE);
    	return 0;
    }
    
    void __cdecl ThreadFun1(PVOID pvParam)
    {
    	cout << "ThreadFun1 is start" << endl;
    }
    
    
    • 除此以外,还有一个鲜为人知的问题,就是调用完 _beginthread 之后会释放线程句柄(MyThread),也就是说创建的线程句柄在线程执行完毕之后就不可以使用了,如果再调用 CloseHandle 函数关闭句柄的话就会引发异常,如下所示
    #include <Windows.h>
    #include <iostream>
    #include <process.h>
    using namespace std;
    
    void __cdecl ThreadFun1(PVOID pvParam);
    
    int main(int argc, char *argv[])
    {
    	DWORD ThreadId;
    	HANDLE MyThread = (HANDLE)_beginthread(ThreadFun1, NULL, NULL);
    	ResumeThread(MyThread);
    
    	WaitForSingleObject(MyThread, INFINITE);
    	// 关闭了已经关闭了的句柄 MyThread
    	CloseHandle(MyThread);
    	return 0;
    }
    
    void __cdecl ThreadFun1(PVOID pvParam)
    {
    	cout << "ThreadFun1 is start" << endl;
    }
    

    在这里插入图片描述

    • 这样做的好处是方便了调用者(菜鸟们不需要再关闭句柄及使用其他复杂的操作),坏处是对线程(句柄)的控制能力降低了

    0x04 了解线程

    • 不论是对于进程还是线程,对其句柄的操作都非常重要,获取句柄也是家常便饭,微软为了方便获取句柄,提供了 GetCurrentProcess 和 GetCurrentThread 这两个函数来获取进程和线程的句柄(两个函数没有任何的参数),只不过获取的是伪句柄,并非正真的句柄
    • 有如下示例,GetProcessTimes 和 GetThreadTimes 的第一个参数都可以传伪句柄
    #include <Windows.h>
    #include <iostream>
    #include <process.h>
    using namespace std;
    
    unsigned WINAPI ThreadFun1(PVOID pvParam);
    
    int main(int argc, char *argv[])
    {
    	// 获取当前进程计时信息
    	FILETIME ftCreationTime, ftExitTime, ftKernelTime, ftUserTime;
    	// 这里就可以用伪句柄代替真正的句柄
    	GetProcessTimes(GetCurrentProcess(), &ftCreationTime, &ftExitTime, &ftKernelTime, &ftUserTime);
    
    	// 创建一个新的线程,查询线程计时信息
    	DWORD ThreadId;
    	HANDLE MyThread = (HANDLE)_beginthreadex(NULL, NULL, ThreadFun1, NULL, NULL, (unsigned *)&ThreadId);
    	
    	WaitForSingleObject(MyThread, INFINITE);
    	CloseHandle(MyThread);
    	return 0;
    }
    
    unsigned WINAPI ThreadFun1(PVOID pvParam)
    {
    	// 获取当前线程及时信息
    	FILETIME ftCreationTime, ftExitTime, ftKernelTime, ftUserTime;
    	// 传递的是伪句柄
    	GetThreadTimes(GetCurrentThread(), &ftCreationTime, &ftExitTime, &ftKernelTime, &ftUserTime);
    	return TRUE;
    }
    
    • 某些函数可以传递伪句柄,但如果必须使用真正的句柄,或者某些情况下无法获得真正的句柄怎么办呢,这里可以使用 DuplicateHandle 函数来将伪句柄转换为真正的句柄;DuplicateHandle 函数真正的作用是进程间的句柄复制,句柄转换只是 DuplicateHandle 函数的一个功能而已,而且知道的人并不多
    • 示例如下
    #include <Windows.h>
    #include <iostream>
    #include <stdio.h>
    using namespace std;
    
    int main(int argc, char *argv[])
    {
    	HANDLE DupProcessHandle = NULL;
    	BOOL res = DuplicateHandle(
    		GetCurrentProcess(),	// 复制句柄的进程,这里是当前进程
    		GetCurrentProcess(),	// 复制的句柄,这里复制当前进程伪句柄
    		GetCurrentProcess(),	// 复制到哪一个进程,这里复制到当前进程
    		&DupProcessHandle,		// 将复制的句柄传递给一个 HANDLE 变量,如果第二个参数传递的是伪句柄,那么这个函数会把它转换成真实的句柄
    		0, FALSE, DUPLICATE_SAME_ACCESS
    	);
    	// 由于只是把当前进程的伪句柄复制到当前进程,所以只是使用了 DupProcessHandle 函数转换伪句柄的功能,并没有用进程间复制句柄的功能
    	if (res)
    	{
    		cout << "[*] 当前进程的真实句柄为: " << DupProcessHandle    << endl;
    		cout << "[*] 当前进程的伪造句柄为: " << GetCurrentProcess() << endl;
    	}
    	return(0);
    }
    
    
  • 相关阅读:
    [ Docker ] 基础安装使用及架构
    [ Docker ] 基础概念
    Nginx
    ELK
    SVN + Jenkins 构建自动部署
    Zabbix
    ELK
    ELK 部署文档
    vue.js在visual studio 2017下的安装
    vue.js是什么
  • 原文地址:https://www.cnblogs.com/csnd/p/11800521.html
Copyright © 2020-2023  润新知