65.9K
CodeProject 正在变化。 阅读更多。
Home

动态库与延迟函数加载

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2011 年 10 月 25 日

公共领域

4分钟阅读

viewsIcon

26029

downloadIcon

321

本文解释了如何创建一个动态库,该库在导出函数首次使用时加载它们,而不是在库加载时加载它们。

引言

本文解释了如何创建一个动态库,该库在导出函数首次使用时加载它们,而不是在库加载时加载它们。我们将以 Win32 平台和 DLL 格式为例进行说明,但该概念可以应用于其他平台和库格式。

请注意,此概念与标准的延迟库加载不同,标准延迟库加载会一起加载整个库(即所有函数)!使用此机制,您可以根据需要从同一 DLL 加载单个函数,然后独立于其他函数释放它们。

动态库如何加载到内存

当应用程序请求操作系统加载特定库(假设该库尚未在内存中)时,OS 加载器会首先定位库文件。然后,会分配具有代码执行属性的内存,并将库从文件读入已分配的缓冲区。在库代码可以执行之前,必须将库代码中的内存引用重新定位,以便在需要时与库的基地址匹配(有时不需要重新定位,当库代码位于默认基地址时)。在此初始化之后,库的二进制代码就可以执行了。通常,库内会调用标准的初始化过程(对于 DLL 文件是 DllMain)。

如何从 DLL 调用函数

如果二进制代码需要调用位于动态库中的函数,它必须首先使用已加载库的句柄和函数名称来定位函数地址。这通过调用 Win32 API 函数 GetProcAddress 来完成。操作系统使用 DLL 导出目录来确定函数地址。DLL 导出目录是位于 DLL 元信息中的特殊表,其中包含所有导出函数的名称及其入口地址,这些入口地址在 DLL 加载后在重定位过程中进行调整。GetProcAddress 返回库函数的入口地址,调用代码可以使用该地址来调用它。调用方必须知道调用约定(函数头)。

利用导出目录动态加载函数

我们如何利用上述加载/调用机制来实现函数级别的懒加载?

一种可能的解决方案是创建一个带有单个加载器函数的代理 DLL。该函数唯一的任务是分配内存,读取有效负载 DLL 文件,并将请求函数的二进制代码复制到已分配的缓冲区,然后将所有参数到位地传递给加载的函数。代理 DLL 的导出条目应更新为已分配缓冲区中函数代码的入口地址。

如果代理 DLL 加载器包含指向单个加载器函数的所有有效负载 DLL 的导出条目,则调用代码可以直接使用代理 DLL,而无需任何更改,只需将库引用更改为另一个 DLL 文件即可。

您可以使用 此工具 来帮助您生成要创建的代理 DLL 的 DEF 文件。

所述技术的特性

复杂性

  • 如果一个导出函数依赖于另一个导出函数或一个非导出函数,则必须考虑依赖关系,并加载其他函数。维护函数级别的依赖关系图至关重要。
  • 像 Windows 7 这样的现代操作系统具有防止执行未标记为代码的内存以及向代码页面写入的机制。需要进一步考虑才能使此技术符合后者的约定。
  • 与杀毒软件结合可能导致误报。

优点

  • 对于具有大量相对独立函数的 DLL,此技术可以通过仅将必要函数加载到内存中来节省操作系统内存。
  • 加载的函数可以由垃圾收集器释放,以节省内存,如果它们在特定时间后不再需要,或者如果系统内存不足。

缺点

  • 开销加载器代码会在函数首次加载时带来性能损失。
  • 在函数数量很少的小型 DLL 或函数高度相互依赖的大型 DLL 中,该技术不会带来任何优势。只有在具有相对独立的导出函数的 DLL 中,使用它才有意义。

附加评论

  • 在适当的情况下,该技术以更长的执行时间为代价,提供了更小的内存占用。

其他用途

所述技术的另一种可能应用是使用代理 DLL 加载有效负载 DLL,然后重定向每个函数调用,在每次调用/返回时执行开销代码。
这可用于调试、收集统计信息、跟踪参数和返回值、对参数和返回值应用转换、监视等。

延伸阅读

代码示例

您可以在本文随附的示例 Visual C++ 2008 解决方案中找到。该代码仅用于说明目的。因此,为了保持代码足够简单,避免分散读者的注意力,有意忽略了错误处理和通用性。鼓励读者开发更健壮的解决方案并提供反馈。

© . All rights reserved.