WINDOWS+PE权威指南读书笔记(14)

目录

延迟加载导人表

延迟加载导入的概念及其作用:

提高应用程序加载速度:

提高应用程序兼容性:

提高应用程序可整合性:

PE 中的延迟加载导入表:

延迟加载导入表数据定位:

延迟加载导入描述符 IMAGE_DELAY_IMPORT_DESCRIPTOR:

延迟加载导入表实例分析:

延迟加载导入机制详解:

延迟加载导入编程:

修改资源文件 pe.rc:

修改源代码 pe.asm:

关于延迟加载导入的两个问题:

异常处理:

DLL 的卸载:

小结:


延迟加载导人表

延迟加载导和人表是 PE 中引入的专门用来描述与动态链接库延迟加载相关的数据,因为这些数据所起的作用和结构与导入表数据基本一致,所以称为延迟加载导入表。

延迟加载导入表和导入表是相互分离的。一个 PE 文件中可以同时存在这两种数据,也可以单独存在一种。延迟加载导入表是一种特殊类型的导人表,同导入表一样,它记录了应用程序要导入的部分或全部动态链接库及相关的函数信息。与导入表不同的是,它所记录的这些动态链接库并不会被操作系统的 PE 加载器加载,只有等到由其登记的相关函数被应用程序调用时,PE 中注册的延迟加载函数才会根据延迟加载导和人表中对该函数的描述,动态加载相关链接库并修正函数的 VA 地址,实现对函数的调用。

延迟加载导入的概念及其作用:

延迟加载导入是一种合理利用进程加载机制提高进程加载效率的技术,使用延迟加载导入能跳过加载前对引入函数的检测及加载后对 IAT 的修正,从而避免出现诸如 “无法找到组件” 的错误提示,提高程序的适应性。

通过前面导入表部分的学习我们知道,一个应用程序要调用动态链接库的某个函数,需要先在程序中静态引入该动态链接库,编译器在编译时会分解调用该引入函数的 invoke 指令,并将其调用最终指向 IAT。PE 加载器要完成的任务就是根据导入表的描述,将 IATI 中的地址修正为函数在进程地址空间的真实地址 VA,这样就能保证该函数被正确调用。

在以上描述中,程序要正确运行,必须保证该动态链接库能够在进程环境变量指定的 PATH 中找到,如果程序已经开始运行,无论指令指针寄存器 eip 是否指向调用引入函数的指令,如果此时相应的 DLL 文件还未出现在路径中,就会导致错误出现:

延迟加载导入的概念:

系统开始运行程序时被指定的延迟加载的 DLL 是不被载入的,只有等到程序调用了该动态链接库的函数时,系统才将该链接库载入内存空间,并执行相关函数代码。

延迟加载导人技术在适应的场合:

口 提高应用程序加载速度

口 提高应用程序兼容性

口 提高应用程序可整合性

提高应用程序加载速度:

如果一个应用程序使用了很多的 DLL,PE 加载器在将程序映像加载到虚拟地址空间的时候,同时也会把所有的 DLL 一起提前加载到进程空间,而且在加载每个 DLL 时还会调用 DLL 的入口函数,对 DLL 进行初始化,尽管这时候程序并没有开始调用这些引入的动态链接库的函数。这些操作的存在使得进程加载时会耗费一些时间,可能会使程序加载速度受到影响,而延迟加载则可以完全避开这一点。这就好像安排一项多人要完成的工作,只有当需要某人的时候才正式通知他一样。

提高应用程序兼容性:

同一个 DLL 在不同时期会有不同的版本。一般情况下,新的 DLL 除了对原有函数的继承和优化外,通常还会增加一些新的函数。如果我们在应用程序中调用了一个 DLL 的新函数,运行时所在环境提供的却是老的 DLL,那么加载时系统就会提示错误,然后拒绝执行应用程序。如果我们在代码中先对运行的环境进行检测,发现存在老的 DLL,则不再调用这个不存在的函数,要么友好提示,要么通过其他方式实现该函数的功能。这样就可以保证在没有新 DLL 的环境中,程序依旧可以被 PE 加载器加载并运行。举个简单例子:

举个简单例子:

假设现在有 DLL 的两个不同版本,旧的 MyDll.dll 和新的 MyDll.dll,其中在新的 MyDIl.dll 中又增加了一个函数 _getImportDescriptor()。

应用程序中部分代码如下:

把这段代码按照正常的编码方式放到一个源文件里,然后编译、链接,链接的时候一定会出现错误。即使你让链接通过了,运行时也会出现错误。如果我们在链接时告诉链接器新的 MyDll.dll 使用加载延迟加载导入的方法,链接器就会为我们单独处理这个函数的调用,从而避免错误的出现。这种提高应用程序适应不同环境的能力,称为可移植的兼容性。

提高应用程序可整合性:

不要被可整合性这么难懂的概念吓倒。在现实中总是存在一些有特别嗜好的程序员,比如笔者,受早期使用MS-DOS 下应用程序的习惯影响,并不喜欢目前 Windows 下应用程序的安装方式。在 Windows 系统下,程序运行需要先安装,不需要的时候还要通过控制面板印载,与程序有关的数据并不是仅仅存储在一个独立的目录下,而是遍及整个磁盘,如运行时库所在目录、注册表、系统目录、系统的配置管理器目录等。这把一个完整的程序变得四分五裂,在程序的后期管理维护和移植上制造了很多麻烦。为了使软件易于安装,于是软件就有了绿色的概念,我喜欢绿色软件,更倾向于将所有的东西全部存储在一个文件里。想拷走的时候就仅仅复制一个文件,与文件有关的配置信息、数据库、链接库等都在一个文件里,那该是多么好的一件事情! 这里指的可整合就是这种情况。

PE 中的延迟加载导入表:

在 Windows XP SP3 的大部分系统 PE 文件中,都存在延迟加载导入表数据。延迟加载导入表数据的整体组织与导入表类似,也存在 INT 和 IAT 双桥结构,在本书第 4 章导入表中已做了详细描述。下面通过一个实例来看 PE 中的延迟加载导入表。

延迟加载导入表数据定位:

首先回顾一下PE头中的数据目录项 IMAGE_DATA_DIRECTORY:

延迟加载导和数据为数据目录中注册的数据类型之一,其描述信息处于数据目录的第 14 个目录项中

使用PEDump 小工具获取 chapter8\HelloWorld.exe 的数据目录表内容如下:

加粗部分即为延迟加载导入数据目录信息。通过以上字节码得到如下信息:

口 延迟加载导入数据所在地址 RVA=0x00000203c

口 延迟加载导入数据大小 =00000040h

以下是使用小工具 PEInfo 获取的该文件所有的节信息:

根据 RVA 与FOA 的换算关系,可以得到:

延迟加载导人数据所在文件的偏移地址为 0x0000083c。

延迟加载导入描述符 IMAGE_DELAY_IMPORT_DESCRIPTOR:

延迟加载导入数据目录指向的位置为延迟加载导入描述结构 IMAGE_DELAY_ IMPORT_DESCRIPTOR,本结构的详细定义如下:

IMAGE_DELAY_IMPORT_DESCRIPTOR.Attributes:

+0000h,双字。属性,暂时未用,链接器在生成映像文件时将此字段设置为 0。用户可以在将来扩展这个结构时用它来指明添加了新字段,或者用它来指明延迟加载导入或卸载辅助函数的行为。

IMAGE_DELAY_IMPORT_DESCRIPTOR.Name:

+0004h,双字。指向延迟加载导和人的动态链接库的名字字符串的地址,该地址是一个 RVA。

IMAGE_DELAY_IMPORT_DESCRIPTOR.ModuleHandle:

+0008h,双字。被延迟加载的 DLL 模块句柄的 RVA。该 RVA 位于 PE 映像的数据节中,延迟加载辅助函数使用这个位置存储要被延迟加载的 DLL 的模块句柄。

IMAGE_DELAY_IMPORT_DESCRIPTOR.DelayIAT:

+000Ch,双字。延迟加载导入地址表的 RVA。延迟加载辅助函数用导入符号的实际地址来更新这些指针,以便起转换作用的这部分代码不会陷入循环调用之中。

