DllMain中不当操作导致死锁问题的分析--导致DllMain中死锁的关键隐藏因子
有了前面两节的基础,我们现在切入正题:研究下DllMain为什么会因为不当操作导致死锁的问题。首先我们看一段比较经典的“DllMain中死锁”代码。(转载请指明出于breaksoftware的csdn博客)
//主线程中
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 我们加载Dll。
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结构体
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。但是此时临界区被主线程占用,工作线程便进入等待状态。而主线程却等待工作线程退出才退出临界区。于是这就是死锁产生的原因。
相关文章:
性能超FPN!北大、阿里等提多层特征金字塔网络
作者 | Qijie Zhao等编译 | 李杰出品 | AI科技大本营(ID:rgznai100)特征金字塔网络具有处理不同物体尺度变化的能力,因此被广泛应用到one-stage目标检测网络(如DSSD,RetinaNet,RefineDet)和two-…

什么是WIFI
WIFI全称Wireless Fidelity,又称802.11b标准,它的最大优点就是传输速度较高,可以达到11Mbps,另外它的有效距离也很长,同时也与已有的各种802.11DSSS设备兼容。 WIFI是由AP(Access Point)和无线网卡组成的无线网络。…
Android入门——电话拨号器和4种点击事件
关于HelloWorld为,电话拨号程序还AndroidA入门demo,从这个样例我们要理清楚做安卓项目的思路。大体分为三步: 1.理解需求,理清思路 2.设计UI 3.代码实现 电话拨号器 1. 理解需求: *一个文本框——用来接收电话号码 *一个button——用来触发事…

DllMain中不当操作导致死锁问题的分析--导致DllMain中死锁的关键隐藏因子2
本文介绍使用Windbg去验证《DllMain中不当操作导致死锁问题的分析--导致DllMain中死锁的关键隐藏因子》中的结论,调试对象是文中刚开始那个例子。(转载请指明出于breaksoftware的csdn博客) 1 g 让程序运行起来 2 ctrlbreak 中断程序 3 ~ 查看…
从入门到深入:移动平台模型裁剪与优化的技术探索与工程实践
可以看到,通过机器学习技术,软件或服务的功能和体验得到了质的提升。比如,我们甚至可以通过启发式引擎智能地预测并调节云计算分布式系统的节点压力,以此改善服务的弹性和稳定性,这是多么美妙。而对移动平台来说&#…

我在不炎熱也不抑鬱的秋天,依然不抽煙
写过几次电影的观后感,挺过瘾.最近看到my little airport的那张新唱片,再也没有办法保持沉默了 为什么人家的唱片名都起的和小说一样,难得是为了证明听歌的人们都不喜欢动笔吗? 于是,我建了个类别,叫 我也会听歌.很明显,这里面会塞一些和歌相关的东西 这是第一篇

ubuntu安装redis的方法以及PHP安装redis扩展、CI框架sess使用redis的方法
为什么80%的码农都做不了架构师?>>> 再一次被网上那些教程误导后决定自己写一个。真心被那些奇怪的教程误导了好几次,之前研究其它东西的时候也是。蛋疼啊。 安装redis 直接用apt-get命令即可 sudo apt-get install redis-server 安装的时候…

浅谈数据库设计技巧
说到数据库,我认为不能不先谈数据结构。1996年,在我初入大学学习计算机编程时,当时的老师就告诉我们说:计算机程序=数据结构+算法。尽管现在的程序开发已由面向过程为主逐步过渡到面向对象为主,…
避免神经网络过拟合的5种技术(附链接) | CSDN博文精选
作者 | Abhinav Sagar翻译 | 陈超校对 | 王琦来源 | 数据派THU(ID:DatapiTHU)(*点击阅读原文,查看作者更多精彩文章)本文介绍了5种在训练神经网络中避免过拟合的技术。 最近一年我一直致力于深度学习领域。这段时间里,我使用过很多神经网络&a…

DllMain中不当操作导致死锁问题的分析--加载卸载DLL与DllMain死锁的关系
前几篇文章一直没有在源码级证明:DllMain在收到DLL_PROCESS_ATTACH和DLL_PROCESS_DETACH时会进入临界区。这个论证非常重要,因为它是使其他线程不能进入临界区从而导致死锁的关键。我构造了在DLL被映射到进程地址空间的场景,请看死锁时加载DL…

LinearLayout增加divider分割线
2019独角兽企业重金招聘Python工程师标准>>> 在android3.0及后面的版本在LinearLayout里增加了个分割线 android:divider"drawable/shape"<!--分割线图片--> android:showDividers"middle|beginning|end" <!--分割线位置--> 分割线…

JAVA游戏编程之二----j2me MIDlet 手机游戏入门开发--贪吃蛇
作者:雷神 QQ:38929568 QQ群:28048051JAVA游戏编程(满) 28047782(将满) 与前一款扫雷比较,这个游戏多了一个 类,用来显示动画,也是蛇要吃的物品类, 也有了代码…

DllMain中不当操作导致死锁问题的分析——线程中调用GetModuleFileName、GetModuleHandle等导致死锁
之前的几篇文章已经讲解了在DllMain中创建并等待线程导致的死锁的原因。是否还记得,我们分析了半天汇编才知道在线程中的死锁位置。如果对于缺乏调试经验的同学来说,可能发现这个位置有点麻烦。那么本文就介绍几个例子,它们会在线程明显的位置…
如何从菜鸡变成收割机,大厂面试的算法,你懂了吗?
是什么?让大厂面试显得逼格很高,是算法和数据结构吗?是的!!!Google工程师曾总结过,大厂之所以爱考察算法和数据结构是因为:算法能力能够准确辨别一个程序员的技术功底是否扎实&#…

