逆向笔记(七)

最近闲了下来,总结一下之前的知识,这些东西都是好久之前留下的了,现在在回味一遍,梳理梳理。

逆向笔记之构造函数和析构函数。

构造函数用来完成对象生成时的数据初始化工作,而析构函数则用在对象销毁时释放对象中所申请的资源。当对象生成时,编译器会自动产生调用其构造函数的代码,在编码过程中可以为类中的数据成员赋予恰当的初值。当对象销毁时,编译器同样会产生调用其类析构函数的代码。

构造函数与析构函数都是类中特殊的成员函数,构造函数支持函数重载,而析构函数只能是一个无参函数。他们不可定义返回值,调用构造函数后,返回值为对象首地址,也就是this指针。

构造函数的出现时机

不同作用域的对象生命周期不同,如局部对象,全局对象,静态对象等等的生命周期不相同,当对象作为函数参数与返回值时,构造函数的出现时机也会有所不同。虽然不同类型对象的构造函数被调用的时机不同,但都会遵循C++语法,定义的同时调用构造函数。因此要将对象进行分类。
根据生命周期将对象可以分为如下几类:
1)局部对象
2)堆对象
3)参数对象
4)返回对象
5)全局对象
6)静态对象

局部对象

局部对象下的构造函数的出现时机比较容易识别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
C代码:
class CNumber
{
public:
CNumber()
{
m_nNum = 1;
}
int m_nNum;
};
int _tmain(int argc, _TCHAR* argv[])
{
CNumber obj;
return 0;
}

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
对应反汇编Debug:
Main函数
013E1630 > 55 push ebp
013E1631 8BEC mov ebp,esp
013E1633 81EC CC000000 sub esp,0xCC
013E1639 53 push ebx
013E163A 56 push esi
013E163B 57 push edi
013E163C 8DBD 34FFFFFF lea edi,dword ptr ss:[ebp-0xCC]
013E1642 B9 33000000 mov ecx,0x33
013E1647 B8 CCCCCCCC mov eax,0xCCCCCCCC
013E164C F3:AB rep stos dword ptr es:[edi]
013E164E 8D4D F8 lea ecx,dword ptr ss:[ebp-0x8]//取对象首地址,this指针
013E1651 E8 BCFBFFFF call TestCode.013E1212 //调用构造函数
013E1656 33C0 xor eax,eax
013E1658 52 push edx
013E1659 8BCD mov ecx,ebp
013E165B 50 push eax
013E165C 8D15 80163E01 lea edx,dword ptr ds:[0x13E1680]
013E1662 E8 2FFAFFFF call TestCode.013E1096
013E1667 58 pop eax
构造函数
013E1420 T> 55 push ebp
013E1421 8BEC mov ebp,esp
013E1423 81EC CC000000 sub esp,0xCC
013E1429 53 push ebx
013E142A 56 push esi
013E142B 57 push edi
013E142C 51 push ecx
013E142D 8DBD 34FFFFFF lea edi,dword ptr ss:[ebp-0xCC]
013E1433 B9 33000000 mov ecx,0x33
013E1438 B8 CCCCCCCC mov eax,0xCCCCCCCC
013E143D F3:AB rep stos dword ptr es:[edi]
013E143F 59 pop ecx
013E1440 894D F8 mov dword ptr ss:[ebp-0x8],ecx //this指针给ebp-0x8
013E1443 8B45 F8 mov eax,dword ptr ss:[ebp-0x8] //this指针给eax
013E1446 C700 01000000 mov dword ptr ds:[eax],0x1 //赋值
013E144C 8B45 F8 mov eax,dword ptr ss:[ebp-0x8]//给eax作为返回值
013E144F 5F pop edi
013E1450 5E pop esi
013E1451 5B pop ebx
013E1452 8BE5 mov esp,ebp
013E1454 5D pop ebp
013E1455 C3 retn

由此可以看出,当在进入对象作用域时,编译器会产生调用构造函数的代码。由于构造函数属于成员函数,因此在调用过程中同样需要传递this指针。构造函数调用结束后,会将this指针作为返回值。返回this指针便是构造函数的特征之一。
结合C++语法,可总结识别局部对象的构造函数的必要条件:
1该成员函数是这个对象在作用域内调用的第一个成员函数,根据this指针即可区分每个对象。
2这个函数返回this指针。

堆对象

