设为首页 加入收藏

TOP

12.1 识别类和类之间的关系(7)
2013-10-07 14:31:10 来源: 作者: 【 】 浏览:57
Tags:12.1 识别 类和 之间 关系

12.1 识别类和类之间的关系(7)

以上代码对普通析构函数与虚析构函数进行了对比,说明了为什么类在有了派生与继承关系后,需要声明虚析构函数的原因。对于没有派生和继承关系的类结构,是否将析构函数声明为虚析构函数不会影响调用的过程,但是在编写析构函数时应养成习惯,无论当前是否有派生或继承关系,都应将析构函数声明为虚析构函数,以防止将来更新和维护代码时发生析构函数的错误调用。

了解了派生和继承的执行流程与实现原理后,又该如何利用这些知识去识别代码中类与类之间的关系呢?最好的办法还是先定位构造函数,有了构造函数就可根据构造的先后顺序得到与之有关的其他类。在构造函数中只构造自己的类很明显是个基类。对于构造函数中存在调用父类构造函数的情况时,可利用虚表,在IDA中使用引用参考的功能便可得到所有的构造函数和析构函数,进而得到了它们之间的派生和继承关系。

将代码清单12-5修改为如下所示的代码,我们以Release选项组对这段代码进行编译,然后利用IDA对其进行分析。

  1. // 综合讲解(建议读者先用VC++(www.cppentry.com)分析一下Debug选项组编译的过程,然后再看本内容)  
  2. class CPerson{                      // 基类—人类  
  3. public:  
  4.     CPerson(){  
  5.     ShowSpeak(); // 注意,构造函数调用了虚函数  
  6.   }  
  7.     virtual ~CPerson(){  
  8.     ShowSpeak(); // 注意,析构函数调用了虚函数  
  9.   }  
  10.     virtual void ShowSpeak(){ // 在这个函数里调用了其他的虚函数GetClassName()  
  11.     printf("%s::ShowSpeak()\r\n", GetClassName());  
  12.     return;  
  13.   }  
  14.   virtual char* GetClassName()  
  15.   {  
  16.     return "CPerson";  
  17.   }  
  18. };  
  19.  
  20. class CChinese : public CPerson{            // 中国人,继承自"人"类  
  21. public:  
  22.     CChinese(){  
  23.     ShowSpeak();  
  24.   }  
  25.     virtual ~CChinese(){  
  26.     ShowSpeak();  
  27.   }  
  28.   virtual char* GetClassName(){  
  29.     return "CChinese";  
  30.   }  
  31. };  
  32.  
  33. void main(int argc, char* argv[]){  
  34.   CPerson *pPerson = new CChinese;  
  35.   pPerson->ShowSpeak();  
  36.   delete pPerson;  
  37. }  
  38.  
  39. ; 反汇编讲解  
  40. ; 在IDA中打开执行文件,载入sig,定位到main函数,得到如下代码  
  41.  
  42. .text:00401080 ; int __cdecl main(int argc, const char **argv, const char **envp)  
  43. .text:00401080 _main proc near ; CODE XREF: start+AFp  
  44. .text:00401080  
  45. .text:00401080 var_10dword ptr -10h  
  46. .text:00401080 var_Cdword ptr -0Ch  
  47. .text:00401080 var_4dword ptr -4  
  48. .text:00401080 argcdword ptr  4  
  49. .text:00401080 argvdword ptr  8  
  50. .text:00401080 envpdword ptr  0Ch  
  51. .text:00401080  
  52. .text:00401080     push 0FFFFFFFFh  
  53. .text:00401082     push offset unknown_libname_35 ; Microsoft VisualC 2-9/net runtime  
  54. .text:00401087     mov eax, large fs:0  
  55. .text:0040108D     push eax  
  56. .text:0040108E     mov large fs:0, esp ; 注册C++(www.cppentry.com)异常处理  
  57. .text:00401095     push ecx  
  58. .text:00401096     push esi ; 保存寄存器环境  
  59. .text:00401097     push 4 ; unsigned int  
  60. .text:00401099     call  2@YAPAXI@Z ; operator new(uint)申请4字节堆空间  
  61. .text:0040109E     mov esi, eax ; esi保存new调用的返回值  
  62. .text:004010A0     add esp, 4 ; 平衡new调用的参数  
  63. .text:004010A3     mov [esp+14h+var_10], esi ; new返回值保存到局部变量var_10中  
  64. ; 编译器插入了检查new返回值的代码,如果返回值为0,则跳过构造函数的调用  
  65. .text:004010A7     test esi, esi  
  66. ; 在IDA中单击var_4,引用处会高亮显示,可以观察出这个变量是计数标记  
  67. .text:004010A9     mov [esp+14h+var_4], 0  
  68. ; 单击下面这个跳转指令的标号loc_4010F2,目标处会高亮显示,结合目标处上面的一条指令(地址  
  69. ; 004010F0处),可以看出这是一个分支结构,跳转的目标是new返回值为0时的处理(将esi置为0)。读  
  70. ; 者可以按照命名规范重新定义这些标号(IDA中重命名的快捷键是N,选中标号以后按N键即可)  
  71. .text:004010B1     jz  short loc_4010F2  
  72. ; 如果new返回值不为0,则ecx保存堆地址,结合004010BB地址处的call指令,可推测是thiscall  
  73. ; 的调用方式,需要到004010BB处看看有没有访问ecx才能进一步确定  
  74. .text:004010B3     mov ecx, esi  
  75. ; 这个地方很关键,需要查看off_40C0DC中的内容  
  76. .text:004010B5     mov dword ptr [esi], offset off_40C0DC  
  77. off_40C0DC中的内容为:  
  78. .rdata:0040C0DC off_40C0DC dd offset sub_401170 ; DATA XREF: _main+35↑o  
  79. .rdata:0040C0DC     ; sub_40ACFB:loc_401120↑o sub_401170+3↑o sub_4011E0+49↑o  
  80. .rdata:0040C0E0     dd offset sub_401140 

IDA以注释的形式给出了反汇编代码中所有引用了标号off_40C0DC的指令地址,以便于我们分析时参考。如“; DATA XREF: _main+35”,这表示在main函数的首地址偏移35h字节处的指令引用了标号off_40C0DC,最后的上箭头“↑”表示引用处的地址在当前标号的上面,也就是说引用处的地址值比这个标号的地址值小。

接着观察sub_401170和sub_401140中的内容,双击后可以看到这两个名称都是函数名称,可证实off_40C0DC是函数指针数组的首地址,而且其中每个函数都有对ecx的引用,在引用前没有给ecx赋值,说明这两个函数都是将ecx作为参数传递的。结合004010B5处的指令“mov dword ptr [esi], offset off_40C0DC”,其中esi中保存的是new调用所申请的堆空间首地址,这条指令在首地址处放置了函数指针数组的地址。

结合以上种种信息,我们可以认定,esi中的地址是对象的地址,而函数指针数组就是虚表。退一步讲,即使源码不是这样,我们按此还原后的C++(www.cppentry.com)代码在功能和内存布局上也是等价的。

】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
分享到: 
上一篇12.1 识别类和类之间的关系(8) 下一篇12.1 识别类和类之间的关系(6)

评论

帐  号: 密码: (新用户注册)
验 证 码:
表  情:
内  容: