原文地址:https://blog.csdn.net/hczhiyue/article/details/18505087

有了前面两节的基础,我们现在切入正题:研究下DllMain为什么会因为不当操作导致死锁的问题。首先我们看一段比较经典的“DllMain中死锁”代码。

//主线程中
HMODULE h = LoadLibraryA(strDllName.c_str());  
// DLL中代码
static DWORD WINAPI ThreadCreateInDllMain(LPVOID) {  return 0;
}  BOOL APIENTRY DllMain( HMODULE hModule,  DWORD  ul_reason_for_call,  LPVOID lpReserved  )
{  DWORD tid = GetCurrentThreadId();  switch (ul_reason_for_call)     {  case DLL_PROCESS_ATTACH: {  printf("DLL DllWithoutDisableThreadLibraryCalls_A:\tProcess attach (tid = %d)\n", tid);  HANDLE hThread = CreateThread(NULL, 0, ThreadCreateInDllMain, NULL, 0, NULL);  WaitForSingleObject(hThread, INFINITE);  CloseHandle(hThread);  }break;  case DLL_PROCESS_DETACH:  case DLL_THREAD_ATTACH:  case DLL_THREAD_DETACH:  break;  }  return TRUE;
}  

简要说下DLL中逻辑:设计该段代码的同学希望在DLL第一次被映射到进程内存空间时,创建一个工作线程,该工作线程内容可能很简单。为了尽可能简单,我们让这个工作线程直接返回0。这样从逻辑和效率上看,都不会因为我们的工作线程写的有问题而导致死锁。然后我们在DllMain中等待这个线程结束才从返回。

粗略看这个问题,我们很难看出这个逻辑会导致死锁。但是事实就是这样发生了。我们跑一下程序,发现程序输出一下结果 后就停住了,光标在闪动,貌似还是在等待我们输入:

可是我们怎么敲击键盘都没有用:它死锁了。 我是在VS2005中调试该程序,于是我们可以Debug->Break All来冻结所有线程。

我们先查看主线程(3096)的堆栈 堆栈不长,

我全部列出来

17   ntdll.dll!_KiFastSystemCallRet@0()
16  ntdll.dll!_NtWaitForSingleObject@12()
15  kernel32.dll!_WaitForSingleObjectEx@12()
14  kernel32.dll!_WaitForSingleObject@8()
13  DllWithoutDisableThreadLibraryCalls_A.dll!DllMain(HINSTANCE__ * hModule=0x10000000, unsigned long ul_reason_for_call=1, void * lpReserved=0x00000000)
12  DllWithoutDisableThreadLibraryCalls_A.dll!__DllMainCRTStartup(void * hDllHandle=0x10000000, unsigned long dwReason=1, void * lpreserved=0x00000000)
11  DllWithoutDisableThreadLibraryCalls_A.dll!_DllMainCRTStartup(void * hDllHandle=0x10000000, unsigned long dwReason=1, void * lpreserved=0x00000000)
10  ntdll.dll!_LdrpCallInitRoutine@16()
9   ntdll.dll!_LdrpRunInitializeRoutines@4()
8   ntdll.dll!_LdrpLoadDll@24()
7   ntdll.dll!_LdrLoadDll@16()
6   kernel32.dll!_LoadLibraryExW@12()
5   kernel32.dll!_LoadLibraryExA@12()
4   kernel32.dll!_LoadLibraryA@4()
3   DllMainSerial.exe!wmain(int argc=3, wchar_t * * argv=0x003b7000)
2   DllMainSerial.exe!__tmainCRTStartup()
1   DllMainSerial.exe!wmainCRTStartup()
0   kernel32.dll!_BaseProcessStart@4()

我们看下这个堆栈。大致我们可以将我们程序分为4段:

0 启动启动我们程序 1~6 我们加载Dll4。

7~10 系统为我们准备DLL的加载。

11~17 DLL内部代码执行。

我们关注一下14~17这段对WaitForSingleObject的调用逻辑。15、16步这个过程显示了Kernel32中的WaitForSingleObjectEx在底层是调用了NtDll中的NtWaitForSingleObject。在NtWaitForSingleObject内部,即17步,我们看到的“_KiFastSystemCallRet@0”。这儿要说明下,这个并不是意味着我们程序执行到这个函数。我们看下这个函数的代码

KiFastSystemCallRet函数是内核态(Ring0层)逻辑回到用户态(Ring3层)的着陆点。与之相对应的KiFastSystemCall函数是用户态进入内核态必要的调用方法。因为内核态代码我们是无法查看的,所以动态断点只能设置到KiFastSystemCallRet开始处。所以实际死锁是因为NtWaitForSingleObject在底层调用了KiFastSystemCall进入内核,在内核态中死锁的。 

我们在《DllMain中不当操作导致死锁问题的分析--死锁介绍》中介绍过,死锁存在的条件是相互等待。主线程中,我们发现其等待的是工作线程结束。那么工作线程在等待主线程什么呢?我们看下工作线程的调用堆栈

我们对这个堆栈进行编号

6    ntdll.dll!_KiFastSystemCallRet@0()
5   ntdll.dll!_NtWaitForSingleObject@12()  + 0xc bytes
4   ntdll.dll!_RtlpWaitForCriticalSection@4()  + 0x8c bytes
3   ntdll.dll!_RtlEnterCriticalSection@4()  + 0x46 bytes
2   ntdll.dll!__LdrpInitialize@12()  + 0xb4bf bytes
1   ntdll.dll!_KiUserApcDispatcher@20()  + 0x7 bytes
0   ntdll.dll!_RtlAllocateHeap@12()  + 0x9b48 bytes

我们看到倒数两步(5、6)和主线程中最后两步(16、17)是相同的,即工作线程也是在进入内核态后死锁的。我们知道主线程在等工作线程结束,那么工作线程在等什么呢?我们追溯栈,请关注“ntdll.dll!__LdrpInitialize@12() + 0xb4bf bytes”处的代码

我们看到,是因为_RtlEnterCriticalSection在底层调用了NtWaitForSingleObject。那么我们关注下_RtlEnterCriticalSection的参数_LdrpLoaderLock,它是什么?我们借助下IDA查看下LdrpInitialize反编译代码

……
v4 = *(_DWORD *)(*MK_FP(__FS__, 0x18) + 0x30);
v3 = *MK_FP(__FS__,0x18);  ……  *(_DWORD *)(v4 + 0xa0) = &LdrpLoaderLock;  if ( !(unsigned __int8)RtlTryEnterCriticalSection(&LdrpLoaderLock) )  {  ……  RtlEnterCriticalSection(&LdrpLoaderLock);  }  ……  if ( *(_DWORD *)(v4 + 0xc) )  {  ……  LdrpInitializeThread(a1);  }  else  {
……  v17 = LdrpInitializeProcess(a1, a2, &v11, v14, v15);
……  }
……

由RtlTryEnterCriticalSection 可知LdrpLoaderLock是_RTL_CRITICAL_SECTION类型。在尝试进入临界区之前,LdrpLoaderLock将被保存到某个结构体变量v4的某个字段(偏移0xA0)中。那么v4是什么类型呢?这儿可能要科普下windows x86操作系统的一些知识:

在windows系统中每个用户态线程都有一个记录其执行环境的结构体TEB(Thread Environment Block)。TEB结构体中第一个字段是一个TIB(ThreadInformation Block)结构体,该结构体中保存着异常登记链表等信息。在x86系统中,段寄存器FS总是指向TEB结构。于是FS:[0]指向TEB起始字段,也就是指向TIB结构体。我们用Windbg查看下TEB的结构体,该结构体很大,我只列出我们目前关心的字段

lkd> dt _TEB
nt!_TEB  +0x000 NtTib            : _NT_TIB  +0x01c EnvironmentPointer : Ptr32 Void  +0x020 ClientId         : _CLIENT_ID
……  

NtTib就是TIB结构体对象名。 我们再看下TIB结构体B

lkd> dt _NT_TIB
nt!_NT_TIB  +0x000 ExceptionList    : Ptr32 _EXCEPTION_REGISTRATION_RECORD  +0x004 StackBase        : Ptr32 Void  +0x008 StackLimit       : Ptr32 Void  +0x00c SubSystemTib     : Ptr32 Void  +0x010 FiberData        : Ptr32 Void  +0x010 Version          : Uint4B  +0x014 ArbitraryUserPointer : Ptr32 Void  +0x018 Self             : Ptr32 _NT_TIB