IMAGE_DELAY_IMPORT_DESCRIPTOR.DelayINT:

+0010h,双字。和延迟加载导入名称表 (INT) 包含了可能需要被加载的导入符号的名称。它们的排列方式与 IAT 中的函数指针一样,它们的结构与标准的 INT 一样。结构的详细信息请参照第 4 章导和入表部分。

IMAGE_DELAY_IMPORT_DESCRIPTOR.BoundDelayIT:

+0014h,双字。延迟绑定导入地址表 (BIAT) 是由 IMAGE_THUNK_DATA 结构组成的数组,它是可选的。它与延迟加载目录表中的 TimeStamp 字段一起被用于后处理绑定阶段。

IMAGE_DELAY_IMPORT_DESCRIPTOR.UnloadDelayIT:

+0018h,双字。延迟卸载导入地址表 (UIAT) 是由 IMAGE_THUNK_DATA 结构组成的数组,它是可选的。卸载代码用它来处理明确的卸载请求。它由只读节中已初始化的数据组成,这些数据是原始 IAT 的精确副本。在处理卸载请求时,可以释放这个 DLL,同时将IMAGE_DELAY_IMPORT_DESCRIPTOR.ModuleHandle 清零,并用 UIAT 覆盖IAT,以便将一切还原到预加载时的状态。

IMAGE_DELAY_IMPORT_DESCRIPTOR.TimeStamp

+001Ch,双字。表示应用程序绑定到 DLL 的时间戳。

延迟加载导入表实例分析:

从文件 chapter8\HW2.exe 的 0x0000083c 处取出 40h 字节,如下黑体部分:

>> 30 20 40 00

IMAGE_DELAY_IMPORT_DESCRIPTOR.Name。指向文件偏移 0x00000830 开始的字符串 “MyDll.dll”。

>> 1C 30 40 00

IMAGE_DELAY_IMPORT_DESCRIPTOR.ModuleHandle。指向 .data 段,文件偏移 0x00000a1c处,此处用来存放 MyDll.dll 的模块句柄。

>> 14 30 40 00

IMAGE_DELAY_IMPORT_DESCRIPTOR.DelayIAT。指向了延迟加载导入的IAT,位于文件偏移的 0x00000a14 位置处。该位置位于 .data 段。以下是该位置的字节码:

红框框起来的部分用于在运行时存放 MyDll.dll 的句柄。

>> 7C 20 40 00

IMAGE_DELAY_IMPORT _DESCRIPTOR.DelayINT。指向了延迟加载导入的 INT,位于文件偏移的 0x0000087c 位置处,该位置位于 .rdata 段。

从该处取出的值为 0x00402084,它指向了函数 sayHello 的 hint/name 描述: 00 00 / 73 61 79 48 65 6C 6C 6F 00 00。

>> 90 20 40 00

IMAGE_DELAY_IMPORT_DESCRIPTOR.BoundDelayIT。指向了绑定延迟加载导入表,位于文件偏移 0x00000890 位置处,该位置位于 .rdata 段。

从该处取出的值为 0x00000000,表示该映像文件无绑定延迟加载导入定义。

源代码分析:

现在来看前面分析的程序 HelloWorld.exe 的源代码,见代码清单 8-1。该文件可以在随书文件 chapter8 目录下的 HelloWorld.asm 中找到。

按照常规方法对该程序进行编译链接,然后执行程序,命令序列如下:

执行结果没有发生问题,如约地弹出了对话框:

接下来对源代码重新指定链接参数,命令序列如下:

将第一次生成的 HelloWorld.exe 更名为hw1.exe,将第二次生成的 HelloWorld.exe 更名为hw2.exe,然后分别运行两个文件,发现两个执行结果完全一样,看起来两个 PE 文件执行并没有区别。

接下来,将 MyDll.dll 换个名字或者直接删除,再来执行这两个程序。现在应该看出区别了。hw1.exe 提示了错误信息,而 hw2.exe 则可以正常运行。原因就是 hw2 使用了延迟加载导入技术,而 hw1 没有使用延迟加载导入技术!

