繁体   English   中英

从 dll 加载 dll?

[英]Loading a dll from a dll?

从 dll 加载 dll 的最佳方法是什么?

我的问题是我无法在process_attach上加载dll,也无法从主程序加载dll,因为我无法控制主程序源。 因此我也不能调用非 dllmain 函数。

在评论中进行了所有辩论之后,我认为最好在“真实”答案中总结我的立场。

首先,目前还不清楚为什么需要使用LoadLibrary在DllMain中加载一个dll。 这绝对是一个坏主意,因为你的DllMain被另一个LoadLibrary调用,其持有加载程序锁,由解释内部运行的DllMain的文件

在初始进程启动期间或调用 LoadLibrary 之后,系统会扫描进程的已加载 DLL 列表。 对于尚未使用 DLL_PROCESS_ATTACH 值调用的每个 DLL,系统将调用 DLL 的入口点函数。 此调用是在导致进程地址空间更改的线程的上下文中进行的,例如进程的主线程或调用 LoadLibrary 的线程。 对入口点的访问由系统在进程范围的基础上进行序列化。 DllMain 中的线程持有加载器锁,因此无法动态加载或初始化额外的 DLL。
入口函数应该执行简单的初始化或终止任务 不能调用 LoadLibrary 或 LoadLibraryEx 函数(或调用这些函数的函数) ,因为这可能会在 DLL 加载顺序中创建依赖循环。 这可能导致在系统执行其初始化代码之前使用 DLL。 类似地,入口函数在进程终止期间不得调用 FreeLibrary 函数(或调用 FreeLibrary 的函数),因为这可能导致在系统执行其终止代码后使用 DLL。
(强调)

所以,这就是为什么它被禁止; 如需更清晰、更深入的解释,请参阅,有关如果您不遵守 DllMain 中的这些规则会发生什么情况的其他示例,另请参阅Raymond Chen 的博客中的一些帖子。

现在,关于 Rakis 的回答。

正如我已经多次重复的那样,你认为的 DllMain,并不是真正的 dll 的 DllMain; 相反,它只是一个由 dll 的真实入口点调用的函数。 反过来,CRT 会自动使用这个来执行其额外的初始化/清理任务,其中包括全局对象和类的静态字段的构造(实际上所有这些从编译器的角度来看几乎是相同的)事物)。 在它完成这些任务之后(或之前,为了清理),它调用您的 DllMain。

它有点像这样(显然我没有写所有的错误检查逻辑,只是为了展示它是如何工作的):

 /* This is actually the function that the linker marks as entrypoint for the dll */ BOOL WINAPI CRTDllMain( __in HINSTANCE hinstDLL, __in DWORD fdwReason, __in LPVOID lpvReserved ) { BOOL ret=FALSE; switch(fdwReason) { case DLL_PROCESS_ATTACH: /* Init the global CRT structures */ init_CRT(); /* Construct global objects and static fields */ construct_globals(); /* Call user-supplied DllMain and get from it the return code */ ret = DllMain(hinstDLL, fdwReason, lpvReserved); break; case DLL_PROCESS_DETACH: /* Call user-supplied DllMain and get from it the return code */ ret = DllMain(hinstDLL, fdwReason, lpvReserved); /* Destruct global objects and static fields */ destruct_globals(); /* Destruct the global CRT structures */ cleanup_CRT(); break; case DLL_THREAD_ATTACH: /* Init the CRT thread-local structures */ init_TLS_CRT(); /* The same as before, but for thread-local objects */ construct_TLS_globals(); /* Call user-supplied DllMain and get from it the return code */ ret = DllMain(hinstDLL, fdwReason, lpvReserved); break; case DLL_THREAD_DETACH: /* Call user-supplied DllMain and get from it the return code */ ret = DllMain(hinstDLL, fdwReason, lpvReserved); /* Destruct thread-local objects and static fields */ destruct_TLS_globals(); /* Destruct the thread-local CRT structures */ cleanup_TLS_CRT(); break; default: /* ?!? */ /* Call user-supplied DllMain and get from it the return code */ ret = DllMain(hinstDLL, fdwReason, lpvReserved); } return ret; }

