• 忘掉天地
  • 仿佛也想不起自己
bingliaolongBingliaolong  2021-07-06 13:24 Aet 隐藏边栏 |   抢沙发  10 
文章评分 2 次,平均分 5.0

概述

  1. 对CreateThread函数的调用,导致系统创建了一个线程内核对象。
  2. 该内核对象最初的使用计数为2,除非线程终止,而且从CreateThread返回的句柄被关闭,否则线程内核对象不会被销毁。
  3. 该线程内核对象的其他属性也被初始化:
    1. 暂停计数被设为1
    2. 退出代码被设为STILL_ACTIVE(0X103)
    3. 而且对象被设为未触发状态
  4. 一旦创建了内核对象,系统就会分配内存,供线程堆栈使用。这里的内存是从进程的地址空间分配的,线程是没有自己的地址空间的。
  5. 然后,系统将两个值写入新线程堆栈的最上端。(线程堆栈始终是从高位内存地址向地位内存地址构建的)
    1. 写入线程堆栈的第一个指是传给CreateThread函数的pvParam参数的值
    2. 第二个被写入线程堆栈的值是传给CreateThread函数的pfnStartAddr值
  6. 每个线程都有自己的CPU寄存器,称为线程的上下文。
    上下文反应了当线程上一次执行时,线程的CPU寄存器的状态。
    线程的CPU寄存器全部保存在一个CONTEXT结构(定义在WinNT.h中)
    CONTEXT结构本身保存在线程内核对象中
  7. 指令指针寄存器和栈指针寄存器是线程上下文中最重要的两个寄存器。
    因为线程始终在进程的上下文中运行。所以,这两个地址标识的内存都位于线程所在的进程的地址空间中。
    当线程的内核对象被初始化的时候,CONTEXT结构体的堆栈指针寄存器被设为pfnStartAddr在线程堆栈中的地址。
    而指令指针寄存器被设为RtlUserThreadStart函数的地址。(此函数是NTDLL.dll模块导出的)

  1. 线程安全初始化好之后,系统将检查CREATE_SUSPENDED标志是否已被传给CreateThread函数。
    1. 如果此标记没有传,系统将线程的挂起计数递减至0,随后线程就可以调度给一个处理器去执行。
  2. 系统在实际的CPU寄存器中加载上一次在线程上下文中保存的值。
    1. 现在线程可以在其进程的地址空间中执行代码并处理数据了。

注意

  1. 函数RtlUserThreadStart有两个参数,可能会让人觉得该函数是从其他地方调用的。
    其实不是,新线程只是在此处产生并开始执行。
  2. 之所以能访问这两个参数,是因为操作系统将值显示的写入线程堆栈里面了(参数通常就是通过这样的方式传给函数的)。
  3. 需要注意的是,有的CPU架构是用CPU寄存器而不是堆栈来传递参数。对于这种架构,系统会在运行线程执行RtlUserThreadStart函数之前正确初始化恰当的寄存器。

新线程执行RtlUserThreadStart过程

  1. 围绕线程函数,会设置一个结构化异常处理帧。
    线程执行期间所产生的任何异常都能得到系统的默认处理。
  2. 系统调用线程函数,把传给CreateThread函数的pvParam参数传给它。
  3. 线程函数返回时,RtlUserThreadStart调用ExitThread,并将我们的线程函数的返回值传给它。
    线程内核对象的使用计数递减,而后线程停止执行。
  4. 如果线程产生了一个未被处理的异常,RtlUserThreadStart函数所设置的结构化异常处理帧会处理这个异常。
    这通常意味着系统向用户显示一个消息框,而且当用户关闭此消息框时,RtlUserThreadStart会调用ExitProcess来终止整个进程,而不只是终止有问题的线程。

细节

  1. 当RtlUserThreadStart调用我们的线程函数时,它会将线程函数的返回地址压入堆栈,使线程函数指定应该在何处返回。
  2. 但是,RtlUserThreadStart函数是不允许返回的。如果它没有在强行杀死线程的前提下尝试返回,几乎肯定会引起访问违规。
    因为线程堆栈上没有函数返回地址,RtlUserThreadStart将尝试返回某个随机的内存位置。
  3. 一个进程的主线程初始化时,其指令指针会被设为同一个未文档化的函数RtlUserThreadStart。
    当RtlUserThreadStart开始执行时,它会调用C/C++运行库的启动代码,C/C++运行库的启动代码继而调用我们的_tmain或_tWinMain函数。
    然后等我们的入口点函数返回时,C/C++运行时启动代码会调用ExitProcess。
    所以对C/C++应用程序来说,主线程永远不会返回到RtlUserThreadStart函数。

C/C++运行库相关

库名称 描述
LibCMt.lib 库的静态链接发行版本
LibCMtD.lib 库的静态链接调试版本
MSVCRt.lib 导入库,用于动态链接MSVCR80.dll库的发行版本
MSVCRtD.lib 导入库,用于动态链接MSVCR80.dll库的调式版本
MSVCMRt.lib 导入库,用于托管/本机代码混合
MSVCURt.lib 导入库,编译程百分百的纯MSIL代码

_beginthreadex

  1. _beginthreadex也会返回新建线程的句柄,就像CreateThread那样。
  2. 可以在比较方便的使用_beginthreadex来替换掉CreateThread。
    只需要转换一些数据结构(下方的宏用于简化工程)。

关于_beginthreadex

  1. 每个线程都有自己的专用_tiddata内存块,它们是从C/C++运行库的堆上分配的。
  2. 传给_beginthreadex的线程函数的地址保存在_tiddata内存块中。
  3. _beginthreadex会在内部调用CreateThread,因为操作系统只知道用这种方式来创建一个线程。
  4. CreateThread在内部被调用时,传给它的函数地址是_threadstartex,而不是pfnStartAddr。参数地址是_tiddata结构的地址,而非pvParam。
  5. 如果成功创建,会返回线程的句柄,任何操作失败,会返回0。

关于_threadstartex

  1. 新的线程首先执行RtlUserThreadStart(NTDLL.dll),然后再跳转到_threadstartex。
  2. _threadstartex唯一的参数就是_tiddata内存块的地址。
  3. TlsSetValue函数是一个操作系统函数,它将一个值与主调线程关联起来,这就是所谓的线程局部存储。
  4. 异常处理帧将预期要执行的线程函数包围起来。
  5. 预期要执行的线程函数被调用,并向其传递预期的参数。
    函数地址和参数由_beginthreadex保存在TLS的_tiddata数据块中。
  6. 线程函数的返回值被认为是线程的退出代码。
    _beginthreadex调用了_endthreadex并向其传递退出代码,以使_tiddata内存块正常销毁。

关于_endthreadex

  1. C运行库的_getptd_noexit函数在内部调用操作系统的TlsGetValue函数,后者获取主调线程的_tiddata内存块的地址。
  2. 然后,_endthreadex将此内存块释放,并调用操作系统的ExitThread函数来实际地销毁线程。

了解自己

  1. 这两个函数返回的都是主调线程的进程内核对象或线程内核对象的一个伪句柄。
    它不会影响进程内核对象或线程内核对象的使用计数。
    而且调用CloseHandle只是简单的忽略此调用并返回FALSE。

将伪句柄转换为真正的句柄

本文为原创文章,版权归所有,欢迎分享本文,转载请保留出处!

bingliaolong
Bingliaolong 关注:0    粉丝:0 最后编辑于:2021-11-20
Everything will be better.

发表评论

表情 格式 链接 私密 签到
扫一扫二维码分享