多共享动态库中同名对象重复析构问题的解决方法

红薯 发布于 2010/10/25 17:13
阅读 562
收藏 1

Linux 支持的共享程序库(lib*.so)技术不仅能够有效利用系统资源,而且还对程序设计带来了很大的便利性、通用性等,因此被各种级别的应用系统广泛采用。 动态链接的共享库是在加载应用程序时被加载的,而且它与应用程序是在运行时绑定的:通过动态链接器,将动态共享库映射进应用程序的可执行内存中(动态链接);在启动应用程序时,动态装载器将所需的共享目标库映射到应用程序的内存(动态装载)。

在通常情况下,共享库都是通过使用附加选项 -fpic-fPIC 进行编译,从目标代码产生位置无关的代码(Position Independent Code,PIC),使用 -shared选项将目标代码放进共享目标库中。位置无关代码需要能够被加载到不同进程的不同地址,并且能得以正确的执行,故其代码要经过特别的编译处理:位置无关代码(PIC)对常量和函数入口地址的操作都是采用基于基寄存器(base register)BASE+ 偏移量的相对地址的寻址方式。即使程序被装载到内存中的不同地址,即 BASE 值不同,而偏移量是不变的,所以程序仍然可以找到正确的入口地址或者常量。

然而,当应用程序链接了多个共享库,如果在这些共享库中,存在相同作用域范围的同名静态成员变量或者同名 ( 非静态 ) 全局变量,那么当程序访问完静态成员变量或全局变量结束析构时,由于某内存块的 double free 会导致 core dump,这是由于 Linux 编译器的缺陷造成的。

应用场景原型

该问题源于笔者所从事的开发项目:IBM Tivoli Workload Scheduler (TWS) LoadLevelerLoadLevelerIBM在高性能计算(High Performance Computing,HPC)领域的一款作业调度软件。它主要分为两个大的模块,分别是调度模块(scheduler)和资源管理模块(resource manger)。 两个模块中分别含有关于配置管理功能的共享库,由于某些配置管理选项为两模块所共同采用,所以两模块之间共享了部分源文件代码,其中包含有同名的类静态成员。

可以通过以下简单的模型进行描述:


图 1. 应用场景
图片示例

对应的各模块代码片段如下图所示:


图 2. 应用场景模拟代码
图片示例

其中,test.c 是主程序,包含有两个头文件:api1.h 与 api2.h;头文件 api1.h 包含头文件 lib1/lib.h 和一功能函数 func_api1(),api2.h 包含头文件 lib2/lib.h 和一功能函数 func_api2();目录 lib1 和 lib2 下的源文件分别编译生成共享库 lib1.so 和 lib2.so。同时,头文件 lib1/lib.h 与 lib2/lib.h 链接到同一共享文件 lib.h。在文件 lib.h 中定义有一静态成员变量“static std::vector<int> vec_int”。

功能函数与各静态成员函数代码清单

功能函数 func_api1() 与 func_api2() 的实现类似,通过调用静态成员函数达到访问静态成员变量 vec_int的目的:


清单 1. 功能函数 func_api1(int)

        
void func_api1(int i) {
printf("%s.\n", __FILE__);

A::set(i);
A::print();
return;
}

 

静态成员函数 A::set() 与 A::print() 的实现如下:


清单 2. 静态成员函数 A::set(int)

        
void A::set(int num) {
vec_int.clear();
for (int i = 0; i < num; i++) {
vec_int.push_back(i);
}
return;
}



清单 3. 静态成员函数 A::print()

        
void A::print() {
for (int i = 0; i < vec_int.size(); i++) {
printf("vec_int[%d] = %d, addr: %p.\n", i, vec_int[i], &vec_int[i]);
}
printf("vec_int addr: %p.\n", &vec_int);
return;
}

 

A::set() 对静态成员 vec_int进行赋值操作,而 A::print() 则打印其中的值与当前项的内存地址。

运行结果

如果两个共享库是通过选项 -fpic-fPIC编译的话,运行程序 test,输出如下:


清单 4. 选项 -fPIC 的测试结果

        
$ export LD_LIBRARY_PATH=./:$LD_LIBRARY_PATH
$ g++ -g -o lib1.so -fPIC-rdynamic -shared lib1/lib.c
$ g++ -g -o lib2.so -fPIC-rdynamic -shared lib2/lib.c
$ g++ -g -o test -L./ -l1 -l2 test.c
$ ./test
api1.h.
vec_int[0] = 0, addr: 0x9cbf028.
vec_int[1] = 1, addr: 0x9cbf02c.
vec_int[2] = 2, addr: 0x9cbf030.
vec_int[3] = 3, addr: 0x9cbf034.
vec_int addr: 0xe89228.
*** glibc detected *** ./test: double free or corruption (fasttop): 0x09cbf028***
======= Backtrace:=========
/lib/libc.so.6[0x2b2b16]
/lib/libc.so.6(cfree+0x90)[0x2b6030]
/usr/lib/libstdc++.so.6(_ZdlPv+0x21)[0x5d1731]
./lib1.so(_ZN9__gnu_cxx13new_allocatorIiE10deallocateEPij+0x1d)[0xe88417]
./lib1.so(_ZNSt12_Vector_baseIiSaIiEE13_M_deallocateEPij+0x33)[0xe88451]
./lib1.so(_ZNSt12_Vector_baseIiSaIiEED2Ev+0x42)[0xe8849a]
./lib1.so(_ZNSt6vectorIiSaIiEED1Ev+0x60)[0xe8850c]
./lib2.so[0x961d6c]
/lib/libc.so.6(__cxa_finalize+0xa9)[0x275c79]
./lib2.so[0x961c34]
./lib2.so[0x962d3c]
/lib/ld-linux.so.2[0x23a7de]
/lib/libc.so.6(exit+0xe9)[0x2759c9]
/lib/libc.so.6(__libc_start_main+0xe4)[0x25fdf4]
./test(__gxx_personality_v0+0x45)[0x80484c1]
======= Memory map:========
......
00960000-00963000 r-xp 00000000 00:1b 7668734 ./lib2.so
00963000-00964000 rwxp 00003000 00:1b 7668734 ./lib2.so
00970000-00971000 r-xp 00970000 00:00 0 [vdso]
00e86000-00e89000 r-xp 00000000 00:1b 7668022 ./lib1.so
00e89000-00e8a000 rwxp 00003000 00:1b 7668022 ./lib1.so
08048000-08049000 r-xp 00000000 00:1b 7668748 ./test
08049000-0804a000 rw-p 00000000 00:1b 7668748 ./test
09cbf000-09ce0000 rw-p 09cbf000 00:00 0 [heap]
......
Abort(coredump)
$

 

从程序的输出直观的看到,core 产生是由于堆内存区域(09cbf000-09ce0000)中起始地址为 0x09cbf028的内存区被释放了两次导致的,该地址正式静态成员变量 vec_int的第一个元素的地址。

为什么会出现同一块内存区,被释放两次的情形呢?

原因分析

我们知道,静态成员变量与全局变量类似,都采用了静态存储方式。对于加了选项 -fpic-fPIC的共享库,这些变量的地址都存放在该共享库的全局偏移表(Global Offset Table,GOT)中。

通过 objdump或者 readelf命令分析共享库 lib1.so,结果如下:


清单 5. objdump 分析共享库 lib1.so 的输出

        
$ objdump -x -R lib1.so

lib1.so: file format elf32-i386
......
Sections:
Idx Name Size VMA LMA File off Algn
0 .gnu.hash 000001e8 000000d4 000000d4 000000d4 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
......
18 .dynamic 000000d8 0000301c 0000301c 0000301c 2**2
CONTENTS, ALLOC, LOAD, DATA
19 .got 00000014 000030f4 000030f4 000030f4 2**2
CONTENTS, ALLOC, LOAD, DATA
20 .got.plt 00000114 00003108 00003108 00003108 2**2
CONTENTS, ALLOC, LOAD, DATA
......
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
......
000030f4 R_386_GLOB_DAT __gmon_start__
000030f8 R_386_GLOB_DAT _Jv_RegisterClasses
000030fc R_386_GLOB_DAT _ZN1A7vec_intE
00003104 R_386_GLOB_DAT __cxa_finalize
......



清单 6. readelf 分析共享库 lib1.so 的输出

        
$ objdump -x -R lib1.so

lib1.so: file format elf32-i386
......
Sections:
Idx Name Size VMA LMA File off Algn
0 .gnu.hash 000001e8 000000d4 000000d4 000000d4 2**2
CONTENTS, ALLOC, LOAD, READONLY, DATA
......
18 .dynamic 000000d8 0000301c 0000301c 0000301c 2**2
CONTENTS, ALLOC, LOAD, DATA
19 .got 00000014 000030f4 000030f4 000030f4 2**2
CONTENTS, ALLOC, LOAD, DATA
20 .got.plt 00000114 00003108 00003108 00003108 2**2
CONTENTS, ALLOC, LOAD, DATA
......
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
......
000030f4 R_386_GLOB_DAT __gmon_start__
000030f8 R_386_GLOB_DAT _Jv_RegisterClasses
000030fc R_386_GLOB_DAT _ZN1A7vec_intE
00003104 R_386_GLOB_DAT __cxa_finalize
......

 

从上面两个命令的输出结果中可以看出,共享库 lib1.soGOT段的起始内存地址为 000030f4,大小为 20 字节 (0x14);静态成员变量 vec_int在共享库 lib1.so中的起始偏移地址为 000030fc。显然,vec_int位于该共享库的 GOT段内。

