我的知识记录

digSelf

C++逆向:浅谈C++的虚函数机制

2022-01-29
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

观察CObjectCCmdTarget 的虚表结构,发现它们都只有两项,且CCmdTarget类的虚表将父类CObject虚表中的两项均替换掉了,观察源代码发现是因为在子类CCmdTarget中将两个虚函数GetRuntimeClassscalar_deleting_destructor函数均重写了。而观察CCmdTargetCWinThread的虚表结构,发现子类CWinThread 除了替换掉了两个重写了的虚函数以外,还追加了两个自身定义的虚函数。

可以得出结论,子类在编译期间构造自身的虚表,按照下列步骤进行构造:

  1. 复制父类虚表
  2. 将子类重写的虚函数地址替换父类对应的表项中的地址
  3. 追加父类中没有的虚函数地址

即:复制、替换和添加 三个步骤。

虚表指针的替换填充

只需要按顺序观察构造和析构即可。

观察构造函数反汇编代码:

; 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

可以得到结论:

  1. 构造时,先构造父类后,使用子类的虚表指针替换掉父类之前填充过的虚表指针
  2. 析构时,先恢复子类的虚表指针,再调用父类的析构函数

即:构造时,先构造后替换;析构时,先替换后析构

  • 0