11.3 本章小结
虚函数在面向对象领域中应用得十分广泛,可以说,没有虚函数也就没有多态性,更谈不上面向对象的软件设计了。虚函数在面向对象软件设计中无处不在。
通过本章的介绍可知,虚函数的调用不难识别,如下所示:
00401092 mov ecx, dword ptr[ebp-14h];ecx得到this指针
00401095 mov edx, dword ptr[ecx];edx得到虚表指针
;Debug选项组中有栈平衡检查,这里用esi保存栈顶,后面的__chkesp会用到
00401097 mov esi, esp
00401099 mov ecx, dword ptr[ebp-14h];成员函数调用,传递this指针
;关键,这里是个间接调用,且为成员函数,可怀疑是虚函数
0040109C call dword ptr[edx+4]
;以下是Debug选项组产生的栈平衡检查代码,与本章内容无关
;有兴趣的读者可以自己独立分析
0040109F cmp esi, esp
004010A1 call__chkesp(00401740)
如何确定[edx+4]一定是虚函数的地址呢?证实edx是虚函数表的首地址是关键,于是识别构造函数和析构函数尤为重要,IDA的引用参考能给我们很大帮助。我们先假设edx是虚表指针,然后去查询引用参考。如果假设成立,就能找到所有的构造函数和唯一的析构函数,再看构造函数和析构函数是否满足第10章中讨论的必要条件。充要条件都满足了,就可以将其认定为虚函数,同时找到了所有的构造函数和唯一的析构函数。反之,如果假设不成立,那就应该怀疑是开发者自定义的函数指针数组。
关于atexit的实现原理,请查阅VC安装目录下的\VC98\CRT\SRC\CRT0DAT.C文件,其中的_cinit函数里会调用_initterm函数。如果程序存在全局对象、静态对象或者有调用atexit函数,那么在执行_initterm函数中(**pfbegin)();的时候会执行_onexitinit函数,这个函数用于初始化终止函数数组,这个终止函数数组在堆内存中,由_onexit函数负责维护。在main函数退出后,调用exit函数,exit函数又会调用doexit。在doexit函数内,遍历终止函数数组,倒序调用。请读者阅读源码文件VC98\CRT\SRC\ONEXIT.C,或者单步跟入_cinit和atexit,对此代码进行分析印证。