现在的位置: 首页 > 综合 > 正文

《WINDOWS核心编程第5版》随笔记录20

2018年02月07日 ⁄ 综合 ⁄ 共 11168字 ⁄ 字号 评论关闭

条目1、有2种方式将DLL的文件映像映射到调用线程所在进程的地址空间:(P524)

(1)   直接让应用程序的源代码引用DLL中所包含的符号(查阅:随笔记录19中条目7)

(2)   让应用程序在运行过程中通过LoadLibrary(Ex)和GetProcAddress显式地载入所需DLL及其函数地址

 

条目2、LoadLibrary(Ex)函数会根据“随笔记录19中条目8”中的搜索算法对DLL的文件映像进行定位。(P526)

 

条目3、LoadLibrary(Ex)函数返回的HMODULE表示文件映像被映射到的虚拟内存地址,HINSTANCE与之等价。(P526)

 

条目4、LoadLibraryEx函数的dwFlags的几个标志组合含义:

DONT_RESOLVE_DLL_REFERENCES -- 让系统只映射文件映像,不调用DllMain函数对DLL进行初始化,也不会将DLL所需的额外DLL自动载入到进程的地址空间中,因此在使用该DLL导出的任何函数时,存在极大风险:代码所依赖的内部数据结构可能尚未初始化,或者代码所引用的DLL尚未载入。(P526)

 

LOAD_LIBRARY_AS_DATAFILE -- 将DLL作为数据文件映射到进程的地址空间中,这点和DONT_RESOLVE_DLL_REFERENCES相似。它还会阻止系统根据DLL中的一些信息对DLL文件中的不同段设置页面保护属性。exe文件可以使用此标识被当作资源文件进行载入。(P527)

 

LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE -- 和LOAD_LIBRARY_AS_DATAFILE相似,多了一层以独占方式打开文件。(P527)

注意:此标识在Vista及其后续版本才被支持 。

 

LOAD_LIBRARY_AS_IMAGE_RESOURCE -- 和LOAD_LIBRARY_AS_DATAFILE相似,多了一层系统会修复DLL导入段的相对虚拟地址。(P527)

注意:此标识在Vista及其后续版本才被支持 。

下面试验验证以上内容的真实性,即LOAD_LIBRARY_AS_DATAFILE和LOAD_LIBRARY_AS_IMAGE_RESOURCE标识在系统是否修复了DLL导入段的相对虚拟地址上的区别。

验证步骤:

(1)  创建一个名为ResLibDemo的DLL。

 

(2)  创建一个名为CheckClient的测试应用程序。

以LOAD_LIBRARY_AS_DATAFILE标识载入DLL,测试结果如下:

Import table address : 0x003994F4
Import module name : USER32.dll
     MessageBoxA
Import module name : KERNEL32.dll
     ... ... 

     VirtualAlloc

     GetCurrentThreadId
     HeapReAlloc     
     MultiByteToWideChar    
     ... ...
    
Test Results:Relative virtual address is not fixed!

 

LOAD_WITH_ALTERED_SEARCH_PATH -- 改变LoadLibraryEx在对指定DLL进行定位时所使用的搜索算法。(P527)

有四种不同的算法:

(1)  如果lpFileName参数不包含'/'字符,那么使用“随笔记录19中条目8”中的搜索算法。

(2)  如果lpFileName参数包含'/'字符并且是全路径,那么LoadLibraryEx会试图直接载入DLL,若没找到就返回NULL。

(3)  如果lpFileName参数包含'/'字符并且是相对路径,那么LoadLibraryEx会将下列文件夹和lpFileName连接起来进行搜索:

      a、进程的当前目录

      b、Windows的系统目录

      c、16位Windows系统目录

      d、Windows目录

      e、PATH环境变量中列出的目录

(4)  调用SetDllDirectory会更改LoadLibrary(Ex)的搜索算法:

      a、进程的当前目录

      b、通过SetDllDirectory所设置的文件夹

      c、Windows的系统目录

      d、16位Windows的系统目录

      e、Windows目录

      f 、PATH环境变量中列出的目录

注意:如果用空字符串作为参数来调用SetDllDirectory,相当于把当前路径从搜索步骤中删除。如果传入的是NULL直,那么会恢复使用默认的算法。GetDllDirectory获取这个特定目录当前的值。(P529)

 

条目5、FreeLibraryAndExitThread可以使得DLL内的代码实现卸载自身并退出当前线程。如果在DLL内实现这个功能则不行,这是因为一旦DLL被卸载,DLL内的任何代码将变得无效。(P529)

 

条目6、系统会在每个进程中为每个DLL维护一个使用计数。线程可以通过GetModuleHandle函数检测仪个DLL是否已经被映射到进程的地址空间中。(P530) GetModuleFileName函数可以获得已加载DLL的全路径。(P531)

 

条目7、混用LoadLibrary和LoadLibraryEx可能会导致将同一个DLL映射到同一个地址空间中的不同位置。(P531)

