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

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

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

  结束语

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


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值