0%

C++反汇编与逆向分析 - 继承

继承和多态是面向对象语言最强大的功能。有了继承和多态,我们可以完成代码重用。在C中有许多技巧可以实现多态。本文的目的就是演示一种简单和容易的技术,在C中应用继承和多态。通过创建一个VTable(virtual table)和在基类和派生类对象之间提供正确的访问,我们能在C中实现继承和多态。VTable能通过维护一张函数表指针表来实现。为了提供基类和派生类对象之间的访问,我们可以在基类中维护派生类的引用和在派生类中维护基类的引用。

继承

构造

  • 当有父类时,以填虚表为界,反是有父类构造或者成员构造,都在填虚表之前
  • 如果内联了父类的构造,判断填写虚表上是否也往同一个地址填虚表
  • 子类构造前会先构造父类和成员构造
  • 构造先父类虚表,在成员,在自身虚表
  • 调用成员对象的构造函数
  • 先填写子类自己的虚表指针
  • 然后执行子类的构造函数体

析构

  • 虚构先自身,在成员,在父类
  • 先填写子类自己的虚表指针
  • 然后执行子类的析构函数体
  • 调用成员对象的析构函数体
  • 然后调用父类的析构函数体

  1. 析构在父类是虚析构,子类也是虚析构
  2. cout是一个全局对象,endl是一个函数指针,flush也是函数指针
  3. 当有父类或成员构造的时候,构造以虚表为界线,前面为父类或成员,call传递this指针(里面还有call继续有父类);
  4. 析构先填写虚表,再虚构的原因:每个类了为了防止将来被继承
  5. 构造调用父类的构造函数,调用成员对象的构造函数指针,先填写子类自己的虚表,然后执行子类的构造函数体
  6. 析构先填写子类自己的虚表指针,然后执行子类的析构函数体,调用成员对象的析构函数指针,再调用父类的析构函数
  7. 纯虚父类被强制调用
    • vc6.0 push 19h ,call asmg_exit(purecall)
    • vs2019指针指向一个错误信息的收集

父类对象引用子类对象,没问题;子类对象引用父类对象,有风险

子类对象

  • 子类对象构造函数
    • 调用父类的构造函数
    • 调用成员对象的构造函数
    • 先填写子类自己的虚表指针
    • 然后执行子类的构造函数体
  • 子类对象析构函数
    • 先填写子类自己的虚表指针
    • 执行子类的析构函数体
    • 调用成员对象的析构函数
    • 调用父类的析构函数

识别类与类的关系

在C++的继承关系中,子类具有父类的所有的数据和成员函数。子类对象可直接使用父类中声明为公有和保护的数据成员与函数;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class CBase
{
public:
CBase();
~CBase();
int m_nBase;
void SetNumber(int n);
int GetNumber();
private:

};

CBase::CBase()
{
}

CBase::~CBase()
{
}

int CBase::GetNumber()
{
return m_nBase;
}


void CBase::SetNumber(int n)
{
m_nBase = n;
}


class CDerive : public CBase //派生类定义
{
public:
void ShowNumber(int nNumber);
};

void CDerive::ShowNumber(int nNumber)
{
int m_nCDerive;
SetNumber(nNumber);
m_nCDerive = nNumber + 1;
printf("%d\n", GetNumber());
printf("%d\n", m_nCDerive);
}


int _tmain(int argc, _TCHAR* argv[])
{
CDerive Derive;
Derive.ShowNumber(argc);
return 0;
}




父类中定义了成员函数、构造函数、析构函数和两个成员函数。子类中只有一个成员函数一个数据成员。根据C++语法规则,子类将继承父类中的成员数据和成员函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
	CDerive Derive;
00FD16BD lea ecx,[Derive] ;获取对象首地址作为this指针
00FD16C0 call CDerive::CDerive (0FD10A5h) ;调用子类的构造函数,编译器默认提供
00FD16C5 mov dword ptr [ebp-4],0
Derive.ShowNumber(argc);
00FD16CC mov eax,dword ptr [argc]
00FD16CF push eax
00FD16D0 lea ecx,[Derive]
00FD16D3 call CDerive::ShowNumber (0FD1032h) ;调用 Derive成员函数,传入this指针
return 0;
00FD16D8 mov dword ptr [ebp-0E0h],0
00FD16E2 mov dword ptr [ebp-4],0FFFFFFFFh
00FD16E9 lea ecx,[Derive]
00FD16EC call CDerive::~CDerive (0FD10E6h) ;调用Derive的析构函数,编译器默认提供
00FD16F1 mov eax,dword ptr [ebp-0E0h]
}


;子类默认构造函数分析
CDerive::CDerive:
00FD146F pop ecx ;还原this指针
00FD1470 mov dword ptr [this],ecx
00FD1473 mov ecx,dword ptr [this] ;以子类对象首地址作为父类的this指针,调用父类构造函数
00FD1476 call CBase::CBase (0FD1172h) ;父类构造函数
00FD147B mov eax,dword ptr [this]
00FD1491 ret


;子类默认析构函数分析
CDerive::~CDerive:
00FD150F pop ecx ;还原this指针
00FD1510 mov dword ptr [this],ecx
00FD1513 mov ecx,dword ptr [this] ;以子类对象作为父类的this指针,调用父类构造函数
00FD1516 call CBase::~CBase (0FD1028h) ;调用父类的析构函数
00FD152E ret




编译器提供了默认的构造与析构函数。当子类中没有构造函数或析构函数时,而其父类却需要构造函数与析构函数时,编译器会为该父类的子类提供默认的析构函数与构造函数;

由于子类继承了父类,因此子类中拥有父类的各成员,类似于在子类中定义了父类的对象作为数据成员使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class CBase
{
public:
CBase();
~CBase();
int m_nBase;
void SetNumber(int n);
int GetNumber();
private:

};

class CDerive
{
public:
CBase m_Base; //原来父类的CBase称为成员对象
int m_nDerive; //原来的子类派生数据
};

父类中有构造函数,但子类中没有构造函数时,编译器会默认为子类生成构造函数,以实现成员构造函数的调用

子类中有构造函数,但父类中没有构造函数,编译器不会默认为父类提供构造函数

子类对象在内存中的数据排列为:先安排父类数据、后安排子类新定义的数据;

在已有初始化列表的情况下,将会优先执行初始化列表中的操作;其次才是自身的构造函数;

顺序为:

  • 先构造父类
  • 按顺序构造成员对象和初始化列表
  • 最后自身的构造函数

多父类

  • 构造和析构时会填多次次虚表
  • 多次虚表部分各覆盖了多次虚表内容
  • 识别双父类:观察填写虚表的次数

菱形结构

  • 菱形结构可以设计为组合关系或者聚合关系
    • 组合关系是同生共死
    • 聚合关系是我死你活
  • 多重继承时,构造会多一个参数(bool值),表示是否构造基类
  • 多重继承会在构造先填baseoffset再填虚表
  • 寻找基类的虚表地址:
    • (baseoffset:记录基类偏移的结构体)
    • this指针+4 得到baseoffset的指针
    • baseoffset指针+4得到baseoffset偏移
    • this指针+4+baseoffset偏移得到虚表地址

内存结构: