20.1 DLL模块的显式载入和符号链接
20.1.1 显式载入DLL模块
(1)构建DLL时,如果至少导出一个函数/变量,那么链接器会同时生成一个.lib文件,但这个文件只是在隐式链接DLL时使用(显示链接时并没有用到这文件)
(2)显式载入DLL的函数:LoadLibrary(Ex)
参数 |
含义 |
pCTSTR pszDllPathName |
LoadLibrary只有这个参数。函数会根据第19章介绍的搜索算法在用户的计算机中对DLL文件进行定位,并映射到进程的地址空间。 |
HANDLE hFile |
该参数为将来扩充所保留的,这里必须为NULL |
DWORD dwFlags |
可为0或下列标志的组合 ①DONT_RESOLVE_DLL_REFERENCES:只将该DLL映射到进程地址空间,但不调用DllMain函数及不检查该DLL导入段中的其他额外DLL,这也意味着不自动载入额外的DLL。(一般应避免使用该标志,因为代码所依赖的DLL可能尚未被载入!) ②LOAD_LIBRARY_AS_DATAFILE:将DLL作为数据文件映射到进程。(一般用在一个DLL只包含资源而没有函数时或想用一个EXE文件中包含的资源时可用这个标志,而且载入EXE时必须使用这个标志) ③LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE:与②标志相似,唯一不同的是以独占方式来打开这个DLL文件,以防止其他程序对其修改。 ④LOAD_LIBRARY_AS_IMAGE_RESOURCE:与②标志相似,但不同之处在于当系统载入DLL的时候,会对相对虚拟地址(RVA)进行修复。这样RVA就可以直接使用,而不必再根据DLL载入的内存地址来转换了。(当需要对DLL进行遍历其PE段时,这个标志特别有用) ⑤LOAD_IGNORE_CODE_AUTHZ_LEVEL:用来关闭UAC对代码在执行过程中可以拥有的特权加以控制。 ⑥LOAD_WITH_ALTERED_SEARCH_PATH:用来改变LoadLibrary对DLL文件进行定位所使用的搜索算法。 A、如果pszDllPathName不包含“\”字符,会使用标准搜索路径算法 B、如果pszDllPathName包含“\”会因全路径(网络共享路径)或相对路径而有所不同。(见课本P557) C、可以调用SetDllDirectory改变搜索算法,搜索指定的目录路径。(具体顺序为:EXE所在目录→SetDlldirectory设置的文件夹→Windows系统目录→16位Windows系统目录→Windows目录→PATH列出的目录。 |
返回值 |
HMODULE类型,等价于HINSTANCE,表示DLL被映射到的虚拟内存地址。当返回NULL,表示映射失败,可进一步调用GetLastError |
(3)混用LoadLibrary和LoadLibraryEx加载同一个DLL可能带来的问题
【情况1】:不会出现问题,此时hDll1=hDll2=hDll3
HMODULE hDll1 = LoadLibrary(TEXT("MyLibrary.dll")); HMODULE hDll2 = LoadLibraryEx(TEXT("MyLibrary.dll"), NULL,LOAD_LIBRARY_AS_IMAGE_RESOURCE); HMODULE hDll3 = LoadLibraryEx(TEXT("MyLibrary.dll"), NULL,LOAD_LIBRARY_AS_DATAFILE);
【情况2】:将上面的调用顺序改变一下,则hDll1≠hDll2≠hDll3,说明DLL被多次映射到进程的地址空间。
HMODULE hDll1 = LoadLibraryEx(TEXT("MyLibrary.dll"), NULL,LOAD_LIBRARY_AS_DATAFILE); HMODULE hDll2 = LoadLibraryEx(TEXT("MyLibrary.dll"), NULL,LOAD_LIBRARY_AS_IMAGE_RESOURCE); HMODULE hDll3 = LoadLibrary(TEXT("MyLibrary.dll"));
【分析原因】当LoadLibraryEx时(使用LOAD_LIBRARY_AS_DATAFILE, LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE, or LOAD_LIBRARY_AS_IMAGE_RESOURCE标志),系统会检测该DLL是否被LoadLibrary(或未使用上述标志LoadLibraryEx)进来,如果己经被载入过,那么函数会返回空间中DLL原先被映射的地址。如果DLL未被载入,那么DLL会将这个DLL载入,但会认为是个未完全载入的DLL,如果这时再载入时会被多次的映射到进程的地址空间,从而产生不同的地址。
20.1.2 显式卸载DLL模块
(1)BOOL FreeLibrary(HMODULE hInstDll);
(2)VOID FreeLibraryAndExitThread(hInstDll,dwExitCode);
①函数的内部实现(在Kernel32.dll中):
VOID FreeLibraryAndExitThread(HMODULE hInstDll,DWORD dwExitCode){ FreeLibrary(hInstDll); ExitThread(dwExitCode);//调用该行指令在kernel32.dll }
②为什么需要FreeLibraryAndExitThread函数?
A、假设在A这个DLL里创建一个线程,当该线程完成一些工作后,先调用FreeLibrary再调ExitThread来撤销对DLL的映射并终止线程,这里会出现一个严重的问题。因为FreeLibrary会立即从进程的地址空间撤销对DLL的映射。当FreeLibrary返回时,线程会试图调用ExitThread,而这行代码本来在DLL里的,这个DLL己经不存在了,这时线程会试图执行不存在的代码,将引发访问违规,并导致整个进程被终止。
B、但如果调用FreeLibraryAndExitThread,由于函数内部会调用FreeLibrary和ExitThread,而这两个函数是在Kernel32.dll的内部调用(而不是A这个DLL)。所以当撤销了A这个DLL后,这个线程可以继续执行ExitThread,只不过当ExitThread时线程不再返回A这个DLL里了,所以也不会出错。
(3)DLL的使用计数问题
①当LoadLibrary(Ex)时使用计数递增,第一个Load时使用计数为1.如果同一个进程的一个线程再调用LoadLibrary时,系统不会再次进行映射而是将使用计数递增。
②FreeLibrary或FreeLibraryAndExitThread使计数递减,但计数递减到0时,系统会这个DLL从进程的地址空间中撤销映射。
③系统为每个进程的每个DLL维护一个使用计数。如进程A和B都加载了MyLib.dll,那么这个DLL会被映射进两个进程的地址空间,但该DLL在进程A和B中的使用计数都是1。如果后来进程B的一个线程再次LoadLibrary这个DLL,则进程B中这个DLL的使用计数为2,但进程A中仍为1。
(4)检测DLL的两个函数
①检测DLL是否被映射:HMODULE GetModuleHandle(PCTSTR pszModuleName);
A、返回NULL时表示未被映射。
B、如果传NULL参数时,会返回应用程序的EXE文件的句柄。
②获得DLl/EXE的全路径名:GetModuleFileName(hInstModule,pszPathName,cchPath);其中的hInstModule为DLL或EXE的句柄。
20.1.3 显示链接到导出符号——获得函数地址:GetProcAddress
参数 |
描述 |
HMODULE hInstDll |
DLL的句柄,即先前调用LoadLibrary(Ex)或GetModuleHandle时的返回值。 |
PCSTR pszSymbolName |
函数的名称或序号,注意这里的类型是PCSTR,而不是PCTSTR,说明这个函数只接受ANSI的字符串。 |
返回值 |
FARPROC,要获得的函数的地址,须转为该函数原型的指针。一般用typedef来声明要获取的函数原型的指针 |
20.2 DLL的入口点函数
BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD fdwReason, PVOID fImpLoad) { switch (fdwReason) { case DLL_PROCESS_ATTACH: // DLL第1次被映射到进程的地址空间时 break; case DLL_THREAD_ATTACH: // 创建一个线程时 break; case DLL_THREAD_DETACH: // 线程终止时 break; case DLL_PROCESS_DETACH: // 进程撤销一个DLL映射时 break; } return(TRUE); // 该返回值只在 DLL_PROCESS_ATTACH通知时有用,用来表示DLL的初始化是否成功,如果return FALSE表示加载DLL失败,如果系统会终止整个进程或撤销对该DLL的映射。其他通知时,系统将忽略这个返回值。 }
20.2.1 DLL_PROCESS_ATTACH通知
(1)只有当DLL第1次被映射到进程地址空间时,才会发送该通知。如果以后一个线程再调用LoadLibrary(Ex)来载入这个DLL,则只会递增该DLL的使用计数,但不会再发该通知。
(2)在该通知里,一般用来执行与进程相关的初始化,如创建DLL中一般函数要使用的堆。
(3)当DllMain处理DLL_PROCESS_ATTACH通知时,返回值用来表示DLL的初始化是否成功。如果return FALSE表示加载DLL失败,系统会终止整个进程(这种情况发生在刚创建进程时)或撤销对该DLL的映射(这种情况发生在显式调用LoadLibrary(Ex)时)。
(4)这个通知由进程中的某个线程来调用的。如果是刚创建新的进程时,则由主线程调用。如果某个线程调用LoadLibrary第1次显式载入这个DLL时,则由这个线程来调用执行这个通知,然后线程继续正常执行其他任务,如果return FALSE表示初始化失败,系统会撤销对DLL的映射并让LoadLibrary(Ex)返回NULL。
20.2.2 DLL_PROCESS_DETACH通知
(1)当系统将一个DLL从进程的地址空间中撤销映射时,发送该通知。(注意:在处理DLL_PROCESS_ATTACH时如果的返回FALSE时,那么就不会收到DLL_PROCESS_DETACH通知。)
(2)如果在处理DLL_PROCESS_ATTACH时返回FALSE,则DllMain就不会收到DLL_PROCESS_DETACH通知。
(3)如果是因调用ExitProcess而导致撤销DLL映射,则调用ExitProcess函数的线程将负责执行DllMain函数的代码。
(4)如果是因线程调用了FreeLibrary(或FreeLibraryAndExitThread)而撤销Dll映射,该线程将执行DllMain函数中的代码。该线程会直到处理完DLL_PROCESS_DETACH完才从FreeLibrary中返回。因此,如果该通知时死循环,则阻碍线程的终止,只有当每个DLL都处理完该通知后,操作系统才会真正地终止进程。
(5)如果某个线程调用TerminateProcess来终止进程,则系统不会发送DLL_PORCESS_DETACH通知。这意味着Dll没有机会执行一些清理代码的操作。因此,不到万不得己,应避免使用TerminateProcess函数。
20.2.3 DLL_THREAD_ATTACH通知
(1)当进程创建一个线程时,系统会向当前映射到该进程地址空间中的所有DLL发送DLL_THREAD_ATTACH通知。告诉这些DLL执行一些与线程相关的初始化。新创建的线程负责执行所有DLL中DllMain函数中相关的代码。只有当所有DLL完成了对该通知的处理后,新线程才会开始执行它的线程函数。
(2)当一个新的DLL映射到进程地址空间时,进程中己经有的线程不会是不会收到DLL_THREAD_ATTACH通知的。(即只有在创建新线程时,己经被映射到进程地址空间中的DLL才会收到这个通知)
(3)因创建进程时,何任被映射到进程地址空间中的DLL都会收到DLL_PROCESS_ATTACH通知,并由主线程负责执行,这里就可以执行一些相关的初始化工作,所以系统不会让主线程用DLL_THREAD_ATTACH来调用DllMain函数(即主线程只接收DLL_PROCESS_ATTACH通知,而不接收DLL_THREAD_ATTACH通知)。
20.2.4 DLL_THREAD_DETACH通知
(1)当线程函数返回后,系统会调用ExitThread来终止线程,但在终止前,这个线程会用DLL_THREAD_DETACH去调用所有己映射DLL的DllMain函数。告诉DLL执行与线程相关的清理操作(如C/C++运行库在这里可释放多线程应用程序的数据块)。
(2)如果该通知里有死循环,将妨碍线程的终止。只有当每个DLL都处理完DLL_THREAD_DETACH通知后,操作系统才会真正的终止线程。
(3)如果某个线程调用了TerminateThread来终止线程,那会系统将不会发送DLL_THREAD_DETACH通知给线程。这意味着DLL没有机会执行任何清理操作。
(4)如果在撤销一个DLL映射时,还有其他线程(正在运行),系统不会发送DLL_THREAD_DETACH给这些线程。(即这些线程不会用DLL_THREAD_DETACH来调用这个DLL的DllMain)
【注意】上面的规则可能会出现一个情况:当进程中的一个线程调用LoadLibrary来载入一个DLL时,系统会用DLL_PROCESS_ATTACH来调用该DLL的DllMain(但该线程不会得到DLL_THREAD_ATTACH通知)。接着,这个载入DLL的线程退出,这时该线程会收到DLL_THREAD_DETACH通知。由于这个原因,当进行与线程相关的清理里必须极为小心。一般调用LoadLibrary与调用FreeLibrary的线程应该是同一个线程。
20.2.5 DllMain的序列化调用
(1)系统会将对DLL的DllMain函数的调用序列化。在创建进程的时候,会同时创建一个锁(关键段,不同进程不会共享这个锁,这是关键段的特点!)
(2)当进程中的线程调用己映射的DLL的DllMain时,会用这个锁来同步各个线程(即不同线程不能同时执行DllMain函数中的代码,这种访问会被串行化)。
(3)禁止系统向某个DLL发送DLL_THREAD_ATTACH和DLL_THREAD_DETACH的方法
BOOL DisableThreadLibraryCall(HMOUDLE hInstDll);//可写在DLL_PROCESS_ATTACH通知中
【DllMain的错误调用】演示DllMain序列化调用导致的死锁问题
BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD fdwReason,PVOID fImpload){ HANDLE hThread; DWORD dwThreadId; switch (fdwReason) { case DLL_PROCESS_ATTACH: //DLL被映射到进程地址空间 //创建线程 hThread = CreateThread(NULL, 0, SomeFunction, NULL, 0, &dwThreadId); //将我们的线程(即调用LoadLibrary函数的线程,设为A)挂起,直到新的线程结束 WaitForSingleObject(hThread, INFINITE);//等待新线程结束 //注意 //1、当新线程(设为B)被创建时,系统会让B线程用DLL_THREAD_ATTACH去调用所有己经映射 //到进程的Dll(包括本Dll的DllMain),当执行本DllMain时,由于系统会用关键段去同步各个 //线程对DllMain调用(即DllMain调用的序列化),这时新线程B会被挂起去等待A线程执行完 //DllMain,但此时的A线程由于调用了WaitForSingleObject而挂起去等待B线程执行完毕, //这就形成了“死锁”。 //2、即使在该DLL里调用DisableThreadLibraryCall来禁止线程执行这个DLL里的 //DLL_THREAD_ATTACH也会发生这种死锁。因为在调用CreateThread函数时,这个函数内部 //会调用WaitForSingleObject,并传入进程的这个关键段,只有当新线程拥有这个关键段时 //才会用DLL_THREAD_ATTACH来调用每个DLL里的DllMain函数。所以在CreateThread新线程时 //这个新线程会因得不到关键段(因为被A线程拥有)而挂起。但A线程执行到WaitFor*的那行 //代码里,会挂起自己,所以仍然会造成死锁。 //不再需要新的线程了 CloseHandle(hThread); break; case DLL_THREAD_ATTACH: //线程被创建时 break; case DLL_THREAD_DETACH: //线程退出时 break; case DLL_PROCESS_DETACH://撤销DLL映射时 break; } return TRUE; }
20.2.6 DllMain和C/C++运行库
(1)用VC++编译器来构建DLL时,链接器会将DllMain函数调用在__DllMainCRTStartup函数里(也就是说,__DllMainCRTStarup才是Dll真正的入口函数!)
(2)在入口函数里会初始化C/C++运行库,并初始化所有全局或静态C++对象,保证收到在DLL_PROCESS_ATTACH通知之前,所有的全局的C++类实例的构造函数己经被调用。
(3)当Dll收到DLL_PROCESS_DETACH通知时,系统会再次调用这个入口函数,并进一步调用DllMain,当DllMain执行完后,__DllMainCRTStartup会调用Dll中所有全局或静态C++对象的析构函数。
(4)当接收DLL_THREAD_ATTACH或DLL_THREAD_DETACH时,__DllMainCRTStartup不会做任何的特殊处理。
(5)Dll源文件中的DllMain函数并不是必需的。如果在链接DLL时,链接器无法找到Dll源文件中(准确讲,应该是.obj里)的DllMain函数时,它会链接到C/C++运行库提供的DllMain函数。但C/C++运行库里的DllMain默认会处理成让该Dll不接收DLL_THREAD_ATTACH和DLL_THREAD_DETACH通知(可以从C/C++运行库提供的DllMain函数的实现中看到这一点,代码如下)
BOOL WINAPI DllMain(HINSTANCE hInstDll,DWORD fdwReason,PVOID fImpload){ if(fdwReason == DLL_PROCESS_ATTACH) DisableThreadLibraryCalls(hInstDll); return (TRUE); }