用汇编的角度剖析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@:表里面。

从内存布局和虚函数布局可以得出简单结论,多继承时候,一般来说,对象会有同时继承类个数的虚函数表和表指针,子类的新虚函数会存在于第一个继承类的新虚函数

未完待续!!!!!!!

posted @ 2017-02-09 11:33  0xc  阅读(575)  评论(0编辑  收藏  举报