用汇编的角度剖析c++的virtual
多态是c++的关键技术,背后的机制就是有一个虚函数表,那么这个虚函数表是如何存在的,又是如何工作的呢?
当然不用的编译器会有不同的实现机制,本文只剖析vs2015的实现。
单串继承
首先看一段简单的代码:
class A {
private:
int a_value;
public:
A() {};
virtual ~A() {};
virtual void my_echo() { std::cout << "A::my_echo" << std::endl; };
virtual void echo() { std::cout << "A::echo" << std::endl; };
virtual void print() { std::cout << "A::print" << std::endl; };
};
class B :public A {
public:
B() {};
~B() {};
int b_value;
virtual void echo()override { std::cout << "B::echo" << std::endl; };
virtual void print()override { std::cout << "B::print" << std::endl; };
void B_Fun() { std::cout << "B::B_Fun" << std::endl; };
};
class C :public B {
private:
int c_value;
public:
C() {};
~C() {};
virtual void echo()override { std::cout << "C::echo" << std::endl; };
virtual void my_print() { std::cout << "C::print" << std::endl; };
};
C继承B,B继承A。
先回顾下类的内存布局,看类C的内存布局。
class C size(16):
+---
0 | +--- (base class B)
0 | | +--- (base class A)
0 | | | {vfptr}
4 | | | a_value
| | +---
8 | | b_value
| +---
12 | c_value
+---
C::$vftable@:
| &C_meta
| 0
0 | &C::{dtor}
1 | &A::my_echo
2 | &C::echo
3 | &B::print
4 | &C::my_print
如上所示,虚函数表指针
在首地址,占用一个指针大小,值得注意的是虚函数表首位指针是D的析构函数
,那是因为基类有虚析构函数。
来个简单调用。
A* b = new C();
b->print();//输出为 B::print
print
这个虚函数到底是怎么执行的?又是如何达到多态效果的呢?
再看print
调用的汇编代码。
32位系统
b->print();
00EF6306 mov eax,dword ptr [b]
00EF6309 mov edx,dword ptr [eax]
00EF630B mov esi,esp
00EF630D mov ecx,dword ptr [b]
00EF6310 mov eax,dword ptr [edx+0Ch]
00EF6313 call eax
64位系统
b->print();
00007FF6336D2CBD mov rax,qword ptr [b]
00007FF6336D2CC1 mov rax,qword ptr [rax]
00007FF6336D2CC4 mov rcx,qword ptr [b]
00007FF6336D2CC8 call qword ptr [rax+18h]
32位解析
00EF6306 mov eax, dword ptr[b]
#将指针b变量指向内存地址的dword大小赋值给eax,实际就是获得b指向的地址,之后eax为b对象的实际地址,
00EF6309 mov edx, dword ptr[eax]
#将地址eax指向的内存地址,取出一个dword赋值给edx,之后edx就是虚函数表的首地址,
00EF630D mov ecx, dword ptr[b]
#同上,将b实际指向的地址赋值给ecx,这句话作用是为了成员函数活得所需的this指针,为call做参数
00EF6310 mov eax, dword ptr[edx+0Ch]
#这里是将edx +12,因为是32位,所以这里是在虚函数表头地址向后偏移了4个函数指针,此时的地址就是虚函数print的指针地址了
#然后将print指针赋值给eax
00EF6313 call eax
#调用print函数
64的汇编与32位相差不大,值得注意的是64位系统指针是8字节,所以是18h大小。可以看出在简单继承情况下,虚函数指针都是在首位的,而且虚函数表事一个共用表
。
比如上面的单继承C=>B=>A,在new出C后,转换为基类B或者A时,是共用的一个虚函数表,接下来用一段代码来证明。
#ifndef _WIN64
typedef unsigned int pointer;//32位指针
#else
typedef unsigned long long pointer;//64位指针
#endif
//------------------------------
B b;
B* b1 = new B();
A* a = new A();
A *a1 = dynamic_cast <A*>(b1);
B* b2 = dynamic_cast<B*>(a1);
pointer vfptr_b = *(pointer*)&b;
pointer vfptr_b1 = *(pointer*)b1;
pointer vfptr_a = *(pointer*)a;
pointer vfptr_a1 = *(pointer*)a1;
pointer vfptr_b2 = *(pointer*)b2;
std::cout
<< vfptr_b << "\n"
<< vfptr_b1 << "\n"
<< vfptr_a << "\n"
<< vfptr_a1 << "\n"
<< vfptr_b2 << std::endl;
上面的代码意思就是将各种类型强行转换为指针,得到虚函数表的地址,从结果我们看到,只有vfptr_a不一样,也就是new A的虚函数表不一样,其余的,特比是dynamic_cast <A*>
转换类型的虚函数表地址,还是为B的虚函数表地址。
我们就可以得到一个简单结论,当是单继承时,子类指针转换为父类指针,指针地址不变,虚函数表不变,父类指针只用虚函数表的前半段。
接下来从汇编结合虚函数表来分析单继承多态实现的原理。
B* b = new C();
b->print();
A *a = dynamic_cast <A*>(b);
a->echo();
B* b2 = new B();
b2->echo();
B* b = new C();
008B660D push 10h
008B660F call operator new (08B131Bh) //new 开辟内存
...........
008B6633 call C::C (08B14D3h) //调用构造
...........
00C06663 mov dword ptr [b],ecx //赋值给b
b->print();
008B6666 mov eax,dword ptr [b] //得到对象地址
008B6669 mov edx,dword ptr [eax] //得到虚函数表首地址
008B666D mov ecx,dword ptr [b] //this 指针
008B6670 mov eax,dword ptr [edx+0Ch] //虚函数表中的print函数实际地址
008B6673 call eax //调用print
A *a = dynamic_cast <A*>(b);
008B667C mov eax,dword ptr [b]
008B667F mov dword ptr [a],eax //编译器知道B是A的子类,所以,b指针转换为a指针,直接copy指针地址。
a->echo();
008B6682 mov eax,dword ptr [a]
008B6685 mov edx,dword ptr [eax]
008B6689 mov ecx,dword ptr [a] //this指针
008B668C mov eax,dword ptr [edx+8] //echo在表里的偏移
008B668F call eax //执行call
B* b2 = new B();
...........
00E5669A call operator new (0E5131Bh)
...........
00E566EE mov dword ptr [b2],ecx
b2->echo();
00E566F1 mov eax,dword ptr [b2]
00E566F4 mov edx,dword ptr [eax]
00E566F8 mov ecx,dword ptr [b2]
00E566FB mov eax,dword ptr [edx+8] //echo函数在表里的偏移,
00E566FE call eax
new的对象C转换基类B,再转换为基类A,a->echo
如何输出C::echo
?下面画个图,描述了单一继承时候的虚函数表运行机制。
多继承
多继承和单继承在虚函数表处理上有很大不同,先看代码。
class Base {
private:
int base_value;
public:
Base() {};
virtual ~Base() {};
virtual void base_echo() { std::cout << "Base::base_echo" << std::endl; };
};
class A {
private:
int a_value;
public:
A() {};
virtual ~A() {};
virtual void a_echo() { std::cout << "A::a_echo" << std::endl; };
virtual void a_print() { std::cout << "A::a_print" << std::endl; };
};
class B {
public:
B() {};
virtual ~B() {};
int b_value;
virtual void b_echo() { std::cout << "B::b_echo" << std::endl; };
virtual void b_print() { std::cout << "B::b_print" << std::endl; };
};
class C :public A,public B, public Base {
private:
int c_value;
public:
C() { };
~C() {};
virtual void a_echo()override { std::cout << "C::a_echo" << std::endl; };
virtual void b_print()override { std::cout << "C::b_print" << std::endl; };
virtual void c_echo() { std::cout << "C::c_echo" << std::endl; };
};
C类同时继承了A,B,Base三个类,此时内存和虚函数是怎样的呢?
A,B,Base原有固定各自虚函数表
:
Base::$vftable@:
| &Base_meta
| 0
0 | &Base::{dtor}
1 | &Base::base_echo
A::$vftable@:
| &A_meta
| 0
0 | &A::{dtor}
1 | &A::a_echo
2 | &A::a_print
B::$vftable@:
| &B_meta
| 0
0 | &B::{dtor}
1 | &B::b_echo
2 | &B::b_print
然后再看C类的内存布局和虚函数表:
class C size(28):
+---
0 | +--- (base class A)
0 | | {vfptr}
4 | | a_value
| +---
8 | +--- (base class B)
8 | | {vfptr}
12 | | b_value
| +---
16 | +--- (base class Base)
16 | | {vfptr}
20 | | base_value
| +---
24 | c_value
+---
C::$vftable@A@:
| &C_meta
| 0
0 | &C::{dtor}
1 | &C::a_echo
2 | &A::a_print
3 | &C::c_echo
C::$vftable@B@:
| -8
0 | &thunk: this-=8; goto C::{dtor}
1 | &B::b_echo
2 | &C::b_print
C::$vftable@Base@:
| -16
0 | &thunk: this-=16; goto C::{dtor}
1 | &Base::base_echo
非常惊讶的发现C类竟然有三个虚函数表,C::$vftable@A@:
, C::$vftable@B@:
, C::$vftable@Base@:
,在同时继承的了A,B,Base三个基类里,各自都有一个虚函数表。
而三个各自的虚函数表和源了A,B,Base三个基类虚函数表是不同的!!是单独属于C类的虚函数。
更值得注意的是,C::c_echo
是C的虚函数,但是却在C::$vftable@A@:
表里面。
从内存布局和虚函数布局可以得出简单结论,多继承时候,一般来说,对象会有同时继承类个数的虚函数表和表指针,子类的新虚函数会存在于第一个继承类的新虚函数
,
未完待续!!!!!!!