C++逆向:浅谈C++的虚函数机制
编辑什么是虚函数
Virtual Functions
,又叫虚函数。这个虚直接给新手整不会了,好像是假的一样。我个人的意见是当时译者在翻译的时候选择了一个不太恰当的解释进行翻译了(PS:没说译者翻译错)。在collins dictionary
查询单词virtual
可以得到以下解释:
You can use virtual to indicate that something is so nearly true that for most purposes it can be regarded as true.
这就能让人明白了,virtual functions
只的不是那些虚假的函数,而是指的真实的、实际上的函数。
为什么需要虚函数
在有些情况下,需要根据对象的实际类型来调用成员函数,而不是根据指针的类型来对该块内存进行解释并调用属于该指针类型所属的成员函数,以简化代码实现多态性。
举个例子,想做一个绘图工具,可以绘制圆形、矩形和三角形这三种形状。这三种图形的绘制方法各不相同,但是都有一个共同的动作就是绘制动作。假设用户已经绘制好了并以文件的形式保存了下来,现在该绘图工具需要加载保存的文件,解析图形并绘制到视图中去,这时该工具就需要根据不同的图形来进行绘制,因此就需要虚函数了。
C++中的虚机制
虚表和虚表指针
虚表结构
根据虚函数的定义,其实指的是对应于该对象实际上应该调用的函数。在cpp
中使用一个指针和一个表格来进行实现的。这个表格称为虚表vftable
,这个指针称为虚表指针。
这个虚表其实是一个指针数组,按照虚函数在该类中定义的顺序,保存这些虚函数的指针。
上图所示的虚表指针及虚表结构的示例代码如下所示。
class CObject
{
public:
static const CRuntimeClass classCObject;
public:
virtual CRuntimeClass* GetRuntimeClass() const;
virtual ~CObject();
};
虚表指针的填充时机
如果一个类中有虚函数,编译器则会在对象的首部“悄悄”的多加4个字节,以存放所属于该对象的虚表。在该类对象的构造函数和析构函数中会对这四个字节的空间填充指向该类对象所属的虚表的指针。如果这个类中用户没有定义构造函数,则编译器此时会添加一个默认构造函数以初始化虚表指针。
为什么要在构造函数和析构函数中都会进行填充所属于该类的虚表指针呢?为了防止脏数据所带来的影响,以避免调用到错误的成员函数使程序变得不稳定或运行结果出错。如:
- 在构造时,构造顺序为先构造父类,后构造子类。而子类中可以调用父类的虚函数。如果不在构造函数中填充,则会导致子类调用错误的函数使得运行不稳定或出错
- 在析构时,析构顺序为先析构子类,后析构父类。而在父类的析构函数中不重新赋值父类对象所属的虚表指针的话,则调用的可能时子类中的某些虚函数,而此时子类已经被析构掉了,因此可能会造成父类调用错误的函数使得运行不稳定或出错
上述示例代码的构造函数和析构函数的反汇编代码如下:
.text:004016D0 CObject__CObject proc near ; CODE XREF: j_CObject__CObject↑j
.text:004016D0
.text:004016D0 var_44 = byte ptr -44h
.text:004016D0 var_4 = dword ptr -4
.text:004016D0
.text:004016D0 push ebp
.text:004016D1 mov ebp, esp
.text:004016D3 sub esp, 44h
.text:004016D6 push ebx
.text:004016D7 push esi
.text:004016D8 push edi
.text:004016D9 push ecx
.text:004016DA lea edi, [ebp+var_44]
.text:004016DD mov ecx, 11h
.text:004016E2 mov eax, 0CCCCCCCCh
.text:004016E7 rep stosd
.text:004016E9 pop ecx
.text:004016EA mov [ebp+var_4], ecx ; ecx为当前对象的this指针
.text:004016ED mov eax, [ebp+var_4]
.text:004016F0 mov dword ptr [eax], offset pCObject__vftable ; 为当前对象填充虚表指针
.text:004016F6 mov eax, [ebp+var_4]
.text:004016F9 pop edi
.text:004016FA pop esi
.text:004016FB pop ebx
.text:004016FC mov esp, ebp
.text:004016FE pop ebp
.text:004016FF retn
.text:004016FF CObject__CObject endp
.text:00401780 CObject__destructor proc near ; CODE XREF: j_CObject__destructor↑j
.text:00401780
.text:00401780 var_44 = byte ptr -44h
.text:00401780 var_4 = dword ptr -4
.text:00401780
.text:00401780 push ebp
.text:00401781 mov ebp, esp
.text:00401783 sub esp, 44h
.text:00401786 push ebx
.text:00401787 push esi
.text:00401788 push edi
.text:00401789 push ecx
.text:0040178A lea edi, [ebp+var_44]
.text:0040178D mov ecx, 11h
.text:00401792 mov eax, 0CCCCCCCCh
.text:00401797 rep stosd
.text:00401799 pop ecx
.text:0040179A mov [ebp+var_4], ecx ; ecx为当前对象的this指针
.text:0040179D mov eax, [ebp+var_4]
.text:004017A0 mov dword ptr [eax], offset pCObject__vftable ; 为当前对象填充虚表指针
.text:004017A6 pop edi
.text:004017A7 pop esi
.text:004017A8 pop ebx
.text:004017A9 mov esp, ebp
.text:004017AB pop ebp
.text:004017AC retn
.text:004017AC CObject__destructor endp
在CObject
类中,定义一个非虚函数,IsKindOf
观察其反汇编:
.text:0040ED50 CObject__IsKindOf proc near ; CODE XREF: j_IsKindOf↑j
.text:0040ED50
.text:0040ED50 var_48 = byte ptr -48h
.text:0040ED50 var_8 = dword ptr -8
.text:0040ED50 var_4 = dword ptr -4
.text:0040ED50 arg_0 = dword ptr 8
.text:0040ED50
.text:0040ED50 push ebp
.text:0040ED51 mov ebp, esp
.text:0040ED53 sub esp, 48h
.text:0040ED56 push ebx
.text:0040ED57 push esi
.text:0040ED58 push edi
.text:0040ED59 push ecx
.text:0040ED5A lea edi, [ebp+var_48]
.text:0040ED5D mov ecx, 12h
.text:0040ED62 mov eax, 0CCCCCCCCh
.text:0040ED67 rep stosd
.text:0040ED69 pop ecx
.text:0040ED6A mov [ebp+var_4], ecx
.text:0040ED6D mov eax, [ebp+var_4]
.text:0040ED70 mov edx, [eax]
.text:0040ED72 mov esi, esp
.text:0040ED74 mov ecx, [ebp+var_4]
.text:0040ED77 call dword ptr [edx] ; 直接取虚表指针后,调用虚函数,没有对当前对象的虚表指针进行赋值
.text:0040ED79 cmp esi, esp
.text:0040ED7B call __chkesp
.text:0040ED80 mov [ebp+var_8], eax
.text:0040ED83 mov eax, [ebp+arg_0]
.text:0040ED86 push eax
.text:0040ED87 mov ecx, [ebp+var_8]
.text:0040ED8A call CRuntimeClass__IsDerivedFrom
.text:0040ED8F pop edi
.text:0040ED90 pop esi
.text:0040ED91 pop ebx
.text:0040ED92 add esp, 48h
.text:0040ED95 cmp ebp, esp
.text:0040ED97 call __chkesp
.text:0040ED9C mov esp, ebp
.text:0040ED9E pop ebp
.text:0040ED9F retn 4
.text:0040ED9F CObject__IsKindOf endp
综上,可以得到虚表指针的填充时机:只有在构造函数和析构函数中会对虚表指针重新赋值,其他位置没有对该指针的操作。
继承关系下的虚机制
定义CCmdTarget
类继承自CObject
类,CWinThread
类继承自CCmdTarget
类。注意:本文只讨论单一继承,不讨论多继承、菱形继承及虚继承。示例代码如下:
// 声明静态数据成员及虚函数
#define DECLARE_DYNAMIC(class_name) \
public: \
static const CRuntimeClass class##class_name; \
virtual CRuntimeClass* GetRuntimeClass() const;
class CCmdTarget : public CObject
{
DECLARE_DYNAMIC(CCmdTarget); // 该宏定义了一个虚函数virtual CRuntimeClass* GetRuntimeClass() const
public:
CCmdTarget();
virtual ~CCmdTarget();
};
class CWinThread : public CCmdTarget
{
DECLARE_DYNAMIC(CWinThread)
public:
CWinThread *m_pCurrentWinThread;
public:
virtual int Run();
virtual BOOL InitInstance();
CWinThread();
virtual ~CWinThread();
};
子类虚表的构造
通过反汇编工具,观察上述示例代码的虚表结构:
; CObject Vftable
.rdata:00423070 pCObject__vftable dd offset CObject__GetRuntimeClass
.rdata:00423070 ; DATA XREF: CObject__CObject+20↑o
.rdata:00423070 ; CObject__destructor+20↑o
.rdata:00423074 dd offset CObject__scalar_deleting_destructor
; CCmdTarget
.rdata:004240C0 pCCmdTarget__vftable dd offset CCmdTarget__GetRuntimeClass
.rdata:004240C0 ; DATA XREF: sub_401270+28↑o
.rdata:004240C0 ; sub_40EB30+20↑o
.rdata:004240C4 dd offset scalar_deleting_destructor
; CWinThread vftable
.rdata:00423114 pCWinThread__vftable dd offset CWinThread__GetRuntimeClass
.rdata:00423114 ; DATA XREF: sub_401FE0+28↑o
.rdata:00423114 ; sub_4020A0+20↑o
.rdata:00423118 dd offset CWinThread__scalar_deleting_destructor
.rdata:0042311C dd offset CWinThread__Run
.rdata:00423120 dd offset CWinThread__InitInstance
观察CObject
和CCmdTarget
的虚表结构,发现它们都只有两项,且CCmdTarget
类的虚表将父类CObject
虚表中的两项均替换掉了,观察源代码发现是因为在子类CCmdTarget
中将两个虚函数GetRuntimeClass
和scalar_deleting_destructor
函数均重写了。而观察CCmdTarget
和CWinThread
的虚表结构,发现子类CWinThread
除了替换掉了两个重写了的虚函数以外,还追加了两个自身定义的虚函数。
可以得出结论,子类在编译期间构造自身的虚表,按照下列步骤进行构造:
- 复制父类虚表
- 将子类重写的虚函数地址替换父类对应的表项中的地址
- 追加父类中没有的虚函数地址
即:复制、替换和添加 三个步骤。
虚表指针的替换填充
只需要按顺序观察构造和析构即可。
观察构造函数反汇编代码:
; CWinThread::CWinThread
.text:00401FFA mov [ebp+var_4], ecx
.text:00401FFD mov ecx, [ebp+var_4]
.text:00402000 call j_CCmdTarget__CCmdTarget ; 1. 先调用父类构造
.text:00402005 mov eax, [ebp+var_4]
.text:00402008 mov dword ptr [eax], offset pCWinThread__vftable ; 2. 执行完父类构造后,在子类构造中替换对象的虚表指针
.text:0040200E mov eax, [ebp+var_4]
; CCmdTarget::CCmdTarget
.text:0040128A mov [ebp+var_4], ecx
.text:0040128D mov ecx, [ebp+var_4]
.text:00401290 call j_CObject__CObject
.text:00401295 mov eax, [ebp+var_4]
.text:00401298 mov dword ptr [eax], offset pCCmdTarget__vftable
.text:0040129E mov eax, [ebp+var_4]
; CObject::CObject
.text:004016EA mov [ebp+var_4], ecx
.text:004016ED mov eax, [ebp+var_4]
.text:004016F0 mov dword ptr [eax], offset pCObject__vftable
.text:004016F6 mov eax, [ebp+var_4]
可以发现,在父类构造函数执行完毕后,返回到子类的构造函数后,用子类的虚表地址替换掉父类的虚表指针。
观察析构函数的反汇编代码:
; CWinThread
.text:004020BA mov [ebp+var_4], ecx
.text:004020BD mov eax, [ebp+var_4]
.text:004020C0 mov dword ptr [eax], offset pCWinThread__vftable ; 1. 先替换为子类的虚表指针
.text:004020C6 mov ecx, [ebp+var_4]
.text:004020C9 call CCmdTarget__~CCmdTarget ; 2. 后调用父类的析构函数
; CCmdTarget
.text:0040EB4A mov [ebp+var_4], ecx
.text:0040EB4D mov eax, [ebp+var_4]
.text:0040EB50 mov dword ptr [eax], offset pCCmdTarget__vftable
.text:0040EB56 mov ecx, [ebp+var_4]
.text:0040EB59 call j_CObject__destructor
; CObject
.text:00401780 CObject__destructor proc near ; CODE XREF: j_CObject__destructor↑j
.text:00401780
.text:00401780 var_44 = byte ptr -44h
.text:00401780 var_4 = dword ptr -4
.text:00401780
.text:00401780 push ebp
.text:00401781 mov ebp, esp
.text:00401783 sub esp, 44h
.text:00401786 push ebx
.text:00401787 push esi
.text:00401788 push edi
.text:00401789 push ecx
.text:0040178A lea edi, [ebp+var_44]
.text:0040178D mov ecx, 11h
.text:00401792 mov eax, 0CCCCCCCCh
.text:00401797 rep stosd
.text:00401799 pop ecx
.text:0040179A mov [ebp+var_4], ecx
.text:0040179D mov eax, [ebp+var_4]
.text:004017A0 mov dword ptr [eax], offset pCObject__vftable
.text:004017A6 pop edi
.text:004017A7 pop esi
.text:004017A8 pop ebx
.text:004017A9 mov esp, ebp
.text:004017AB pop ebp
.text:004017AC retn
.text:004017AC CObject__destructor endp
可以得到结论:
- 构造时,先构造父类后,使用子类的虚表指针替换掉父类之前填充过的虚表指针
- 析构时,先恢复子类的虚表指针,再调用父类的析构函数
即:构造时,先构造后替换;析构时,先替换后析构。
- 0
-
分享