如何在一个dll中加载另一个dll是最佳方式?
我的问题是我无法在process_attach时加载dll,也不能从主程序中加载dll,因为我无法控制主程序源代码。因此,我也不能调用非dllmain函数。
如何在一个dll中加载另一个dll是最佳方式?
我的问题是我无法在process_attach时加载dll,也不能从主程序中加载dll,因为我无法控制主程序源代码。因此,我也不能调用非dllmain函数。
在评论中进行了许多辩论后,我认为总结我的立场并进行“真正”的回答会更好。
首先,仍然不清楚为什么需要在DllMain中使用LoadLibrary加载dll。这绝对是一个坏主意,因为您的DllMain正在运行另一个调用LoadLibrary的函数内部,该函数持有加载器锁定,如DllMain文档所解释:
在初始进程启动或调用LoadLibrary之后,系统会扫描进程的已加载DLL列表。对于尚未使用DLL_PROCESS_ATTACH值调用的每个DLL,系统都会调用DLL的入口点函数。 此调用是在导致进程地址空间更改的线程上下文中进行的,例如进程的主线程或调用LoadLibrary的线程。进入点的访问由系统在整个进程范围内进行序列化。在DllMain中的线程持有加载器锁,因此无法动态加载或初始化其他DLL。入口函数应只执行简单的初始化或终止任务。它不得调用LoadLibrary或LoadLibraryEx函数(或调用这些函数的函数),因为这可能会在DLL加载顺序中创建依赖项循环。这可能导致系统在执行其初始化代码之前使用DLL。同样,在进程终止期间,入口点函数不能调用FreeLibrary函数(或调用FreeLibrary的函数),因为这可能导致系统在执行其终止代码后仍使用DLL。 因此,这就是为什么禁止这样做;有关更清晰、更深入的解释,请参见this和this,有关在DllMain中不遵守这些规则可能会发生的其他示例,请参见Raymond Chen's blog中的一些帖子。 现在,回到Rakis的答案。
/* 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中描述的大多数规则,但并非总是如此。
还有一些 DllMain 的等效函数(_init 和 _fini 函数),这些函数 - 真是巧合! - 由 CRT 自动调用,在此过程中,从 _init 开始,调用所有全局对象和标记为 __attribute__ constructor 的函数的构造函数(它们在某种程度上相当于 Win32 中程序员提供的“假” DllMain 的等效函数)。类似的过程也会在 _fini 中进行析构。
由于 _init 在 dll 加载时仍在进行中(dlopen 尚未返回),因此我认为你在其中可以做的事情受到了类似的限制。但在我看来,在 Linux 上这个问题感觉不那么明显,因为 (1) 你必须明确选择一个类似 DllMain 的函数,所以你不会立即倾向于滥用它,而且 (2) Linux 应用程序似乎倾向于使用较少的动态加载 dll。
因此,不要从 DllMain 中执行任何重要操作,无论是直接执行(即在 CRT 调用“你的” DllMain 中)还是间接执行(在全局类/静态字段构造函数中),尤其是不要加载其他 dll,同样,无论是直接加载(通过 LoadLibrary)还是间接加载(调用延迟加载 dll 中的函数,这会触发 LoadLibrary 调用)。
将另一个dll作为依赖项加载的正确方法是将其标记为静态依赖项。只需链接其静态导入库并引用其中至少一个函数:链接器将其添加到可执行映像的依赖关系表中,加载程序将自动加载它(在调用DllMain之前或之后初始化它,您不需要知道它,因为您不能从DllMain中调用它)。不,这并不能回答我的问题。它只是说:“动态链接不可能,你必须静态链接”,并且“你不能从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?
如果你想知道,使用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调用它,因为我们知道它在哪里。
在我看来,这是最好的方法,也是最简单的方法,因为代码已经存在于这个好文章中。看一下它,它非常有趣,可能会解决你的问题。
最可靠的方式是将第一个DLL与第二个DLL的导入库链接起来。这样,第二个DLL的实际加载将由Windows自己完成。听起来非常琐碎,但并不是每个人都知道DLL可以链接其他DLL。Windows甚至可以处理循环依赖关系。如果A.DLL加载B.DLL,而B.DLL需要A.DLL,则B.DLL中的导入将在不再加载A.DLL的情况下得到解析。
我建议您使用延迟加载机制。DLL将在第一次调用导入函数时被加载。此外,您可以修改加载函数和错误处理。有关更多信息,请参见Linker Support for Delay-Loaded DLLs。