zoukankan      html  css  js  c++  java
  • [转]浅析C++中虚函数的调用及对象的内部布局(利用汇编深刻理解C++虚函数底层实现机制)

    在我那篇《浅析C++中的this指针》中,我通过分析C++代码编译后生成的汇编代码来分析this指针的实现方法。这次我依然用分析C++代码编译后生成的汇编代码来说明C++中虚函数调用的实现方法,顺便也说明一下C++中的对象内部布局。下面所有的汇编代码都是用VC2005编译出来的。虽然,不同的编译器可能会编译出不同的结果,对象的内部布局也不尽相同;但是,只要是符合C++标准的编译器,编译结果和对象的内部布局应该是大同小异。
        首先,是一个有着简单继承关系的两个类:

    class CBase
    {
    public:
        
    virtual void VFun1() = 0;
        
    virtual void VFun2() = 0;
        
    void Fun1();
    };

    // 这里仅仅是为了生成函数的汇编代码,因此函数体为空
    void CBase::Fun1()
    {
    }

    class CDerived : public CBase
    {
    public:
        
    virtual void VFun1();
        
    virtual void VFun2();
        
    void Fun2();
    private:
        
    int m_iValue1;
        
    int m_iValue2;
    };

    // 这里仅仅是为了生成函数的汇编代码,因此函数体为空
    void CDerived::VFun1()
    {
    }

    // 这里仅仅是为了生成函数的汇编代码,因此函数体为空
    void CDerived::VFun2()
    {
    }

    // 这里是为了分析对象的内部布局,因此仅仅是给成员变量赋值
    void CDerived::Fun2()
    {
        m_iValue1 
    = 13;
        m_iValue2 
    = 13;
    }

        现在用下面的代码来调用成员函数:

    CDerived derived;

    // 用对象调用虚函数
    derived.VFun1();
    derived.VFun2();
    // 用对象调用非虚函数
    derived.Fun1();
    derived.Fun2();

    // 用指向派生类的基类的指针调用虚函数,实现多态
    CBase *pTest = &derived;
    pTest
    ->VFun1();
    pTest
    ->VFun2();

        下面就是用VC2005编译上面的代码后生成的汇编代码:

        CDerived derived;
    0041195E  lea         ecx,[derived] 
    00411961  call        CDerived::CDerived (411177h) 

    // 代码段1
        derived.VFun1();
    00411966  lea         ecx,[derived] 
    00411969  call        CDerived::VFun1 (411078h) 
        derived.VFun2();
    0041196E  lea         ecx,[derived] 
    00411971  call        CDerived::VFun2 (4111B8h) 
        derived.Fun1();
    00411976  lea         ecx,[derived] 
    00411979  call        CBase::Fun1 (411249h) 
        derived.Fun2();
    0041197E  lea         ecx,[derived] 
    00411981  call        CDerived::Fun2 (4111BDh) 

    // 代码段2
        CBase *pTest = &derived;
    00411986  lea         eax,[derived] 
    00411989  mov         dword ptr [pTest],eax 
        pTest
    ->VFun1();
    0041198C  mov         eax,dword ptr [pTest] 
    // 行1
    0041198F  mov         edx,dword ptr [eax] // 行2
    00411991  mov         esi,esp 
    00411993  mov         ecx,dword ptr [pTest] 
    00411996  mov         eax,dword ptr [edx] // 行3
    00411998  call        eax // 行4  
    0041199A  cmp         esi,esp 
    0041199C  call        @ILT
    +495(__RTC_CheckEsp) (4111F4h) 
        pTest
    ->VFun2();
    004119A1  mov         eax,dword ptr [pTest] 
    004119A4  mov         edx,dword ptr [eax] 
    004119A6  mov         esi,esp 
    004119A8  mov         ecx,dword ptr [pTest] 
    004119AB  mov         eax,dword ptr [edx
    +4// 行5
    004119AE  call        eax  
    004119B0  cmp         esi,esp 
    004119B2  call        @ILT
    +495(__RTC_CheckEsp) (4111F4h) 

        通过对代码段1的观察我们可以发现:通过对象调用类的虚成员函数和调用非虚成员函数是相同的(对调用成员函数的汇编代码的分析可以看我的那篇《浅析C++中的this指针》)。也就是说,用对象是无法实现多态的。
        下面主要来分析实现多态的代码段2。
        行1、将pTest指针指向的地址前2个字(4个字节,也就是32位系统中一个指针的大小)的内容当成一个指针放到eax寄存器中
        行2、将eax寄存器中的指针的值放入edx寄存器
        行3、将dex寄存器中的指针的值放入eax寄存器
        行4、调用eax寄存器指向的函数
        这样分析似乎对怎样调用对象derived的虚函数VFun1()并不是很清楚。那么我们先来看下面的这张图:

        这张图是一个假设的对象derived在内存中的内部布局图。指针pTest指向对象derived,而对象derived的前4个字节是一个虚表指针,指向虚函数表。
        看着这张图再来分析上面的汇编代码就会清晰很多:
        行1、取得虚表指针值放入eax寄存器中
        行2、取得虚表指针的值放入edx寄存器中
        行3、取得虚表指针指向的地址的值(也就是VFun1)放入eax寄存器中
        行4、调用eax寄存器指向的函数
        行5证明了上面图中对虚函数表的假设。第二个虚函数VFun2()的地址就是通过在第一虚函数VFun1()的地址加4(32位系统中一个指针的大小)而得到的。
        通过上面的分析,可以得出C++中虚函数的调用方法:首先,取得对象中的虚表指针;然后,通过虚表指针找到相应的虚表;最后,通过在虚表内的偏移量找到相应的函数来调用。
        下面通过分析类CDerived的非虚成员函数Fun2()来证明上面图中虚函数表指针的存在。

    void CDerived::Fun2()
    {
    004118F0  push        ebp  
    004118F1  mov         ebp,esp 
    004118F3  sub         esp,0CCh 
    004118F9  push        ebx  
    004118FA  push        esi  
    004118FB  push        edi  
    004118FC  push        ecx  
    004118FD  lea         edi,[ebp
    -0CCh] 
    00411903  mov         ecx,33h 
    00411908  mov         eax,0CCCCCCCCh 
    0041190D  rep stos    dword ptr es:[edi] 
    0041190F  pop         ecx  
    00411910  mov         dword ptr [ebp-8],ecx 
        m_iValue1 
    = 13;
    00411913  mov         eax,dword ptr [this// 行6
    00411916  mov         dword ptr [eax+4],0Dh // 行7
        m_iValue2 = 13;
    0041191D  mov         eax,dword ptr [
    this
    00411920  mov         dword ptr [eax+8],0Dh 
    }
    00411927  pop         edi  
    00411928  pop         esi  
    00411929  pop         ebx  
    0041192A  mov         esp,ebp 
    0041192C  pop         ebp  
    0041192D  ret  

        上面是类CDerived的非虚成员函数Fun2()的汇编代码。可以看到,行6是将this指向的地址放入eax寄存器,而行7是给this指针指向的地址加4的地址赋值(具体的分析,可以看《浅析C++中的this指针》),而这个地址里面存放的是类CDerived的第一个成员变量。我们知道this指针是指向对象首地址的,那么为什么要给第一个成员变量赋值的时候要向后移动4个字节?答案是因为对象的前4个字节是用来存放虚表指针的。
        下面的代码是《浅析C++中的this指针》一文中的不含虚函数的类的C++代码和编译后的汇编代码:

    class CTest
    {
    public:
        
    void SetValue();

    private:
        
    int m_iValue1;
        
    int m_iValue2;
    };

    void CTest::SetValue()
    {
        m_iValue1 
    = 13;
        m_iValue2 
    = 13;
    }

    void CTest::SetValue()
    {
    004117E0  push        ebp  
    004117E1  mov         ebp,esp 
    004117E3  sub         esp,0CCh 
    004117E9  push        ebx  
    004117EA  push        esi  
    004117EB  push        edi  
    004117EC  push        ecx  
    004117ED  lea         edi,[ebp
    -0CCh] 
    004117F3  mov         ecx,33h 
    004117F8  mov         eax,0CCCCCCCCh 
    004117FD  rep stos    dword ptr es:[edi] 
    004117FF  pop         ecx  
    00411800  mov         dword ptr [ebp-8],ecx 
        m_iValue1 
    = 13;
    00411803  mov         eax,dword ptr [this// 行8
    00411806  mov         dword ptr [eax],0Dh // 行9
        m_iValue2 = 13;
    0041180C  mov         eax,dword ptr [
    this
    0041180F  mov         dword ptr [eax
    +4],0Dh 
    }
    00411816  pop         edi  
    00411817  pop         esi  
    00411818  pop         ebx  
    00411819  mov         esp,ebp 
    0041181B  pop         ebp  
    0041181C  ret  

        通过行8、行9和行6、行7的比较就可以看出:类CTest的对象前4个字节存放的是自己的第一个成员变量;而类CDerived的对象从第5个字节开始才是存放的自己的第一个成员变量,它的前4个字节是用来存放虚表指针的。这再一次证明了上面图中对象内部布局的正确性。

    PS:

        这篇文章可以说是《浅析C++中的this指针》的续篇,最后我说说我为什么会用这种方法来分析C++,也算是对《浅析C++中的this指针》一文中网友评论的回复吧。
        dch4890164建议我看inside the c++ object model;而hacker47却说了风凉话:“孔乙己说:回字有三种写法,你们知道么?”;最直接的是wengch,直接反问我:“用汇编分析C++.....有意义么?”。而我要说的是,《Inside The C++ Object Model》这本书我看过,确实是一本非常好的讲解C++底层的书。可是由于平时写C++代码的时候,很少会关心底层的实现,所以那本书看过之后留下的印象并不深刻。而用汇编代码来分析C++也是源于一个很偶然的事件:就是《浅析C++中的this指针》一文中提到的可以用一个类的空指针来调用成员函数。我发现我的C++知识不能解释那种现象,在Debug代码的时候,我转到了汇编代码中来寻找答案。后来就把我的分析结果写成了那篇《浅析C++中的this指针》。说实话,这也是我第一次接触Windows下的汇编语言,文章中的分析都是边看资料边揣摩得出的。也许会有人觉得我这种方法不值一提,但是我却通过这种方法对C++的底层实现加深了了解。如果网友们看了觉得有收获,那我就心满意足了。呵呵~~

  • 相关阅读:
    xamarin开发UWP元素的初始化设置顺序
    MailKit---状态更改和删除
    MailKit---获取邮件
    xamarin MasterDetailPage点击Master时卡顿现象
    xamarin UWP ActivityIndicator
    wpf ListView DataTemplate方式的鼠标悬停和选中更改背景色
    wpf Webbrowser 乱码问题及弹窗被遮挡
    47.go get安装库以及gopm替换方式——2020年04月12日21:04:30
    46.GRPC初识——2020年04月12日20:45:43
    45.解决github仓库下载慢问题——2020年04月12日
  • 原文地址:https://www.cnblogs.com/taoxu0903/p/1068235.html
Copyright © 2011-2022 走看看