堆对象的识别重点在于识别堆空间的申请与使用。堆空间申请需要使用malloc函数,new运算符或其他同类功能的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
C代码:
class CNumber
{
public:
CNumber()
{
m_nNum = 1;
}
int m_nNum;
};
int _tmain(int argc, _TCHAR* argv[])
{
CNumber *obj = NULL;
obj = new CNumber;
return 0;
}

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
对应反汇编Debug:
01163FF0 T> 55 push ebp
01163FF1 8BEC mov ebp,esp
01163FF3 6A FF push -0x1
01163FF5 68 8E4F1601 push TestCode.01164F8E
01163FFA 64:A1 00000000 mov eax,dword ptr fs:[0]
01164000 50 push eax
01164001 81EC E8000000 sub esp,0xE8
01164007 53 push ebx
01164008 56 push esi
01164009 57 push edi
0116400A 8DBD 0CFFFFFF lea edi,dword ptr ss:[ebp-0xF4]
01164010 B9 3A000000 mov ecx,0x3A
01164015 B8 CCCCCCCC mov eax,0xCCCCCCCC
0116401A F3:AB rep stos dword ptr es:[edi]
0116401C A1 04901601 mov eax,dword ptr ds:[__security_cookie]
01164021 33C5 xor eax,ebp
01164023 50 push eax
01164024 8D45 F4 lea eax,dword ptr ss:[ebp-0xC]
01164027 64:A3 00000000 mov dword ptr fs:[0],eax
0116402D C745 EC 0000000>mov dword ptr ss:[ebp-0x14],0x0 //指针初始化为Null
01164034 6A 04 push 0x4 //压入类的大小
01164036 E8 78D1FFFF call TestCode.011611B3 //调用new
0116403B 83C4 04 add esp,0x4 //堆栈平衡
0116403E 8985 20FFFFFF mov dword ptr ss:[ebp-0xE0],eax //返回值给ebp-0xe0
01164044 C745 FC 0000000>mov dword ptr ss:[ebp-0x4],0x0 //这里保存申请次数
0116404B 83BD 20FFFFFF 0>cmp dword ptr ss:[ebp-0xE0],0x0 //比较结果
01164052 74 13 je short TestCode.01164067 //申请空间失败结束
01164054 8B8D 20FFFFFF mov ecx,dword ptr ss:[ebp-0xE0] //对象首地址给ecx
0116405A E8 B3D1FFFF call TestCode.01161212 //调用构造函数
0116405F 8985 0CFFFFFF mov dword ptr ss:[ebp-0xF4],eax //返回值给ebp-0xf4
01164065 EB 0A jmp short TestCode.01164071
01164067 C785 0CFFFFFF 0>mov dword ptr ss:[ebp-0xF4],0x0 //申请失败指针为null
00204071 8B85 0CFFFFFF mov eax,dword ptr ss:[ebp-0xF4]
00204077 8985 14FFFFFF mov dword ptr ss:[ebp-0xEC],eax //this指针给ebp-0xec
0020407D C745 FC FFFFFFF>mov dword ptr ss:[ebp-0x4],-0x1
00204084 8B8D 14FFFFFF mov ecx,dword ptr ss:[ebp-0xEC]
0020408A 894D EC mov dword ptr ss:[ebp-0x14],ecx //ecx为this指针
0020408D 8B45 EC mov eax,dword ptr ss:[ebp-0x14]
00204090 C700 02000000 mov dword ptr ds:[eax],0x2 //赋值
00204096 8BF4 mov esi,esp
00204098 8B45 EC mov eax,dword ptr ss:[ebp-0x14]
0020409B 8B08 mov ecx,dword ptr ds:[eax] //取内容给ecx,也就是第一个成员变量
0020409D 51 push ecx //参数入栈
0020409E 68 6C682000 push TestCode.0020686C ; ASCII "&d\n"
002040A3 FF15 18A12000 call dword ptr ds:[<&MSVCR120D.printf>] ; MSVCR120.printf
002040A9 83C4 08 add esp,0x8
002040AC 3BF4 cmp esi,esp
002040AE E8 ABD0FFFF call TestCode.0020115E
002040B3 33C0 xor eax,eax
002040B5 8B4D F4 mov ecx,dword ptr ss:[ebp-0xC]
002040B8 64:890D 0000000>mov dword ptr fs:[0],ecx
002040BF 59 pop ecx
002040C0 5F pop edi
002040C1 5E pop esi
002040C2 5B pop ebx
002040C3 81C4 F4000000 add esp,0xF4
002040C9 3BEC cmp ebp,esp
002040CB E8 8ED0FFFF call TestCode.0020115E
002040D0 8BE5 mov esp,ebp
002040D2 5D pop ebp
002040D3 C3 retn

由此可以看出,在使用new申请了堆空间以后,需要调用构造函数,以完成对象的数据成员初始化过程。如果堆空间申请失败,则会避开构造函数的调用。因为在C++语法中,如果new运算执行成功,返回值为对象的首地址,否则为Null。因此需要编译器检查堆空间的申请结果,产生一个双分支结构,以决定是否触发构造函数。在识别堆对象的构造函数时,应重点分析双分支结构,找到new运算的调用后,可立即在下文寻找判定new返回值的代码,在判定成功(new的返回值非0)的分支处可迅速定位并得到构造函数。
C中的malloc函数和C++中的new运算的区别很大,很重要的两点是malloc不负责出发构造函数,他也不是运算符,无法进行运算符重载。

参数对象

