zoukankan      html  css  js  c++  java
  • 用汇编的角度剖析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 << "
    "
    	<< vfptr_b1 << "
    "
    	<< vfptr_a << "
    "
    	<< vfptr_a1 << "
    "
    	<< 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@:表里面。

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

    未完待续!!!!!!!

  • 相关阅读:
    Kali Linux NetHunter教程Kali NetHunter支持的设备和ROMs
    Kali Linux常用服务配置教程获取IP地址
    Kali Linux常用服务配置教程启动DHCP服务
    Kali Linux常用服务配置教程安装及配置DHCP服务
    KaliLinux常用服务配置教程DHCP服务工作流程
    Kali Linux常用服务配置教程DHCP服务原理
    iOS12系统应用发送邮件中的附件
    组件内的导航守卫
    vuex使用
    消息组件
  • 原文地址:https://www.cnblogs.com/luconsole/p/6124241.html
Copyright © 2011-2022 走看看