当应用程序同时链接 lib1.solib2.so时,同名静态成员变量 vec_int分别位于其共享库的 GOT区。当程序运行时,系统从符号表中查找并装载构造一份 vec_int数据,这点从程序运行的输出结果(清单 4)的“Backtrace”部分可以看到:只有 lib1.so中的静态成员变量被装载构造;同时,通过内存映射(Memory map)部分(清单 4),可以观察到 vec_int对象的地址 0xe89228正好处在为共享库 lib1.so分配的可读内存区 00e89000-00e8a000中:

        00e89000-00e8a000 rwxp 00003000 00:1b 7668022    ./lib1.so

 

然后,当程序结束时,却对该变量进行了两次析构操作,通过 gdb分析 core 文件:


清单 7. core 文件分析结果

        
$ gdb ./test core.28440
……
Core was generated by `./test'.
Program terminated with signal 6, Aborted.
#0 0x00970402 in __kernel_vsyscall ()
(gdb)
(gdb) where
#0 0x00970402 in __kernel_vsyscall ()
#1 0x00272d10 in raise () from /lib/libc.so.6
#2 0x00274621 in abort () from /lib/libc.so.6
#3 0x002aae5b in __libc_message () from /lib/libc.so.6
#4 0x002b2b16 in _int_free () from /lib/libc.so.6
#5 0x002b6030 in free () from /lib/libc.so.6
#6 0x005d1731 in operator delete () from /usr/lib/libstdc++.so.6
#7 0x00e88417 in __gnu_cxx::new_allocator<int>::deallocate
(this=0xe89228, __p=0x9cbf028)
at /usr/lib/gcc/i386-redhat-linux/.../ext/new_allocator.h:94
#8 0x00e88451 in std::_Vector_base<int, ... (this=0xe89228, __p=0x9cbf028, __n=4)
at /usr/lib/gcc/.../include/c++/4.1.2/bits/stl_vector.h:133
#9 0x00e8849a in ~_Vector_base (this=0xe89228)
at /usr/lib/gcc/.../include/c++/4.1.2/bits/stl_vector.h:119
#10 0x00e8850cin ~vector (this=0xe89228) at /usr/lib/gcc/.../stl_vector.h:272
#11 0x00961d6c in __tcf_0 () at lib2/lib.c:3
#12 0x00275c79 in __cxa_finalize () from /lib/libc.so.6
#13 0x00961c34 in __do_global_dtors_aux () from ./lib2.so
#14 0x00962d3c in _fini () from ./lib2.so
#15 0x0023a7de in _dl_fini () from /lib/ld-linux.so.2
#16 0x002759c9 in exit () from /lib/libc.so.6
#17 0x0025fdf4 in __libc_start_main () from /lib/libc.so.6
#18 0x080484c1 in _start ()
(gdb)

 

从清单 7 中可以看出,从帧 #14 开始,程序进行 lib2.so中的析构操作,直到 #11,都运行在 lib2.so中,当进入帧 #10 时,进行变量析构时,其地址为 0x00e8850c,该地址中的对象是程序启动时由共享库 lib1.so装载构造出来的(清单 1):

        ./lib1.so(_ZNSt6vectorIiSaIiEED1Ev+0x60)[0xe8850c]

 

当程序结束时,运行库 glibc检测到共享库 lib2.so析构了并非由其构造的对象,导致了 core dump。

这种情况下,如果替换使用选项 -fpie-fPIE,操作步骤与运行结果如下所示:


清单 8. 选项 -fPIE 的测试结果

        
$ export LD_LIBRARY_PATH=./:$LD_LIBRARY_PATH
$ g++ -g -o lib1.so -fPIE-rdynamic -shared lib1/lib.c
$ g++ -g -o lib2.so -fPIE-rdynamic -shared lib2/lib.c
$ g++ -g -pie -o test -L./ -l1 -l2 test.c
$ ./test
api1.h.
vec_int[0] = 0, addr: 0x80e3028.
vec_int[1] = 1, addr: 0x80e302c.
vec_int[2] = 2, addr: 0x80e3030.
vec_int[3] = 3, addr: 0x80e3034.
vec_int addr: 0x75e224.
$

 

程序运行结果符合期望并正常结束。

这是因为,当使用选项 -fpie-fPIE时,生成的共享库不会为静态成员变量或全局变量在 GOT中创建对应的条目(通过 objdumpreadelf命令可以查看,此处不再赘述),从而避免了由于静态对象“构造一次,析构两次”而对同一内存区域释放两次引起的程序 core dump。

选项 -fpie-fPIE-fpic-fPIC的用法很相似,区别在于前者总是将生成的位置无关代码看作是属于程序本身,并直接链接进该可执行程序,而非存入全局偏移表 GOT中;这样,对于同名的静态或全局对象的访问,其构造与析构操作将保持一一对应。

结束语

通过使用选项 -fpie-fPIE代替 -fpic或者 -fPIC,使得生成的共享库不会为静态成员变量或全局变量在 GOT中创建对应的条目,同时也就避免了针对同名静态对象“构造一次,析构两次”的不当操作。

加载中
返回顶部
顶部