从汇编的角度理解C++虚函数(一)

写在前面

这篇文章适合和我一样的汇编小白还有c++小白 大牛可绕过(打扰了)

如果哪里有错误 还请指出  不胜感激 orz

希望大家一起探讨 共同进步 

我写这个博客主要是因为我们的C++考试 有一道关于虚析构函数的一道题 我当时没有做出来。我当时自认为我理解了多态 加上当时要复习高数 物理等一些科目 还有一些比赛 所以我就没有把精力放在c++上 结果很打脸的是 我当时 没有做出来那个我认为我理解的一道题  原题的代码如下(可能变量什么的有变化)

#include <iostream>
#include <algorithm>
#include <stdio.h>
#include <math.h>
#include <string.h>
#include <queue>
#include <vector>
#include <algorithm>
#include <map>
using namespace std;
class p
{
    public:
        virtual ~p()
        {
                cout<<"Pppx"<<endl;
        }
};
class pp:public p
{
public:
    virtual ~pp()
    {
         cout<<"PPppx"<<endl;
    }
};
class ppx:public pp
{
public:
    virtual ~ppx()
    {
        cout<<"PPXppx"<<endl;
    }
};
int main()
{
     p *x=new ppx;
     delete x;
     system("pause");
    return 0;
}

若是对这方面做的比较好的同学 应该能看得出来 正确答案是

PPXppx
PPppx
Pppx

其实这个很好解释 ,就是多态,通过父类的指针去访问子类的函数,而虚函数可以使子类的函数代替父类的函数 导致父类像子类 这就是我理解的多态,然后 第二问难了很多同学 就是问的是当p 的析构函数不是虚函数的时候 输出的是多少。

这个答案是 Pppx 其实在当时我和我朋友想了一下可能是因为p没有虚函数表 然后导致没有办法去访问子类的函数 当时也没有去立马验证 ,然后当忙完了我们的考试 我就试着用汇编来解释一下 这个原因  个人感觉学习汇编很重要 因为当你在高级语言有什么不很理解的时候 完全可以通过汇编来理解 一个典型的例子就是 指针和引用到底有什么区别(有兴趣的朋友可以看看汇编出来会是什么样子)

然后我们就可以通过汇编来看看底层到底是怎么运作的

我们先来看 有虚函数的时候是什么样子

    30:     virtual ~ppx()
    31:     {
009321B0 55                   push        ebp  
009321B1 8B EC                mov         ebp,esp  
009321B3 6A FF                push        0FFFFFFFFh  
009321B5 68 B8 5A 93 00       push        offset __ehhandler$??1ppx@@UAE@XZ (935AB8h)  
009321BA 64 A1 00 00 00 00    mov         eax,dword ptr fs:[00000000h]  
009321C0 50                   push        eax  
009321C1 81 EC CC 00 00 00    sub         esp,0CCh  
009321C7 53                   push        ebx  
009321C8 56                   push        esi  
009321C9 57                   push        edi  
009321CA 51                   push        ecx  
009321CB 8D BD 28 FF FF FF    lea         edi,[ebp-0D8h]  
009321D1 B9 33 00 00 00       mov         ecx,33h  
009321D6 B8 CC CC CC CC       mov         eax,0CCCCCCCCh  
009321DB F3 AB                rep stos    dword ptr es:[edi]  
009321DD 59                   pop         ecx  
009321DE A1 58 A0 93 00       mov         eax,dword ptr [___security_cookie (93A058h)]  
009321E3 33 C5                xor         eax,ebp  
009321E5 50                   push        eax  
009321E6 8D 45 F4             lea         eax,[ebp-0Ch]  
009321E9 64 A3 00 00 00 00    mov         dword ptr fs:[00000000h],eax  
009321EF 89 4D EC             mov         dword ptr [ebp-14h],ecx  
009321F2 8B 45 EC             mov         eax,dword ptr [ebp-14h]  
009321F5 C7 00 3C 78 93 00    mov         dword ptr [eax],offset ppx::`vftable' (93783Ch)  
009321FB C7 45 FC 00 00 00 00 mov         dword ptr [ebp-4],0  
    32:         cout<<"PPXppx"<<endl;
00932202 8B F4                mov         esi,esp  
00932204 A1 28 B3 93 00       mov         eax,dword ptr [__imp_std::endl (93B328h)]  
00932209 50                   push        eax  
0093220A 68 54 78 93 00       push        offset string "PPXppx" (937854h)  
0093220F 8B 0D 2C B3 93 00    mov         ecx,dword ptr [__imp_std::cout (93B32Ch)]  
00932215 51                   push        ecx  
00932216 E8 75 EF FF FF       call        std::operator<<<std::char_traits<char> > (931190h)  
0093221B 83 C4 08             add         esp,8  
0093221E 8B C8                mov         ecx,eax  
00932220 FF 15 1C B3 93 00    call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (93B31Ch)]  
00932226 3B F4                cmp         esi,esp  
00932228 E8 B3 EF FF FF       call        @ILT+475(__RTC_CheckEsp) (9311E0h)  
    33:     }
0093222D C7 45 FC FF FF FF FF mov         dword ptr [ebp-4],0FFFFFFFFh  
00932234 8B 4D EC             mov         ecx,dword ptr [ebp-14h]  
00932237 E8 9E F0 FF FF       call        pp::~pp (9312DAh)  
0093223C 8B 4D F4             mov         ecx,dword ptr [ebp-0Ch]  
0093223F 64 89 0D 00 00 00 00 mov         dword ptr fs:[0],ecx 
00932237 E8 9E F0 FF FF       call        pp::~pp (9312DAh)  

上面 932237一句 可以知道调用了~pp 函数 那么我们就可以知道当调用完~ppx的时候 然后后面就调用了~pp 我们就可以知道它们是先调用自己的析构函数 然后去找自己继承的父类的析构函数。

这里可以和大家分享一下我理解的虚函数  如果一个类有虚函数 那么类的字节数将增加4个 这四个字节存储的是虚函数的地址 这四个字节也被称为虚函数表 有兴趣的大家可以试着汇编去看一看 在这里 

009321E6 8D 45 F4             lea         eax,[ebp-0Ch]  
009321E9 64 A3 00 00 00 00    mov         dword ptr fs:[00000000h],eax  
009321EF 89 4D EC             mov         dword ptr [ebp-14h],ecx  
009321F2 8B 45 EC             mov         eax,dword ptr [ebp-14h]  
009321F5 C7 00 3C 78 93 00    mov         dword ptr [eax],offset ppx::`vftable' (93783Ch)  
009321FB C7 45 FC 00 00 00 00 mov         dword ptr [ebp-4],0  

eax就可以理解为this指针 然后 它比没有虚函数的类多了 后面两句 至于后面两句什么意思 大家可以百度看看 搜一下vftable 在这里就不做详解

那么当没有虚函数的时候将是什么样子

我特地写了具有代表性的代码

#include <iostream>
#include <stdio.h>
#include <algorithm>
#include <string.h>
#include <fstream>
#include <climits>
#include <bitset>
using namespace std;
class ppx
{
    public:
     ~ppx(){
     printf("appxppx\n");
    }
	 ppx()
	 {
	 cout<<"appxde 构析函数被使用了"<<endl;
	 }
};
class xx:public ppx
{
public:
	 virtual ~xx()
	{
	  printf("新增的类\n");
	}
};
class qqx:public xx
{

    public:
       ~qqx()
     {
         printf("bppxppx\n");
     }
	  qqx()
	  {
	  cout<<"bppxde 构析函数被使用了 "<<endl;
	  }
};
class qq:public qqx
{

public:
    ~qq()
    {
         printf("cppxppx\n");
    }
	qq()
	{
	cout<<"cppxde 构析函数被使用了"<<endl;
	}
};
int main()
{
     xx *x=new qq;
     delete x;
     system("pause");
	 return 0;
}

这个就是我写的 一段用来分析的代码 然后运行结果是

appxde 构析函数被使用了
bppxde 构析函数被使用了
cppxde 构析函数被使用了
cppxppx
bppxppx
新增的类

appxppx

这个应该没有什么疑问 

和上面的一样

至于为什么会有构造函数会是先a b c  我可以把 构造函数的汇编粘贴出来 大家应该就明白了

   36: 	  qqx()
01341870 55                   push        ebp  
01341871 8B EC                mov         ebp,esp  
01341873 6A FF                push        0FFFFFFFFh  
01341875 68 38 56 34 01       push        offset __ehhandler$??0qqx@@QAE@XZ (1345638h)  
0134187A 64 A1 00 00 00 00    mov         eax,dword ptr fs:[00000000h]  
01341880 50                   push        eax  
01341881 81 EC CC 00 00 00    sub         esp,0CCh  
01341887 53                   push        ebx  
01341888 56                   push        esi  
01341889 57                   push        edi  
0134188A 51                   push        ecx  
0134188B 8D BD 28 FF FF FF    lea         edi,[ebp-0D8h]  
01341891 B9 33 00 00 00       mov         ecx,33h  
01341896 B8 CC CC CC CC       mov         eax,0CCCCCCCCh  
0134189B F3 AB                rep stos    dword ptr es:[edi]  
0134189D 59                   pop         ecx  
0134189E A1 00 90 34 01       mov         eax,dword ptr [___security_cookie (1349000h)]  
013418A3 33 C5                xor         eax,ebp  
013418A5 50                   push        eax  
013418A6 8D 45 F4             lea         eax,[ebp-0Ch]  
013418A9 64 A3 00 00 00 00    mov         dword ptr fs:[00000000h],eax  
013418AF 89 4D EC             mov         dword ptr [ebp-14h],ecx  
013418B2 8B 4D EC             mov         ecx,dword ptr [ebp-14h]  
013418B5 E8 8B F8 FF FF       call        xx::xx (1341145h)  
013418BA C7 45 FC 00 00 00 00 mov         dword ptr [ebp-4],0  
    37: 	  {
    38: 	  cout<<"bppxde 构析函数被使用了 "<<endl;
013418C1 8B F4                mov         esi,esp  
013418C3 A1 24 A3 34 01       mov         eax,dword ptr [__imp_std::endl (134A324h)]  
013418C8 50                   push        eax  
013418C9 68 78 78 34 01       push        offset string "bppxde \xb9\xb9\xce\xf6\xba\xaf\xca\xfd\xb1\xbb\xca\xb9\xd3\xc3\xc1\xcb " (1347878h)  
013418CE 8B 0D 28 A3 34 01    mov         ecx,dword ptr [__imp_std::cout (134A328h)]  
013418D4 51                   push        ecx  
013418D5 E8 89 F8 FF FF       call        std::operator<<<std::char_traits<char> > (1341163h)  
013418DA 83 C4 08             add         esp,8  
013418DD 8B C8                mov         ecx,eax  
013418DF FF 15 14 A3 34 01    call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (134A314h)]  
013418E5 3B F4                cmp         esi,esp  
013418E7 E8 C2 F8 FF FF       call        @ILT+425(__RTC_Check
013418B5 E8 8B F8 FF FF       call        xx::xx (1341145h)  
013418BA C7 45 FC 00 00 00 00 mov         dword ptr [ebp-4],0  
13418b5 这个就是执行的它上面继承父类的构造函数 那么就很容易知道我们看到的它是先执行它父类的构造函数 再去执行自己的 那么为什么会是a b c也就一目了然了

接下来分析的就是 把新增的类那个虚函数改成不是虚函数 那么 运行结果是

appxde 构析函数被使用了
bppxde 构析函数被使用了
cppxde 构析函数被使用了
新增的类

appxppx

那么 我们这个可以先分析 为什么会有appxppx的出现  去看汇编语言

01341790 55                   push        ebp  
01341791 8B EC                mov         ebp,esp  
01341793 81 EC CC 00 00 00    sub         esp,0CCh  
01341799 53                   push        ebx  
0134179A 56                   push        esi  
0134179B 57                   push        edi  
0134179C 51                   push        ecx  
0134179D 8D BD 34 FF FF FF    lea         edi,[ebp-0CCh]  
013417A3 B9 33 00 00 00       mov         ecx,33h  
013417A8 B8 CC CC CC CC       mov         eax,0CCCCCCCCh  
013417AD F3 AB                rep stos    dword ptr es:[edi]  
013417AF 59                   pop         ecx  
013417B0 89 4D F8             mov         dword ptr [ebp-8],ecx  
    25: 	  printf("新增的类\n");
013417B3 8B F4                mov         esi,esp  
013417B5 68 60 78 34 01       push        offset string "\xd0\xc2\xd4\xf6\xb5\xc4\xc0\xe0\n" (1347860h)  
013417BA FF 15 CC A3 34 01    call        dword ptr [__imp__printf (134A3CCh)]  
013417C0 83 C4 04             add         esp,4  
013417C3 3B F4                cmp         esi,esp  
013417C5 E8 E4 F9 FF FF       call        @ILT+425(__RTC_CheckEsp) (13411AEh)  
    26: 	}
013417CA 8B 4D F8             mov         ecx,dword ptr [this]  
013417CD E8 38 F8 FF FF       call        ppx::~ppx (134100Ah)  
013417D2 5F                   pop         edi  
013417D3 5E                   pop         esi  
013417D4 5B                   pop         ebx  
013417D5 81 C4 CC 00 00 00    add         esp,0CCh  
013417DB 3B EC                cmp         ebp,esp  
013417DD E8 CC F9 FF FF       call        @ILT+425(__RTC_CheckEsp) (13411AEh)  
013417E2 8B E5                mov         esp,ebp  
013417E4 5D                   pop         ebp  
013417E5 C3             
013417CA 8B 4D F8             mov         ecx,dword ptr [this]  
013417CD E8 38 F8 FF FF       call        ppx::~ppx (134100Ah)  

由这两个指令我们可以知道 这个是直接就调用了 父类的析构函数 那么我们可以猜测 指针类型的父类构析直接调用(在我后面验证的时候起码没有出现特例)  

然后我们看看后面为什么没有出现

 45:     ~qq()
    46:     {
    47:          printf("cppxppx\n");
    48:     }
    49: 	qq()
01341650 55                   push        ebp  
01341651 8B EC                mov         ebp,esp  
01341653 6A FF                push        0FFFFFFFFh  
01341655 68 08 56 34 01       push        offset __ehhandler$??0qq@@QAE@XZ (1345608h)  
0134165A 64 A1 00 00 00 00    mov         eax,dword ptr fs:[00000000h]  
01341660 50                   push        eax  
01341661 81 EC CC 00 00 00    sub         esp,0CCh  
01341667 53                   push        ebx  
01341668 56                   push        esi  
01341669 57                   push        edi  
0134166A 51                   push        ecx  
0134166B 8D BD 28 FF FF FF    lea         edi,[ebp-0D8h]  
01341671 B9 33 00 00 00       mov         ecx,33h  
01341676 B8 CC CC CC CC       mov         eax,0CCCCCCCCh  
0134167B F3 AB                rep stos    dword ptr es:[edi]  
0134167D 59                   pop         ecx  
0134167E A1 00 90 34 01       mov         eax,dword ptr [___security_cookie (1349000h)]  
01341683 33 C5                xor         eax,ebp  
01341685 50                   push        eax  
01341686 8D 45 F4             lea         eax,[ebp-0Ch]  
01341689 64 A3 00 00 00 00    mov         dword ptr fs:[00000000h],eax  
0134168F 89 4D EC             mov         dword ptr [ebp-14h],ecx  
01341692 8B 4D EC             mov         ecx,dword ptr [ebp-14h]  
01341695 E8 B9 FB FF FF       call        qqx::qqx (1341253h)  
0134169A C7 45 FC 00 00 00 00 mov         dword ptr [ebp-4],0  
    50: 	{
    51: 	cout<<"cppxde 构析函数被使用了"<<endl;
013416A1 8B F4                mov         esi,esp  
013416A3 A1 24 A3 34 01       mov         eax,dword ptr [__imp_std::endl (134A324h)]  
013416A8 50                   push        eax  
013416A9 68 38 78 34 01       push        offset string "cppxde \xb9\xb9\xce\xf6\xba\xaf\xca\xfd\xb1\xbb\xca\xb9\xd3\xc3\xc1\xcb" (1347838h)  
013416AE 8B 0D 28 A3 34 01    mov         ecx,dword ptr [__imp_std::cout (134A328h)]  
013416B4 51                   push        ecx  
013416B5 E8 A9 FA FF FF       call        std::operator<<<std::char_traits<char> > (1341163h)  
013416BA 83 C4 08             add         esp,8  
013416BD 8B C8                mov         ecx,eax  
013416BF FF 15 14 A3 34 01    call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (134A314h)]  
013416C5 3B F4                cmp         esi,esp  
013416C7 E8 E2 FA FF FF       call        @ILT+425(__RTC_CheckEsp) (13411AEh)  
    52: 	}
013416CC C7 45 FC FF FF FF FF mov         dword ptr [ebp-4],0FFFFFFFFh  
013416D3 8B 45 EC             mov         eax,dword ptr [ebp-14h]  
013416D6 8B 4D F4             mov         ecx,dword ptr [ebp-0Ch]  
013416D9 64 89 0D 00 00 00 00 mov         dword ptr fs:[0],ecx  
013416E0 59                   pop         ecx  
013416E1 5F                   pop         edi  
013416E2 5E                   pop         esi  
013416E3 5B                   pop         ebx  
013416E4 81 C4 D8 00 00 00    add         esp,0D8h  
013416EA 3B EC                cmp         ebp,esp  
013416EC E8 BD FA FF FF       call        @ILT+425(__RTC_CheckEsp) (13411AEh)  
013416F1 8B E5                mov         esp,ebp  
013416F3 5D                   pop         ebp  

我们竟然看到~qq的构析函数连汇编代码都没有 那么证明在内部 编译器都没有执行 那么我们看看 ~qqx的汇编代码

  28: class qqx:public xx
    29: {
    30: 
    31:     public:
    32:        ~qqx()
    33:      {
01341720 55                   push        ebp  
01341721 8B EC                mov         ebp,esp  
01341723 81 EC CC 00 00 00    sub         esp,0CCh  
01341729 53                   push        ebx  
0134172A 56                   push        esi  
0134172B 57                   push        edi  
0134172C 51                   push        ecx  
0134172D 8D BD 34 FF FF FF    lea         edi,[ebp-0CCh]  
01341733 B9 33 00 00 00       mov         ecx,33h  
01341738 B8 CC CC CC CC       mov         eax,0CCCCCCCCh  
0134173D F3 AB                rep stos    dword ptr es:[edi]  
0134173F 59                   pop         ecx  
01341740 89 4D F8             mov         dword ptr [ebp-8],ecx  
    34:          printf("bppxppx\n");
01341743 8B F4                mov         esi,esp  
01341745 68 54 78 34 01       push        offset string "bppxppx\n" (1347854h)  
0134174A FF 15 CC A3 34 01    call        dword ptr [__imp__printf (134A3CCh)]  
01341750 83 C4 04             add         esp,4  
01341753 3B F4                cmp         esi,esp  
01341755 E8 54 FA FF FF       call        @ILT+425(__RTC_CheckEsp) (13411AEh)  
    35:      }
0134175A 8B 4D F8             mov         ecx,dword ptr [this]  
0134175D E8 BF FA FF FF       call        xx::~xx (1341221h)  
01341762 5F                   pop         edi  
01341763 5E                   pop         esi  
01341764 5B                   pop         ebx  
01341765 81 C4 CC 00 00 00    add         esp,0CCh  
0134176B 3B EC                cmp         ebp,esp  
0134176D E8 3C FA FF FF       call        @ILT+425(__RTC_CheckEsp) (13411AEh)  
01341772 8B E5                mov         esp,ebp  
01341774 5D                   pop         ebp  
01341775 C3                   ret  

我们看到了 这个析构函数执行了 什么鬼 它为什么会执行 明明没有打印它 啊 为了了解它的工作顺序 我们来看看它们是怎么运作的 我们 用一个工具叫做OD 来调试这个程序 


跟进这个函数 我们进入了一个页面 我们看出


其实从这里我们就可以看出来 其实~ppx这个函数是编译的了但是并没有被程序所使用 程序直接调用了 ~xx 

那么 我们上面只是分析了析构函数在是不是虚函数的情况下 而并没有分析 不是析构函数的情况

下一篇 我打算分析一下是普通函数的情况 

如果各位大佬有什么建议 或者哪里有错误 欢迎大佬们指出 谢谢

orz

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值