在大型项目中,很容易出现版本不匹配的问题,其中导致的虚函数飘移的问题比较难解决。
在这里,用一个例子来说明如何解决这种问题。
建立三个源文件:testso.h,testso.cpp,xuzhina_dump_c6_s3_ex.cpp。
testso.h的代码如下:
1 #ifndef __TESTSO_H__ 2 #define __TESTSO_H__ 3 4 class xuzhina_dump_c6_s3_ex 5 { 6 public: 7 virtual char* encode( char* str ); 8 }; 9 10 #endif
testso.cpp的代码如下:
1 #include <string.h> 2 #include "testso.h" 3 4 char* xuzhina_dump_c6_s3_ex::encode( char* str ) 5 { 6 return str; 7 } 8
xuzhina_dump_c6_s3_ex.cpp的代码如下:
1 #include <stdio.h> 2 #include "testso.h" 3 4 int main() 5 { 6 char* hello = (char*)"hello"; 7 xuzhina_dump_c6_s3_ex* test = new xuzhina_dump_c6_s3_ex; 8 9 char* p = test->encode( hello ); 10 11 printf( "the second char:%c\n", p[1] ); 12 13 return 0; 14 }
编译代码:
[buckxu@xuzhina 3_ex]$ g++ -o libtestso.so -shared -fpic testso.cpp [buckxu@xuzhina 3_ex]$ g++ -o xuzhina_dump_c6_s3_ex xuzhina_dump_c6_s3_ex.cpp -L. -ltestso
运行xuzhina_dump_c6_s3_ex可以得到这样的结果:
[buckxu@xuzhina 3_ex]$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.;./xuzhina_dump_c6_s3_ex the second char:e
现在在testso.h的类新增一个虚函数(注意虚函数声明的顺序):
1 #ifndef __TESTSO_H__ 2 #define __TESTSO_H__ 3 4 class xuzhina_dump_c6_s3_ex 5 { 6 public: 7 virtual char* parseVal( char* str ); 8 virtual char* encode( char* str ); 9 }; 10 11 #endif 在testso.cpp新增它的实现代码: 1 #include <string.h> 2 #include "testso.h" 3 4 char* xuzhina_dump_c6_s3_ex::encode( char* str ) 5 { 6 return str; 7 } 8 9 char* xuzhina_dump_c6_s3_ex::parseVal( char* str ) 10 { 11 char* p = strstr( str, "=" ); 12 if ( p != NULL ) 13 { 14 return p+1; 15 } 16 return NULL; 17 }
重新生成so文件,但不重新生成xuzhina_dump_c6_s3_ex(为什么?这是模拟几个部门协作的产品开发过程,往往有些版本制作人为了省事,没有清除重新编译)
[buckxu@xuzhina 3_ex]$ g++ -o libtestso.so -shared -fpic testso.cpp
运行xuzhina_dump_c6_s3_ex,它coredump了。
[buckxu@xuzhina 3_ex]$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.;./xuzhina_dump_c6_s3_ex ./xuzhina_dump_c6_s3_ex: Symbol `_ZTV21xuzhina_dump_c6_s3_ex' has different size in shared object, consider re-linking Segmentation fault (core dumped)
打开coredump文件,可以见到如下堆栈:
(gdb) bt #0 0x08048630 in main ()
看一下main函数的汇编:
(gdb) disassemble main Dump of assembler code for function main: 0x080485e0 <+0>: push %ebp 0x080485e1 <+1>: mov %esp,%ebp 0x080485e3 <+3>: push %ebx 0x080485e4 <+4>: and $0xfffffff0,%esp 0x080485e7 <+7>: sub $0x20,%esp 0x080485ea <+10>: movl $0x80486f4,0x1c(%esp) 0x080485f2 <+18>: movl $0x4,(%esp) 0x080485f9 <+25>: call 0x80484d0 <_Znwj@plt> 0x080485fe <+30>: mov %eax,%ebx 0x08048600 <+32>: mov %ebx,(%esp) 0x08048603 <+35>: call 0x8048650 <_ZN21xuzhina_dump_c6_s3_exC2Ev> 0x08048608 <+40>: mov %ebx,0x18(%esp) 0x0804860c <+44>: mov 0x18(%esp),%eax 0x08048610 <+48>: mov (%eax),%eax 0x08048612 <+50>: mov (%eax),%eax 0x08048614 <+52>: mov 0x1c(%esp),%edx 0x08048618 <+56>: mov %edx,0x4(%esp) 0x0804861c <+60>: mov 0x18(%esp),%edx 0x08048620 <+64>: mov %edx,(%esp) 0x08048623 <+67>: call *%eax 0x08048625 <+69>: mov %eax,0x14(%esp) 0x08048629 <+73>: mov 0x14(%esp),%eax 0x0804862d <+77>: add $0x1,%eax => 0x08048630 <+80>: movzbl (%eax),%eax 0x08048633 <+83>: movsbl %al,%eax 0x08048636 <+86>: mov %eax,0x4(%esp) 0x0804863a <+90>: movl $0x80486fa,(%esp) 0x08048641 <+97>: call 0x80484c0 <printf@plt> 0x08048646 <+102>: mov $0x0,%eax 0x0804864b <+107>: mov -0x4(%ebp),%ebx 0x0804864e <+110>: leave 0x0804864f <+111>: ret End of assembler dump.
看一下寄存器eax的值
(gdb) i r eax eax 0x1 1
可见,是由于eax的值为1而导致的。而eax由
0x08048623 <+67>: call *%eax 0x08048625 <+69>: mov %eax,0x14(%esp) 0x08048629 <+73>: mov 0x14(%esp),%eax 0x0804862d <+77>: add $0x1,%eax
可知是
0x08048623 <+67>: call *%eax
的返回值。
那么,eax所存放的函数指针又是从哪里来的?
0x0804860c <+44>: mov 0x18(%esp),%eax 0x08048610 <+48>: mov (%eax),%eax 0x08048612 <+50>: mov (%eax),%eax 0x08048614 <+52>: mov 0x1c(%esp),%edx 0x08048618 <+56>: mov %edx,0x4(%esp) 0x0804861c <+60>: mov 0x18(%esp),%edx 0x08048620 <+64>: mov %edx,(%esp) 0x08048623 <+67>: call *%eax
显然,最终是从esp+0x18所指向单元取来的,而由
0x08048600 <+32>: mov %ebx,(%esp) 0x08048603 <+35>: call 0x8048650 <_ZN21xuzhina_dump_c6_s3_exC2Ev> 0x08048608 <+40>: mov %ebx,0x18(%esp)
可知,esp+0x18存放着类xuzhina_dump_c6_s3_ex(shell c++filt _ZN21xuzhina_dump_c6_s3_exC2Ev)对象的this指针。
那么,
0x0804860c <+44>: mov 0x18(%esp),%eax 0x08048610 <+48>: mov (%eax),%eax 0x08048612 <+50>: mov (%eax),%eax
就是从this指针中取出虚函数的操作了。看一下类xuzhina_dump_c6_s3_ex的虚函数表:
(gdb) x $esp+0x18 0xbfc0fee8: 0x08c07008 (gdb) x /x 0x08c07008 0x8c07008: 0x08049958 (gdb) x /4x 0x08049958 0x8049958 <_ZTV21xuzhina_dump_c6_s3_ex+8>: 0xb77b16c8 0x00000000 0x00000000 0x00000000 (gdb) info symbol 0xb77b16c8 xuzhina_dump_c6_s3_ex::parseVal(char*) in section .text of libtestso.so
非常奇怪.按照代码逻辑,应该是xuzhina_dump_c6_s3_ex::encode(char *)才对的。
好了,定位到这个地步,就可以知道类xuzhina_dump_c6_s3_ex所在的头文件已经修改了,但没有进行重新编译,而且是在成员虚函数encode的前面增加了虚函数。为什么不是后面呢?有兴趣的读者可以试一下这种情况。
已经知道新版本的头文件被修改,剩下的工作用比较工具,如diff之类就可以找出修改处了。
当然还有另外一个方法来看增加了多少虚函数。
由上面的分析,可以知道0x08049958是类xuzhina_dump_c6_s3_ex的虚函数表地址。
(gdb) x /4x 0x08049958 0x8049958 <_ZTV21xuzhina_dump_c6_s3_ex+8>: 0xb77b16c8 0x00000000 0x00000000 0x00000000
(gdb) shell c++filt _ZTV21xuzhina_dump_c6_s3_ex vtable for xuzhina_dump_c6_s3_ex
那么,_ZTV21xuzhina_dump_c6_s3_ex的地址是0x08049950。看一下它的内容
(gdb) x /4x 0x08049950 0x8049950 <_ZTV21xuzhina_dump_c6_s3_ex>: 0x00000000 0xb77b2808 0xb77b16c8 0x00000000 (gdb) info symbol 0xb77b2808 typeinfo for xuzhina_dump_c6_s3_ex in section .data.rel.ro of libtestso.so (gdb) info symbol 0xb77b16c8 xuzhina_dump_c6_s3_ex::parseVal(char*) in section .text of libtestso.so
也就是说,类xuzhina_dump_c6_s3_ex后面紧接着虚函数表。有兴趣可以试一下其它的类,也是如此。
那么,看一下类xuzhina_dump_c6_s3_ex所在so文件libtestso.so,通过objdump来看。
[buckxu@xuzhina 3_ex]$ objdump -R libtestso.so libtestso.so: file format elf32-i386 DYNAMIC RELOCATION RECORDS OFFSET TYPE VALUE 000017e4 R_386_RELATIVE *ABS* 000017e8 R_386_RELATIVE *ABS* 000017f0 R_386_RELATIVE *ABS* 000017fc R_386_32 _ZTI21xuzhina_dump_c6_s3_ex 00001800 R_386_32 _ZN21xuzhina_dump_c6_s3_ex8parseValEPc 00001804 R_386_32 _ZN21xuzhina_dump_c6_s3_ex6encodeEPc 00001808 R_386_32 _ZTVN10__cxxabiv117__class_type_infoE 0000180c R_386_32 _ZTS21xuzhina_dump_c6_s3_ex 00001908 R_386_GLOB_DAT __gmon_start__ 0000190c R_386_GLOB_DAT _Jv_RegisterClasses 00001910 R_386_GLOB_DAT _ITM_deregisterTMCloneTable 00001914 R_386_GLOB_DAT _ITM_registerTMCloneTable 00001918 R_386_GLOB_DAT __cxa_finalize 00001928 R_386_JUMP_SLOT __gmon_start__ 0000192c R_386_JUMP_SLOT strstr 00001930 R_386_JUMP_SLOT __cxa_finalize [buckxu@xuzhina 3_ex]$ c++filt _ZTI21xuzhina_dump_c6_s3_ex typeinfo for xuzhina_dump_c6_s3_ex [buckxu@xuzhina 3_ex]$ c++filt _ZN21xuzhina_dump_c6_s3_ex8parseValEPc xuzhina_dump_c6_s3_ex::parseVal(char*) [buckxu@xuzhina 3_ex]$ c++filt _ZN21xuzhina_dump_c6_s3_ex6encodeEPc xuzhina_dump_c6_s3_ex::encode(char*)
可以看到类xuzhina_dump_c6_s3_ex有两个虚函数parseVal和encode,parseVal在encode前面。也就是说,在encode前面只是增加了一个虚函数。如果增加多个,可以从这里的顺序数出来的。