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);
}

CreateThread 线程操作与 _beginthreadex 线程安全(Windows核心编程)的更多相关文章

  1. windows核心编程---第八章 使用内核对象进行线程同步

    使用内核对象进行线程同步. 前面我们介绍了用户模式下线程同步的几种方式.在用户模式下进行线程同步的最大好处就是速度非常快.因此当需要使用线程同步时用户模式下的线程同步是首选. 但是用户模式下的线程同步 ...

  2. 【windows核心编程】 第八章 用户模式下的线程同步

    Windows核心编程 第八章 用户模式下的线程同步 1. 线程之间通信发生在以下两种情况: ①    需要让多个线程同时访问一个共享资源,同时不能破坏资源的完整性 ②    一个线程需要通知其他线程 ...

  3. 【windows核心编程】 第六章 线程基础

    Windows核心编程 第六章 线程基础 欢迎转载 转载请注明出处:http://www.cnblogs.com/cuish/p/3145214.html 1. 线程的组成 ①    一个是线程的内核 ...

  4. 《windows核心编程系列》十九谈谈使用远程线程来注入DLL。

    windows内的各个进程有各自的地址空间.它们相互独立互不干扰保证了系统的安全性.但是windows也为调试器或是其他工具设计了一些函数,这些函数可以让一个进程对另一个进程进行操作.虽然他们是为调试 ...

  5. 用户模式下的线程同步的分析(Windows核心编程)

    线程同步 同一进程或者同一线程可以生成许多不同的子线程来完成规定的任务,但是多个线程同时运行的情况下可能需要对某个资源进行读写访问,比如以下这个情况:创建两个线程对同一资源进行访问,最后打印出这个资源 ...

  6. windows核心编程 - 线程同步机制

    线程同步机制 常用的线程同步机制有很多种,主要分为用户模式和内核对象两类:其中 用户模式包括:原子操作.关键代码段 内核对象包括:时间内核对象(Event).等待定时器内核对象(WaitableTim ...

  7. Windows核心编程学习九:利用内核对象进行线程同步

    注:源码为学习<Windows核心编程>的一些尝试,非原创.若能有助于一二访客,幸甚. 1.程序框架 #include "Queue.h" #include <t ...

  8. 使用同步或异步的方式完成 I/O 访问和操作(Windows核心编程)

    0x01 Windows 中对文件的底层操作 Windows 为了方便开发人员操作 I/O 设备(这些设备包括套接字.管道.文件.串口.目录等),对这些设备的差异进行了隐藏,所以开发人员在使用这些设备 ...

  9. 《Windows核心编程》读书笔记 上

    [C++]<Windows核心编程>读书笔记 这篇笔记是我在读<Windows核心编程>第5版时做的记录和总结(部分章节是第4版的书),没有摘抄原句,包含了很多我个人的思考和对 ...

随机推荐

  1. ASP.NET Core扩展库

    亲爱的.Neter们,在我们日复一日的编码过程中是不是会遇到一些让人烦恼的事情: 日志配置太过复杂,各种模板.参数也搞不清楚,每次都要去查看日志库的文档,还需要复制粘贴一些重复代码,好无赖 当需要类型 ...

  2. redis使用ssh密钥远控靶机

      首先说明一下我们的实验目的,我们这个实验需要利用一种公有密码,将公有密钥写入要攻击的服务器的redis数据库,然后使用我们自己的私钥进行远控肉鸡的操作. 实验环境:centos7(靶机,版本无太大 ...

  3. Python基础(1)——变量和数据类型[xiaoshun]

    目录 一.变量 1.概述 Variables are used to store information to be referenced(引用)and manipulated(操作) in a co ...

  4. Docker SDK api操作Docker

    下载包 go get "github.com/docker/docker/api/types" go get "github.com/docker/docker/clie ...

  5. Ignatius and the Princess III HDU - 1028

    题目传送门:https://vjudge.net/problem/HDU-1028 思路:整数拆分构造母函数的模板题 1 //#include<bits/stdc++.h> 2 #incl ...

  6. Git本地仓库基本操作-1

    code[class*="language-"], pre[class*="language-"] { color: rgba(51, 51, 51, 1); ...

  7. 自动QQ邮箱发送邮件

    语言:python 参考:https://www.runoob.com/python/python-email.html 前提: 1.QQ邮箱开启了SMTP服务 2.生成了授权码,这个授权码将作为自己 ...

  8. 99%的Python用户都不知道的f-string隐秘技巧

    f-string想必很多Python用户都基础性的使用过,作为Python3.6版本开始引入的特性,通过它我们可以更加方便地向字符串中嵌入自定义内容,但f-string真正蕴含的功能远比大多数用户知道 ...

  9. Install Tensorflow object detection API in Anaconda (Windows)

    This blog is to explain how to install Tensorflow object detection API in Anaconda in Windows 10 as ...

  10. TP6学习笔记一:安装与基本配置

    1 说明与概述 1.1 说明 以下内容大部分来源于TP6完全开发手册,以手册为主附上个人理解,仅作学习使用. 1.2 概述 第一篇学习笔记,主要记录TP6的基础,包括TP6简介,安装,Hello Wo ...