细心的你一定发现了在第二次链接时,我们使用了一个外来的 delayimp.lib 库,该库函数从 C 语言的 SDK 中获得。那么到底是什么机制使得延迟加载导入的技术生效了呢? 仔细对比两个可执行程序,尽管源代码完全一样,但由于链接时链接器根据参数对 PE 文件进行了改动,所以其最终生成的 PE 文件大小却大相径庭,前者 2560 字节,后者 3072 字节。多出的部分即为辅助实现延迟加载机制的代码。

延迟加载导入机制详解:

系统开始运行程序时被指定的延迟加载的 DLL 是不被载入的,只有等到程序调用了该动态链接库的函数时,系统才将该链接库载和内存空间,并执行相关函数代码

当链接器接收到以下参数(加黑部分) 时,会做以下事情:

link -subsystem:windows -delayload:MyDll.dll delayimp.lib Helloworld.obj

首先,将一个函数 _delayLoadHelper 嵌入 PE 文件的可执行模块。其次,从可执行模块的导入表部分删除 MyDll.dll 及相关信息。这样当进程初始化的时候,操作系统的加载程序就不会显式加载该动态链接库了。

最后,在 PE 中把刚才删除的相关信息重新构造好(注意构造的是新的信息),以便告诉 _delayLoadHelper 哪些函数是从 MyDll.dll 中导出的。

当应用程序运行时,对延迟加载函数的调用实际上是对函数 _delayLoadHelper 的调用。该函数知道链接器创建的与 MyDll.dll 有关的导入信息,并且还能自己通过函数 LoadLibrary 动态加载 DLL 文件,然后调用函数 GetProcAddress 获取引入函数的地址信息。一旦获得延迟加载函数的地址,函数 _delayLoadHelper 的使命就终止了。下一次该函数再被调用时,就会直接跳转到函数的 VA 处执行,而不再像第一次执行函数 _delayLoadHelper 那样了。

以下是 hwl.exe 和 hw2.exe 指令字节码的对比情况:

hwl.exe 的指令字节码如下:

嵌入代码 _delayLoadHelper 的 hw2.exe 指令字节码如下:

可以看到,未加黑部分是完全一样的,链接器修改了hw1.exe 的最后一个跳转指令 FF 25 00 20 40 00,在其后加入了大量的代码。

关于新增加的这些指令字节码对应的反汇编,大家可以通过调试 hw2.exe 自己分析,此处只从 OD 中截选了一些友好的提示信息,大家可以从这些提示中获取一些关于函数 _delayLoadHelper 的信息。

由上面所列的零零星星的函数调用可以看出,函数调用了动态链接库 kernel32.dll 的一些比较特殊的函数,如 GetProcAddress、LoadLibrary 和 FreeLibrary 等。这些 API 函数主要实现的功能是动态加载 / 卸载动态链接库,获取指定函数的地址。可以看出,函数 _delay LoadHelper接管了本该 PE 加载器要做的工作,在合适的时机将动态链接库调入内存,并覆盖相关调用函数的指令操作数,执行函数调用。

延迟加载导入编程:

本节以一个相对复杂的 “提高应用程序可整合性” 为例,为读者演示延迟加载导和人的作用。该例子以第 2 章中的 pe.asm 为基础,在现有代码上进行简单地修改。

修改资源文件 pe.rc:

在资源文件中将动态链接库 winResult.dll 文件作为自定义资源加入,相关定义如下:(nameID 资源类型 [DISCARDABLE] 位图文件名)

修改源代码 pe.asm:

在源代码中增加释放资源中的动态链接库函数 winResult.dll 的代码,详见代码清单:

函数 _createDll 使用了操作 PE 资源的 API 函数,要访问资源中的自定义资源,需要经过以下几步:

步骤1 调用函数 FindResource 查找资源。函数需要传人要查找资源的类别及资源 ID (代码第 10 行)。

步骤2 调用函数 SizeofResource 获取资源的尺寸。将获取的尺寸存储在变量 dwResSize 中(代码第 13 行)。

步骤3 调用函数 LoadResource 将查找到的资源加载进内存,以便访问(代码第 15 行)。

