写在前面
这篇文章适合和我一样的汇编小白还有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