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 查询线程存活状态
- 通常情况下查询线程是否终止可以使用 GetExitCodeThread 函数,根据返回的第二个参数即可判断,示例如下所示,其中调用了 ErrorCodeTransformation 函数,这个函数是根据错误代码打印出详细的错误信息,方便检查出程序的错误,在进行 Windows 核心编程时应该经常使用
- 链接:https://docs.microsoft.com/zh-cn/windows/desktop/api/processthreadsapi/nf-processthreadsapi-getexitcodethread
#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);
}
CreateThread 线程操作与 _beginthreadex 线程安全(Windows核心编程)的更多相关文章
- windows核心编程---第八章 使用内核对象进行线程同步
使用内核对象进行线程同步. 前面我们介绍了用户模式下线程同步的几种方式.在用户模式下进行线程同步的最大好处就是速度非常快.因此当需要使用线程同步时用户模式下的线程同步是首选. 但是用户模式下的线程同步 ...
- 【windows核心编程】 第八章 用户模式下的线程同步
Windows核心编程 第八章 用户模式下的线程同步 1. 线程之间通信发生在以下两种情况: ① 需要让多个线程同时访问一个共享资源,同时不能破坏资源的完整性 ② 一个线程需要通知其他线程 ...
- 【windows核心编程】 第六章 线程基础
Windows核心编程 第六章 线程基础 欢迎转载 转载请注明出处:http://www.cnblogs.com/cuish/p/3145214.html 1. 线程的组成 ① 一个是线程的内核 ...
- 《windows核心编程系列》十九谈谈使用远程线程来注入DLL。
windows内的各个进程有各自的地址空间.它们相互独立互不干扰保证了系统的安全性.但是windows也为调试器或是其他工具设计了一些函数,这些函数可以让一个进程对另一个进程进行操作.虽然他们是为调试 ...
- 用户模式下的线程同步的分析(Windows核心编程)
线程同步 同一进程或者同一线程可以生成许多不同的子线程来完成规定的任务,但是多个线程同时运行的情况下可能需要对某个资源进行读写访问,比如以下这个情况:创建两个线程对同一资源进行访问,最后打印出这个资源 ...
- windows核心编程 - 线程同步机制
线程同步机制 常用的线程同步机制有很多种,主要分为用户模式和内核对象两类:其中 用户模式包括:原子操作.关键代码段 内核对象包括:时间内核对象(Event).等待定时器内核对象(WaitableTim ...
- Windows核心编程学习九:利用内核对象进行线程同步
注:源码为学习<Windows核心编程>的一些尝试,非原创.若能有助于一二访客,幸甚. 1.程序框架 #include "Queue.h" #include <t ...
- 使用同步或异步的方式完成 I/O 访问和操作(Windows核心编程)
0x01 Windows 中对文件的底层操作 Windows 为了方便开发人员操作 I/O 设备(这些设备包括套接字.管道.文件.串口.目录等),对这些设备的差异进行了隐藏,所以开发人员在使用这些设备 ...
- 《Windows核心编程》读书笔记 上
[C++]<Windows核心编程>读书笔记 这篇笔记是我在读<Windows核心编程>第5版时做的记录和总结(部分章节是第4版的书),没有摘抄原句,包含了很多我个人的思考和对 ...
随机推荐
- Hi3559AV100外接UVC/MJPEG相机实时采图设计(四):VDEC_Send_Stream线程分析
下面随笔将对Hi3559AV100外接UVC/MJPEG相机实现实时采图设计的关键点-VDEC_Send_Stream线程进行分析,一两个星期前我写了有三篇系列随笔,已经实现了项目功能,大家可以参考下 ...
- 免费报表工具 积木报表(JiMuReport)的安装
分享一b/s报表工具(服务),积木报表(JiMuReport),张代浩大佬出品. 官网:http://www.jimureport.com/ 离线版官方下载:https://github.com/zh ...
- redis一句话木马控电脑
(1)在redis管理工具内写入木马并保存: 输入命令行: config set dbfilename shell.php set shell "<?php @assert($_P ...
- Python爬虫学习一------HTTP的基本原理
昨天刚买的崔大大的<Python3网络爬虫开发实战>,今天就到了,开心的读完了爬虫基础这一章,现记录下自己的浅薄理解,如有见解不到位之处,望指出. 1.HTTP的基本原理 ①我们经常会在浏 ...
- SQL练习——LeetCode解题和总结(2)
602. Friend Requests II: Who Has the Most Friends[M] 一.表信息 In social network like Facebook or Twitte ...
- 【odoo14】第十五章、网站客户端开发
odoo的web客户端.后台是员工经常使用的地方.在第九章中,我们了解了如何使用后台提供的各种可能性.本章,我们将了解如何扩展这种可能性.其中web模块包含了我们在使用odoo中的各种交互行为. 本章 ...
- 《进击吧!Blazor!》系列入门教程 第一章 7.图表
<进击吧!Blazor!>是本人与张善友老师合作的Blazor零基础入门教程视频,此教程能让一个从未接触过Blazor的程序员掌握开发Blazor应用的能力. 视频地址:https://s ...
- Java中的Set集合
Set接口简介 Set接口和List接口一样,同样继承自Collection接口,它与Collection接口中的方法基本一致,并没有对Collection接口进行功能上的扩充,它是比Collecti ...
- HDFS设置配额的命令
1 文件个数限额 #查看配额信息 hdfs dfs -count -q -h /user/root/dir1 #设置N个限额数量,只能存放N-1个文件 hdfs dfsadmin -setQuota ...
- x64 下记事本WriteFile() API钩取
<逆向工程核心原理>第30章 记事本WriteFile() API钩取 原文是在x86下,而在x64下函数调用方式为fastcall,前4个参数保存在寄存器中.在原代码基础上进行修改: 1 ...