zoukankan      html  css  js  c++  java
  • <转>DllMain和多线程死锁

    估计很多人都知道装载DLL过程中的多线程死锁是因为DllMain的顺序调用规则,但是很少人了解卸载DLL过程中的多线程死锁也是由于同样的原因。例如,如果一个DLLDllMain的代码写成下面的形式,且进程中有至少一个DLLDllMain没有调用DisableThreadLibraryCalls函数的话,那么卸载该DLL过程中就会因为DllMain的顺序操作特性带来DLL内部线程没有完全退出的错误。   

    //----------------------start ------------
    
    HANDLE       g_thread_handle =NULL;      // 该DLL内部线程的句柄
    
    DWORD       g_thread_id =0;      // 该DLL内部线程的ID
    
    HANDLE g_hEvent=NULL;// 应答事件的句柄
    
     
    
    DWORD WINAPI InSideDll_ThreadProc( LPVOID p )
    
    {
    
           while(1){ 
    
                  // 收到通知就退出线程函数
    
                  DWORD ret = ::WaitForSingleObject( g_hEvent, INFINITE );
    
                  if(WAIT_TIMEOUT = =ret|| WAIT_OBJECT_0 = =ret) break;
    
           }     
    
           return true ;
    
    }
    
     
    
    BOOL APIENTRY DllMain( HANDLE hModule, 
    
                           DWORD ul_reason_for_call, 
    
                           LPVOID lpReserved
    
                                       )
    
    {
    
        switch (ul_reason_for_call)   
    
           {
    
                  case DLL_PROCESS_ATTACH:
    
                  //线程正在映射到进程地址空间中
    
                         {
    
                                // 创建DLL内的线程使用的事件对象
    
                                g_hEvent = ::CreateEvent( NULL, FALSE, FALSE, _T("hello11" ));
    
                                //创建DLL内的线程对象
    
                                g_thread_handle = ::CreateThread(NULL,0, 
    
                                       InSideDll_ThreadProc,(LPVOID)0,0,   &( g_thread_id) ) ;
    
                                // 禁止线程库调用,
    
                                DisableThreadLibraryCalls((HINSTANCE)hModule);
    
                         }
    
                         break;
    
                  case DLL_PROCESS_DETACH:
    
                  // DLL正在从进程地址空间中卸载
    
                         {
    
                                // 通知内部的线程g_thread_handle 退出
    
                                ::SetEvent(g_hEvent);
    
                                // 等待内部的线程g_thread_handle 退出
    
                                ::WaitForSingleObject(g_thread_handle, INFINITE ) ;
    
                                // 清除资源
    
    ::CloseHandle(g_thread_handle);
    
                                g_thread_id = 0 ;
    
                                g_thread_handle = NULL ;                  
    
                                ::CloseHandle(g_hEvent);
    
                                g_hEvent=NULL;
    
                         }
    
                         break;
    
        }
    
        return TRUE;
    
    }
    
    //----------------------end ------------

    上述代码的流程是这样的:

           (1)装载DLL时,创建一个 DLL内部的线程g_thread_handle及事件对象g_hEvent,且线程g_thread_handle在事件对象g_hEvent上等待。

           (2)卸载DLL时,通过SetEvent(g_hEvent)通知线程g_thread_handle退出,随即调用WaitForSingleObject函数等待线程g_thread_handle终止运行。如果线程g_thread_handle终止运行,则执行清除工作。

    但是如果对这样的程序进行调试,就会发现程序在退出时该DllMain没有退出,等待了很长时间也没有退出。

    查看了一下线程Call Stack窗口,注意到程序正在等待DllMain内部的线程g_thread_handle的退出。尽管线程g_thread_handle的线程函数已经返回了,但是整个g_thread_handle线程走到了操作系统的ntdll.dll中并没有完全终止,从而导致整个DLL不能顺利释放。

           线程g_thread_handle为什么没有完全退出呢?

    原来,线程函数返回时,系统并不立即将它撤消。相反,系统要取出这个即将被撤消的线程,让它调用已经映射的DLL的所有带有DLL_THREAD_DETACH值的、且没有调用DisableThreadLibraryCalls函数的DllMain函数。DLL_THREAD_DETACH通知告诉所有的DLL执行每个线程的清除操作,例如,DLL版本的C/C++运行期库能够释放它用于管理多线程应用程序的数据块。DisableThreadLibraryCalls函数告诉系统说,特定的DLLDllMain函数不用接收DLL_THREAD_ATTACHDLL_THREAD_DETACH通知。

    但是,系统是顺序调用DLLDllMain函数的。

    当线程函数返回时,系统检查进程中是否存在没有调用DisableThreadLibraryCalls函数的DllMain函数,如果存在,系统就以进程的互斥对象的句柄作为第一个参数,在线程内部调用WaitForSingleObject函数。一旦这个将要终止运行的线程拥有该进程互斥对象,系统就让该线程用DLL_THREAD_DETACH的值依次调用每个没有调用DisableThreadLibraryCalls函数的DLLDllMain函数。此后,系统才释放对进程互斥对象的所有权。

    在本例所述的应用程序中,进程的退出引起操作系统获取进程互斥对象使操作系统可以为DLL_PROCESS_DETACH通知调用DllMain()。该DLLDllMain()函数通知线程g_thread_handle终止运行。无论何时当进程终止一个线程时,操作系统将获取进程互斥对象,以便于它可以为DLL_THREAD_DETACH通知调用每个加载的、没有调用DisableThreadLibraryCalls函数的DLLDllMain函数。在这个特定的程序中,线程g_thread_handle当线程函数返回后就阻塞了,因为CMySingletonDllMain()所处的线程还保持着进程互斥对象。不幸的是,DllMain所处的线程然后调用WaitForSingleObject确认g_thread_handle线程是否完全终止。因为g_thread_handle线程被阻塞在进程互斥对象上,这个进程互斥对象还被DllMain线程所持有, DllMain线程要等待g_thread_handle线程从而也被阻塞,结果就导致了死锁。如下图所示:

    注意,从图2可以看出,如果当前进程中的所有 DLL都调用了DisableThreadLibraryCalls函数,那么上述代码中的DLL也能正常退出。曾经写过一个程序,除了加载一个这样有问题的DLL没有加载其他DLL(系统的DLL除外),程序能够正常退出。

    3、结论

        很显然的一个教训就是在DllMain内部应该避免任何Wait*调用。但是进程互斥对象的问题不仅仅限于Wait*函数。操作系统在CreateProcessGetModuleFileNameGetProcAddresswglMakeCurrentLoadLibraryFreeLibrary等函数中在后台获取进程互斥对象,因此在DllMain中不应该调用任何这些函数。因为DllMain获取进程互斥对象,所以一次只能有一个线程执行DllMain

           ATL singleton FinalConstruct函数和FinalRelease函数分别是DllMain在响应DLL_PROCESS_ATTACHDLL_PROCESS_DETACH时被调用的,所以也要同样注意本文所述的问题

    本人新博客网址为:http://www.hizds.com
    本博客注有“转”字样的为转载文章,其余为本人原创文章,转载请务必注明出处或保存此段。c++/lua/windows逆向交流群:69148232
  • 相关阅读:
    WPF中更改键盘默认指令小结
    WPF自己喜欢用的数据验证方式
    重写Windows基类,自定义WPF窗口,实现改回车键为TAB
    用CSS控制表格的框格线
    获取当前鼠标的坐标
    SQL 中的转义字符
    資料站點
    jquery 弹出浮层(div) + 遮蔽层
    Jquery放大镜插件[JMagazine]使用参数简介
    邏輯題 交通事故篇
  • 原文地址:https://www.cnblogs.com/zhangdongsheng/p/2545744.html
Copyright © 2011-2022 走看看