Just enjoy programming

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

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 的测试结果
          $ 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.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中创建对应的条目(通过 objdumpreadelf命令可以查看,此处不再赘述),从而避免了由于静态对象“构造一次,析构两次”而对同一内存区域释放两次引起的程序 core dump。

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

回页首

结束语

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

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





posted on 2012-03-02 16:04 周强 阅读(396) 评论(0)  编辑 收藏 引用 所属分类: linux


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理