标志LOAD_LIBRARY_AS_DATAFILE、LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE、LOAD_LIBRARY_AS_IMAGE_RESOURCE先检查该DLL是否已经被LoadLibrary(Ex)加载过(不是使用上面这几个标志加载)。若是,返回已经加载的地址。若否,则将DLL载入到地址空间中一个可用的地址并标识这个DLL未完全载入。

 

条目8、编译器/连接器始终都是将符号的名称以ANSI字符串的形式保存在DLL的导出段中,因此要注意GetProcAddress中的lpProcName参数接收的是PCSTR类型,非PCTSTR。(P532)

 

条目9、显式连接到DLL中的函数地址有2种方式:名称或符号。

(1)  FARPROC proc = GetProcAddress(hModule,"SomeFunction");

(2)  FARPROC proc = GetProcAddress(hModule,MAKEINTRESOURCE(2));

方式2比方式1效率高,但不通用,返回无效地址的几率较高。强烈不建议使用!

 

条目10、DLL的入口点函数为DllMain,其第一个参数代表DLL文件被映射到进程地址空间中所在的位置,第二个参数代表系统调用入口点函数的原因,第三个参数代表DLL被载入的方式,TRUE为显式载入,FALSE为隐式载入。(P562)

 

条目11、入口点函数DllMain的一段关键性说明:(P534)

必须记住,DLL使用DllMain函数来对自己进行初始化。DllMain函数执行的时候,同一个地址空间中的其他DLL可能还没有执行它们的DllMain。这意味着它们尚未初始化,因此我们应该避免调用那些从其他DLL中导入的函数。此外,我们应该避免在DllMain中调用LoadLibrary(Ex),因为这些函数可能会产生循环依赖。

DllMain入口点函数在执行的时候存在一些限制,这些限制与获取进程范围内的加载程序锁(loader lock)有关。具体细节参阅http://www.microsoft.com/whdc/driver/kernel/DLL_bestprac.mspx

 

条目12、系统调用入口点函数的几个原因(fdwReason参数):

DLL_PROCESS_ATTACH  -- 当DLL第一次被映射到进程的地址空间中时,系统会调用DllMain并传入该值。当DllMain处理DLL_PROCESS_ATTACH通知的时候,DllMain的返回值用来表示该DLL初始化是否成功。如果fdwReason是其他值,那么系统将忽略DllMain的返回值。创建进程时,由主线程负责调用各个被载入DLL的DllMain入口点函数。动态加载DLL时,由调用LoadLibrary(Ex)函数的线程负责调用DLL的入口点函数。(P534)

 

DLL_PROCESS_DETACH  -- 当系统将一个DLL从进程的地址空间中撤销映射时,会调用DllMain并传入该值。如果撤销映射的原因是因为进程要终止,那么调用ExitProcess函数的线程将负责执行DllMain函数的代码。如果撤销映射的原因是因为进程中的某个线程调用了FreeLibrary或FreeLibraryAndExitThread,那么由这个线程负责执行DllMain函数的代码。(P535)

注意:DLL可能会阻碍进程的终止,只要当每个DLL都处理完DLL_PROCESS_DETACH通知之后,系统才会真正地终止进程。如果进程终止是调用TerminateProcess,那么系统不会用DLL_PROCESS_DETACH 调用DLL的DllMain函数,这会使系统没机会执行DLL清理代码,导致数据丢失。(P536)

 

DLL_THREAD_ATTACH  -- 当进程创建一个新线程时,会用这个新线程调用已经载入的DLL的DllMain并传入该值。当新的DLL载入进程时,进程内任何已有的线程不会传入该值调用DllMain。(P537) 系统不会让进程的主线程用DLL_THREAD_ATTACH值来调用DllMain函数。(P538)

 

DLL_THREAD_DETACH  -- 当线程终止时,该线程会用DLL_THREAD_DETACH值调用已载入的DLL的DllMain函数。只有当所有的DLL处理完该通知,系统才会让该线程终止。调用TerminateThread会使得系统不会调用DllMain来执行该线程的DLL清理工作。(P538)

 

条目13、系统会将对DLL的DllMain函数的调用序列化。(P538) 通过进程级别的锁进行控制。

 

条目14、DisableThreadLibraryCalls可以让系统不向某个指定的DLL的DllMain函数发送DLL_THREAD_ATTACH和DLL_THREAD_DETACH通知。(P540)

 

条目15、C/C++运行库的DLL启动代码能够在DllMain函数安全使用全局或静态C++对象之前,保证它的构造函数已经被调用过。(P541)

 

条目16、一个延迟载入的DLL是隐式链接的。Virtual C++提供的延迟载入特性存在以下局限性:(P543)

(1)   一个导出了字段(数据,即全局变量)的DLL无法延迟载入。

(2)   Kernel32.dll模块无法延迟载入。

(3)   不应该在DllMain函数中调用一个延迟载入的函数,这会导致程序崩溃。

查阅:http://msdn.microsoft.com/en-us/library/yx1x886y(VS.80).aspx

 

条目17、延迟载入DLL的使用步骤:

(1)   创建一个DLL命名为DelayLib

(2)   创建一个应用程序命名为DelayClient,使用链接开关 /Lib:DelayImp.lib和/DelayLoad:DelayLib.dll。

