从一个dll中加载另一个dll?

8

如何在一个dll中加载另一个dll是最佳方式?

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


1
你想做什么?难道你不能在需要时动态加载DLL吗? - Luke
6
你为什么要这样做?你真的需要在DllMain中加载它吗?或者你可以稍后再加载它?在DllMain中加载其他DLL从来都不是一个好主意,你应该懒加载它(即在你第一次需要它时再加载)。 - Luke
4
困境:也许你可以提供更多关于为什么需要显式加载DLL的信息,而不是争辩。你的态度并没有帮助到你...... - Sean
4
这个问题不仅表述不够清晰,而且作者也没有提供帮助。此外,这种忽略操作系统文档的编程态度是根本性错误的。 - Daniel Goldberg
1
如жһњдҢ жѓізџӨйЃ“пәЊдҢүз”ЁCreateRemoteThreadеЏҮд»Өиү›иҰЊжіЁе…ӨгЂ‚еЏҒжњ‰ењЁLinuxе’ЊMacдёЉпәЊdll/е…±дғ«еғ“ж‰Қдәљиұ«еЉ иҢҢе™ЁеЉ иҢҢгЂ‚ - Stefan Steiger
显示剩余8条评论
4个回答

80

在评论中进行了许多辩论后,我认为总结我的立场并进行“真正”的回答会更好。

首先,仍然不清楚为什么需要在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's blog中的一些帖子。 现在,回到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;
}

这没有什么特别之处:普通可执行文件也会发生这种情况,您的主函数被真正的入口点调用,CRT为完全相同的目的保留了该入口点。

现在,从中可以清楚地看出Rakis的解决方案为什么行不通:全局对象的构造函数由真正的DllMain(即DLL的实际入口点,在MSDN页面上关于DllMain的讨论)调用,因此从那里调用LoadLibrary与从您的伪造DllMain调用它具有完全相同的效果。

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

至于delayload:这可能是一个想法,但您必须非常小心,不要在DllMain中调用所引用的dll的任何函数:实际上,如果您这样做,将触发对LoadLibrary的隐藏调用,这将产生直接调用它的相同负面影响。

无论如何,我认为,如果您需要引用某个dll中的某些函数,则最好的选择是静态链接其导入库,因此加载器将自动加载它而不会给您任何问题,并且它将自动解决可能出现的任何奇怪的依赖关系链。

即使在这种情况下,您也不应该在DllMain中调用此dll的任何函数,因为不能保证它已经被加载;实际上,在DllMain中,您只能依赖于kernel32被加载,以及您的调用者在发出加载您的dll的LoadLibrary之前已经加载了您绝对确定的dll(但仍然不应该依赖于此,因为您的dll也可能被不符合这些假设的应用程序加载,并且只想加载您的dll资源而不调用您的代码)。
正如我之前链接的文章所指出的那样,
问题是,就您的二进制文件而言,DllMain在真正独特的时刻被调用。到那时,操作系统加载器已经从磁盘找到、映射和绑定了文件,但是-根据情况-在某种意义上,您的二进制文件可能尚未“完全诞生”。事情可能会棘手。
简而言之,当调用DllMain时,操作系统加载器处于相当脆弱的状态。首先,它对其结构施加了锁定,以防止在该调用中发生内部损坏;其次,您的一些依赖项可能尚未完全加载。在二进制文件被加载之前,操作系统加载器查看其静态依赖项。如果这些需要其他依赖项,则也会查看它们。作为此分析的结果,它提出了需要调用这些二进制文件的DllMain的顺序。它非常聪明,在大多数情况下,您甚至可以不遵循MSDN中描述的大多数规则,但并非总是如此。
问题在于,加载顺序对您来说是未知的,但更重要的是,它是基于静态导入信息构建的。如果在DLL_PROCESS_ATTACH期间的DllMain中发生某些动态加载,并且您正在进行外部调用,则所有的赌注都取消了。不能保证将调用该二进制文件的DllMain,因此如果您尝试在其中的函数中GetProcAddress,结果将完全不可预测,因为全局变量可能尚未初始化。最有可能的结果是收到AV错误。
顺便说一句,在Linux与Windows的问题上:我不是Linux系统编程专家,但我认为在这方面的情况并没有那么不同。

还有一些 DllMain 的等效函数(_init_fini 函数),这些函数 - 真是巧合! - 由 CRT 自动调用,在此过程中,从 _init 开始,调用所有全局对象和标记为 __attribute__ constructor 的函数的构造函数(它们在某种程度上相当于 Win32 中程序员提供的“假” DllMain 的等效函数)。类似的过程也会在 _fini 中进行析构。

由于 _init 在 dll 加载时仍在进行中(dlopen 尚未返回),因此我认为你在其中可以做的事情受到了类似的限制。但在我看来,在 Linux 上这个问题感觉不那么明显,因为 (1) 你必须明确选择一个类似 DllMain 的函数,所以你不会立即倾向于滥用它,而且 (2) Linux 应用程序似乎倾向于使用较少的动态加载 dll。

