动态共享库引发的double free错误的分析

问题背景

项目中遇到了这么个场景:项目中,所有文件可以生成一个动态链接库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)

扫描二维码关注公众号,回复: 1926018 查看本文章

问题初步分析

直接原因,肯定是因为某个变量被重复释放了。可是,这可能是什么原因导致呢?直接换一种编绎方式去生成目标可执行程序,就不会出现问题,真是太奇怪了!

这时候,自己确实没有什么思路,一般常用手段,就是求助百度君,谷歌君了,看看有没有前人一些经验可以借鉴。

在查找问题的过程中,自己发现理论知识上的几个盲区:

1、GCC对静态链接库的链接过程是怎么样?

2、对动态链接库的过程是怎么样?

3、项目代码比较庞大,想办法缩小问题,进行定位。

对于问题1,2,可以参考一本书籍《程序员的自我修养—链接、装载与库》,花了一个晚上时间把相关章节学习。

问题缩小

对于研究问题,找到网上一则示例代码,可以非常完美的重现项目中的错误。

foo.h
#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
foo.cpp
#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
//bar.h
#ifndef _BAR_H
#define _BAR_H
extern "C" {
     void bar();
}
#endif  //_BAR_H
bar.cpp
#include "foo.h"
#include "bar.h"
void bar() {
     Foo::s_foo.foo();
}
main.cpp
#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;
}
makefile
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

总结

对于库的链接与底层实现,本来不是C++标准的部分,这与每个编绎器的厂商的实现细节相关。像这个问题,就是与具体实现相关了。
同样的一份代码,我在mac上跑,mac上的编绎器版本是 clang-700.1.18
则不会有这个问题。
对同样的代码,执行结果如下:
由结果可以看到,mac上的编绎器,把这两块的地址处理成不个对象了。所以构造两次与析构两次都不会有问题。

猜你喜欢

转载自blog.csdn.net/dreamvyps/article/details/80890309