(3)   运行结果:

DelayLib is not loaded
fun(1,2)=3
DelayLib has been loaded

 

(4)   卸载延迟载入的DLL:应用程序添加链接选项/Delay:unload,源代码中调用__FUnloadDelayLoadedDLL2函数进行卸载。

运行结果:

DelayLib is not loaded
fun(1,2)=3
DelayLib has been loaded
DelayLib is not loaded

 

条目18、函数转发器是DLL输出段中的一个条目,用来将一个函数调用转发到另一个DLL中的函数上。系统在调用转发函数时,会递归的调用GetProcAddress来获取真实的函数地址。(P553)

查看:dumpbin.exe -exports FunForwarder.dll

Dump of file FunForwarder.dll

File Type: DLL

  Section contains the following exports for FunForwarder.dll

    ... ...

    ordinal hint RVA      name

          1    0          MyMessageBox (forwarded to User32.MessageBoxA) //这里!函数转发!
          2    1 00001000 fun

   ... ...

 

条目19、系统对已知DLL做了一些特殊处理,它们的位置由HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Control/Session Manager/KnownDLLs指定。当LoadLibrary(Ex)传入的DLL名称不包含后缀.dll时,系统会按正常的搜索规则来搜索这个DLL。若包含后缀.dll,那么系统先将后缀去掉,然后从以上的注册表中查找是否有与之相符合的值名,并加载与值名对应的数据,并试图用该数据载入DLL。例如:注册表中存在SomeLib - SomeLibOther.dll,当您使用LoadLibrary(Ex)并传入"SomeLib .dll",那么系统实际上加载的DLL是SomeLibOther.dll而不是SomeLib.dll。(P554)

 

条目20、DLL重定向强制操作系统的加载程序首先从应用程序的目录中载入模块。可以通过创建一个<AppName>.local文件或文件夹,来强制加载程序总是先检查应用程序的目录。(P555)

注意:为了安全起见,Vista中这项特性默认情况是关闭的,为了打开这项特性,我们必须在HKLM/Software/Microsoft/WindowsNT/CurrentVersion/Image File Executiion Options项中添加一个条目DWORD DevOverrideEnable,并将它设置为1。(P555)

 

条目21、Dumpbin.exe -headers <DllName> 可以查看DLL文件的首选基地址。

Dump of file FunForwarder.dll

PE signature found

File Type: DLL

FILE HEADER VALUES
            ... ...

OPTIONAL HEADER VALUES
            ... ...
        10000000 image base (10000000 to 1000CFFF)  <--- 这里!
            ... ...

Summary

        2000 .data
        2000 .rdata
        1000 .reloc
        7000 .text

 

条目22、当链接器在构建我们的模块时,会将重定位段嵌入到生成的文件中。如果加载程序无法将模块载入到它的首选基地址,那么系统会打开模块的重定位段并遍历修复其中所有的条目。(P558)

 

条目23、如果一个模块无法被载入到它的首选基地址时,存在两个主要缺点:

(1)   增加了应用程序的初始化时间,降低了性能。

(2)   当加载程序修复重定位条目时,写时拷贝机制会将系统的页交换文件作为该模块的后备存储器,这样减少了系统的虚拟内存数量。

 

条目24、Rebase.exe实用工具可以根据应用程序的具体情况修复DLL的首选基地址,这样可以避免重定位段条目的修复。(P561)

Rebase工具的内部执行步骤:

(1)   模拟创建一个进程地址空间

(2)   打开应该被载入到这个地址空间中的所有模块,并得到每个模块的大小以及他们的首选基地址

(3)   在模拟的地址空间中对模块重定位的过程进行模拟,使各模块之间没有交叠

(4)   对每个重定位过的模块,解析该模块的重定位段,并修改模块在磁盘文件中的代码

(5)   为了反映新的首选基地址,它会更新每个重定位过的模块的文件头

使用办法:Rebase -b 0x00400000 xx.exe xx.dll xxx.dll ... ...

 

条目25、加载程序将符号的虚拟地址写入到可执行文件模块的导入段中,实际是写入到导入段的后备存储页面。我们直接采用模块绑定技术,用模块导入的所有符号的虚拟地址,对模块的导入段进行预处理。这样可以使得应用程序更快的初始化并使用更少的后备存储器。

 

条目26、Bind.exe实用工具用于模块绑定。其内部执行步骤如下:

(1)   打开指定的映像文件的导入段

(2)   对导入段中列出的每个DLL,它会查看该DLL文件的文件头,来确定该DLL的首选基地址

(3)   在DLL的导出段中查看每个符号

(4)   提取符号的RVA,并将它与模块的首选基地址相加,结果写入到映像文件的导入段中

(5)   在映像文件的导入段中添加一些额外的信息。这些信息包括映像文件被绑定到的各DLL模块的名称,以及各模块的时间戳

整个过程中,Bind做了两个重要的假设:

(1)   所需DLL载入到其首选基地址

(2)   绑定完成后,DLL导出段中所引用的符号的位置没有发生变化

使用办法:Bind xx.exe xx.dll xxx.dll .. ...

抱歉!评论已关闭.