参数对象属于局部对象中的一种特殊情况。当对象作为函数参数时,调用一个特殊的构造函数—拷贝构造函数。该构造函数只有一个参数,类型为对象的引用。
当对象为参数时,会触发此类对象的拷贝构造函数。如果在函数调用时传递参数对象,参数会进行复制,形参是实参的副本,相当于拷贝构造了一个全新的对象。由于定义了新对象,因此会触发拷贝构造函数,在这个特殊的构造函数中完成两个对象间数据的复制。如没有定义拷贝构造函数,编译器会对原对象与拷贝对象中的各数据成员直接进行数据复制,称为默认拷贝构造函数,这种拷贝方式属于浅拷贝。
虽然使用编译的提供的默认拷贝构造函数很方便,但在某些特殊情况下,这种拷贝会导致程序错误,如前面的资源释放错误。当类中有资源申请,并以数据成员来保存这些资源时,就需要使用者自己提供一个拷贝构造函数。在拷贝构造函数中,要处理的不仅仅是资源对象的各数据成员,还有他们指向的资源数据。把这种源对象中的数据成员间接访问到其他资源并制作副本的拷贝构造函数称为深拷贝。

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
C代码:
class CMyString
{
public:
CMyString()
{
m_pString = NULL;
}
CMyString (CMyString &obj)
{
//如果在拷贝构造函数中直接复制指针值,那么对象内的两个成员指针会指向同一个资源,浅拷贝
//this->m_pString = obj.m_pString;
//为实参对象中的指针指向的堆空间制作一份副本,深拷贝
int nLen = strlen(obj.m_pString);
this->m_pString = new char[nLen + sizeof(char)];
strcpy(this->m_pString, obj.m_pString);
}
~CMyString()
{
if (m_pString != NULL)
{
delete[] m_pString;
m_pString = NULL;
}
}
void SetString(char *pString)
{
int nLen = strlen(pString);
if (m_pString != NULL)
{
delete[] m_pString;
m_pString = NULL;
}
m_pString = new char[nLen + sizeof(char)];
strcpy(m_pString, pString);
}
char *m_pString;
};
int _tmain(int argc, _TCHAR* argv[])
{
CMyString MyString;
MyString.SetString("hello");
Show(MyString);
return 0;
}

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
对应反汇编Debug:
01284050 > 55 push ebp
01284051 8BEC mov ebp,esp
01284053 6A FF push -0x1
01284055 68 584F2801 push TestCode.01284F58
0128405A 64:A1 00000000 mov eax,dword ptr fs:[0]
01284060 50 push eax
01284061 81EC E8000000 sub esp,0xE8
01284067 53 push ebx
01284068 56 push esi
01284069 57 push edi
0128406A 8DBD 0CFFFFFF lea edi,dword ptr ss:[ebp-0xF4]
01284070 B9 3A000000 mov ecx,0x3A
01284075 B8 CCCCCCCC mov eax,0xCCCCCCCC
0128407A F3:AB rep stos dword ptr es:[edi]
0128407C A1 04902801 mov eax,dword ptr ds:[__security_cookie]
01284081 33C5 xor eax,ebp
01284083 50 push eax
01284084 8D45 F4 lea eax,dword ptr ss:[ebp-0xC]
01284087 64:A3 00000000 mov dword ptr fs:[0],eax
0128408D 8D4D EC lea ecx,dword ptr ss:[ebp-0x14] //取对象首地址
01284090 E8 8CD1FFFF call TestCode.01281221 //调用构造函数
01284095 C745 FC 0000000>mov dword ptr ss:[ebp-0x4],0x0
0128409C 68 7C682801 push TestCode.0128687C ; ASCII "hello" //参数入栈
012840A1 8D4D EC lea ecx,dword ptr ss:[ebp-0x14] //取对象首地址
012840A4 E8 6ED1FFFF call TestCode.01281217 //调用成员函数
012840A9 51 push ecx //这里的push ecx等价于sub esp,4,但是原指令机器码更短,效率更高。
012840AA 8BCC mov ecx,esp //取参数对象地址,保存到ecx
012840AC 89A5 14FFFFFF mov dword ptr ss:[ebp-0xEC],esp
012840B2 8D45 EC lea eax,dword ptr ss:[ebp-0x14] //取对象首地址给eax
012840B5 50 push eax //参数入栈
012840B6 E8 6BD1FFFF call TestCode.01281226 //调用拷贝构造函数
012840BB 8985 0CFFFFFF mov dword ptr ss:[ebp-0xF4],eax //返回值给ebp-0xf4
012840C1 E8 CAD0FFFF call TestCode.01281190 //调用Show函数
012840C6 83C4 04 add esp,0x4
012840C9 C785 20FFFFFF 0>mov dword ptr ss:[ebp-0xE0],0x0
012840D3 C745 FC FFFFFFF>mov dword ptr ss:[ebp-0x4],-0x1
012840DA 8D4D EC lea ecx,dword ptr ss:[ebp-0x14]
012840DD E8 95D0FFFF call TestCode.01281177 //调用析构函数
012840E2 8B85 20FFFFFF mov eax,dword ptr ss:[ebp-0xE0]
012840E8 52 push edx
012840E9 8BCD mov ecx,ebp
012840EB 50 push eax
012840EC 8D15 18412801 lea edx,dword ptr ds:[0x1284118]
012840F2 E8 9FCFFFFF call TestCode.01281096
012840F7 58 pop eax
012840F8 5A pop edx
012840F9 8B4D F4 mov ecx,dword ptr ss:[ebp-0xC]
012840FC 64:890D 0000000>mov dword ptr fs:[0],ecx
01284103 59 pop ecx
01284104 5F pop edi
01284105 5E pop esi
01284106 5B pop ebx
01284107 81C4 F4000000 add esp,0xF4
0128410D 3BEC cmp ebp,esp
0128410F E8 4AD0FFFF call TestCode.0128115E
01284114 8BE5 mov esp,ebp
01284116 5D pop ebp
01284117 C3 retn

由此可以看出,在执行Show函数之前,先执行拷贝构造函数。在拷贝构造中,使用深拷贝。
这时两个数据成员保存地址不同,但内容相同。

由于使用了深拷贝,对对象中的数据成员所指向的堆空间数据也进行了复制,因此当参数被销毁时,释放的堆空间数据是拷贝对象所制作的数据副本,对源对象没有影响。

返回对象

返回对象与参数对象相似,都是局部对象中的一种特殊情况。由于函数返回时需要对返回对象进行拷贝,因此同样会使用到拷贝构造函数。单数两者使用拷贝构造函数的时机不同,当对象为参数时,在进入函数前使用拷贝构造函数,而返回对象则在函数返回时使用拷贝构造函数。

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
C代码:
class CMyString
{
public:
CMyString()
{
m_pString = NULL;
}
CMyString(CMyString &obj)
{
//如果在拷贝构造函数中直接复制指针值,那么对象内的两个成员指针会指向同一个资源,浅拷贝
//this->m_pString = obj.m_pString;
//为实参对象中的指针指向的堆空间制作一份副本,深拷贝
int nLen = strlen(obj.m_pString);
this->m_pString = new char[nLen + sizeof(char)];
strcpy(this->m_pString, obj.m_pString);
}
~CMyString()
{
if (m_pString != NULL)
{
delete[] m_pString;
m_pString = NULL;
}
}
void SetString(char *pString)
{
int nLen = strlen(pString);
if (m_pString != NULL)
{
delete[] m_pString;
m_pString = NULL;
}
m_pString = new char[nLen + sizeof(char)];
strcpy(m_pString, pString);
}
char *m_pString;
};
void Show(CMyString obj)
{
printf(obj.m_pString);
}
CMyString GetString()
{
CMyString MyString;
MyString.SetString("Test");
return MyString;
}
int _tmain(int argc, _TCHAR* argv[])
{
CMyString MyString = GetString();
return 0;
}

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
对应反汇编Debug:
Main函数
00A21680 > 55 push ebp
00A21681 8BEC mov ebp,esp
00A21683 81EC D8000000 sub esp,0xD8
00A21689 53 push ebx
00A2168A 56 push esi
00A2168B 57 push edi
00A2168C 8DBD 28FFFFFF lea edi,dword ptr ss:[ebp-0xD8]
00A21692 B9 36000000 mov ecx,0x36
00A21697 B8 CCCCCCCC mov eax,0xCCCCCCCC
00A2169C F3:AB rep stos dword ptr es:[edi]
00A2169E 8D45 F8 lea eax,dword ptr ss:[ebp-0x8] //取对象首地址
00A216A1 50 push eax //参数入栈
00A216A2 E8 84FBFFFF call TestCode.00A2122B //调用函数
00A216A7 83C4 04 add esp,0x4
00A216AA C785 2CFFFFFF 0>mov dword ptr ss:[ebp-0xD4],0x0
00A216B4 8D4D F8 lea ecx,dword ptr ss:[ebp-0x8]
00A216B7 E8 BBFAFFFF call TestCode.00A21177
00A216BC 8B85 2CFFFFFF mov eax,dword ptr ss:[ebp-0xD4]
00A216C2 52 push edx
00A216C3 8BCD mov ecx,ebp
00A216C5 50 push eax
00A216C6 8D15 E816A200 lea edx,dword ptr ds:[0xA216E8]
00A216CC E8 C5F9FFFF call TestCode.00A21096
00A216D1 58 pop eax
00A216D2 5A pop edx
00A216D3 5F pop edi
00A216D4 5E pop esi
00A216D5 5B pop ebx
00A216D6 81C4 D8000000 add esp,0xD8
00A216DC 3BEC cmp ebp,esp
00A216DE E8 7BFAFFFF call TestCode.00A2115E
00A216E3 8BE5 mov esp,ebp
00A216E5 5D pop ebp
00A216E6 C3 retn
GetString函数
00A215A0 > 55 push ebp
00A215A1 8BEC mov ebp,esp
00A215A3 6A FF push -0x1
00A215A5 68 774FA200 push TestCode.00A24F77
00A215AA 64:A1 00000000 mov eax,dword ptr fs:[0]
00A215B0 50 push eax
00A215B1 81EC D8000000 sub esp,0xD8
00A215B7 53 push ebx
00A215B8 56 push esi
00A215B9 57 push edi
00A215BA 8DBD 1CFFFFFF lea edi,dword ptr ss:[ebp-0xE4]
00A215C0 B9 36000000 mov ecx,0x36
00A215C5 B8 CCCCCCCC mov eax,0xCCCCCCCC
00A215CA F3:AB rep stos dword ptr es:[edi]
00A215CC A1 0490A200 mov eax,dword ptr ds:[__security_cookie]
00A215D1 33C5 xor eax,ebp
00A215D3 50 push eax
00A215D4 8D45 F4 lea eax,dword ptr ss:[ebp-0xC]
00A215D7 64:A3 00000000 mov dword ptr fs:[0],eax
00A215DD C785 20FFFFFF 0>mov dword ptr ss:[ebp-0xE0],0x0
00A215E7 8D4D EC lea ecx,dword ptr ss:[ebp-0x14] //取对象首地址给ecx
00A215EA E8 32FCFFFF call TestCode.00A21221 //调用构造函数
00A215EF C745 FC 0100000>mov dword ptr ss:[ebp-0x4],0x1 //对象计数器
00A215F6 68 7C68A200 push TestCode.00A2687C ; ASCII "Test"
00A215FB 8D4D EC lea ecx,dword ptr ss:[ebp-0x14] //取对象首地址
00A215FE E8 14FCFFFF call TestCode.00A21217//调用SetString函数
00A21603 8D45 EC lea eax,dword ptr ss:[ebp-0x14] //取对象首地址
00A21606 50 push eax //参数入栈
00A21607 8B4D 08 mov ecx,dword ptr ss:[ebp+0x8] //获取参数中的this(当对象作为返回值时,函数会隐式传递一个返回地址的this指针参数)
00A2160A E8 17FCFFFF call TestCode.00A21226 //调用拷贝构造函数
00A2160F 8B8D 20FFFFFF mov ecx,dword ptr ss:[ebp-0xE0]
00A21615 83C9 01 or ecx,0x1
00A21618 898D 20FFFFFF mov dword ptr ss:[ebp-0xE0],ecx
00A2161E C645 FC 00 mov byte ptr ss:[ebp-0x4],0x0
00A21622 8D4D EC lea ecx,dword ptr ss:[ebp-0x14]
00A21625 E8 4DFBFFFF call TestCode.00A21177 //调用析构函数
00A2162A 8B45 08 mov eax,dword ptr ss:[ebp+0x8]
00A2162D 52 push edx
00A2162E 8BCD mov ecx,ebp
00A21630 > 50 push eax
00A21631 8D15 6016A200 lea edx,dword ptr ds:[0xA21660]
00A21637 E8 5AFAFFFF call TestCode.00A21096
00A2163C 58 pop eax
00A2163D 5A pop edx
00A2163E 8B4D F4 mov ecx,dword ptr ss:[ebp-0xC]
00A21641 64:890D 0000000>mov dword ptr fs:[0],ecx
00A21648 59 pop ecx
00A21649 5F pop edi
00A2164A 5E pop esi
00A2164B 5B pop ebx
00A2164C 81C4 E4000000 add esp,0xE4
00A21652 3BEC cmp ebp,esp
00A21654 E8 05FBFFFF call TestCode.00A2115E
00A21659 8BE5 mov esp,ebp
00A2165B 5D pop ebp
00A2165C C3 retn

由此可以看出,GetString将返回对象的地址作为函数参数。在函数返回之前,利用拷贝构造函数将函数中局部对象的数据复制到参数指向的对象中,起到了返回对象的作用。

等价的函数声明

全局对象和静态对象

全局对象与静态对象的构造时机相同。全局对象的构造函数的初始化是在_cinit函数中实现的。

全局对象构造函数识别的两种方法:
1)直接定位初始化函数,先进入mainCRTStartup函数中,找到初始化函数_cinit,在_cinit函数的第二个_initterm处下段。运行程序后,进入_initterm的实现代码内,断点在(**pfbegin)()执行处,单步进入代理构造,即可得到全局对象的构造函数。
2)利用栈回溯

每个对象都有默认的构造函数吗?

有两种情况编译器会提供默认的构造函数
1)本类,本类中定义的成员对象或者父类中有虚函数存在。由于需要初始化虚表,且这个工作原理应在构造函数中隐式完成,因此没有定义构造函数的情况下,编译器会添加默认的构造函数用于隐式完成徐彪的初始化工作。
2)父类或本类中定义的成员对象带有构造函数。在对象被定义时,由于对象本身为派生类,因此构造顺序是先构造父类在构造自身。当父类中带有构造函数时,将会调用父类构造函数,而这个调用过程需要在构造函数内完成,因此编译器添加了默认的构造函数来完成这个过程。成员对象带有构造函数的情况与此相同。

析构函数的出现时机

析构函数的触发时机也需要是情况而定,分为以下几种情况
1)局部对象:作用域结束前调用析构函数
2)堆对象:释放堆空间前调用析构函数
3)参数对象:退出函数前,调用参数对象的析构函数
4)返回对象:如无对象引用定义,退出函数后,调用返回对象的析构函数,否则与对象引用的作用域一致
5)全局对象:main函数退出后调用析构函数
6)静态对象:main函数退出后调用析构函数

局部对象

要考察局部对象的析构函数的出现时机,应重点考察其作用域的结束处。与构造函数相比较而言,析构函数的出现时机相对固定。对于局部对象,当对象所在作用域结束后,将销毁该作用域的所有变量的栈空间,此时便是析构函数的出现时机。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
C代码:
class CNumber
{
public:
CNumber()
{
m_nNum = 1;
}
~CNumber()
{
printf("~CNumber");
}
int m_nNum;
};
int _tmain(int argc, _TCHAR* argv[])
{
CNumber obj;
return 0;
}

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
对应反汇编Debug:
Main函数
000515F0 > 55 push ebp
000515F1 8BEC mov ebp,esp
000515F3 81EC D8000000 sub esp,0xD8
000515F9 53 push ebx
000515FA 56 push esi
000515FB 57 push edi
000515FC 8DBD 28FFFFFF lea edi,dword ptr ss:[ebp-0xD8]
00051602 B9 36000000 mov ecx,0x36
00051607 B8 CCCCCCCC mov eax,0xCCCCCCCC
0005160C F3:AB rep stos dword ptr es:[edi]
0005160E 8D4D F8 lea ecx,dword ptr ss:[ebp-0x8] //获取对象首地址
00051611 E8 1AFCFFFF call TestCode.00051230 //调用构造函数
00051616 C785 2CFFFFFF 0>mov dword ptr ss:[ebp-0xD4],0x0
00051620 8D4D F8 lea ecx,dword ptr ss:[ebp-0x8] //获取对象首地址
00051623 E8 0DFCFFFF call TestCode.00051235 //调用析构函数
00051628 8B85 2CFFFFFF mov eax,dword ptr ss:[ebp-0xD4]
0005162E 52 push edx
0005162F 8BCD mov ecx,ebp
00051631 50 push eax
00051632 8D15 54160500 lea edx,dword ptr ds:[0x51654]
00051638 E8 59FAFFFF call TestCode.00051096
0005163D 58 pop eax
0005163E 5A pop edx
0005163F 5F pop edi
00051640 5E pop esi
00051641 5B pop ebx
00051642 81C4 D8000000 add esp,0xD8
00051648 3BEC cmp ebp,esp
0005164A E8 0FFBFFFF call TestCode.0005115E
0005164F 8BE5 mov esp,ebp
00051651 5D pop ebp
00051652 C3 retn
析构函数
000515A0 > 55 push ebp
000515A1 8BEC mov ebp,esp
000515A3 81EC CC000000 sub esp,0xCC
000515A9 53 push ebx
000515AA 56 push esi
000515AB 57 push edi
000515AC 51 push ecx
000515AD 8DBD 34FFFFFF lea edi,dword ptr ss:[ebp-0xCC]
000515B3 B9 33000000 mov ecx,0x33
000515B8 B8 CCCCCCCC mov eax,0xCCCCCCCC
000515BD F3:AB rep stos dword ptr es:[edi]
000515BF 59 pop ecx //还原this指针到ecx
000515C0 894D F8 mov dword ptr ss:[ebp-0x8],ecx //保存this指针到ebp-0x8
000515C3 8BF4 mov esi,esp
000515C5 68 D8680500 push TestCode.000568D8 ; ASCII "~CNumber"
000515CA FF15 18A10500 call dword ptr ds:[<&MSVCR120D.printf>] ; MSVCR120.printf
000515D0 83C4 04 add esp,0x4
000515D3 3BF4 cmp esi,esp
000515D5 E8 84FBFFFF call TestCode.0005115E
000515DA 5F pop edi
000515DB 5E pop esi
000515DC 5B pop ebx
000515DD 81C4 CC000000 add esp,0xCC
000515E3 3BEC cmp ebp,esp
000515E5 E8 74FBFFFF call TestCode.0005115E
000515EA 8BE5 mov esp,ebp
000515EC 5D pop ebp
000515ED C3 retn

由此可以看出在对象的作用域结束处调用了析构函数。析构函数同样属于成员函数,因此在调用过程中也需要传递this指针。
析构函数与构造函数略有不同,析构函数不支持函数重载,且只有一个参数,就是this指针,且编译器隐藏了这个参数的传递过程。

堆对象

堆对象比较特殊。使用new对象申请堆对象空间后,可以看在那里调用了delete函数释放。Delete的使用便是找到堆对象调用析构函数的关键。

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
C代码:
class CNumber
{
public:
CNumber()
{
m_nNum = 1;
}
~CNumber()
{
printf("~CNumber");
}
int m_nNum;
};
int _tmain(int argc, _TCHAR* argv[])
{
CNumber *pObj = NULL;
pObj = new CNumber;
pObj->m_nNum = 2;
printf("%d\n", pObj->m_nNum);
if (pObj != NULL)
{
delete pObj;
pObj = NULL;
}
return 0;
}

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
对应反汇编Debug:
Main函数
000243B0 > 55 push ebp
000243B1 8BEC mov ebp,esp
000243B3 6A FF push -0x1
000243B5 68 AE4F0200 push TestCode.00024FAE
000243BA 64:A1 00000000 mov eax,dword ptr fs:[0]
000243C0 50 push eax
000243C1 81EC 00010000 sub esp,0x100
000243C7 53 push ebx
000243C8 56 push esi
000243C9 57 push edi
000243CA 8DBD F4FEFFFF lea edi,dword ptr ss:[ebp-0x10C]
000243D0 B9 40000000 mov ecx,0x40
000243D5 B8 CCCCCCCC mov eax,0xCCCCCCCC
000243DA F3:AB rep stos dword ptr es:[edi]
000243DC A1 04900200 mov eax,dword ptr ds:[__security_cookie]
000243E1 33C5 xor eax,ebp
000243E3 50 push eax
000243E4 8D45 F4 lea eax,dword ptr ss:[ebp-0xC]
000243E7 64:A3 00000000 mov dword ptr fs:[0],eax
000243ED C745 EC 0000000>mov dword ptr ss:[ebp-0x14],0x0 //指针
000243F4 6A 04 push 0x4
000243F6 E8 B8CDFFFF call TestCode.000211B3 //调用new函数
000243FB 83C4 04 add esp,0x4
000243FE 8985 08FFFFFF mov dword ptr ss:[ebp-0xF8],eax
00024404 C745 FC 0000000>mov dword ptr ss:[ebp-0x4],0x0
0002440B 83BD 08FFFFFF 0>cmp dword ptr ss:[ebp-0xF8],0x0
00024412 74 13 je short TestCode.00024427 //判断是否分配空间成功
00024414 8B8D 08FFFFFF mov ecx,dword ptr ss:[ebp-0xF8]
0002441A E8 11CEFFFF call TestCode.00021230 //调用构造函数
0002441F 8985 F4FEFFFF mov dword ptr ss:[ebp-0x10C],eax
00024425 EB 0A jmp short TestCode.00024431
00024427 C785 F4FEFFFF 0>mov dword ptr ss:[ebp-0x10C],0x0
00024431 8B85 F4FEFFFF mov eax,dword ptr ss:[ebp-0x10C]
00024437 8985 FCFEFFFF mov dword ptr ss:[ebp-0x104],eax
0002443D C745 FC FFFFFFF>mov dword ptr ss:[ebp-0x4],-0x1
00024444 8B8D FCFEFFFF mov ecx,dword ptr ss:[ebp-0x104]
0002444A 894D EC mov dword ptr ss:[ebp-0x14],ecx
0002444D 8B45 EC mov eax,dword ptr ss:[ebp-0x14]
00024450 C700 02000000 mov dword ptr ds:[eax],0x2
00024456 8BF4 mov esi,esp
00024458 8B45 EC mov eax,dword ptr ss:[ebp-0x14]
0002445B 8B08 mov ecx,dword ptr ds:[eax]
0002445D 51 push ecx
0002445E 68 6C680200 push TestCode.0002686C ; ASCII "%d\n"
00024463 FF15 18A10200 call dword ptr ds:[<&MSVCR120D.printf>] ; MSVCR120.printf
00024469 83C4 08 add esp,0x8
0002446C 3BF4 cmp esi,esp
0002446E E8 EBCCFFFF call TestCode.0002115E
00024473 837D EC 00 cmp dword ptr ss:[ebp-0x14],0x0 //判断this指针是否为空
00024477 74 44 je short TestCode.000244BD
00024479 8B45 EC mov eax,dword ptr ss:[ebp-0x14]
0002447C 8985 20FFFFFF mov dword ptr ss:[ebp-0xE0],eax
00024482 8B8D 20FFFFFF mov ecx,dword ptr ss:[ebp-0xE0]
00024488 898D 14FFFFFF mov dword ptr ss:[ebp-0xEC],ecx //把this指针给ebp-0xec
0002448E 83BD 14FFFFFF 0>cmp dword ptr ss:[ebp-0xEC],0x0
00024495 74 15 je short TestCode.000244AC //如果为空则跳过析构函数
00024497 6A 01 push 0x1 //标记
00024499 8B8D 14FFFFFF mov ecx,dword ptr ss:[ebp-0xEC] //this指针
0002449F E8 96CDFFFF call TestCode.0002123A //调用析构函数
000244A4 8985 F4FEFFFF mov dword ptr ss:[ebp-0x10C],eax
000244AA EB 0A jmp short TestCode.000244B6
000244AC C785 F4FEFFFF 0>mov dword ptr ss:[ebp-0x10C],0x0
000244B6 C745 EC 0000000>mov dword ptr ss:[ebp-0x14],0x0
000244BD 33C0 xor eax,eax
000244BF 8B4D F4 mov ecx,dword ptr ss:[ebp-0xC]
000244C2 64:890D 0000000>mov dword ptr fs:[0],ecx
000244C9 59 pop ecx
000244CA 5F pop edi
000244CB 5E pop esi
000244CC 5B pop ebx
000244CD 81C4 0C010000 add esp,0x10C
000244D3 3BEC cmp ebp,esp
000244D5 E8 84CCFFFF call TestCode.0002115E
000244DA 8BE5 mov esp,ebp
000244DC 5D pop ebp
000244DD C3 retn
析构代理函数
00024140 > 55 push ebp
00024141 8BEC mov ebp,esp
00024143 81EC CC000000 sub esp,0xCC
00024149 53 push ebx
0002414A 56 push esi
0002414B 57 push edi
0002414C 51 push ecx
0002414D 8DBD 34FFFFFF lea edi,dword ptr ss:[ebp-0xCC]
00024153 B9 33000000 mov ecx,0x33
00024158 B8 CCCCCCCC mov eax,0xCCCCCCCC
0002415D F3:AB rep stos dword ptr es:[edi]
0002415F 59 pop ecx //还原this指针
00024160 894D F8 mov dword ptr ss:[ebp-0x8],ecx
00024163 8B4D F8 mov ecx,dword ptr ss:[ebp-0x8] //this指针给ecx
00024166 E8 CAD0FFFF call TestCode.00021235//调用析构函数
0002416B 8B45 08 mov eax,dword ptr ss:[ebp+0x8]
0002416E 83E0 01 and eax,0x1 //检查析构函数标记
00024171 74 0C je short TestCode.0002417F
00024173 8B45 F8 mov eax,dword ptr ss:[ebp-0x8]
00024176 50 push eax //压入堆空间首地址
00024177 E8 1FCFFFFF call TestCode.0002109B //调用delete函数
0002417C 83C4 04 add esp,0x4
0002417F 8B45 F8 mov eax,dword ptr ss:[ebp-0x8]
00024182 5F pop edi
00024183 5E pop esi
00024184 5B pop ebx
00024185 81C4 CC000000 add esp,0xCC
0002418B 3BEC cmp ebp,esp
0002418D E8 CCCFFFFF call TestCode.0002115E
00024192 8BE5 mov esp,ebp
00024194 5D pop ebp
00024195 C2 0400 retn 0x4

由此可以看出,析构函数比较特殊,在释放过程中,需要使用析构代理函数间接调用析构函数。原因是在某些情况下,需要释放的对象不止一个,如果直接调用析构函数,则无法完成多对象的析构。

这里体现的是有两个对象的一个例子。

1
2
3
4
5
6
7
8
9
10
11
12
C代码:
int _tmain(int argc, _TCHAR* argv[])
{
CNumber *pObj = NULL;
pObj = new CNumber[2];
if (pObj != NULL)
{
delete pObj;
pObj = NULL;
}
return 0;
}

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
65
66
67
68
69
70
71
72
73
74
75
76
77
对应反汇编Debug:
Main函数
00924410 > 55 push ebp
00924411 8BEC mov ebp,esp
00924413 6A FF push -0x1
00924415 68 AE4F9200 push TestCode.00924FAE
0092441A 64:A1 00000000 mov eax,dword ptr fs:[0]
00924420 50 push eax
00924421 81EC 00010000 sub esp,0x100
00924427 53 push ebx
00924428 56 push esi
00924429 57 push edi
0092442A 8DBD F4FEFFFF lea edi,dword ptr ss:[ebp-0x10C]
00924430 B9 40000000 mov ecx,0x40
00924435 B8 CCCCCCCC mov eax,0xCCCCCCCC
0092443A F3:AB rep stos dword ptr es:[edi]
0092443C A1 04909200 mov eax,dword ptr ds:[__security_cookie]
00924441 33C5 xor eax,ebp
00924443 50 push eax
00924444 8D45 F4 lea eax,dword ptr ss:[ebp-0xC]
00924447 64:A3 00000000 mov dword ptr fs:[0],eax
0092444D C745 EC 0000000>mov dword ptr ss:[ebp-0x14],0x0
00924454 6A 0C push 0xC //这里申请12字节大小,每个对象占四字节,多出的四字节是堆空间首地址处保存的对象对象总个数
00924456 E8 58CDFFFF call TestCode.009211B3
0092445B 83C4 04 add esp,0x4
0092445E 8985 08FFFFFF mov dword ptr ss:[ebp-0xF8],eax //堆空间的首地址
00924464 C745 FC 0000000>mov dword ptr ss:[ebp-0x4],0x0
0092446B 83BD 08FFFFFF 0>cmp dword ptr ss:[ebp-0xF8],0x0 //检查是否成功
00924472 74 3A je short TestCode.009244AE
00924474 8B85 08FFFFFF mov eax,dword ptr ss:[ebp-0xF8]
0092447A C700 02000000 mov dword ptr ds:[eax],0x2
00924480 68 35129200 push TestCode.00921235 //压入析构函数地址
00924485 68 30129200 push TestCode.00921230 //压入构造函数地址
0092448A 6A 02 push 0x2 //压入对象个数
0092448C 6A 04 push 0x4 //压入对象大小
0092448E 8B8D 08FFFFFF mov ecx,dword ptr ss:[ebp-0xF8] //对象首地址
00924494 83C1 04 add ecx,0x4 //首地址+4,跳过保存个数的空间
00924497 51 push ecx //压入第一个对象地址
00924498 E8 A7CDFFFF call TestCode.00921244 //调用析构代理函数
0092449D 8B95 08FFFFFF mov edx,dword ptr ss:[ebp-0xF8]//对象首地址
009244A3 83C2 04 add edx,0x4//首地址+4,跳过保存个数的空间
009244A6 8995 F4FEFFFF mov dword ptr ss:[ebp-0x10C],edx
009244AC EB 0A jmp short TestCode.009244B8
009244AE C785 F4FEFFFF 0>mov dword ptr ss:[ebp-0x10C],0x0
009244B8 8B85 F4FEFFFF mov eax,dword ptr ss:[ebp-0x10C] //对象首地址给eax
009244BE 8985 FCFEFFFF mov dword ptr ss:[ebp-0x104],eax
009244C4 C745 FC FFFFFFF>mov dword ptr ss:[ebp-0x4],-0x1
009244CB 8B8D FCFEFFFF mov ecx,dword ptr ss:[ebp-0x104]
009244D1 894D EC mov dword ptr ss:[ebp-0x14],ecx
009244D4 837D EC 00 cmp dword ptr ss:[ebp-0x14],0x0 //检测对象首地址是否为空
009244D8 74 44 je short TestCode.0092451E
009244DA 8B45 EC mov eax,dword ptr ss:[ebp-0x14]
009244DD 8985 20FFFFFF mov dword ptr ss:[ebp-0xE0],eax
009244E3 8B8D 20FFFFFF mov ecx,dword ptr ss:[ebp-0xE0]
009244E9 898D 14FFFFFF mov dword ptr ss:[ebp-0xEC],ecx
009244EF 83BD 14FFFFFF 0>cmp dword ptr ss:[ebp-0xEC],0x0
009244F6 74 15 je short TestCode.0092450D
009244F8 6A 01 push 0x1 //压入对象释放标志,1为单个对象,3为释放对象数组,0表示只执行析构函数
009244FA 8B8D 14FFFFFF mov ecx,dword ptr ss:[ebp-0xEC]
00924500 E8 35CDFFFF call TestCode.0092123A //调用析构代理函数
00924505 8985 F4FEFFFF mov dword ptr ss:[ebp-0x10C],eax
0092450B EB 0A jmp short TestCode.00924517
0092450D C785 F4FEFFFF 0>mov dword ptr ss:[ebp-0x10C],0x0
00924517 C745 EC 0000000>mov dword ptr ss:[ebp-0x14],0x0
0092451E 33C0 xor eax,eax
00924520 8B4D F4 mov ecx,dword ptr ss:[ebp-0xC]
00924523 64:890D 0000000>mov dword ptr fs:[0],ecx
0092452A 59 pop ecx
0092452B 5F pop edi
0092452C 5E pop esi
0092452D 5B pop ebx
0092452E 81C4 0C010000 add esp,0x10C
00924534 3BEC cmp ebp,esp
00924536 E8 23CCFFFF call TestCode.0092115E
0092453B 8BE5 mov esp,ebp
0092453D 5D pop ebp
0092453E C3 retn

由此可以看出,在申请对象数组时,由于对象都在同一个堆空间中,编译器使用了堆空间的前4字节数据来保存对象的总个数。注意:这样释放空间时需要delete[]。

参数和返回对象

参数对象与返回对象会在不同的时机触发拷贝构造函数,他们的析构时机与所在作用域相关。只要函数的参数为对象类型,就会在函数调用结束后调用他的析构函数,然后释放掉参数对象所在内存空间。当返回值为对象是会有不同,返回对象时有赋值。

全局对象与静态对象

全局对象与静态对象相同,其构造函数在_cinit的第二个_initterm调用中被构造。他们的析构函数的调用时机是在main函数执行完毕之后。对应来说析构函数就应在main函数结束后。Main函数结束后,会调用exit终止程序,全局对象的析构函数也在其中。由exit函数内的doexit实现。