0%

C++反汇编与逆向分析 - 构造析构

构造函数与析构函数是类的重要组成部分,构造函数常用来完成对象生成时的数据初始化工作,而析构函数则常用于在对象销毁时释放对象中所申请的资源;

当对象生成时,编译器会自动产生调用其类构造函数的代码,在编译过程中可以为类中的数据成员赋予恰当的初值。当对象销毁后,编译器调用析构函数;

运行环境:

  • 操作系统: Windows 7家庭版
  • 编译器:VC6 VS2013

构造析构

构造函数出现时机

对象生成时会自动调用构造函数,只要找到了定义对象的地方就找了构造函数的调用时机。不同作用域对象声明周期不同。

将对象进行分类:不同类型的对象的构造函数被调用的时机会发生变化,但都会遵循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
21:   int main(int argc, char* argv[])
22: {
00401060 push ebp
00401061 mov ebp,esp
00401063 sub esp,44h
00401066 push ebx
00401067 push esi
00401068 push edi
00401069 lea edi,[ebp-44h]
0040106C mov ecx,11h
00401071 mov eax,0CCCCCCCCh
00401076 rep stos dword ptr [edi]
23: CNumber Number;
00401078 lea ecx,[ebp-4] ;获得对象首地址,传入ecx中作为参数
0040107B call @ILT+0(CNumber::CNumber) (00401005)
24: return 0;
00401080 xor eax,eax
25: }


16: CNumber::CNumber()
17: {
00401020 push ebp
00401021 mov ebp,esp
00401023 sub esp,44h
00401026 push ebx
00401027 push esi
00401028 push edi
00401029 push ecx
0040102A lea edi,[ebp-44h]
0040102D mov ecx,11h
00401032 mov eax,0CCCCCCCCh
00401037 rep stos dword ptr [edi]
00401039 pop ecx ;还原ecx
0040103A mov dword ptr [ebp-4],ecx ;this指针
18: m_nNumber = 1;
0040103D mov eax,dword ptr [ebp-4] ;[ebp-4]为this指针,eax保存了对象的首地址
00401040 mov dword ptr [eax],1 ;偏移量为0,所以eax不需要加
19: }
00401046 mov eax,dword ptr [ebp-4]

当在进入对象的作用域时,编译器会产生调用构造函数的代码。由于构造函数属于成员函数,因此在调用过程中需要传递this指针。构造函数调用结束后,会将this指针作为返回值;

  • 该成员函数是这个对象在作用域内调用的第一个成员函数,根据this指针即可区分每个对象;
  • 这个函数必须返回this指针
  • 使用其他调用约定,编译器会提示调用约定无效
  • 构造函数返回的值不被调用方使用

堆对象

堆对象的识别重点在于识别堆空间的申请与释放。在C++中堆申请需要使用malooc、new运算或其他具有相同功能的函数;

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
59
60
61
62
63
64
class CNumber
{
public:
int i;
int j;
CNumber();
~CNumber();
protected:
private:

};

CNumber::CNumber()
{
;
}

CNumber::~CNumber()
{
;
}

int main(int argc, char* argv[])
{
CNumber *pNumber = NULL;
pNumber = new CNumber;

pNumber->i = 100;
printf("%d\n",pNumber->i);
return 0;
}

30: CNumber *pNumber = NULL;
004010BD mov dword ptr [ebp-10h],0 ;给指针赋值为0
31: pNumber = new CNumber;
004010C4 push 8 ;压入类的大小
004010C6 call operator new (00401280) ;函数重载,后续介绍
004010CB add esp,4 ;
004010CE mov dword ptr [ebp-18h],eax ;使用临时变量保存new的返回值,new返回值为对象首地址
004010D1 mov dword ptr [ebp-4],0 ;[ebp-4]保存堆申请次数
004010D8 cmp dword ptr [ebp-18h],0 ;检测堆空间是否申请成功
004010DC je main+5Bh (004010eb) ;失败则跳过构造函数
004010DE mov ecx,dword ptr [ebp-18h] ;使用ecx传递this指针
004010E1 call @ILT+0(CNumber::CNumber) (00401005) ;构造函数
004010E6 mov dword ptr [ebp-1Ch],eax ;构造函数返回this指针
004010E9 jmp main+62h (004010f2) ;编译器产生双分支结构,用于检查new是否成功
004010EB mov dword ptr [ebp-1Ch],0 ;堆申请失败,设置指针值为NULL
004010F2 mov eax,dword ptr [ebp-1Ch] ;
004010F5 mov dword ptr [ebp-14h],eax
004010F8 mov dword ptr [ebp-4],0FFFFFFFFh
004010FF mov ecx,dword ptr [ebp-14h]
00401102 mov dword ptr [ebp-10h],ecx
32:
33: pNumber->i = 100;
00401105 mov edx,dword ptr [ebp-10h] ;edx获得this指针
00401108 mov dword ptr [edx],64h ;对成员函数赋值
34: printf("%d\n",pNumber->i);
0040110E mov eax,dword ptr [ebp-10h]
00401111 mov ecx,dword ptr [eax]
00401113 push ecx
00401114 push offset string "%d\n" (0042501c)
00401119 call printf (00401200)
0040111E add esp,8

如果堆申请失败,则会避开调用构造函数,因为在C++语法中,new成功后,返回值为对象的首地址,否则为NULL。因此编译器需要检查堆空间申请结果,产生一个双分支,以此决定是否触发构造函数。

全局对象

  • debug版本下在_cinit函数的第二个_initterm函数中的函数指针,有个$e4开头的标号
  • $e4:构造函数的代理的代理,先执行构造函数的代理然后执行析构函数的代理的代理
    • $e1:执行构造函数代理
    • $e3:注册析构函数的代理的代理
      • $e2:析构函数代理,调用atexit内部的函数指针
  • $E4(构造函数的代理的代理)->$E1(构造函数的代理)。析构在$E4->$E3(注册析构函数代理的代理)->$E2(析构函数的代理.调用atexit(offset E2)

快速识别方法:

  • 首先找到cinit
  • 查看__initterm值

全局对象与静态对象

全局对象与静态对象的构造时机相同,它们的构造函数的调用被隐藏起来,但识别很容易。因为程序中所有全局对象将会在同一地点调用构造函数以初始化数据。

构造函数需要传递对象的首地址作为this指针,而且构造函数可以带各类参数。

  • 直接定位初始化函数
    • 先进入MainCRTStartup函数中,然后找到初始化函数_cinit,在_cinit函数的第二个__initterm处设置断点。
  • 栈回溯

对象数组

构造
  • new[]指针往上4个字节可以看到对象总个数,增加是正序增加
  • delete[]会逆序释放
  • 对象数组构造的五个参数
    1. 对象数组的首地址
    2. 构造函数的指针
    3. 数组对象的总个数
    4. 对象的大小
    5. 析构函数指针
析构
  • 会无条件执行析构函数
  • 对象数组析构必要条件
    1. 对象数组的首地址
    2. 对象的大小
    3. 对象的数量
    4. 析构函数的地址
    5. 当对象数组的时候多了一个位表示是否数组对象

地址004014D7的位置为对象数组的首地址

地址004014DC的位置为构造函数的指针

地址004014EA的位置为数组对象的个数

地址004014EC的位置为数组对象的大小

地址004014F4的位置为构造函数的指针,也就是this指针

析构的顺序也是一样的;

这里有一个需要注意的地方,就是00401690处的and 2指令,这条指令大概意思是取到处第二位判断,是否为对象数组;

004016B1处的and 1指令为取最低位,如果为1,则释放空间;

参数对象

参数对象属于局部对象中一种特殊的饿情况。当对象作为参数时,调用一个特殊的构造函数-拷贝构造函数。该构造函数只有一个参数,类型为对象的引用;

当对象为参数时,会触发此类对象的拷贝构造函数。如果在函数调用时传递该对象,参数会进行复制,形参是实参的副本,相当于拷贝构造了一个全新的对象。由于定义了新对象,会触发拷贝构造函数,在这个特殊的构造函数中完成对象的数据赋值;

当然我们不对构造函数进行拷贝构造函数的书写,那么编译器会为我们默认提供一个浅拷贝函数;

  • 函数调用前执行构造(拷贝),里面执行析构

  • 分小对象和大对象、有没有拷贝构造

  • 小对象会简单push

  • 大对象会生成:mov edi, esp这一句汇编,相当于浅拷贝

  • 有拷贝构造时,

    1
    mov ecx, esp

    this指向栈顶

    • 构造再函数外,析构再函数内
1
2
3
4
5
6
7
8
125:      CMyString str;
0040155D lea ecx,[ebp-14h] ;获取str对象的首地址
00401560 call @ILT+5(CMyString::CMyString) (0040100a) ;调用构造函数,没有拷贝构造函数
00401565 mov dword ptr [ebp-4],0
126: CMyString Two(str);
0040156C mov eax,dword ptr [ebp-14h] ;取出对象str中数据成员信息
0040156F mov dword ptr [ebp-1Ch],eax ;赋值对象Two中数据成员的值

虽然编译器提供了默认浅拷贝很方便,但在某些情况下会导致程序出错;

拷贝构造函数在函数外构造,函数内析构;

返回对象

非赋值返回

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
CMyString GetString()
{
CMyString str = "Hello,World!";
return str;
}

CMyString str1 = GetString(); 并非赋值,调用拷贝构造 不产生临时对象
133: CMyString str1 = GetString();
0040F5DD lea eax,[ebp-14h]
0040F5E0 push eax
0040F5E1 call @ILT+55(GetString) (0040103c)
0040F5E6 add esp,4
0040F5E9 mov dword ptr [ebp-4],0

;构造函数
004015F8 lea eax,[ebp-14h]
004015FB push eax
004015FC mov ecx,dword ptr [ebp+8]
004015FF call @ILT+5(CMyString::CMyString) (0040100a) ;退出之前构造


0040F606 mov dword ptr [ebp-18h],0
0040F60D mov dword ptr [ebp-4],0FFFFFFFFh
0040F614 lea ecx,[ebp-14h]
0040F617 call @ILT+40(CMyString::~CMyString) (0040102d)
0040F61C mov eax,dword ptr [ebp-18h]


  • 会再函数内构造,函数外析构
  • 拷贝构造的软条件:
    • 是个构造函数
    • 该构造函数的参数是本类对象的指针
    • 如果没有拷贝构造,则就是以参数为目标的memcpy的行为
1
2
3
4
5
6
7
8
9
10
11
12
CMyString GetString()
{
CMyString str("teste");
return str;
}

void main()
{
CMyString str = GetString(); //此处属于拷贝构造,不属于赋值运算,所以不产生临时对象
//相当于
//CMyString str = GetString(&str);
}

识别方式:

  • 函数的参数1为返回对象的指针
  • 返回对象的函数返回值为参数1
  • 再返回对象的函数内(退出前),会执行以参数1为this的拷贝构造函数,
    • 如果没有拷贝构造,则执行浅拷贝(大对象有memcpy的行为,小对象寄存器赋值)
  • 返回对象的析构实际再返回对象函数以外

隐含参数

隐藏一个返回对象的指针,做返回值;

1
2
3
4
5
6
7
8
9
10
CMyString GetString(&str1)
{
CMyString str = "Hello,World!";
return str;
}

CMyString str1 = GetString(); 并非赋值,调用拷贝构造



拷贝构造

  • 识别为构造函数
  • 该函数的参数为本类对象的指针,有且仅有这一个参数
  • 在对象传参的时候,调用了这个函数
  • 在对象作为返回值的时候,调用了这个函数
  • 拷贝构造函数的识别可以和参数对象以及返回对象相互举证

临时对象

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
CMyString GetString()
{
CMyString str = "Hello,World!";
return str;
}

int main(int argc, char* argv[])
{
//CMyString str = "sYstemk1t";
//CMyString str = "sYstemk1t";
//CMyString str1 = GetString();
//printf("%d\n",str1.GetLength());
CMyString str1;
str1 = GetString();
}

135: CMyString str1;
0040F5DD lea ecx,[ebp-14h] ;传递局部对象首地址
0040F5E0 call @ILT+10(CMyString::CMyString) (0040100f) ;调用构造函数
0040F5E5 mov dword ptr [ebp-4],0 ;局部变量给0
136: str1 = GetString();
0040F5EC lea eax,[ebp-1Ch] ;临时变量
0040F5EF push eax ;返回局部结果
0040F5F0 call @ILT+55(GetString) (0040103c) ;构造函数
0040F5F5 add esp,4
0040F5F8 mov dword ptr [ebp-24h],eax ;浅拷贝开始
0040F5FB mov ecx,dword ptr [ebp-24h]
0040F5FE mov edx,dword ptr [ecx]
0040F600 mov eax,dword ptr [ecx+4]
0040F603 mov dword ptr [ebp-14h],edx
0040F606 mov dword ptr [ebp-10h],eax ;浅拷贝结束
0040F609 lea ecx,[ebp-1Ch] ;获得首地址
0040F60C call @ILT+40(CMyString::~CMyString) (0040102d) ;析构局部对象


临时对象的生命期从函数内执行构造开始,到本条语句结束结束为止(遇到分号则析构)

无名对象

  • 无名对象必须由引用去维护
  • 此处产生无名对象,其特点为使用指针保留并维护这个返回对象的地址
  • 无名对象的作用域和其引用的作用域定义一致,故此处不会析构这个返回对象
  • 先识别返回对象,观察对象如何操作,拷贝构造给到第三方后观察有没有析构,如果有析构即可判定为临时对象,如果没有通过拷贝,将返回对象的指针交给第三方维护,即可判定为无名对象
1
2
3
4
5
6
7
8
9
10
11
CMyString GetString()
{
CMyString str("teste");
return str;
}

void main()
{
CMyString &str = GetString() ;

}