该结构体其他字段不解释,我们只看最后一个字段(FS:[18])指向_NT_TIB结构体的指针Self。正如其名,该字段指向的是TIB结构体在进程空间中的虚拟地址。为什么要指向自己?那我们是否可以直接使用FS:[0]地址?不可以。举个例子:我用windbg挂载到我电脑上一个运行中的calc(计算器)。我们查看fs:[0]指向空间保存的值,7ffdb000是TIB的Self字段。

我们查看TIB结构体去匹配该地址指向的空间的。

可以看到7ffdb000所指向的空间的各字段的值和FS:[0]指向的空间的值一致。但是如果我们这样输入就会失败

介绍完这些后,我们再回到IDA反汇编的代码中。v4 = *(_DWORD*)(*MK_FP(__FS__, 0x18) + 0x30);这段中MK_FP不是一个函数,是一个宏。它的作用是在基址上加上偏移得出一个地址。于是MK_FP(__FS__, 0x18)就是FS:[0x18],即TIB的Self字段。在该地址再加上0x30得到的地址已经超过了TIB空间,于是我们继续查看TEB结构体

发现0x30偏移的是PEB(Process Environment Block)。

lkd> dt _PEB
nt!_PEB  +0x000 InheritedAddressSpace : UChar  +0x001 ReadImageFileExecOptions : UChar
……
+0x09c GdiDCAttributeList : Uint4B  +0x0a0 LoaderLock       : Ptr32 Void  +0x0a4 OSMajorVersion   : Uint4B  

可以发现该结构体偏移0xa0处是一个名字为LoaderLock的变量。 《windows核心编程》中有关于DllMain序列化执行的讲解,大致意思是:线程在调用DllMain之前,要先获取锁,等DllMain执行完再解开这个锁。这样不同线程加载DLL就可以实现序列化操作。而在微软官方文档《Best Practices for Creating DLLs》中也有对这个说法的佐证

The DllMain entry-point function. This function is called by the loader when it loads or unloads a DLL.
The loader serializes calls to DllMain so that only a single DllMain function is run at a time .  

其中还有段关于这个锁的介绍

The loader lock. This is a process-wide synchronization primitive that the loader uses to ensure serialized loading of DLLs.
Any function that must read or modify the per-process library-loader data structures must acquire this lock
before performing such an operation.
The loader lock is recursive, which means that it can be acquired again by the same thread. 

在该文中多处对这个锁的说明值暗示这个锁是PEB中的LoaderLock。

那么刚才为什么要*(_DWORD *)(v4 + 0xa0) = &LdrpLoaderLock;?因为该LdrpLoaderLock是进程内共享的变量。这样每个线程在执行初期,会先进入该 临界区,从而实现在进程内DllMain的执行是序列化的。于是我们得出以下结论: 进程内所有线程共用了同一个临界区来序列化DllMain的执行。

结合《DllMain中不当操作导致死锁问题的分析--进程对DllMain函数的调用规律的研究和分析》中介绍的规律 二 线程创建后会调用已经加载了的DLL的DllMain,且调用原因是DLL_THREAD_ATTACH。 我们发现

HANDLE hThread = CreateThread(NULL, 0, ThreadCreateInDllMain, NULL, 0, NULL);
WaitForSingleObject(hThread, INFINITE);  

主线程进入临界区去调用DllMain时进入了临界区,而工作线程也要进入临界区去执行DllMain。但是此时临界区被主线程占用,工作线程便进入等待状态。而主线程却等待工作线程退出才退出临界区。于是这就是死锁产生的原因。

导致DllMain中死锁的关键隐藏因子相关推荐

  1. DllMain中不当操作导致死锁问题的分析--导致DllMain中死锁的关键隐藏因子2

    本文介绍使用Windbg去验证<DllMain中不当操作导致死锁问题的分析--导致DllMain中死锁的关键隐藏因子>中的结论,调试对象是文中刚开始那个例子.(转载请指明出于breakso ...

  2. DllMain中不当操作导致死锁问题的分析--导致DllMain中死锁的关键隐藏因子

    有了前面两节的基础,我们现在切入正题:研究下DllMain为什么会因为不当操作导致死锁的问题.首先我们看一段比较经典的"DllMain中死锁"代码.(转载请指明出于breaksof ...

  3. DllMain中不当操作导致死锁问题的分析——DllMain中要谨慎写代码(完结篇)

    之前几篇文章主要介绍和分析了为什么会在DllMain做出一些不当操作导致死锁的原因.本文将总结以前文章的结论,并介绍些DllMain中还有哪些操作会导致死锁等问题.(转载请指明出于breaksoftw ...

  4. DllMain中不当操作导致死锁问题的分析--加载卸载DLL与DllMain死锁的关系

    前几篇文章一直没有在源码级证明:DllMain在收到DLL_PROCESS_ATTACH和DLL_PROCESS_DETACH时会进入临界区.这个论证非常重要,因为它是使其他线程不能进入临界区从而导致 ...

  5. DllMain中不当操作导致死锁问题的分析--进程对DllMain函数的调用规律的研究和分析

    不知道大家是否思考过一个过程:系统试图运行我们写的程序,它是怎么知道程序起始位置的?很多同学想到,我们在编写程序时有个函数,类似Main这样的名字.是的!这就是系统给我们提供的控制程序最开始的地方(注 ...

  6. DllMain中不当操作导致死锁问题的分析——线程中调用GetModuleFileName、GetModuleHandle等导致死锁

    之前的几篇文章已经讲解了在DllMain中创建并等待线程导致的死锁的原因.是否还记得,我们分析了半天汇编才知道在线程中的死锁位置.如果对于缺乏调试经验的同学来说,可能发现这个位置有点麻烦.那么本文就介 ...

  7. DllMain中不当操作导致死锁问题的分析--死锁介绍

    最近在网上看到一些关于在DllMain中不当操作导致死锁的问题,也没找到比较确切的解答,这极大吸引了我研究这个问题的兴趣.我花了一点时间研究了下,正好也趁机研究了下进程对DllMain的调用规律.因为 ...

  8. 深入理解MySQL8中死锁及线上故障解决

    深入理解MySQL8中死锁及线上故障解决 一.什么是死锁 死锁是指两个或两个以上的事务在执行过程中,因争夺锁资源而造成的一种互相等待的现象. 若无外力作用,事务都将无法推进下去. 解决死锁问题最简单的 ...

  9. AntDesignVue表格中列的自定义隐藏与展示

    Vue版本:2.6.x,AntDesignVue版本:1.7.x 在工作中难免会遇到这种情况,较多列的表格展示,如果只想看某些关键列的数据,就需要用到筛选,官方文档的筛选是针对行的数据筛选,没有对列筛 ...

最新文章

  1. 微信公众号服务器数据情况,获取新榜微信公众号指数信息,并服务器上部署
  2. python知识:all、dict()、min、setattr、any函数
  3. C++多态:多态实现原理剖析,虚函数表,评价多态,常见问答与实战【C++多态】(55)
  4. MongoDB安装与副本集配置
  5. 算法练习day11——190329(平衡二叉树、搜索二叉树、完全二叉树)
  6. 小码哥30小时快速精通C++和外挂实战特训营
  7. 2016云栖大会马云畅谈未来五大创新趋势
  8. 转: vim 的编辑格式设置
  9. 数据结构与算法——冒泡排序(改进后)
  10. [Node.js] Module.Require机制研究
  11. android 屏幕宽高
  12. 西门子Step7和TIA软件“交叉引用”的使用
  13. 韦东山freeRTOS系列教程:入门文档教程+进阶视频教程(全部免费的freeRTOS系列教程、freeRTOS学习路线)
  14. 【图像处理技术】 | 黑科技解读 之 PS检测、弯曲拉平、切边增强、摩尔纹
  15. PMI-ACP敏捷项目认证练习题(二)
  16. js 时间格式Wed Mar 22 13:38:37 CST 2022 转为yyyy-mm-dd
  17. java应该知道什么
  18. 公司新来了个00后卷王,一副毛头小子的样儿,哪想到...
  19. Samba文件服务器
  20. 基于BIM+3DGIS物联网技术,如是实现智慧园区(楼宇)可视化管控平台的?

热门文章

  1. wikioi 1550 不明飞行物
  2. c语言车辆限行,机动车尾号限行提示器
  3. ElasticSearch学习之Kibana(一)
  4. 英语口语116之每日十句口语
  5. http://hlhpyasd.iteye.com/blog/865865
  6. python执行CMD指令,并获取返回
  7. 第14天 [网络配置]
  8. Vegas如何刻录DVD?
  9. IBM 上海 LBS offer入手总结
  10. 互动抽奖背后的随机性与算法实现