技巧:多共享動態庫中同名對象重復析構問題的解決方法(轉載)
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中創建對應的條目,同時也就避免了針對同名靜態對象“構造一次,析構兩次”的不當操作。
轉自:http://www.ibm.com/developerworks/cn/linux/l-cn-sdlstatic/
posted on 2012-03-02 16:04 周強 閱讀(426) 評論(0) 編輯 收藏 引用 所屬分類: linux