12.1 识别类和类之间的关系(8)
接着按N键将off_40C0DC重命名,这里先命名为vTable_40C0DC,在接下来的分析中如果找到更详细的信息,还可以继续修改这个名称,使代码的可读性更强。
- .text:004010B5 mov dword ptr [esi], offset vTable_40C0DC
既然是对虚表指针进行初始化,就要满足构造函数的充分条件,但是我们看到这里并没有调用构造函数,而是直接在main函数中完成了虚表指针的初始化,这说明构造函数被编译器内联优化了。接下来我们来看一个内存间接调用:
- .text:004010BB call ds:off_40C0E4
off_40C0E4中的内容如下:- .rdata:0040C0DC vTable_40C0DC dd offset sub_401170 ; DATA XREF: _main+35↑o
- .rdata:0040C0DC ; sub_40ACFB:loc_401120↑o sub_401170+3↑o sub_4011E0+49↑o
- .rdata:0040C0E0 dd offset sub_401140
- .rdata:0040C0E4 off_40C0E4 dd offset sub_401160 ; DATA XREF: _main+3Br
不难发现,这个地址就在刚才我们分析的虚函数表的首地址附近,这很可能是虚表中的一部分!不过现在只能是怀疑,我们还没有证据。先看看这个函数的功能。双击地址0040C0E4 处“off_40C0E4 dd offset sub_401160”中的sub_401160,定位到sub_401160的代码实现处,此处内容如下所示: - .text:00401160 sub_401160 proc near
- .text:00401160 mov eax, offset aCperson ; "CPerson" ; 功能很简单,返回名称字符串
- .text:00401165 retn
- .text:00401165 sub_401160 endp
顺手修改sub_401160的名称,这里先修改为GetCPerson,以后有更多信息时再进一步修改。对应地,由于在off_40C0E4中保存了函数GetCPerson的地址,说明它是一个函数指针,因此也可以将其名称修改为pfnGetCPerson,修改完毕后如下所示:- .rdata:0040C0E4 pfnGetCPerson dd offset GetCPerson ; DATA XREF: _main+3Br
接着分析其后的代码:- .text:004010C1 push eax
- .text:004010C2 push offset aSShowspeak ; "%s::ShowSpeak()\r\n"
- .text:004010C7 call _printf
- .text:004010CC add esp, 8 ; 调用printf,并平衡参数
- .text:004010CF mov ecx, esi
- .text:004010D1 mov byte ptr [esp+14h+var_4], 1 ; 计数器加1
- .text:004010D6 mov dword ptr [esi], offset off_40C0D0 ;写入虚表指针,分析过程与上;面的内容一致,略
- .text:004010DC call ds:off_40C0D8 ; 内存间接调用
双击off_40C0D8,查看调用目标:- .rdata:0040C0D8 off_40C0D8 dd offset sub_4011B0 ; DATA XREF: _main+5Cr
off_40C0D8中保存了函数sub_4011B0的地址,双击sub_4011B0,其功能如下所示:- .text:004011B0 sub_4011B0 proc near
- .text:004011B0 mov eax, offset aCchinese ; "CChinese" ; 功能很简单,返回名称字符串
- .text:004011B5 retn
- .text:004011B5 sub_4011B0 endp
修改一下这个函数的名称,这里改为GetCChinese,也对应修改函数指针off_40C0D8的名称为pfnGetCChinese,修改完毕后如下所示:- .rdata:0040C0D8 pfnGetCChinese dd offset GetCChinese ; DATA XREF: _main+5Cr
接着分析后面的代码:
- .text:004010E2 push eax
- .text:004010E3 push offset aSShowspeak ; "%s::ShowSpeak()\r\n"
- .text:004010E8 call _printf
- .text:004010ED add esp, 8 ; 调用printf并平衡参数
- .text:004010F0 jmp short loc_4010F4 ; 跳过else分支
- .text:004010F2 ; --------------------------------------------------------------
- .text:004010F2
- .text:004010F2 loc_4010F2: ; CODE XREF: _main+31j
- .text:004010F2 xor esi, esi ; 如果new调用的返回值为0,则esi为0
到此为止,我们分析了new调用后的整个分支结构。当new调用成功时,会执行对象的构造函数,而编译器对这里的构造函数进行了内联优化,但这不会影响我们对构造函数的鉴定。首先存在写入虚表指针的充分条件,同时也满足前面章节讨论的必要条件,还要出现在new调用的正确分支中,因此,我们可以把new调用的正确分支中的代码判定为构造函数的内联方式。在new调用的正确分支内,由于esi所指向的对象有两次写入虚表指针的代码,如下所示:- .text:004010B5 mov dword ptr [esi], offset vTable_40C0DC
- ;中间代码略
- .text:004010D6 mov dword ptr [esi], offset vTable_40C0D0