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]
;将参数指针的值存入eax
00DD19A4 mov edx,dword ptr [eax]
;将虚函数表的地址存入edx
00DD19A6 mov esi,esp
00DD19A8 mov ecx,dword ptr [ebp+8]
;ecx传参(this指针)
00DD19AB mov eax,dword ptr [edx]
;通过虚函数表间接调用,实现多态
;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
005812BC  jmp         00581A90 

继续跟踪

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
;保存ecx以获取this指针
00DD1AB0 mov ecx,0DDC035h
00DD1AB5 call 00DD133E
20: Base::test();
00DD1ABA mov ecx,dword ptr [ebp-8]
00DD1ABD call 00DD10D2

第15行又将ECX储存为一个局部变量,所以只要是成员函数,不管有没有使用this指针,内部始终存在一个地址局部变量储存这个结构体实例的首部地址。