C++多态的本质就是间接调用和虚函数表
示例代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| #include<iostream> #include<Windows.h> #include<stdlib.h>
class Base { private: int a = 10; public: virtual void test() { std::cout << this->a << std::endl; } };
class Sub :public Base { private: int b = 20; public:
void test() { Base::test(); std::cout << this->b << std::endl; } };
void print(Base *p) {
p->test(); } int main() {
__asm { mov eax, eax mov eax, eax mov eax, eax }
Base a; Sub b; print(&b);
std::cout << std::endl; system("pause"); }
|
反汇编分析
虚表
只要基类具有虚函数,那么这个类和其继承的子类都会在内存中生成一个虚函数表,在函数调用时函数的地址一般是固定的,但如果具有虚函数编译器会通过虚函数表间接调用。
如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 26: 27: p->test() 00DD19A1 mov eax,dword ptr [ebp+8]
00DD19A4 mov edx,dword ptr [eax]
00DD19A6 mov esi,esp 00DD19A8 mov ecx,dword ptr [ebp+8]
00DD19AB mov eax,dword ptr [edx]
00DD19AD call eax 00DD19AF cmp esi,esp 00DD19B1 call 00DD1258
|
ebp+8里的值就是类的地址,这个地址前4字节储存的就是虚函数的地址

如图所示,b的实例里储存了一个void**类型的指针,这个指针指向的地址就是成员函数的地址。
1 2 3
| 0x00587B3C e0 89 58 00 ??X. 0x00587B40 bc 12 58 00 ?.X. 0x00587B44 00 00 00 00 ....
|
内存里储存的正是0x005812bc
跟踪地址发现这是一个跳转指令
继续跟踪
1 2 3 4 5
| 19: void test() { 00581A90 push ebp 00581A91 mov ebp,esp 00581A93 sub esp,0CCh 00581A99 push ebx
|
0x00581A90 这个地址正是派生类重写的test函数。
成员函数的执行过程
C++中的成员函数使用时会在参数入栈后将类的地址传参(也就是this指针),不是通过压栈的方式,而是通过ECX寄存器传参,如果函数调用前通过ECX传参了那么基本可以断定这是C++程序而不是C语言程序
1
| 00DD19A8 mov ecx,dword ptr [ebp+8]
|
在取完结构体数据后又将EBP+8存到ECX里,调用成员函数test()后通过ECX获取this指针,继续跟进到成员函数内部。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 19: void test() { 00DD1A90 push ebp 00DD1A91 mov ebp,esp 00DD1A93 sub esp,0CCh 00DD1A99 push ebx 00DD1A9A push esi 00DD1A9B push edi 00DD1A9C push ecx 00DD1A9D lea edi,[ebp-0Ch] 00DD1AA0 mov ecx,3 00DD1AA5 mov eax,0CCCCCCCCh
00DD1AAA rep stos dword ptr es:[edi] 00DD1AAC pop ecx 00DD1AAD mov dword ptr [ebp-8],ecx
00DD1AB0 mov ecx,0DDC035h 00DD1AB5 call 00DD133E 20: Base::test() 00DD1ABA mov ecx,dword ptr [ebp-8] 00DD1ABD call 00DD10D2
|
第15行又将ECX储存为一个局部变量,所以只要是成员函数,不管有没有使用this指针,内部始终存在一个地址局部变量储存这个结构体实例的首部地址。