这没有什么特别之处:它也发生在普通的可执行文件中,您的 main 由真正的入口点调用,CRT 保留用于完全相同的目的。

现在,从这里可以清楚为什么 Rakis 的解决方案不起作用:全局对象的构造函数由真正的 DllMain 调用(即 dll 的实际入口点,这是关于 DllMain 上的 MSDN 页面的那个入口点)谈论),因此从那里调用 LoadLibrary 与从您的 fake-DllMain 调用它具有完全相同的效果。

因此,遵循该建议,您将获得与在 DllMain 中直接调用 LoadLibrary 相同的负面影响,并且您还将问题隐藏在看似无关的位置,这将使下一个维护者努力寻找此错误所在位于。

至于延迟加载:这可能是一个想法,但你必须非常小心,不要在你的 DllMain 中调用引用的 dll 的任何函数:事实上,如果你这样做了,你会触发对 LoadLibrary 的隐藏调用,这将具有相同的直接调用它的负面影响。

无论如何,在我看来,如果你需要引用 dll 中的某些函数,最好的选择是静态链接它的导入库,这样加载器会自动加载它而不会给你任何问题,它会自动解决任何奇怪的依赖可能出现的链条。

即使在这种情况下,您也不能在 DllMain 中调用此 dll 的任何函数,因为不能保证它已被加载; 实际上,在 DllMain 中,您只能依赖正在加载的 kernel32,并且也许在 dll 上,您绝对确定您的调用者在加载您的 dll 的 LoadLibrary 发出之前已经加载(但您仍然不应该依赖于此,因为您的 dll 也可能被与这些假设不匹配的应用程序加载,并且只想加载您的 dll 资源而不调用您的代码)。

正如我之前链接的文章所指出的,

问题是,就您的二进制文件而言, DllMain 在真正独特的时刻被调用。 到那时,操作系统加载程序已经从磁盘找到、映射和绑定了文件,但是 - 根据情况 - 从某种意义上说,您的二进制文件可能还没有“完全诞生”。 事情可能很棘手。
简而言之,当调用 DllMain 时,OS 加载程序处于相当脆弱的状态。 首先,它已对其结构应用了锁定,以防止在该调用内部发生内部损坏,其次,您的某些依赖项可能未处于完全加载状态 在加载二进制文件之前,OS Loader 会查看其静态依赖项。 如果这些需要额外的依赖项,它也会查看它们。 作为这种分析的结果,它提出了一个序列,其中需要调用这些二进制文件的 DllMains。 它对事物非常聪明,在大多数情况下,您甚至可以不遵守 MSDN 中描述的大多数规则——但并非总是如此
问题是,您不知道加载顺序,但更重要的是,它是基于静态导入信息构建的。 如果在 DLL_PROCESS_ATTACH 期间在您的 DllMain 中发生了一些动态加载,并且您正在进行出站调用,则所有赌注都将关闭 不能保证该二进制文件的 DllMain 将被调用,因此如果您随后尝试将 GetProcAddress 放入该二进制文件中的函数中,结果是完全不可预测的,因为全局变量可能尚未初始化。 很可能你会得到一个 AV。

(再次强调强调)

顺便说一下,关于 Linux 与 Windows 的问题:我不是 Linux 系统编程专家,但我认为这方面的情况并没有太大不同。

and functions), which are - what a coincidence!仍然有一些 DllMain 的等价物( 函数),它们是 - 多么巧合! , calls all the constructors for the global objects and the functions marked with (which are somehow the equivalent of the "fake" DllMain provided to the programmer in Win32). - 由 CRT 自动获取,CRT 反过来从调用全局对象的所有构造函数和用函数标记的(这在某种程度上相当于在 Win32 中提供给程序员的“假”DllMain)。 . 析构函数也有类似的过程。

too is called while the dll loading is still taking place ( didn't return yet), I think that you're subject to similar limitations in what you can do in there.由于在 dll 加载仍在进行时也被调用( 尚未返回),我认为您在那里可以做的事情受到类似的限制。 尽管如此,在我看来,Linux 上的问题较少,因为 (1) 您必须明确选择加入类似 DllMain 的功能,因此您不会立即试图滥用它以及 (2) Linux 应用程序,据我所知,倾向于使用较少的动态加载dll。

简而言之

没有“正确”的方法将允许您在 DllMain 中引用除 kernel32.dll 之外的任何 dll。

因此,不要从 DllMain 做任何重要的事情,既不要直接(即在 CRT 调用的“您的”DllMain 中)也不要间接(在全局类/静态字段构造函数中),尤其不要加载其他 dll ,同样,也不要直接(通过 LoadLibrary)既不是间接的(调用延迟加载的 dll 中的函数,这会触发 LoadLibrary 调用)。

将另一个 dll 作为依赖项加载的正确方法是 - 哦! - 将其标记为静态依赖项。 只需链接它的静态导入库并至少引用它的一个函数:链接器会将它添加到可执行映像的依赖项表中,加载器将自动加载它(在调用 DllMain 之前或之后初始化它,您不需要知道它,因为你不能从 DllMain 调用它)。

如果由于某种原因这不可行,那么仍然有延迟加载选项(有我之前说过的限制)。

如果您仍然出于某种未知原因,莫名其妙地需要在 DllMain 中调用 LoadLibrary,那么,继续,用脚射击,这取决于您的能力。 但别告诉我我没有警告你。


我忘记了:关于该主题的另一个基本信息来源是来自 Microsoft 的 [Best Practices for Creating DLLs][6] 文档,该文档实际上几乎只讨论了加载器、DllMain、加载器锁及其交互; 查看它以获取有关该主题的其他信息。

附录

不,这不是我问题的真正答案。 它只是说:“动态链接是不可能的,你必须静态链接”,以及“你不能从 dllmain 调用”。
这*是*你的问题的答案:在你强加的条件下,你不能做你想做的事。 简而言之,从 DllMain 你不能调用 *除 kernel32 函数之外的任何东西 *。 时期。
虽然很详细,但我对为什么它不起作用并不感兴趣,
相反,你应该这样做,因为理解为什么以这种方式制定规则可以让你避免大错误。
事实是,加载程序没有正确解析依赖项,并且加载过程从 Microsoft 部分不正确地线程化。
不,亲爱的,加载器正确地完成了它的工作,因为 * 在 * LoadLibrary 返回之后,所有的依赖项都被加载了,一切都可以使用了。 加载程序尝试按依赖顺序调用 DllMain(以避免依赖于 DllMain 中的其他 dll 的损坏的 dll 出现问题),但在某些情况下这是根本不可能的。

例如,可能有两个相互依赖的 dll(比如 A.dll 和 B.dll):现在,首先调用谁的 DllMain? 如果加载器首先初始化了 A.dll,并且在它的 DllMain 中调用了 B.dll 中的一个函数,那么任何事情都可能发生,因为 B.dll 还没有初始化(它的 DllMain 还没有被调用)。 如果我们扭转局势,这同样适用。

在其他情况下可能会出现类似的问题,所以简单的规则是:不要在DllMain中调用任何外部函数,DllMain只是为了初始化你的dll的内部状态。

问题是除了在 dll_attach 上做这件事之外别无他法,所有关于不做任何事情的好话都是多余的,因为别无选择,至少在我的情况下没有。

这个讨论是这样进行的:你说“我想在实域中解一个像 x^2+1=0 这样的方程”。 每个人都说你不可能; 你说这不是答案,并责怪数学。

有人告诉你:嘿,你可以,这是一个技巧,解决方案就是 +/-sqrt(-1); 每个人都对这个答案投反对票(因为您的问题是错误的,我们超出了真正的领域),并且您指责谁投反对票。 我向您解释为什么根据您的问题该解决方案不正确,以及为什么无法在实际领域中解决此问题。 你说你不在乎为什么它不能做,你只能在真正的领域做,并再次责怪数学。

现在,因为正如解释和重申了一百万次,在你的条件下你的答案没有解决方案,你能解释我们为什么你“必须”做这样一件愚蠢的事情,比如在 DllMain 中加载一个 dll 通常“不可能”的问题出现是因为我们选择了一条奇怪的路线来解决另一个问题,这使我们陷入僵局。 如果你解释了更大的图景,我们可以建议一个更好的解决方案,它不涉及在 DllMain 中加载 dll。

PS:如果我将 DLL2(ole32.dll,Vista x64)与 DLL1(mydll)静态链接,在旧操作系统上链接器需要哪个版本的 dll?
存在的那个(显然我假设你正在编译 32 位); 如果您的应用程序所需的导出函数不存在于找到的 dll 中,则您的 dll 根本没有加载(LoadLibrary 失败)。

附录 (2)

如果您想知道,请使用 CreateRemoteThread。 仅在 Linux 和 Mac 上,dll/共享库由加载程序加载。
将 dll 添加为静态依赖项(从一开始就建议)使其完全像 Linux/Mac 一样由加载器加载,但问题仍然存在,因为正如我所解释的,在 DllMain 中你仍然不能依赖在 kernel32.dll 以外的任何东西上(即使加载程序通常足够智能以首先初始化依赖项)。

不过,问题还是可以解决的。 使用 CreateRemoteThread 创建线程(实际上调用 LoadLibrary 来加载你的 dll); 在 DllMain 中使用一些 IPC 方法(例如命名共享内存,其句柄将保存在某个地方以在 init 函数中关闭)将您的 dll 将提供的“真实”init 函数的地址传递给注入程序。 DllMain 然后将退出而不做任何其他事情。 相反,注入器应用程序将使用 CreateRemoteThread 提供的句柄使用 WaitForSingleObject 等待远程线程的结束。 然后,当远程线程结束后(这样LoadLibrary就完成了,所有的依赖都会被初始化),注入器会从DllMain创建的命名共享内存中读取远程进程中init函数的地址,并启动它与 CreateRemoteThread。

问题:在 Windows 2000 上禁止使用来自 DllMain 的命名对象,因为

在 Windows 2000 中,命名对象由终端服务 DLL 提供。 如果此 DLL 未初始化,则调用 DLL 会导致进程崩溃。
所以,这个地址可能必须以另一种方式传递。 一个非常干净的解决方案是在 dll 中创建一个共享数据段,将它加载到注入器应用程序和目标应用程序中,并将它放在这样的数据段中所需的地址。 dll 显然必须首先在注入器中加载,然后在目标中加载,否则“正确”的地址将被覆盖。

另一个真正有趣的方法是在另一个进程内存中写入一个小函数(直接在汇编中),它调用 LoadLibrary 并返回我们的 init 函数的地址; 因为我们是在那里写的,所以我们也可以用 CreateRemoteThread 调用它,因为我们知道它在哪里。

在我看来,这是最好的方法,也是最简单的,因为代码已经在那里了,写在这篇不错的文章中 看看它,它很有趣,它可能会解决您的问题。

最可靠的方法是将第一个 DLL 链接到第二个的导入库。 这样,第二个 DLL 的实际加载将由 Windows 自己完成。 听起来很琐碎,但并不是每个人都知道 DLL 可以链接到其他 DLL。 Windows 甚至可以处理循环依赖。 如果 A.DLL 加载了需要 A.DLL 的 B.DLL,则解析 B.DLL 中的导入,而无需再次加载 A.DLL。

我建议你使用延迟加载机制。 DLL 将在您第一次调用导入函数时加载。 此外,您可以修改加载功能和错误处理。 有关详细信息,请参阅对延迟加载的 DLL 的链接器支持

一种可能的答案是通过使用 LoadLibrary 和 GetProcAddress 来访问指向在加载的 dll 中找到/位于的函数的指针 - 但您的意图/需求不够明确,无法确定这是否是合适的答案。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM