从DllMain函数中调用LoadLibrary函数

3

MSDN指出:

不得调用LoadLibrary或LoadLibraryEx函数(或调用这些函数的函数),因为这可能会在DLL加载顺序中创建依赖项循环。这可能导致DLL在系统执行其初始化代码之前被使用。

我尝试从DllMain调用LoadLibrary,但没有任何反应。

我唯一看到的问题是,在我的DllMain执行其余部分之前,已加载的DLL将使用我的DLL中的函数。

为什么我不能在DllMain中调用LoadLibrary?

编辑:

好吧,我意识到我不能在DllMain中调用LoadLibrary只是因为我必须像其他人一样相信MSDN(我看到了一些错误的东西,但我也应该忘记它们)。
而且因为在较新版本的Windows中可能会发生一些情况(尽管在过去十年中没有任何更改)。

但是有人可以展示一个代码,以重现当在DllMain中调用LoadLibrary时发生的糟糕情况吗?在任何现有的Windows操作系统中?
不仅仅是在另一个单例初始化函数中调用一个,而是在DllMain中调用LoadLibrary


6
这里有一个很好的解释:http://blogs.msdn.com/b/oldnewthing/archive/2004/01/27/63401.aspx。 - Simon Mourier
1
@Simon Mourier:这个问题与此无关。 - Abyx
6
这个问题的潜台词是:“我计划在我的代码中从DllMain调用LoadLibrary,而且不想采纳不要这样做的建议”? - David Heffernan
这与任何人是否“相信”MSDN无关;MSDN中的API规范是微软保证始终有效的接口。您必须考虑到其他任何行为都可能会发生变化。确实,十年来没有什么改变,但这并不能阻止微软在未来更改事物。不要忘记,Windows 9x具有与Windows NT完全不同的加载程序,尽管它们都支持Win32。您真的希望您的代码成为在不同实现上无法工作的东西吗? - Aaron Klotz
6个回答

16

有一些简单的,甚至不那么简单的情况下,从DllMain中调用LoadLibrary是完全安全的。但设计理念是,DllMain被信任不会改变已加载模块的列表。

虽然拥有加载器锁确实限制了DllMain中可以执行的操作,但它仅间接与LoadLibrary规则相关。加载器锁的相关目的是对已加载模块的列表进行串行访问。当NTDLL在一个线程上处理此列表时,拥有加载器锁可确保该列表不会被在另一个线程中执行的NTDLL代码更改。但是,加载器锁是一个关键部分。如果相同的线程重新获取加载器锁并更改列表,则无法阻止这种情况发生。

如果NTDLL在处理列表时完全保留自己,则这将无关紧要。但是,NTDLL提供了涉及其他代码的选项,例如在初始化新加载的DLL时。每次NTDLL在处理列表时外部调用时,都需要进行设计选择。广泛地说,有两个选择。一个是稳定列表并释放加载器锁,进行调用外部代码,然后获取加载器锁并从头开始继续处理列表,因为外部调用可能已更改它。另一个是保持加载器锁并信任被调用代码不会执行任何更改列表的操作。因此,从DllMain中调用LoadLibrary变得不可行。

问题不在于加载器锁会做任何事情来阻止DllMain调用LoadLibrary,甚至不是因为加载器锁本身使此类调用不安全。相反,通过保留加载器锁,NTDLL信任DllMain不会调用LoadLibrary。

对比一下,考虑DllMain规则关于不等待同步对象的规定。在这里,加载器锁直接导致了这种做法的不安全性。在DllMain中等待同步对象会导致死锁的可能性。只需要另一个线程已经持有你正在等待的对象,那么这个线程调用任何一个需要等待加载器锁的函数(例如LoadLibrary,以及看似无害的GetModuleHandle等函数)就可以发生死锁。

想要扩展或违反DllMain规则可能是恶意的,甚至完全愚蠢的。然而,我必须指出,微软公司至少部分地应该为人们询问这些规则的强度或含义负责。毕竟,有些规则并没有被清晰、有力地记录下来,而且在我最近查看时它们仍然没有在所有需要的情况下得到记录。(我所指的例外是,至少在Visual Studio 2005之前,编写DLL的MFC程序员被告知将其初始化代码放置在CWinApp::InitInstance中,但他们并没有被告知这些代码受到DllMain规则的限制。)

此外,任何微软公司的工程师都不能不遵循DllMain规则而自称是正确的,这也有点荒谬。存在一些微软自己的程序员打破规则的例子,甚至在违反规则导致严重真实世界麻烦之后仍然继续这样做。


15
你为继续进行此操作辩护的论点是,简而言之:微软表示不要这样做,但我的一个测试用例似乎可行,因此我无法理解为什么没有人应该这样做。
你正在基于一个重要的假设来操作:你假设Windows加载程序的底层实现永远不会改变。如果在“Windows 8”中以某种方式更改了加载程序,使你的代码不能正常工作怎么办?现在,Microsoft会因此受到指责,并且他们必须包含额外的兼容性hack来解决他们最初告诉你不要编写的代码问题。
遵循指南。它们不仅仅是为了让你的生活更加困难,而且还为保证你的代码将来能够像现在一样在Windows上正常工作而存在。

9

4
@Abyx - 有什么意义吗?同一页明确禁止从DllMain中调用LoadLibrary(Ex)。如果你这样做,显然要自己承担后果。 - Steve Townsend
1
@Fritschy:这并不一定意味着有问题。如果你在 DllMain 中调用 LoadLibrary,它将从文档所说的当前持有加载器锁的线程中被调用,因此不应该出现死锁。虽然我不主张使用 LoadLibrary,但文档说你不应该这样做已经足够理由不去这么做了。 - Praetorian
7
@Praetorian:它不会死锁,只是可能会崩溃。崩溃可能会发生得更晚一些。如果你在DllMain中调用LoadLibrary,不能保证DllInitialization的正确顺序。你可能会运气好使其正常工作,但也可能失败惨重。你无法知道会发生什么。 - Larry Osterman
1
你测试过了吗?什么?!! - David Heffernan
@Praetorian:如果意图在DllMain中LoadLibrary,则会出现问题。还可能出现其他问题:http://www.mail-archive.com/osg-users@lists.openscenegraph.org/msg30386.html - Marcus Borkenhagen
1
@Abyx:我没有测试过它,但有一些与它相关的错误经验。然而,我倾向于按照文档告诉我的去做 ;) - Marcus Borkenhagen

3
我正在处理一个可能需要在DllMain中使用LoadLibrary的案例,所以在调查时发现了这个讨论。从我的今天的经验来看,对此进行更新。
阅读这篇文章可能会让人感到非常可怕。不仅各种锁很重要,而且传递给链接器的库的顺序也很重要。http://blogs.msdn.com/b/oleglv/archive/2003/10/28/56142.aspx。例如,假设有一个bi
现在,我已经在win7下使用vc9尝试过这个功能。是的,就是这样。根据将库传递给链接器的顺序,使用LoadLibrary的效果好坏不同。然而,在win8下使用vc11时,无论链接顺序如何,都可以正常工作。应用程序验证器没有对此进行指责。
我并不是要呼吁立即在任何地方都这样使用它 :) 但只是提供信息,如果win10及更高版本也是如此,则可能具有更多的实用性。无论如何,似乎win8下的加载程序机制发生了一些明显的变化。
谢谢。

0

以下是如何在Windows 8 / Server 2012及更高版本中重现加载器锁定挂起的方法。请注意,此代码并非直接调用load library,而是使用触发Load Library调用的Windows API。

创建一个Visual Studio C++ DLL项目,并在DLL主函数中使用此代码:

#define WIN32_LEAN_AND_MEAN

#include "framework.h"

#include <windows.h>
#include <winsock2.h>
#include <iphlpapi.h>
#include <ws2tcpip.h>
#include <stdio.h>
#pragma comment(lib, "IPHLPAPI.lib")

#define MALLOC(x) HeapAlloc(GetProcessHeap(), 0, (x))
#define FREE(x) HeapFree(GetProcessHeap(), 0, (x))
// Need to link with Ws2_32.lib
#pragma comment(lib, "ws2_32.lib")