Ejabberd源码解析前奏--配置
一、基本配置 配置文件将在你第一次启动ejabberd时加载,从该文件中获得的内容将被解析并存储到内部的ejabberd数据库中,以后的配置将从数据库加载,并且任何配置文件里的命令都会被添加到数据库里。 需要注意的是:ejabberd从不编辑…

DllMain中不当操作导致死锁问题的分析——DllMain中要谨慎写代码(完结篇)
之前几篇文章主要介绍和分析了为什么会在DllMain做出一些不当操作导致死锁的原因。本文将总结以前文章的结论,并介绍些DllMain中还有哪些操作会导致死锁等问题。(转载请指明出于breaksoftware的csdn博客) DllMain的相关特性 首先列出…
滴滴叶杰平:年运送乘客百亿次,AI如何“服务”出行领域?| BDTC 2019
出品 | AI科技大本营(ID:rgznai100)“如果把北京一天滴滴的轨迹数据放在一起,要覆盖北京所有道路差不多四百次,数据非常大、非常完整。”超5.5亿用户,年运送乘客100亿人次,除了中国地区,滴滴也在…

分析部署无线局域网的关键要素
在部署无线局域网时需要考虑的关键问题包括:确定单个接入点的RF覆盖,保证足够的支持所有用户的容量,以及考虑RF信号损耗因素。 单个AP的覆盖 网络设计师必须通过研究AP的服务范围来决定单个AP的覆盖。数据速率是一种距离函数ÿ…

Delphi调用java开发的WebService,传入参数出错
http://www.cnblogs.com/zhangzhifeng/p/3397053.html 调用没有参数的服务正常,当调用有参数的服务出现以下错误java.util.concurrent.ExecutionException: java.lang.NullPointerException 另外加了RIO.HTTPWebNode.UseUTF8InHeader : True;InvRegistry.RegisterInvokeOptions…
B站收藏6.1w+!这门课拯救你薄弱的计算机基础
作者 | Rocky0429来源 | Python空间大家好,我是 Rocky0429,一个对计算机基础一无所知的蒟蒻...作为一个所谓的计算机科班出身的人来说,特别难为情的是自己的计算机基础很差,比如计算机网络当年一度差点挂掉,多亏当时…

一种不会导致资源泄露的“终止”线程的方法
在项目工程中,我们可能会使用第三方开发的模块。该模块提供一个接口用于完成非常复杂和耗时的工作。我们一般不会将该API放在UI线程中执行,而是启动一个线程,用工作线程去执行这个耗时的操作。(转载请指明出于breaksoftware的csdn…

TCP/IP详解学习笔记(9)-TCP协议概述
终于看到了TCP协议,这是TCP/IP详解里面最重要也是最精彩的部分,要花大力气来读。前面的TFTP和BOOTP都是一些简单的协议,就不写笔记了,写起来也没啥东西。TCP和UDP处在同一层---运输层,但是TCP和UDP最不同的地方是&…

在windows程序中嵌入Lua脚本引擎--使用VS IDE编译Luajit脚本引擎
前些天听到一个需求:某业务方需要我们帮忙清理用户电脑上的一些废弃文件。同事完成这个逻辑的方案便是在我们程序中加入了一个很“独立”的业务逻辑:检索和删除某个程序产生的废弃文件。试想,该“独立”的逻辑之后会如何?被删掉&a…
优酷智能档在大型直播场景下的技术实践
作者 | 阿里文娱高级技术专家 肖文良 本文为阿里文娱高级技术专家肖文良在【阿里文娱2019双11猫晚技术沙龙】中的演讲,主要内容为如何通过优酷智能档,降低用户卡顿尤其是双11直播场景下,提升用户观看体验。具体包括智能档的落地挑战、算法架…

主题:CS0016: 未能写入输出文件“c:#92;WINDOWS#92;Microsoft.NET#92;***.dll”错误处理...
刚装完.NET环境,在编译时出现了如下错误: 编译器错误信息:CS0016: 未能写入输出文件“c:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\Temporary ASP.NET Files\***\*****.dll”--“拒绝访问。” 错误的处理: 出现CS0016的原因一般是…

关于 android 加载 res 图片 out of memory 问题 解决 同样适用于 sd卡图片
2019独角兽企业重金招聘Python工程师标准>>> 发现android 加载res图片如果过多也会崩溃 android 也是使用 Bitmap bm BitmapFactory.decodeResourceStream(res, value, is, pad, opts); 来加载图片,不同他一般不会释放,如果图片太多就崩溃了 不过解决方法就更简…
近期必读的6篇NeurIPS 2019零样本学习论文
来源 | 专知(ID:Quan_Zhuanzhi) 【导读】NeurIPS 是全球最受瞩目的AI、机器学习顶级学术会议之一,每年全球的人工智能爱好者和科学家都会在这里聚集,发布最新研究。NIPS 2019大会已经在12月8日-14日在加拿大温哥华举行,…

在windows程序中嵌入Lua脚本引擎--建立一个简易的“云命令”执行的系统
在《在windows程序中嵌入Lua脚本引擎--使用VS IDE编译Luajit脚本引擎》开始处,我提到某公司被指责使用“云命令”暗杀一些软件。本文将讲述如何去模拟一个简易的“云指令”执行系统。(转载请指明出于breaksoftware的csdn博客) 首先我们思考下…

oracle9i.rar下载
甲骨文数据库9I转载于:https://blog.51cto.com/263054/46968

每天一道算法题(24)——自定义幂函数pow
double myPower(double base, int exponent){if(exponent0)return 1;if(exponent1)return base;if(exponent-1)//当为是负数的情况return 1.0/base;double result1.0;resultmyPower(base,exponent>>1);result*result;if(exponent&1)//绝对值为奇数result*base;return…