标签:linear wrap off http 覆盖 布局 toc url cli
VC++编译器在编译时发现如果一个类有虚函数,那么编译器将会为这个类生成一个虚表(类似函数指针数组),
并且VC++编译器会在该类的第一个数据成员前插入一个指向该虚表的指针
下面用简单的代码测下:
class CTest { int m_nTest; public: CTest():m_nTest(1){} void virtual ShowInfo() { std::cout << m_nTest << std::endl; } void virtual ShowInfo1() { std::cout << m_nTest+1 << std::endl; } void virtual ShowInfo2() { std::cout << m_nTest+2 << std::endl; } }; int main(int argc, char* argv[]) { CTest t; t.ShowInfo(); t.ShowInfo1(); t.ShowInfo2(); return 0; }
让程序停在这个断点处:
在监视窗口中查看类对象t所在内存地址,并在内存窗口中查看t的内存布局:
可以看出对象t的起始地址为0x0048F730,但是这个地址存放的并不是数据成员m_nTest,其实VS的监视窗口已
经将其解释为_vfptr(虚表指针),_vfptr指针的值为0x001babdc,现在转到这个地址处的内存:
这个三个指针分别对应虚函数ShowInfo,ShowInfo1,ShowInfo3,如下图:
因为这是debug版的程序,所以会有jmp跳转,方便调试,如果是Release版的程序将不会有这些jmp指令,
调用时直接转移到对应的函数中
虚表中的虚函数顺序与虚函数在类中的声明位置有关,在类中第一个声明的虚函数在虚表中的位置总是第一个
第二个虚函数则排放在虚表中的第二个位置,依次排放
下面做个测试,调整虚函数在类中的声明位置,查看其在虚表中的位置变化:
例:虚函数在类中声明如下:
class CTest { int m_nTest; public: CTest():m_nTest(1){} void virtual ShowInfo() { std::cout << m_nTest << std::endl; } void virtual ShowInfo1() { std::cout << m_nTest+1 << std::endl; } void virtual ShowInfo2() { std::cout << m_nTest+2 << std::endl; } };
此时虚函数在虚表中的位置如下:
调整虚函数在类中的声明位置如下:
class CTest { int m_nTest; public: CTest():m_nTest(1){} void virtual ShowInfo1() { std::cout << m_nTest + 1 << std::endl; } void virtual ShowInfo() { std::cout << m_nTest << std::endl; } void virtual ShowInfo2() { std::cout << m_nTest+2 << std::endl; } };
此时虚函数在虚表中的位置如下:
可以看出随着虚函数在类中声明位置的变化,虚函数在虚表中的位置也发生对应的改变
通过类对象的方式调用虚函数称为直接调用,编译器直接生成调用该虚函数的代码
例:
int main(int argc, char* argv[]) { CTest t; t.ShowInfo(); t.ShowInfo1(); t.ShowInfo2(); return 0; }
观察上面代码的反汇编代码:
CTest t; 008D1A58 lea ecx,[t] 008D1A5B call CTest::CTest (08D1096h) t.ShowInfo(); 008D1A60 lea ecx,[t] 008D1A63 call CTest::ShowInfo (08D10B4h) t.ShowInfo1(); 008D1A68 lea ecx,[t] 008D1A6B call CTest::ShowInfo1 (08D11B3h) t.ShowInfo2(); 008D1A70 lea ecx,[t] 008D1A73 call CTest::ShowInfo2 (08D104Bh) return 0; 008D1A78 xor eax,eax
可以看出VC++编译器生成的直接调用对应虚函数的代码
通过指向对象的指针或引用调用虚函数,称为间接调用,编译器生成直接调用虚函数的代码,而是通过虚表指针
取出虚表内的虚函数指针,然后用虚函数指针调用虚函数
例:
int main(int argc, char* argv[]) { CTest t; CTest & rt = t; CTest * pt = &t; rt.ShowInfo2(); pt->ShowInfo(); return 0; }
观察上面代码的反汇编代码:
CTest t; 00121A58 lea ecx,[t] 00121A5B call CTest::CTest (0121096h) CTest & rt = t; 00121A60 lea eax,[t] 00121A63 mov dword ptr [rt],eax CTest * pt = &t; 00121A66 lea eax,[t] 00121A69 mov dword ptr [pt],eax rt.ShowInfo2(); 00121A6C mov eax,dword ptr [rt] //取对象t的地址 00121A6F mov edx,dword ptr [eax] //取虚表指针 00121A71 mov esi,esp 00121A73 mov ecx,dword ptr [rt] 00121A76 mov eax,dword ptr [edx+8] //取出ShowInfo2在虚表中的函数指针 00121A79 call eax //调用虚函数ShowInfo2 00121A7B cmp esi,esp 00121A7D call __RTC_CheckEsp (0121140h) pt->ShowInfo(); 00121A82 mov eax,dword ptr [pt] //取对象t的地址 00121A85 mov edx,dword ptr [eax] //取虚表指针 00121A87 mov esi,esp 00121A89 mov ecx,dword ptr [pt] 00121A8C mov eax,dword ptr [edx+4] //取出ShowInfo在虚表中的函数指针 00121A8F call eax //调用虚函数ShowInfo 00121A91 cmp esi,esp 00121A93 call __RTC_CheckEsp (0121140h) return 0; 00121A98 xor eax,eax
在普通成员函数中调用虚函数依然是间接调用
例:在CTest类中加入一个如下的成员函数:
void Test() { ShowInfo(); }
使用如下代码测试:
int main(int argc, char* argv[]) { CTest t; t.Test(); return 0; }
Test函数对应的反汇编代码如下:
void Test() { ShowInfo(); } 013719D0 push ebp 013719D1 mov ebp,esp 013719D3 sub esp,0CCh 013719D9 push ebx 013719DA push esi 013719DB push edi 013719DC push ecx 013719DD lea edi,[ebp-0CCh] 013719E3 mov ecx,33h 013719E8 mov eax,0CCCCCCCCh 013719ED rep stos dword ptr es:[edi] 013719EF pop ecx 013719F0 mov dword ptr [this],ecx 013719F3 mov eax,dword ptr [this] 013719F6 mov edx,dword ptr [eax] 013719F8 mov esi,esp 013719FA mov ecx,dword ptr [this] //取虚表指针 013719FD mov eax,dword ptr [edx+4] //从虚表中取虚函数ShowInfo的指针 01371A00 call eax //调用虚函数 01371A02 cmp esi,esp 01371A04 call __RTC_CheckEsp (01371140h) 01371A09 pop edi 01371A0A pop esi 01371A0B pop ebx 01371A0C add esp,0CCh 01371A12 cmp ebp,esp 01371A14 call __RTC_CheckEsp (01371140h) 01371A19 mov esp,ebp 01371A1B pop ebp 01371A1C ret
可以看出在成员函数中调用虚函数是间接调用,也是根据虚表来调用
在构造函数和析构函数中不会通过虚表来调用虚函数,而是直接在编译时生成直接调用虚函数的代码
例:
CTest():m_nTest(1) { ShowInfo1(); } ~CTest() { ShowInfo1(); }
对应反汇编代码如下:
CTest():m_nTest(1) 000D181C mov eax,dword ptr [this] 000D181F mov dword ptr [eax+4],1 ShowInfo1(); 000D1826 mov ecx,dword ptr [this] 000D1829 call CTest::ShowInfo1 (0D11C2h)
~CTest() { 000D1860 push ebp 000D1861 mov ebp,esp 000D1863 push 0FFFFFFFFh 000D1865 push 0D60D0h 000D186A mov eax,dword ptr fs:[00000000h] 000D1870 push eax 000D1871 sub esp,0CCh 000D1877 push ebx 000D1878 push esi 000D1879 push edi 000D187A push ecx 000D187B lea edi,[ebp-0D8h] 000D1881 mov ecx,33h 000D1886 mov eax,0CCCCCCCCh 000D188B rep stos dword ptr es:[edi] 000D188D pop ecx 000D188E mov eax,dword ptr [__security_cookie (0DB004h)] 000D1893 xor eax,ebp 000D1895 push eax 000D1896 lea eax,[ebp-0Ch] 000D1899 mov dword ptr fs:[00000000h],eax 000D189F mov dword ptr [this],ecx 000D18A2 mov eax,dword ptr [this] 000D18A5 mov dword ptr [eax],offset CTest::`vftable‘ (0D8B34h) ShowInfo1(); 000D18AB mov ecx,dword ptr [this] 000D18AE call CTest::ShowInfo1 (0D11C2h) //直接调用 }
标签:linear wrap off http 覆盖 布局 toc url cli
原文地址:https://www.cnblogs.com/UnknowCodeMaker/p/11279021.html