BOOL APIENTRY DllMain(HMODULE hModule,
    DWORD  ul_reason_for_call,
    LPVOID lpReserved
)
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        WORD wVersionRequested;
        WSADATA wsaData;
        int err;

        /* Use the MAKEWORD(lowbyte, highbyte) macro declared in Windef.h */
        wVersionRequested = MAKEWORD(2, 2);

        err = WSAStartup(wVersionRequested, &wsaData);
        if (err != 0) {
            printf("WSAStartup failed with error: %d\n", err);
            return 1;
        }
    
        if (LOBYTE(wsaData.wVersion) != 2 || HIBYTE(wsaData.wVersion) != 2) {
            printf("Could not find a usable version of Winsock.dll\n");
            WSACleanup();
            return 1;
        }
        else
            printf("The Winsock 2.2 dll was found okay\n");
    
        
        FIXED_INFO* pFixedInfo;
        ULONG ulOutBufLen;
        DWORD dwRetVal;
        IP_ADDR_STRING* pIPAddr;

        pFixedInfo = (FIXED_INFO*)MALLOC(sizeof(FIXED_INFO));
        if (pFixedInfo == NULL) {
            printf("Error allocating memory needed to call GetNetworkParams\n");
            return 1;
        }
        ulOutBufLen = sizeof(FIXED_INFO);

        // Make an initial call to GetAdaptersInfo to get
        // the necessary size into the ulOutBufLen variable
        if (GetNetworkParams(pFixedInfo, &ulOutBufLen) == ERROR_BUFFER_OVERFLOW) {
            FREE(pFixedInfo);
            pFixedInfo = (FIXED_INFO*)MALLOC(ulOutBufLen);
            if (pFixedInfo == NULL) {
                printf("Error allocating memory needed to call GetNetworkParams\n");
                return 1;
            }
        }

        if (dwRetVal = GetNetworkParams(pFixedInfo, &ulOutBufLen) == NO_ERROR) {

            printf("Host Name: %s\n", pFixedInfo->HostName);
            printf("Domain Name: %s\n", pFixedInfo->DomainName);

            printf("DNS Servers:\n");
            printf("\t%s\n", pFixedInfo->DnsServerList.IpAddress.String);

            pIPAddr = pFixedInfo->DnsServerList.Next;
            while (pIPAddr) {
                printf("\t%s\n", pIPAddr->IpAddress.String);
                pIPAddr = pIPAddr->Next;
            }

            printf("Node Type: ");
            switch (pFixedInfo->NodeType) {
            case BROADCAST_NODETYPE:
                printf("Broadcast node\n");
                break;
            case PEER_TO_PEER_NODETYPE:
                printf("Peer to Peer node\n");
                break;
            case MIXED_NODETYPE:
                printf("Mixed node\n");
                break;
            case HYBRID_NODETYPE:
                printf("Hybrid node\n");
                break;
            default:
                printf("Unknown node type %0lx\n", pFixedInfo->NodeType);
                break;
            }

            printf("DHCP scope name: %s\n", pFixedInfo->ScopeId);

            if (pFixedInfo->EnableRouting)
                printf("Routing: enabled\n");
            else
                printf("Routing: disabled\n");

            if (pFixedInfo->EnableProxy)
                printf("ARP proxy: enabled\n");
            else
                printf("ARP Proxy: disabled\n");

            if (pFixedInfo->EnableDns)
                printf("DNS: enabled\n");
            else
                printf("DNS: disabled\n");

        }
        else {
            printf("GetNetworkParams failed with error: %d\n", dwRetVal);
            return 1;
        }

        if (pFixedInfo)
            FREE(pFixedInfo);
        //WSACleanup();
        break;
    case DLL_THREAD_ATTACH:
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

从第二个应用程序(尚未导入任何网络API或调用任何网络函数)创建一个控制台桌面C++应用程序,其中包含以下代码:

HMODULE hModule;
hModule = LoadLibrary(L"<specify DLL created in previous example>"); // application will hang here

0

虽然已经很晚了,但是如果在线程1(T1)中的DllMain加载其他库,则会调用这些其他库的DllMain;这本身没问题,但是假设它们的DLLMain创建了一个线程(T2)并等待事件以便T2完成。

现在,如果T2在其处理过程中加载库,则加载器将无法获取锁定,因为T1已经获取了锁定。由于T2挂起在LoaderLock上,它永远不会发出T1正在等待的事件信号。

这将导致死锁。

可能还有更多类似的情况,我想这里的广泛推理是我们不能确定其他库中将运行什么代码,因此最好的做法是不要这样做。


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