C++ Linux静态变量的双重销毁。链接符号重叠。

24

环境:linux x64,编译器gcc 4.x

项目具有以下结构:

static library "slib"
-- inside this library, there is static object "sobj"

dynamic library "dlib"
-- links statically "slib"

executable "exe":
-- links "slib" statically
-- links "dlib" dynamically
在程序结束时,“sobj”被析构两次。这种行为是预期的,但在相同的内存地址下进行了两次析构,即在析构函数中使用了相同的"this"指针,导致了双重析构问题。我认为这是由于某些符号重叠引起的。
那么这个冲突的解决方案是什么?也许可以通过一些链接选项来解决?
以下是测试用例:
main_exe.cpp
#include <cstdlib>

#include "static_lib.h"
#include "dynamic_lib.h"

int main(int argc, char *argv[])
{
    stat_useStatic();
    din_useStatic();
    return EXIT_SUCCESS;
}

static_lib.h

#ifndef STATIC_LIB_H
#define STATIC_LIB_H

#include <cstdio>

void stat_useStatic();
struct CTest
{
    CTest(): status(isAlive)
    {
        printf("CTest() this=%d\n",this);
    }
    ~CTest()
    {
        printf("~CTest() this=%d, %s\n",this,status==isAlive?"is Alive":"is Dead");
        status=isDead;
    }
    void use()
    {
        printf("use\n");
    }
    static const int isAlive=12385423;
    static const int isDead=6543421;
    int status;

    static CTest test;
};

#endif

静态库 static_lib.cpp

#include "static_lib.h"

CTest CTest::test;

void stat_useStatic()
{
    CTest::test.use();
}

dynamic_lib.h

#ifndef DYNAMIC_LIB_H
#define DYNAMIC_LIB_H

#include "static_lib.h"

#ifdef WIN32
#define DLLExport __declspec(dllexport)
#else
#define DLLExport 
#endif
DLLExport void din_useStatic();


#endif

动态链接库.cpp

#include "dynamic_lib.h"

DLLExport void din_useStatic()
{
    CTest::test.use();
}

CMakeLists.txt

project( StaticProblem )
cmake_minimum_required(VERSION 2.6)
if(WIN32)
else(WIN32)
    ADD_DEFINITIONS(-fPIC)
endif(WIN32)

ADD_LIBRARY( static_lib  STATIC static_lib.cpp static_lib.h)

ADD_LIBRARY( dynamic_lib SHARED dynamic_lib.cpp dynamic_lib.h)
TARGET_LINK_LIBRARIES( dynamic_lib static_lib )

ADD_EXECUTABLE( main_exe main_exe.cpp )
TARGET_LINK_LIBRARIES( main_exe static_lib dynamic_lib )

这个示例在 Windows 上运行得很好,但在 Linux 上会出现问题。 既然它在 Windows 上可以正常运行,解决方案应该是更改某些链接选项或类似的东西,而不是更改项目结构或不使用静态变量。

输出:

Windows

CTest() this=268472624
CTest() this=4231488
use
use
~CTest() this=4231488, is Alive
~CTest() this=268472624, is Alive

Linux

CTest() this=6296204
CTest() this=6296204
use
use
~CTest() this=6296204, is Alive
~CTest() this=6296204, is Dead

5
你确定你没有只删除了指向同一对象的两个指针吗?奥卡姆剃刀原则表明这可能是问题所在。 - Chris Frederick
4
你能提供一个经典的“最小可编译示例”,展示出问题所在吗? - Matteo Italia
我非常确定你删除了两次 - 我从未听说过“符号重叠”。请检查你的代码。 - Josh
没有代码,我们只能猜测(英语描述从来不精确)。请提供一些代码和编译说明以展示问题。 - Martin York
2
Josh,你还是100%确定吗? - John
4个回答

17
TL;DR: 不应该将库作为静态依赖和动态依赖同时链接。

在 Itanium ABI(由 clang、gcc、icc 等使用)中,静态变量的解构函数是如何执行的呢?

C++ 标准库提供了一种标准方法,在程序关闭之后(即 main 函数结束后)调用一个函数来定时执行程序关闭操作,这个方法叫做 atexit

它的行为相对简单,atexit 建立了一个回调函数栈,在程序关闭时按照它们被注册的相反顺序执行。

每当一个静态变量被构造,它的构造结束后就会在 atexit 栈中注册一个回调函数,在程序关闭时销毁它。


如果一个静态变量同时存在于静态链接库和动态链接库中会发生什么呢?

它会尝试重复存在。

每个库都会有:

  • 一个专门为该变量保留的内存区域,由相应的符号(变量的 mangled 名称)指向,
  • 一个加载段 (load section) 来构建该变量并安排其销毁。

意外出现在加载器中符号解析方式的工作方式上。加载器实质上是以先到先得的方式,建立符号和位置(指针)之间的映射。

但是,加载/卸载段没有名称,因此每个段都会被完整执行。

因此:

  • 静态变量第一次构造
  • 静态变量第二次构造 覆盖了第一个 (因为它们拥有相同的符号名称),这个时候第一个静态变量泄漏了,
  • 静态变量第一次销毁,
  • 静态变量第二次销毁;通常此时问题就被检测到了。

那么呢?

解决方案很简单:不要同时链接静态库A(直接链接)和动态库B,因为B也会链接A(动态或静态链接)。

根据用例不同,您可以选择:

  • 静态链接B
  • 动态链接A和B

由于在Windows上工作正常,解决方案应该是更改某些链接选项之类的东西,但不要更改项目结构或不使用静态变量。

如果您确实需要两个独立实例的静态变量,除了重构代码外,还可以将符号隐藏在动态库中。

这是Windows的默认行为,因此在那里需要使用DLLExport属性,因为对于CTest::test而言,它被忘记了,所以在Windows上的行为是不同的。

请注意,但是,如果您选择这种行为,该项目的任何未来维护者都会大声诅咒您。没有人希望静态变量有多个实例。


我知道这是一个旧帖子,但如果你正在寻找最佳解决方案,那么你应该看这篇帖子而不是被采纳的那个。此外,这篇帖子提供了一些很好的参考资料,可以帮助更好地理解这个问题。 - fogo

10

好的,我已经找到了解决方案:

http://gcc.gnu.org/wiki/Visibility

例如,如果更改

static CTest test;

to

__attribute__ ((visibility ("hidden"))) static CTest test;

问题将会消失。 Linux:

CTest() this=-1646158468
CTest() this=6296196
use
use
~CTest() this=6296196, is Alive
~CTest() this=-1646158468, is Alive

修复前的 nm 输出如下:

0000000000200dd4 B _ZN5CTest4testE

修复后:

0000000000200d7c b _ZN5CTest4testE

将全局符号"B"更改为局部符号"b"是它的区别。

不必向符号添加"attribute ((visibility ("hidden")))",可以使用编译器选项"-fvisibility=hidden"。该选项使gcc的行为更像Windows环境。


3

顺便说一下,如果在函数stat_useStatic内定义静态变量,则在linux中整个程序中只会有一个该静态变量实例(但在Windows中会有两个实例)- 这是我们用于解决该问题的方法。 以下是更改:

void stat_useStatic()
{
    static CTest stest;
    stest.use();
    CTest::test.use();
}


DLLExport void din_useStatic()
{
    stat_useStatic();
    CTest::test.use();
}

现在,Linux和Windows的行为差异更大了:
Windows
CTest() this=268476728
CTest() this=4235592
CTest() this=4235584
use
use
CTest() this=268476720
use
use
use
~CTest() this=4235584, is Alive
~CTest() this=4235592, is Alive
~CTest() this=268476720, is Alive
~CTest() this=268476728, is Alive

Linux

CTest() this=6296376
CTest() this=6296376
CTest() this=6296392
use
use
use
use
use
~CTest() this=6296392, is Alive
~CTest() this=6296376, is Alive
~CTest() this=6296376, is Dead

正如您所看到的,Linux 只创建一个静态变量,而 Windows 则创建两个实例。

事实上,从逻辑上讲,Linux 在第一种情况下不应该双倍创建和销毁静态变量,就像在第二种情况下一样(函数内的静态变量)。

使用函数局部的静态变量而不是类静态变量只是一种解决方法,而不是真正的解决方案。因为库源代码可能无法获取。


2

没有看到代码很难说,但这个领域(动态加载库)确实没有被标准明确覆盖,因此不同的实现可能会以不同的方式处理边缘情况。

你不能通过为两个静态库实例使用不同的命名空间(例如通过定义一个用于静态对象的命名空间,并使用命令行选项来定义)来避免这种混淆吗?


你的意思是要多次编译静态库,使用不同的命名空间吗? - John
是的。如果您将命名空间放在编译时参数中(例如g++ -D...选项),那么您可以使用命名空间编译动态加载库中使用的静态库,并使用另一个命名空间链接到可执行文件中的静态库。这样,两个对象将是不同的,而无需更改源代码中的用法。 - 6502
我也考虑过那个解决方案,但它并不可行 - 它只是一个权宜之计。如果静态库没有代码,即来自某个外部项目,该怎么办?由于该项目在Windows上运行良好,我认为这应该是Linux的好解决方案。 - John

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