昨天在安装包中加入一个同事新编写的Dll,此Dll为自注册插件,但是安装包运行安装过程报错,提示访问违规。于是我看他的注册代码,也仅仅是向注册表写入一些键值,并无其它,并且注册表操作也已成功。一时摸不到头脑,后来发现在DllMain函数中DLL_PROCESS_ATTACH分支他创建了一个线程,原因变得明了起来。
msiexec会调用LoadLibrary载入自注册dll,DllMain被触发,创建工作线程。而后msiexec调用导出函数注册dll,然后调用FreeLibrary卸载dll。就在此时出现了问题。工作线程执行属于此dll的代码时,会发生访问违规,除非至进程结束时,不会发生工作线程执行属于此dll的代码。
那用什么办法解决呢?考虑如下吧!
1. 在DllMain函数分支DLL_PROCESS_DETACH等待线程结束,即WaitForSingleObject线程句柄。可是这样会产生死锁,因为线程结束时后要求获取Loader Lock,然后使用DLL_THREAD_DETACH调用每个dll的dllmain函数。而此时此Loader Lock由等待此线程退出的线程占用,这样造成了死锁。有的同学可能说,在DLL_PROCESS_ATTACH调用DisableThreadLibraryCall不可以吗?不行,因为获取此Loader Lock是线程的必有行为,无论如何都会调用。另外就算你调用了DisableThreadLibraryCall,但是其它的Dll并不会调用DisableThreadLibraryCall,所以此路不通啊。
2.在DllMain函数DLL_PROCESS_ATTACH创建一个事件,在DLL_PROCESS_DETACH通知线程退出(全局变量或者事件),然后等待此事件。线程收到退出事件后,设置此事件,通知自己退出。这个方法更是有问题,1)若dll的客户没有调用FreeLibrary卸载此dll,十有八九进程无法正常结束。因为进程退出时,会先结束工作线程,若工作线程被结束时没有执行到设置事件代码,则之后的DLL_PROCESS_DETACH会一直等待此事件。2)即使dll的客户调用FreeLibrary卸载Dll,线程设置事件后,还需要执行返回代码。若执行此代码之前线程切换到了DLL_PROCESS_DETACH位置处,并且FreeLibrary返回,dll卸载完毕,此时再切换至工作线程,也会产生违规访问的bug。
讲了这么半天,到底咋解决呢,其实吧就是少在DllMain中做线程创建、线程同步的事情,重新设计工作线程的创建及关闭。哈哈哈,废话!若非要在DLL中创建线程(不单指DllMain),可以考虑一下办法(并非本人原创,别给我发律师信):
在创建工作线程前调用LoadLibrary增加dll的引用计数(在工作线程内调用此方法是不合适的,那可能有点晚了),在工作线程回调函数中调用FreeLibraryAndExitThread。使用这种方法应该注意,FreeLibraryAndExitThread之后的代码不会去执行了,特别是c++对象(或智能指针)的析构函数,你需要在调用此函数之前做必需的清理工作。
说点其它的:
调试程序时,发现NtTerminateProcess被调用了两次,表示不解。后经了解,第一次调用ntdll!NtTerminateProcess(NULL)时为结束所有的工作线程,而第二次调用ntdll!NtTerminateProcess(-1)时为最终退出进程,这次调用不会返回到用户模式。