【问题标题】:Why it is a mistake to 'allocate in one library and free in the other'为什么“在一个库中分配而在另一个库中释放”是错误的
【发布时间】:2018-07-08 14:25:12
【问题描述】:

谷歌在Android ndk guides site写道:

在一个库中分配内存,在另一个库中释放,导致内存泄漏或堆损坏。

  1. 为什么?
  2. 总是正确的?

编辑

正如@Galik 所写,这句话的上下文是:

在 C++ 中,在单个程序中定义多个相同函数或对象的副本是不安全的。这是 C++ 标准中存在的单一定义规则的一个方面。

当使用静态运行时(以及一般的静态库)时,很容易意外违反此规则。例如,以下应用程序违反了此规则:

...

在这种情况下,STL,包括全局数据和静态构造函数,将出现在两个库中。这个应用程序的运行时行为是未定义的,在实践中崩溃是很常见的。其他可能的问题包括:

  • 在一个库中分配内存,在另一个库中释放,导致内存泄漏或堆损坏。
  • libfoo.so 中引发的异常在 libbar.so 中未被捕获,导致您的应用崩溃。
  • std::cout 的缓冲无法正常工作。

【问题讨论】:

  • 不同的库可能使用不同的堆。
  • 如果开发者不使用另一个怎么办?
  • 根据您链接的文档,这种情况似乎仅在您通过在两个不同的共享库中使用静态 c++ 运行时打破单一定义规则时适用。您只能使用静态运行时来构建单库应用程序。
  • @y30 我不确定,但是通过静态链接,您可以获得包含链接到的所有代码的副本。所以,我想,如果你有两组不同的函数来分配和删除内存,他们可能会各自设置自己的内部簿记数据,告诉他们如何释放他们分配的内容。我想他们每个人都有操作系统提供的完全独立的内存池。
  • 我认为这样的问题最好进入Software Engineering

标签: c++ c android-ndk shared-libraries


【解决方案1】:

将其视为错误的一个可能原因是,分配通常伴随着某种初始化,而释放则伴随着一些破坏逻辑。


理论:

主要的危险是初始化/销毁逻辑不匹配。

让我们将两个不同的 STL 版本视为两个不同且独立的库。

考虑一下:每个库都允许您分配/取消分配某些东西。在获取资源后,每个图书馆都以自己的方式对那个东西进行一些内务处理,这些内容被封装(阅读:你不知道,也不需要)。 如果每个人做的家务有很大不同会怎样?


示例:

class Foo
{
private:
    int x;

public:
    Foo() : x(42) {}
};


namespace ModuleA
{
    Foo* createAFoo()
    {
        return new Foo();
    }

    void deleteAFoo(Foo* foo)
    {
        if(foo != nullptr)
            delete foo;
    }
}

namespace ModuleB
{
    std::vector<Foo*> all_foos;

    Foo* createAFoo()
    {
        Foo* foo = new Foo();
        all_foos.push_back(foo);
        return foo;
    }

    void deleteAFoo(Foo* foo)
    {
        if(foo != nullptr)
        {
            std::vector<int>::iterator position = std::find(all_foos.begin(), all_foos.end(), foo);
            if (position != myVector.end())
            {
                myVector.erase(position);
            }
            delete foo;
        }
    }
}

问题:如果我们执行以下操作会发生什么?

Foo* foo = ModuleB::createAFoo();
ModuleA::deleteAFoo(foo);

答案:ModuleB 现在有一个dangling pointer。 这可能会导致各种可怕且难以调试的问题。 我们也没有让all_foos 变小,这可能被认为是内存泄漏(每次指针的大小)。


问题:如果我们执行以下操作会发生什么?

Foo* foo = ModuleA::createAFoo();
ModuleB::deleteAFoo(foo);

Answer: 看起来……没有什么不好的事情发生! 但是如果我删除了if (position != myVector.end()) 支票呢?那我们就有问题了。 STL 可能会以优化的名义这样做,所以...

【讨论】:

  • 好点!但我认为你可以举出更好的例子。您不需要“ModuleA”,只需两次“ModuleB”(来自两个不同的库)。 [这与两个静态 STL 的情况完全相同]
  • “一个 STL 可能会以优化的名义这样做,所以......” - 有人可以给出一个真实的 STL 示例吗?
【解决方案2】:

我写了文档的那一部分。我不得不调试一个问题,其中一个标准流对象(cout 或类似的)被双重链接到库,导致对象的两个不同实例。对象的构造函数运行了两次,但在对象的同一个实例上运行了两次。一个对象被双重初始化,另一个未初始化。当使用未构造的对象时,它会尝试访问一些未初始化的内存并崩溃。

对于未定义行为的奇异性确实没有限制。我记得的错误完全有可能是我们当时使用的编译器、链接器或加载器版本所独有的。

编辑:这是一个复制案例:

// foo.cpp
#include <stdio.h>

class Foo {
 public:
  Foo() { printf("this: %p\n", this); }
};

Foo foo;

// main.cpp
int main() {
}

构建:

$ clang++ --version
clang version 7.0.0 (trunk 330210)
Target: x86_64-unknown-linux-gnu
Thread model: posix
InstalledDir: /usr/local/bin
$ clang++ foo.cpp -shared -o libfoo.so
$ clang++ foo.cpp -shared -o libbar.so
$ clang++ main.cpp -L. -lfoo -lbar -rpath  '$ORIGIN'

libfoo 和 libbar 都将被加载,并且每个都有自己的对象副本。构造函数将运行两次,但正如您所见,只有一个对象实例运行其构造函数;它只运行了两次。

$ ./a.out
this: 0x7f9475d48031
this: 0x7f9475d48031

【讨论】:

  • 如果您能提供一些示例代码,将会很有帮助。我想分析它,以了解确切的原因。谢谢
  • 我实际上花了一段时间尝试重新创建测试用例,但无法记住足够的细节。
  • 设法找出一个不良行为的例子。更新了答案。
猜你喜欢
  • 1970-01-01
  • 2019-02-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-11-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多