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版的书),没有摘抄原句,包含了很多我个人的思考和对 ...
随机推荐
- C#类中的成员
@ 目录 字段 属性 方法 构造函数 类和对象的简单解释 创建类和对象 类中成员的归属问题 字段 字段的声明与声明变量类似,可以添加访问修饰符,通常情况下字段设置为私有的,然后定义属性对字段的读写进行 ...
- 漏洞复现-Discuz-命令执行(wooyun-2010-080723)
0x00 实验环境 攻击机:win10 靶机:Ubuntu18 (docker搭建的vulhub靶场) 0x01 影响版本 Discuz 7.x 6.x版本 0x02 实验目的 学习d ...
- python3 批量处理域名解析
域名批量解析,快速确认域名的存活性及IP地址,脚本中包含了具体的用法和简要说明 #!/usr/bin/env python # -*- coding:utf-8 -*- # python3.6 fro ...
- 鸿蒙应用程序Ability(能力)看这一篇就够
本节概述 什么是Ability Ability分类 Ability生命周期 Ability之间跳转 什么是Ability Ability意为能力,是HarmonyOS应用程序提供的抽象功能.在Andr ...
- Mark一个代码量统计工具-Statistic
安装方式 IDEA.Goland系列插件市场搜索Statistic 简单说明 统计纬度比较丰富 基本覆盖常见纬度,如代码行数,文件大小等,各指标取最大最小及平均值. 统计目录为当前项目目录 只有在当前 ...
- 【odoo14】第十六章、odoo web库(OWL)
odoo14引入了名为OWL(Odoo Web Library)的JavaScript框架.OWL是以组件为基础的UI框架,通过QWeb模板作为架构.OWL与传统的组件系统相比更快,并引入了一些新的特 ...
- python学习之类的装饰器进阶版
装饰器可以修饰函数,同样,也可以修饰类 装饰器 def deco(func): print('======>被修饰的')return func 装饰器装饰函数的方式,语法糖 @decode ...
- android分析之Binder 02
分析Java层的ServiceManager,看看Binder在Java层是如何实现的. public final class ServiceManager { private static fina ...
- python-链队列的实现
7 class Node(object): 8 def __init__(self,data): 9 self.data = data 10 self.next = None 11 12 class ...
- 1-认识c指针
1.指针和内存 c程序在编译后,会以三种形式使用内存 1静态/全局内存 静态声明的变量分配在这里,全局变量也使用这部分内存.这些变量在程序开始运行时分配,直到程序终止时才会消失 2.自动内存 这些变量 ...