步骤4 调用函数 LockResource 锁定资源。只有被锁定的资源才可以通过内存地址指针进行读写(代码第 17 行)。

行 19 一28是复制已锁定的资源内容到文件,从而完成对 winResult.dll 的重新创建。

主程序启动后从标号 start 处开始运行,通过调用函数 _createDll 释放存储在 PE 资源里的动态链接库 winResult.dll。这意味着 pe.exe 程序在调用 winResult.dll 里的函数 sayHello 之前,已经完成了此操作。当 sayHello 被调用时相关的动态链接库文件就已经可以在当前路径里找到了。

以下是主程序代码中释放动态链接库的代码:

按照常规步骤编译资源文件、编译源文件、链接程序,然后将最终生成的 pe.exe 复制到其他位置(没有动态链接库的位置),执行时是失败的。

重新按延迟加载导入的步骤编译、链接程序,最终生成的 pel.exe 即可随意复制到任何目录下运行了。程序在运行前会先从自体资源中释放要调用的 winResult.dll。

当然,你也可以通过这种方法,将所有与你要发布的程序相关的其他文件附加到程序本体的资源中,然后在运行期的开始阶段重新从资源表里将这些文件复制出来。你也可以在本书的第 18 章看到另外一种绑定文件的方法。

关于延迟加载导入的两个问题:

异常处理:

通常情况下,当操作系统的加载程序加载可执行模块时,它都会设法加载必要的 DLL。如果一个 DLL 无法加载 (比如不存在该 DLL 文件),那么加载程序就会显示一条错误消息。如果该DLL 是通过延迟加载的DLL,在进行初始化时操作系统并不负责检查是否存在该DLL。如果调用延迟加载的函数时无法找到该DLL,函数 _delayLoadHelper 就会引发一个软件异常。该异常可以使用结构化异常处理(SEH) 方法捕获。关于 SEH 的详细介绍,请参照第 10 章。如果不跟踪该异常,你的进程就会被终止运行。

当函数 _delayLoadHelper 确实找到了你的 DLL,但是要调用的函数却不在该 DLL 中时,将会出现另一个问题。比如前面提到的,如果加载程序找到一个老的 DLL 版本,就会发生这种情况。在这种情况下,函数 _delayLoadHelper 也会引发一个软件异常,针对这个软件异常的处理方法与上面相同。

DLL 的卸载:

如果程序中调用完通过 _delayLoadHelper 加载的 DLL 文件的函数后,很长一段时间不再需要该动态链接库,那么就可以释放该 DLL,比如有的打印任务,只需调用打印操作一次,即可以将打印任务相关的函数对应的动态链接库卸载。这要求程序开发者首先要在链接器的参数中加入 -delay:unload 开关来设置允许对 DLL 的释放操作,该开关负责将另外一段代码 _FUnloadDelayLoadedDLL 加入 PE 文件中,然后在需要释放 DLL 的代码位置调用该函数。该函数会将PE 中加载进来的相关信息进行清理,最后主动调用函数 FreeLibrary 来卸载 DLL。这类似于程序设计中的对的操作一样:

切记千万不能自己去调用 FreeLibrary 来卸载 DLL,否则函数的地址将不会被清除。这样,当下次试图调用 DLL 中的函数时,就会导致访问违规。另外,当调用函数_FUnload DelayLoadedDLL 时,传递的 DLL 名字不应该包含路径,而且名字中的字母必须与你将 DLL 名字传递给 -delayload 链接程序开关时使用的字母大小写相同,否则,对函数 _FUnloadDelayLoadedDLL 的调用将会失败。如果你永远不打算卸载延迟加载的 DLL,那么请不要在链接器中设定 -delay:unload 链接程序开关。

小结:

本章首先从延迟加载导人技术出发,探讨了该技术的应用背景,并分析了 PE 中的延迟加载导入表数据述,最后通过一个实例演示了延迟加载导入在程序设计中的编程方法。

PE 中的许多数据其实都是为某些特性而存在的,在不同的场合使用不同的技术,这充分显示出了 PE 文件良好的可扩展性。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

沐一 · 林

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值