08从汇编的角度深入理解c++_构造、析构函数与虚函数的关系
前篇文章留下1个疑问,现在来解决,分析一下构造、析构与虚函数的关系。
先看下代码:
#include "stdafx.h"
class CVirtual
{
public:
int GetNumber()
{
return m_nNumber;
}
void SetNumber(int nNumber)//普通函数
{
m_nNumber = nNumber;
}
private:
int m_nNumber;
};
void Test(){
CVirtual cv;
getchar();
}
int _tmain(int argc, _TCHAR* argv[])
{
Test();
return 0;
}
看下反汇编:
21: void Test(){
002113C0 55 push ebp
002113C1 8B EC mov ebp,esp
002113C3 81 EC CC 00 00 00 sub esp,0CCh
002113C9 53 push ebx
002113CA 56 push esi
002113CB 57 push edi
002113CC 8D BD 34 FF FF FF lea edi,[ebp-0CCh]
002113D2 B9 33 00 00 00 mov ecx,33h
002113D7 B8 CC CC CC CC mov eax,0CCCCCCCCh
002113DC F3 AB rep stos dword ptr es:[edi]
22: CVirtual cv;
23:
24: getchar();
002113DE 8B F4 mov esi,esp
002113E0 FF 15 D4 82 21 00 call dword ptr [__imp__getchar (2182D4h)]
002113E6 3B F4 cmp esi,esp
002113E8 E8 53 FD FF FF call @ILT+315(__RTC_CheckEsp) (211140h)
25: }
改变一下源码:添加一个Virtual,再看反汇编
#include "stdafx.h"
class CVirtual
{
public:
int GetNumber()
{
return m_nNumber;
}
virtual void SetNumber(int nNumber) //这里加了1个Virtual
{
m_nNumber = nNumber;
}
private:
int m_nNumber;
};
void Test(){
CVirtual cv;
getchar();
}
int _tmain(int argc, _TCHAR* argv[])
{
Test();
return 0;
}
查看反汇编:
21: void Test(){
00D51420 55 push ebp
00D51421 8B EC mov ebp,esp
00D51423 81 EC D0 00 00 00 sub esp,0D0h
00D51429 53 push ebx
00D5142A 56 push esi
00D5142B 57 push edi
00D5142C 8D BD 30 FF FF FF lea edi,[ebp-0D0h]
00D51432 B9 34 00 00 00 mov ecx,34h
00D51437 B8 CC CC CC CC mov eax,0CCCCCCCCh
00D5143C F3 AB rep stos dword ptr es:[edi]
22: CVirtual cv;
00D5143E 8D 4D F4 lea ecx,[cv] //ecx = ebp-8
00D51441 E8 9A FD FF FF call CVirtual::CVirtual (0D511E0h) //构造函数
23:
24: getchar();
00D51446 8B F4 mov esi,esp
00D51448 FF 15 E4 82 D5 00 call dword ptr [__imp__getchar (0D582E4h)]
00D5144E 3B F4 cmp esi,esp
00D51450 E8 09 FD FF FF call @ILT+345(__RTC_CheckEsp) (0D5115Eh)
25: }
00D51455 52 push edx
00D51456 8B CD mov ecx,ebp
00D51458 50 push eax
00D51459 8D 15 7C 14 D5 00 lea edx,[ (0D5147Ch)]
00D5145F E8 32 FC FF FF call @ILT+145(@_RTC_CheckStackVars@8) (0D51096h)
00D51464 58 pop eax
00D51465 5A pop edx
00D51466 5F pop edi
00D51467 5E pop esi
00D51468 5B pop ebx
00D51469 81 C4 D0 00 00 00 add esp,0D0h
00D5146F 3B EC cmp ebp,esp
00D51471 E8 E8 FC FF FF call @ILT+345(__RTC_CheckEsp) (0D5115Eh)
00D51476 8B E5 mov esp,ebp
00D51478 5D pop ebp
00D51479 C3 ret
发现里面多了1段构造函数的调用,什么情况?
就是说,如果类里面有虚函数,编译器会默认的为我们的类生成1个构造函数,并且先调用这个构造函数。
看看构造函数里面偷偷做了什么事情?
CVirtual::CVirtual: 00411550 55 push ebp 00411551 8B EC mov ebp,esp 00411553 81 EC CC 00 00 00 sub esp,0CCh 00411559 53 push ebx 0041155A 56 push esi 0041155B 57 push edi 0041155C 51 push ecx 0041155D 8D BD 34 FF FF FF lea edi,[ebp+FFFFFF34h] 00411563 B9 33 00 00 00 mov ecx,33h 00411568 B8 CC CC CC CC mov eax,0CCCCCCCCh 0041156D F3 AB rep stos dword ptr es:[edi] 0041156F 59 pop ecx 00411570 89 4D F8 mov dword ptr [ebp-8],ecx 00411573 8B 45 F8 mov eax,dword ptr [ebp-8] 00411576 C7 00 40 57 41 00 mov dword ptr [eax],415740h //最关键的位置 0041157C 8B 45 F8 mov eax,dword ptr [ebp-8] 0041157F 5F pop edi 00411580 5E pop esi 00411581 5B pop ebx 00411582 8B E5 mov esp,ebp 00411584 5D pop ebp 00411585 C3 ret
最关键的一句:
mov [eax],offset vftable 即把VFtable(Virtual Table)的地址放在[eax]中
首先
mov [ebp-8],eax
mov ecx,[ebp-8]
即:mov eax,ecx所以eax是this指针。
mov [this],offset vftable
即下图:

那0x415740h 里面存的是什么呢?

更新:

既然构造函数初始化了虚表,那么构造函数会不会去虚表进行操作呢?我们做实验看一下:
#include "stdafx.h"
class CVirtual
{
public:
int GetNumber()
{
return m_nNumber;
}
virtual void SetNumber(int nNumber)
{
m_nNumber = nNumber;
}
~CVirtual(){//观察反汇编
}
private:
int m_nNumber;
};
void Test(){
CVirtual cv;
getchar();
}
int _tmain(int argc, _TCHAR* argv[])
{
Test();
return 0;
}
查看析构函数的反汇编:
18: ~CVirtual(){
004114C0 55 push ebp
004114C1 8B EC mov ebp,esp
004114C3 81 EC CC 00 00 00 sub esp,0CCh
004114C9 53 push ebx
004114CA 56 push esi
004114CB 57 push edi
004114CC 51 push ecx
004114CD 8D BD 34 FF FF FF lea edi,[ebp+FFFFFF34h]
004114D3 B9 33 00 00 00 mov ecx,33h
004114D8 B8 CC CC CC CC mov eax,0CCCCCCCCh
004114DD F3 AB rep stos dword ptr es:[edi]
004114DF 59 pop ecx
004114E0 89 4D F8 mov dword ptr [ebp-8],ecx
004114E3 8B 45 F8 mov eax,dword ptr [ebp-8]
004114E6 C7 00 40 57 41 00 mov dword ptr [eax],415740h //关键位置
19: }
004114EC 5F pop edi
004114ED 5E pop esi
004114EE 5B pop ebx
004114EF 8B E5 mov esp,ebp
004114F1 5D pop ebp
004114F2 C3 ret
析构函数也是又重新设置了虚表指针的值为0x415740,看起来没什么变化,因为构造的时候也是这个值。
因为现在没有继承关系,所以值看起来没什么变化,但是我们知道了,析构也是在修改虚表指针的值。
总结:
1、当有虚函数存在的时候,对象首地址的位置会多出一个4字节的成员变量,我们称这个成员变量为虚表指针。
2、编译器会自动调用构造函数,初始化这个虚表指针(即把0xCCCCCCCC修改为0x00415740)。
3、当对象析构的时候,会回收资源,把原来析构函数中初始化后的地址(由于继承的时候,存在地址覆盖,这个地址可能是子类的虚表地址)重新修改为这个对象中的本来虚表指针(没有覆盖之前的父类虚表地址)。
注意:
构造的时候,初始化虚表指针是:给虚表指针赋值,让这个虚表指针指向1个虚表。
析构的时候,是还原虚表指针:还原是指,虚表指针已经指向了1个虚表,现在修改为指向另1个虚表。
构造的时候好理解,关键是析构的时候,为什么还要这么做?
因为,你要明白,在构造函数和析构函数中都是可以调用虚函数的,如果我们在类的构造函数和析构函数中调用虚函数,应该调用这个类本身的虚函数吧,
class CVirtual
{
public:
int GetNumber()
{
return m_nNumber;
}
virtual void SetNumber(int nNumber)
{
m_nNumber = nNumber;
}
~CVirtual(){
SetNumber(2);//这里如果调用的话,因为实在类内部调用的,因此调用的应该是我们类自己的虚函数
}
private:
int m_nNumber;
};
但是由于继承的关系,这个虚函数地址存在覆盖的可能,因此这就出现问题了:
就是如果在析构函数中调用虚函数,那么如果覆盖了,那么调用的时候实际输出子类的虚函数,
因此析构函数做的事情,就是重写虚函数指针,恢复为原来覆盖掉之前的父类虚函数地址。
由于构造和析构函数与虚表的关系,在反汇编的时候可以根据虚表找到构造和析构函数。

浙公网安备 33010602011771号