8

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

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

4

4 回答 4

79

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

首先,仍然不清楚为什么需要在 DllMain 中使用 LoadLibrary 加载 dll。这绝对是一个坏主意,因为您的 DllMain 正在另一个对 LoadLibrary 的调用中运行该调用持有加载程序锁,如DllMain 的文档所述

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

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

现在,关于拉基斯的回答。

正如我已经多次重复的那样,您认为的 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 Loader 会查看其静态依赖项。如果这些需要额外的依赖项,它也会查看它们。作为该分析的结果,它提出了一个序列,其中需要调用这些二进制文件的 DllMains。它非常聪明,在大多数情况下,您甚至可以不遵守 MSDN 中描述的大多数规则而侥幸逃脱 -但并非总是如此
问题是,您不知道加载顺序,但更重要的是,它是基于静态导入信息构建的。如果在 DLL_PROCESS_ATTACH 期间您的 DllMain 中发生了一些动态加载并且您正在进行出站呼叫,那么所有的赌注都是 off无法保证将调用该二进制文件的 DllMain,因此如果您随后尝试将 GetProcAddress 放入该二进制文件内的函数中,则结果是完全不可预测的,因为全局变量可能尚未初始化。您很可能会获得 AV。

(再次强调)

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

还有一些 DllMain 的等价物(_init_fini函数),它们是 - 多么巧合!- 由 CRT 自动获取,而 CRT 又从_init调用全局对象的所有构造函数和标有__attribute__ 构造函数的函数(在某种程度上相当于 Win32 中提供给程序员的“假” DllMain)。_fini中的析构函数也有类似的过程。

由于_init在 dll 加载仍在进行时也被调用(dlopen尚未返回),我认为您在那里可以做的事情受到类似的限制。尽管如此,在我看来,在 Linux 上这个问题感觉较少,因为 (1) 你必须明确选择加入类似 DllMain 的函数,所以你不会立即想滥用它和 (2) Linux 应用程序,据我所知,倾向于使用较少的动态加载dll。

简而言之

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

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

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

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

如果您仍然出于某种未知原因,莫名需要在 DllMain 中调用 LoadLibrary,那么,继续前进,在您的脚下开枪,这在您的能力范围内。但是不要告诉我我没有警告过你。


我忘记了:关于该主题的另一个基本信息来源是 Microsoft 的 [创建 DLL 的最佳实践][6] 文档,它实际上几乎只讨论了加载器、DllMain、加载器锁及其交互;查看它以获取有关该主题的更多信息。

附录

不,不是我的问题的真正答案。它只是说:“动态链接是不可能的,你必须静态链接”,“你不能从 dllmain 调用”。
哪个*是*您的问题的答案:在您施加的条件下,您无法做您想做的事。简而言之,从 DllMain 你不能调用*除了 kernel32 函数之外的任何东西*。时期。
虽然很详细,但我对它为什么不起作用并不感兴趣,
相反,您应该这样做,因为了解为什么以这种方式制定规则可以避免大错。
事实上,加载程序没有正确解析依赖项,加载过程从微软的部分不正确地线程化。
不,亲爱的,加载器正确地完成了它的工作,因为 * 在 * 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 调用它,因为我们知道它在哪里。

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

于 2010-04-21T19:40:08.170 回答
13

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

于 2010-04-20T14:50:59.933 回答
5

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

于 2010-04-21T08:36:01.040 回答
0

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

于 2010-04-20T12:01:37.690 回答