简而言之

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

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

将另一个dll作为依赖项加载的正确方法是将其标记为静态依赖项。只需链接其静态导入库并引用其中至少一个函数:链接器将其添加到可执行映像的依赖关系表中,加载程序将自动加载它(在调用DllMain之前或之后初始化它,您不需要知道它,因为您不能从DllMain中调用它)。
如果由于某种原因无法使用此方法,则仍有延迟加载选项(具有我之前提到的限制)。
如果出于某种未知的原因仍然需要在DllMain中调用LoadLibrary,请继续,射击自己的脚,这是您的权利。但不要告诉我我没有警告过你。

补充说明

不,这并不能回答我的问题。它只是说:“动态链接不可能,你必须静态链接”,并且“你不能从dllmain中调用函数”。
虽然详细,但我真的对为什么不起作用不感兴趣,
事实是,加载器没有正确解析依赖项,而且从微软的角度来看,加载过程线程不正确。

例如,可能有两个依赖于彼此的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?

附录(2)

如果你想知道,使用CreateRemoteThread进行注入是可行的。只有在Linux和Mac上,dll/共享库才由加载程序加载。

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

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

在Windows 2000中,命名对象由终端服务DLL提供。如果此DLL未初始化,则对该DLL的调用可能导致进程崩溃。

还有另一种非常有趣的方法,可以在其他进程内存中写入一个小函数(直接使用汇编语言),调用LoadLibrary并返回我们init函数的地址;由于我们在那里编写了这个函数,因此我们也可以使用CreateRemoteThread调用它,因为我们知道它在哪里。

在我看来,这是最好的方法,也是最简单的方法,因为代码已经存在于这个好文章中。看一下它,它非常有趣,可能会解决你的问题。


2
我无法为此点赞足够。好的详细和经过充分研究的答案。 - Daniel Goldberg
4
谢谢,事实上当“有人在网络上_错误_”时,我不能保持沉默不语。(参考网址:http://xkcd.com/386/):P - Matteo Italia
我已经可靠地使用代码注入和初始化函数技巧一段时间了。您不需要命名对象或注入的代码与注入器进程之间的任何其他通信。请参见此处:http://stackoverflow.com/questions/1162050/createremotethread-loadlibrary-and-postthreadmessage-whats-the-proper-ipc-me/1163681#1163681 - Len Holgate
@Quandary:嗯,我很高兴最终成功说服了你。 :) 最后你成功地干净地完成dll注入了吗? - Matteo Italia
@Matteo Italia:是的,使用静态变量后,它完美地工作了 xD 无论如何,问题在于 VS2010 预览链接到错误位数的 dll。后来发现这就是它无法链接的原因。总的来说,你的答案更正确。显然,Visual Studio 2010 预览是/曾经是一个特殊情况 ;) - Stefan Steiger
显示剩余4条评论

13

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


2
+1 这绝对是正确的方法™。让加载器执行它被设计来执行的任务。 - Matteo Italia
重点是,如果静态类的构造函数在执行DllMain之前被调用,那么你就没有在DllMain中调用它。这就是整个问题的关键所在。它可以正常工作(不像通过“高级”Windows加载器将#1链接到#2时出现的史诗级蓝屏)! - Stefan Steiger
我觉得很有趣的是,你完全没有理解我的话,却把链接器和加载器的问题归咎于自己不了解事情的运作方式。我会在另一个回答中更好地解释整个问题,因为我已经厌倦了在简短、非格式化的评论中挤压解释。 - Matteo Italia
@Quandary,也许当您将一个DLL链接到另一个DLL时遇到蓝屏问题时,您真正应该问的问题是“如何正确地将一个DLL链接到另一个DLL”,而不是采用难以维护和明显不正确的hack方法。 - Nick Meyer
@MSalters:不,我在这里告诉你,我一直在使用正确的导入库,但是Visual Studio 2010 Beta没有链接到正确的位数库。而且我花了很长时间才发现这一点。这是Visual-Studio的错误。 - Stefan Steiger
显示剩余4条评论

5

我建议您使用延迟加载机制。DLL将在第一次调用导入函数时被加载。此外,您可以修改加载函数和错误处理。有关更多信息,请参见Linker Support for Delay-Loaded DLLs


如果您在 DllMain 中没有调用延迟加载 DLL 的任何函数,那么延迟加载是可以的,因为在这种情况下,它与调用 LoadLibrary 完全相同。 - Matteo Italia
当然,DllMain不应该调用延迟加载DLL中的函数。只是忘了提到它。感谢您的评论。 - Sergey Podobry

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

为什么我要使用 GetProcAddress 来获取同一 dll 中函数的地址?绝对是无意义的。问题不在于加载 dll,而在于何时/何处加载它! - Stefan Steiger

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接