浅墨浓香

想要天亮进城,就得天黑赶路。

导航

第20章 DLL高级技术(1)

Posted on 2015-11-27 22:26  浅墨浓香  阅读(1787)  评论(0编辑  收藏  举报

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);
}