关闭

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

762人阅读 评论(0) 收藏 举报
分类:

http://www.ibm.com/developerworks/cn/linux/l-cn-sdlstatic/

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) LoadLevelerLoadLeveler是 IBM在高性能计算(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 的测试结果

点击查看代码清单

从程序的输出直观的看到,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.so中 GOT段的起始内存地址为 000030f4,大小为 20 字节 (0x14);静态成员变量 vec_int在共享库 lib1.so中的起始偏移地址为 000030fc。显然,vec_int位于该共享库的 GOT段内。

当应用程序同时链接 lib1.so和 lib2.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中创建对应的条目(通过 objdump或 readelf命令可以查看,此处不再赘述),从而避免了由于静态对象“构造一次,析构两次”而对同一内存区域释放两次引起的程序 core dump。

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

结束语

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

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:120855次
    • 积分:1520
    • 等级:
    • 排名:千里之外
    • 原创:23篇
    • 转载:35篇
    • 译文:0篇
    • 评论:10条
    最新评论