问题背景
项目中遇到了这么个场景:项目中,所有文件可以生成一个动态链接库A.so
场景1:
A.so 假设由N个.o生成,不妨假设由a.o,b.o生成。
如果将main.cpp -> main.o
main.o, a.o, b.o -> main_bin,运行./main_bin,运行正常。
场景2:
将A.so、main.cpp,两个一起生成一个可执行程序
即是main.cpp -> main.o
A.so , main.o -> main_bin,运行./main_bin,出现在double free问题。真实的现场长这个样子
glibc detected *** double free or corruption(fasttop)
问题初步分析
直接原因,肯定是因为某个变量被重复释放了。可是,这可能是什么原因导致呢?直接换一种编绎方式去生成目标可执行程序,就不会出现问题,真是太奇怪了!
这时候,自己确实没有什么思路,一般常用手段,就是求助百度君,谷歌君了,看看有没有前人一些经验可以借鉴。
在查找问题的过程中,自己发现理论知识上的几个盲区:
1、GCC对静态链接库的链接过程是怎么样?
2、对动态链接库的过程是怎么样?
3、项目代码比较庞大,想办法缩小问题,进行定位。
对于问题1,2,可以参考一本书籍《程序员的自我修养—链接、装载与库》,花了一个晚上时间把相关章节学习。
问题缩小
对于研究问题,找到网上一则示例代码,可以非常完美的重现项目中的错误。
#ifndef _FOO_H
#define _FOO_H
#include <stdio.h>
#include <string>
class
Foo {
public
:
static
Foo s_foo;
Foo();
~Foo();
void
foo();
private
:
std::string _data;
};
#endif //_FOO_H
|
#include "foo.h"
Foo Foo::s_foo;
Foo::Foo() :
_data(
"foo"
) {
printf
(
"Foo this:%p\n"
,
this
);
}
Foo::~Foo() {
printf
(
"~Foo this:%p\n"
,
this
);
}
void
Foo::foo() {
printf
(
"foo this:%p\n"
,
this
);
}
|
//bar.h
#ifndef _BAR_H
#define _BAR_H
extern
"C"
{
void
bar();
}
#endif //_BAR_H
|
#include "foo.h"
#include "bar.h"
void
bar() {
Foo::s_foo.foo();
}
|
#include <iostream>
#include "foo.h"
#include "bar.h"
int
main() {
std::cout <<
"enter main"
<< std::endl;
Foo::s_foo.foo();
printf
(
"foo addr[%p]\n"
, &Foo::s_foo);
bar();
std::cout <<
"leave main"
<< std::endl;
return
0;
}
|
main:main.cpp libfoo.a libbar.so
g++ -o main main.cpp -L. -lfoo -lbar -g
libfoo.a:foo.cpp
g++ -c -fPIC $<
ar crv $@ foo.o
libbar.so:bar.cpp libfoo.a
g++ -c -fPIC bar.cpp
g++ -shared -o $@ bar.o -L. -lfoo
clean:
rm
libfoo.a libbar.so main -f
|
执行makefile,可以生成可执行程序main,即可简单复现问题。如下图
可以很清晰的看到,Foo类的构造函数与析构函数均被调用了两次。所以出现了double free错误。
问题解析
1、对于动态链接库而言,它的加载与卸载,它会自己去分配与释放自己管理的内存。
2、对于全局变量而言,如果在动态链接库中使用,它的地址,是在加载时候重定位获得。具体的细节,可以详细研究ELF文件中的GOT、PLT的原理了解,而且与编绎器的具体实现相关。
3、在这个具体问题中,动态链接库中,对静态全局变量static Foo s_foo的地址,计算得与静态库中一样。(这里我个人认为可能是编绎器实现的BUG)
由于本身静态库中存在一个static Foo s_foo, 生成main的时候,会有一个static Foo s_foo的对象链接到main之中。而动态链接的加载和卸载的时候,也会对同一个对象进行构造和析构(问题1有说明)。
解决方案
1、可以让总的链接过程之中,只存在一个static Foo s_foo对象。
即把最终链接时候,把动态链接库放到前面。即是:
g++ -o main main.cpp -L. -lfoo -lbar -g
这行修改成:
g++ -o main main.cpp -L. -lbar -lfoo -g
即可。
进行链接的时候,由于动态库在前面,则最终对符号s_foo链接的时候,由于其在动态链接库中,则不会将对象链接到最终的可执行main中,会由bar.so加载的时候再把对象载入。
2、也可以把libfoo.a编成一个动态链接库。原理同方案1