【问题标题】:Loading DLL not initializing static C++ classes加载 DLL 未初始化静态 C++ 类
【发布时间】:2011-07-04 03:14:51
【问题描述】:

我有一个在运行时加载的 DLL。 DLL 依赖于一个静态变量进行内部工作(它是一个 std::map),这个变量是在 DLL 中定义的。

当我在加载后从 DLL 调用第一个函数时,我从 DLL 中得到一个 SegFault,映射从未初始化。从我从 DLL Loading 中读到的所有内容来看,静态和全局数据初始化应该在调用 DLLMain 之前发生。

为了测试静态初始化,我添加了一个打印出消息的静态结构,甚至还设置了一个断点。

static struct a
{
  a(void) { puts("Constructing\n"); }
}statica;

在 DLLMain 或函数被调用之前没有消息或中断。

这是我的加载代码:

  dll = LoadLibrary("NetSim");
  //Error Handling

  ChangeReliability   = reinterpret_cast<NetSim::ChangeReliability>
                        (GetProcAddress(dll, "ChangeReliability"));


ChangeReliability(100);

我已经验证了dll版本是正确的,多次重建整个项目,但没有区别。我的想法很新鲜。

【问题讨论】:

  • 是否引用了您的statica 对象?如果没有,它可以被优化出来。

标签: c++ winapi dll static


【解决方案1】:

虽然我不确定初始化失败的原因,但一种解决方法是显式创建和初始化 DllMain 中的变量。根据Microsoft's best practices 论文,您应该避免在 DllMain 中使用 CRT 分配函数,因此我使用带有自定义分配器的 windows 堆分配函数(注意:未经测试的代码,但应该或多或少正确):

template<typename T> struct HeapAllocator
{
    typedef T value_type, *pointer, &reference;
    typedef const T *const_pointer, &const_reference;
    typedef size_t size_type;
    typedef ptrdiff_t difference_type;

    HeapAllocator() throw() { }
    HeapAllocator(const HeapAllocator&) throw() { }
    typedef<typename U>
    HeapAllocator(const HeapAllocator<U>&) throw() { }

    pointer address(reference x) const { return &x; }
    const_pointer address(const_reference x) const { return &x; }

    pointer allocate(size_type n, HeapAllocator<void>::const_pointer hint = 0)
    {
        LPVOID rv = HeapAlloc(GetProcessHeap(), 0, n * sizeof(value_type));
        if (!rv) throw std::bad_alloc();
        return (pointer)rv;
    }

    void deallocate(pointer p, size_type n)
    {
        HeapFree(GetProcessHeap(), 0, (LPVOID)p);
    }

    size_type max_size() const throw()
    {
        // Make a wild guess...
        return (2 * 1024 * 1024 * 1024) / sizeof(value_type);
    }

    void construct(pointer p, const_reference val)
    {
        new ((void*)p) T(val);
    }

    void destroy(pointer p)
    {
        p->~T();
    }
};

std::map<foo, HeapAllocator> *myMap;

BOOL WINAPI DllMain(HANDLE hInst, ULONG ul_reason, LPVOID lpReserved)
{
    switch(ul_reason) {
        case DLL_PROCESS_ATTACH:
            myMap = (std::map<foo, HeapAllocator> *)HeapAlloc(GetProcessHeap(), 0, sizeof(*myMap));
            if (!myMap) return FALSE; // failed DLL init

            new ((void*)myMap) std::map<foo, HeapAllocator>;
            break;
        case DLL_PROCESS_DETACH:
            myMap->~map();
            HeapFree(GetProcessHeap(), 0, (LPVOID)myMap);
            break;
    }
    return TRUE;
}

【讨论】:

  • 在 DllMain 中分配动态内存是不安全的,应该避免。例如。看到这篇文章:go.microsoft.com/FWLink/?LinkId=84138
  • 是的,但是存储在地图中的对象呢?它们有自己的析构函数,将在卸载期间从 DllMain 调用。这种方法是可行的,但我个人会将它保留在其他一切都无法选择的极端情况下。
【解决方案2】:

“经典”的简单单例实现将起作用:

std::map<Key,Value>& GetMap() {
  static std::map<Key,Value> the Map;
  return theMap;
}

当然,您不应该从 DllMain 调用它。

【讨论】:

  • 它仍然存在释放问题,这将在 DLL 卸载时在 DllMain 中发生(并且可能会产生有趣的后果:blogs.msdn.com/b/oldnewthing/archive/2010/01/22/9951750.aspx)。
  • 你确定?那是关于关键部分的。据我了解,现代 CRT 并不关心关机期间的内存释放。无论如何,操作系统都会清理干净。
  • 在 DLL 卸载时,将从 DllMain 调用 map 的析构函数。这个析构函数将释放映射持有的动态内存。 CRT 堆受临界区保护,因此可能会出现链接中描述的效果。另外,map中KeyValue对象的析构函数也会被调用,我们不知道它们是否是DllMain安全的。
  • @atzz:我刚刚修复了一个由这些后果引起的错误。只能确认!
【解决方案3】:

实际上,您可能做出了错误的假设:

加载、静态和全局数据初始化应该在调用 DLLMain 之前进行。

参见 Effective C++ 的第 4 条:

初始化的顺序 中定义的非局部静态对象 不同的翻译单位是 未定义

原因是确保“正确”的初始化顺序是不可能的,因此 C++ 标准干脆放弃它,并将其保留为未定义。

因此,如果您的 DllMain 与声明静态变量的代码位于不同的文件中,则行为未定义,并且您很有可能在实际完成初始化之前调用 DllMain。

解决方案非常简单,在 Effective C++ 的同一项目中进行了概述(顺便说一句:我强烈建议您阅读那本书!),并且需要在函数中声明静态变量,然后简单地返回它。

【讨论】:

  • 您混淆了标准 C++(甚至不了解 DLL)和 Windows。在 Windows 中,静态和全局初始化仍然以未指定的顺序发生,但它确实发生在 DllMain 中。
  • 初始化的顺序是编译器特定的。 Windows 确实知道有关初始化的任何事情(以及它是如何做到的?) - 它只是使用 DLL_PROCESS_ATTACH 调用 DllMain。编译器可能会在 DllMain 之前初始化所有静态变量,但不能保证。至少,我记得在 VC 6.0 中(是的,很久以前)我遇到了与报告的问题完全相同的问题,而罪魁祸首最终是对静态初始化的错误假设。
  • @Roberto:在函数内部声明静态变量非常危险,因为它不是线程安全的(因此可能导致双重初始化)。函数内部的静态初始化器只能用于简单的 POD 变量。
  • 如果要保证线程安全,就需要使用一些同步对象。依赖非本地静态数据来保证线程安全绝对不是解决办法。
  • @Roberto:为什么?全局变量在单个线程中初始化。 (我怀疑即使使用动态链接也可能不会初始化 CRT)并且此时可能会进行一些限制性初始化。
【解决方案4】:

我想指出应该避免在 DLL 中使用复杂的静态对象。

请记住,DLL 中的静态初始化程序是从 DllMain 调用的,因此对 DllMain 代码的所有限制都适用于静态对象的构造函数和析构函数。特别是,std::map 可以在构造过程中分配动态内存,如果 C++ 运行时尚未初始化(如果您使用动态链接的运行时),这可能会导致不可预知的结果。

有一篇写DllMain的好文章:Best Practices for Creating DLLs

我建议将您的静态映射对象更改为静态指针(可以安全地进行零初始化),并添加单独的 DLL 导出函数进行初始化,或使用延迟初始化(即在访问指针之前检查指针并如果对象为空,则创建对象)。

【讨论】:

  • 投票赞成,因为 atzz 提出了一个很好的观点。您确实希望避免在加载程序锁定下做很多事情,即使这样,您也应该将自己限制在您知道本质上安全的操作(列表不长,这是肯定的)。
  • 惰性初始化无济于事 - 当 DLL 被卸载时,您最终会调用 CRT 函数来释放 DllMain() 中的内存
  • @bdonlan - 这取决于实现。就像我上面所说的那样,根本没有解除分配,并且对象将在 DLL 卸载时泄漏:)。但我同意你的观点,在这里使用延迟初始化会使清理变得复杂。
  • 确实如此。请参阅我更新的答案以了解另一种可能性 - 如果使用基于进程堆的自定义 C++ 分配器分配映射,则初始化顺序没有问题(kernel32.dll 将始终首先初始化)。
  • @atzz - 静态链接的运行时泄漏将在 DLL 卸载时被删除。在这种情况下,延迟初始化效果很好。
【解决方案5】:

链接 DLL 时,是否指定了 /ENTRY 开关?如果是这样,它将覆盖链接器的默认值,即 DllMainCRTStartup。因此,_CRT_INIT 不会被调用,而全局初始化器也不会被调用,这将导致未初始化的全局(静态)数据。

如果您为自己的入口点指定 /ENTRY,则需要在进程附加和进程分离期间调用 _CRT_INIT。

如果您未指定 /ENTRY,则链接器应使用 CRT 的入口点,该入口点在调用 DllMain 之前在进程附加/分离上调用 _CRT_INIT。

【讨论】:

  • /ENTRY 未指定。我是从微软的 DllMain 文档中假设的。我会尝试明确地调用它。
  • 这似乎行得通。微软的文档非常混乱。完成了support.microsoft.com/kb/94248 第 2 节中的步骤,并且有效。感谢您的帮助。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-05-04
  • 2015-05-18
  • 1970-01-01
  • 2020-04-14
相关资源
最近更新 更多