虚表和虚表指针2.0
继续从汇编内存层次上对虚表和虚表指针进行分析
(任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法)
0x01 对象调用自身虚函数
class CVirtual { public: virtual int GetNumber() { return m_nNumber; return 0; } virtual void SetNumber(int nNumber) { m_nNumber = nNumber; } private: int m_nNumber; }; int main() { //int a = sizeof(CVirtual); CVirtual TheVirtual; TheVirtual.SetNumber(20); printf("%d\n", TheVirtual.GetNumber()); return 0; }
反汇编:
21: int main() 22: { 00EC3DA0 55 push ebp 00EC3DA1 8B EC mov ebp,esp 00EC3DA3 81 EC D0 00 00 00 sub esp,0D0h 00EC3DA9 53 push ebx 00EC3DAA 56 push esi 00EC3DAB 57 push edi 00EC3DAC 8D BD 30 FF FF FF lea edi,[ebp-0D0h] 00EC3DB2 B9 34 00 00 00 mov ecx,34h 00EC3DB7 B8 CC CC CC CC mov eax,0CCCCCCCCh 00EC3DBC F3 AB rep stos dword ptr es:[edi] 23: //int a = sizeof(CVirtual); 24: CVirtual TheVirtual; 00EC3DBE 8D 4D F4 lea ecx,[TheVirtual] 00EC3DC1 E8 6E D5 FF FF call CVirtual::CVirtual (0EC1334h) 25: 26: TheVirtual.SetNumber(20); 00EC3DC6 6A 14 push 14h 00EC3DC8 8D 4D F4 lea ecx,[TheVirtual] 00EC3DCB E8 0B D4 FF FF call CVirtual::SetNumber (0EC11DBh) 27: printf("%d\n", TheVirtual.GetNumber()); 00EC3DD0 8D 4D F4 lea ecx,[TheVirtual] 00EC3DD3 E8 BC D4 FF FF call CVirtual::GetNumber (0EC1294h) 00EC3DD8 50 push eax 00EC3DD9 68 3C 6B EC 00 push offset string "%d\n" (0EC6B3Ch) 00EC3DDE E8 97 D5 FF FF call _printf (0EC137Ah) 00EC3DE3 83 C4 08 add esp,8 28: return 0; 00EC3DE6 33 C0 xor eax,eax 29: }
虚函数SetNumber()反汇编分析:、
13: virtual void SetNumber(int nNumber) 14: { 00EC1770 55 push ebp 00EC1771 8B EC mov ebp,esp 00EC1773 81 EC CC 00 00 00 sub esp,0CCh 00EC1779 53 push ebx 00EC177A 56 push esi 00EC177B 57 push edi 00EC177C 51 push ecx 00EC177D 8D BD 34 FF FF FF lea edi,[ebp-0CCh] 00EC1783 B9 33 00 00 00 mov ecx,33h 00EC1788 B8 CC CC CC CC mov eax,0CCCCCCCCh 00EC178D F3 AB rep stos dword ptr es:[edi] 00EC178F 59 pop ecx 00EC1790 89 4D F8 mov dword ptr [this],ecx 15: m_nNumber = nNumber; 00EC1793 8B 45 F8 mov eax,dword ptr [this] 00EC1796 8B 4D 08 mov ecx,dword ptr [nNumber] 00EC1799 89 48 04 mov dword ptr [eax+4],ecx 16: } 00EC179C 5F pop edi 16: } 00EC179D 5E pop esi 00EC179E 5B pop ebx 00EC179F 8B E5 mov esp,ebp 00EC17A1 5D pop ebp 00EC17A2 C2 04 00 ret 4
可以看到,虚函数与普通函数的实现流程并无差别,并没有看到虚表指针之类的操作。
也就是说,直接通过对象调用自身的成员虚函数的时候,编译器使用了直接调用函数的方式,没有访问续表指针,来间接获取虚函数地址。
0x02 析构函数对虚表指针的操作
class CVirtual { public: virtual int GetNumber() { return m_nNumber; return 0; } virtual void SetNumber(int nNumber) { m_nNumber = nNumber; } ~CVirtual() { } private: int m_nNumber; }; int main() { //int a = sizeof(CVirtual); CVirtual TheVirtual; //TheVirtual.SetNumber(20); //printf("%d\n", TheVirtual.GetNumber()); return 0; }
反汇编:
17: ~CVirtual() 18: { 00A51730 55 push ebp 00A51731 8B EC mov ebp,esp 00A51733 81 EC CC 00 00 00 sub esp,0CCh 00A51739 53 push ebx 00A5173A 56 push esi 00A5173B 57 push edi 00A5173C 51 push ecx 00A5173D 8D BD 34 FF FF FF lea edi,[ebp-0CCh] 00A51743 B9 33 00 00 00 mov ecx,33h 00A51748 B8 CC CC CC CC mov eax,0CCCCCCCCh 00A5174D F3 AB rep stos dword ptr es:[edi] 00A5174F 59 pop ecx 00A51750 89 4D F8 mov dword ptr [this],ecx 00A51753 8B 45 F8 mov eax,dword ptr [this] 00A51756 C7 00 34 6B A5 00 mov dword ptr [eax],offset CVirtual::`vftable' (0A56B34h) 19: 20: }
从析构函数中摘取关键的汇编指令:
00A5174F 59 pop ecx 00A51750 89 4D F8 mov dword ptr [this],ecx 00A51753 8B 45 F8 mov eax,dword ptr [this]
可以看出这和我写的上一篇博客:虚表与虚表指针中构造函数的操作是一模一样的,pop ecx还原this指针的值到ecx中,然后通过ecx赋值给this指针,再由eax得到this指针。
最后一步:
00A51756 C7 00 34 6B A5 00 mov dword ptr [eax],offset CVirtual::`vftable' (0A56B34h)
将当前类的续表首地址赋值到虚表指针中。
通过分析构造函数和析构函数的流程可知:
两者对虚标的操作过程几乎一致,都是将虚表指针设置为当前对象所属类的虚表首地址。看起来相同,其实差别很大。
构造函数当中的虚表指针初始化,是将虚表指针初始化为正确的虚函数表基地址;而析构函数写入虚表指针,是将原对象的虚表指针重新赋值,其指针可能指向了另外一个虚表。
下面有一份继承代码的反汇编来验证析构函数中对象虚表指针的重新赋值(验证结果为:父类指针指向子类的对象,并调用子类中的同名虚函数时,虚表指针会指向子类的虚函数表,再调用子类的函数,对象析构时,父类析构函数会将当前的虚表指针重新赋值为父类的虚表指针。)
代码流程:
#include<iostream> using namespace std; class A { public: void 刘大大() { printf("1\n"); } virtual void 刘小() { printf("2\n"); } ~A() { } }; class B : public A { public: void 刘大大() { printf("3\n"); } void 刘小() { printf("4\n"); } }; int main(void) { B b; A *p = &b; p->刘小(); return 0; }
准备调用刘小()函数前,对象中的虚表指针指向B类的虚表:
结束后B的析构函数中callA的析构函数:
A的析构函数重新赋值虚表指针后,虚表指针指向了A的虚表: