0%

C++反汇编与逆向分析 - 类

在C++中,结构体和类都具有构造函数、析构函数和成员函数,两者只有一个区别,结构体访问控制默认为public,而类的默认访问控制为private。而对C++的结构来说,访问控制是在编译期间进行的,在编译成功后,是不会在访问控制层面做任何检查和限制,所以本质来说,C++的结构与类并无区别;两者原理相同,只是类型名称不同;

运行环境:

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

对象的内存布局

由于类和结构体都是抽象概念,当两个类的特征相同时,它们之间应该是相等的关系。而对象是实际存在的,即使它们之间包含的数据相同,也不能视为同一个类;

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

class CNumber
{
public:
CNumber();

int GetNumberOne()
{
return m_nOne;
}
int GetNumberTwo() ;类成员函数
{
return m_nTwo;
}
private:
int m_nOne; ;类成员数据
int m_nTwo;
};

CNumber::CNumber()
{
m_nOne = 1;
m_nTwo = 2;
}


int _tmain(int argc, _TCHAR* argv[])
{
CNumber Number;
printf("%d\n", sizeof(Number)); //8字节
getchar();
return 0;
}

对象的大小只包含数据成员,类成员函数属于执行代码,不属于对象的数据;

类对象不可以定义包含自身的对象,因为类在申请内存时需要计算自身的实际大小,用于实例化;

类的不是简单的sizeof(数据成员1)+sizeof(数据成员2),即使类中没有继承和虚函数的定义;

有三种特殊情况使用公式计算的对象长度与实际长度不相符:

  • 空类

    • 空类中没有任何数据成员,根据公式得出对象长度为0字节,类型长度为0,则此类的对象真的不占用空间嘛?而实际情况是,空类长度为1。如果对象完全不占用内存,那么空类永远无法实例化对象的地址,this指针失效,因此不能被实例化;

  • 内存对齐

    • 在VC6中,类和结构体中的数据成员是根据它们在类或结构中出现的顺序依次来申请内存空间,尤其内存对齐的原因,它们不一定会像数组那样连续排列,由于数组类型的不同,因此占用的内存空间大小也不同;

编译器默认对齐为8字节对齐,所以最终的结果一定要除8为0

  • 结构体嵌套
    • 当结构体中出现结构体类型的数据成员时,不会讲嵌套的结构体类型的整体长度参与到对齐值计算中,而是以嵌套定义的结构体所使用的对齐值对齐;
  • 静态数据成员
    • 当类中的数据被修饰为静态时,对象的长度计算又会发生变化,虽然静态数据成员在类中被定义,但它与静态局部变量类似,存放的位置和全局变量一直,只是增加了作用域检查,在作用域外不可见;

当对象为全局对象时,其内存布局与局部对象相同,只是所在内存地址,以及构造函数和析构函数触发时机不同。全局对象所在的内存地址空间为全局数据区,而局部对象的内存地址在栈中

this指针

类中并没有this指针的定义,但是在成员函数却可以使用,由于this指针在使用过程中被编译器隐藏了,我们可以理解为this指针就是指针类型,在32位环境下占4字节,保存的数据位地址信息;this指针保存了所属对象的首地址

debug版本下:

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
#include "stdafx.h"
#include <stdlib.h>
class CTest
{
public:
void SetNumber(int nNumber);

public:
int m_nInt;
};

void CTest::SetNumber(int nNumber)
{
m_nInt = nNumber;
}


int _tmain(int argc, _TCHAR* argv[])
{
CTest Test;
Test.SetNumber(10);
printf("%d\n", Test.m_nInt);

getchar();
return 0;
}


CTest Test;
Test.SetNumber(10);
010B143E push 0Ah ;压入参数10
010B1440 lea ecx,[Test] ;取出Test首地址传入ECX中
010B1443 call CTest::SetNumber (010B10F0h)
printf("%d\n", Test.m_nInt);
00221448 mov esi,esp
0022144A mov eax,dword ptr [Test] ;取出Test首地址放入EAX中
0022144D push eax ;将eax中保存的数据成员m_nInt香成员函数传参
0022144E push 225858h
00221453 call dword ptr ds:[229110h] ;printf
00221459 add esp,8

010B13D0 push ebp
010B13D1 mov ebp,esp
010B13D3 sub esp,0CCh
010B13D9 push ebx
010B13DA push esi
010B13DB push edi
010B13DC push ecx ;注意ECX寄存器,这里没有任何赋值,直接使用,ECX中保存Test首地址
010B13DD lea edi,[ebp-0CCh]
010B13E3 mov ecx,33h
010B13E8 mov eax,0CCCCCCCCh
010B13ED rep stos dword ptr es:[edi]
010B13EF pop ecx ;还原ECX寄存器
010B13F0 mov dword ptr [this],ecx ;将ecx的值存入ebp-4的位置,因为我使用了高版本编译器,这里直接解析了this,该地址处保存了调用对象的首地址,即this指针
m_nInt = nNumber;
010B13F3 mov eax,dword ptr [this] ;取出首对象地址存入EAX
010B13F6 mov ecx,dword ptr [nNumber] ;取出参数中的数据保存到ECX中
010B13F9 mov dword ptr [eax],ecx ;给成员m_nInt赋值,由于eax是对象的首地址,成员m_nInt的偏移量为0,这么书写更容易辨别: mov dword ptr [eax + 0],ecx
}
010B13FB pop edi
010B13FC pop esi
010B13FD pop ebx
010B13FE mov esp,ebp
010B1400 pop ebp
010B1401 ret 4



00401020 push ebp
00401021 mov ebp,esp
00401023 sub esp,44h
00401026 push ebx
00401027 push esi
00401028 push edi
00401029 push ecx ;注意ECX寄存器,这里没有赋值,直接使用,ECX保存了Test首地址
0040102A lea edi,[ebp-44h]
0040102D mov ecx,11h
00401032 mov eax,0CCCCCCCCh
00401037 rep stos dword ptr [edi]
00401039 pop ecx
0040103A mov dword ptr [ebp-4],ecx ;ecx中存放调用地址受对象,即this指针
23: m_nInt = nNumber;
0040103D mov eax,dword ptr [ebp-4] ;取出对象首地址放入eax中
00401040 mov ecx,dword ptr [ebp+8] ;取出参数中的数据放入ecx中
00401043 mov dword ptr [eax],ecx ;给成员赋值,由于eax是对象的首地址,成员m_nInt偏移量为0
24: }
00401045 pop edi
00401046 pop esi
00401047 pop ebx
00401048 mov esp,ebp
0040104A pop ebp
0040104B ret 4

在调用成员函数时,编译器进行了”微处理”,利用寄存器ecx保存了对象的首地址,并以寄存器传参的方式传递到成员函数中,这便是this指针;

在成员函数中访问数据成员也是通过this指针间接访问,这便是为什么字成员函数中可以直接使用数据成员的原因;

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

class CTest
{
public:
void Show()
{
printf("%d\n",GetNumber());
}
int GetNumber()
{
return m_nInt;
}
protected:
private:
int m_nInt;
};

int main(int argc, char* argv[])
{
CTest Test;
Test.Show();
printf("Hello World!\n");
return 0;
}



24: CTest Test;
25: Test.Show();
00401048 lea ecx,[ebp-4]
0040104B call @ILT+10(CTest::Show) (0040100f)




00401080 push ebp
00401081 mov ebp,esp
00401083 sub esp,44h
00401086 push ebx
00401087 push esi
00401088 push edi
00401089 push ecx
0040108A lea edi,[ebp-44h]
0040108D mov ecx,11h
00401092 mov eax,0CCCCCCCCh
00401097 rep stos dword ptr [edi]
00401099 pop ecx
0040109A mov dword ptr [ebp-4],ecx ;ebp-4存放对象首地址,也就是this指针
11: printf("%d\n",GetNumber());
0040109D mov ecx,dword ptr [ebp-4]
004010A0 call @ILT+5(CTest::GetNumber) (0040100a)
004010A5 push eax
004010A6 push offset string "%d\n" (0042202c)
004010AB call printf (00401120)
004010B0 add esp,8
12: }

代码定式:

  • 默认使用thiscall调用约定(此约定可以更改)
  • 如果是thiscall,参数、所声明的函数参数、参数的传递方式、顺序、传输媒介、平栈,使用ecx传递this指针,thiscall和stdcall的唯一区别:多一个this指针
  • 如果一个函数,传递了this指针,但是没有对类成员的间接访问,从面向对象的理念中应该不属于这个类的成员函数,应该还原成全局函数

VC++中,识别this指针的关键点是在函数的调用过程中使用了ECX寄存器作为一个参数,并在ECX中保存了数据为对象的首地址,但并非所有的this指针传递都是如此。

Release版本识别类

  • 如果成员函数被内联了,可以酌情考虑还原成全局函数
  • 任何语言、任何平台只要支持stdcall,this指针就是第一个入栈的指针

Release版本:

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
.text:00401010 var_4           = dword ptr -4
.text:00401010
.text:00401010 push ebp
.text:00401011 mov ebp, esp
.text:00401013 push ecx
.text:00401014 push 0Ah
.text:00401016 lea ecx, [ebp+var_4]
.text:00401019 call unknown_libname_1 ; Microsoft VisualC 2-14/net runtime
.text:0040101E push [ebp+var_4]
.text:00401021 push offset Format ; "%d\n"
.text:00401026 call ds:printf
.text:0040102C add esp, 8
.text:0040102F call ds:getchar
.text:00401035 xor eax, eax
.text:00401037 mov esp, ebp
.text:00401039 pop ebp
.text:0040103A retn
.text:0040103A _wmain endp
.text:0040103A

.text:00401000 unknown_libname_1 proc near ; CODE XREF: _wmain+9↓p
.text:00401000
.text:00401000 arg_0 = dword ptr 8
.text:00401000
.text:00401000 push ebp
.text:00401001 mov ebp, esp
.text:00401003 mov eax, [ebp+arg_0]
.text:00401006 mov [ecx], eax
.text:00401008 pop ebp
.text:00401009 retn 4
.text:00401009 unknown_libname_1 endp
.text:00401009

使用thiscall调用方式的成员函数要点分析:

1
2
3
4
5
6
7
lea	ecx,[mem]			;取对象首地址存入ecx中,观察内存

call FUN_ADDRESS ;调用成语函数

mov XXX,eax ;发现函数内使用ecx数据,说明函数调用前对ecx赋值了,实际在传递参数

mov [reg+i],xxx ;发现了寄存器相对间接寻址,如果能排除数组访问,那就说明reg中保存的是结构体或对象的首地址

静态数据成员

由于静态数据成员和静态变量原理相同(是一个含有作用域的特殊全局变量),因此静态数据成员的初值会被写入编译连接后的执行文件中。当程序被加载时,操作系统将执行文件中数据读到对应的内存单元中,静态数据成员便已存在,而这时类并没有实例对象;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

class CStatic ;类定义
{
public:
static int m_nStatic; ;静态数据成员
int m_nInt; ;普通数据成员
protected:
private:
};

int CStatic::m_nStatic = 0; ;静态数据成员赋值

int main(int argc, char* argv[])
{
CStatic Static; ;第一个局部变量
int nSize = sizeof(Static); ;计算长度
printf("%d\n",nSize);
printf("Hello World!\n");
return 0;
}

20: int nSize = sizeof(Static);
00401028 mov dword ptr [ebp-8],4 ;常量折叠
21: printf("%d\n",nSize);

通过sizeof获得对象Static所占的内存长度为4,静态数据成员m_nStatic没有参与对象Static的长度计算。

1
2
3
4
5
6
7
8
9
10
11
12
22:       printf("%08X\n",&Static.m_nStatic);					;对象直接调用静态数据成员
0040D740 push offset CStatic::m_nStatic (00427c48) ;静态数据成员所在地址0x00427c48
0040D745 push offset string "%08X\n" (00422fb4)
0040D74A call printf (00401080)
0040D74F add esp,8
23: printf("%08X\n",&Static.m_nInt);
0040D752 lea ecx,[ebp-4] ;获取对象首地址存入ecx中,将ecx压入栈
0040D755 push ecx
0040D756 push offset string "%08X\n" (00422fb4)
0040D75B call printf (00401080)
0040D760 add esp,8

普通数据成员的地址是在ebp-4中,是一个栈空间地址;在使用中,静态数据成员是常量,可以通过立即数寻址,普通数据成员只有在类产生之后才能产生,地址值无法确定,只能以寄存器相对间接寻址的方式访问。

debug版本下的在成员函数中使用静态数据成员与普通数据成员:

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
27:       Static.m_nInt = 10;
00401098 mov dword ptr [ebp-4],0Ah ;普通数据成员
28: Static.m_SnInt = 2;
0040109F mov dword ptr [CStatic::m_SnInt (00427c48)],2 ;静态数据成员
29: Static.ShowNumber();
004010A9 lea ecx,[ebp-4] ;传递this指针
004010AC call @ILT+0(CStatic::ShowNumber) (00401005)


19: void CStatic :: ShowNumber()
20: {
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
0040103A mov dword ptr [ebp-4],ecx ;获取this指针
21: printf("m_nInt = %d SnInt = %d\n",m_nInt,m_SnInt);
0040103D mov eax,[CStatic::m_SnInt (00427c48)] ;直接访问静态数据成员
00401042 push eax ;
00401043 mov ecx,dword ptr [ebp-4] ;获取this指针
00401046 mov edx,dword ptr [ecx] ;通过this指针访问数据成员
00401048 push edx
00401049 push offset string "m_nInt = %d SnInt = %d\n" (0042201c)
0040104E call printf (004010f0)
00401053 add esp,0Ch
22: }


Release版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
;为了直观,我们增加一个新的数据成员
.text:00401020 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:00401020 _main proc near ; CODE XREF: start+AF↓p
.text:00401020
.text:00401020 var_8 = dword ptr -8
.text:00401020 var_4 = dword ptr -4
.text:00401020 argc = dword ptr 4
.text:00401020 argv = dword ptr 8
.text:00401020 envp = dword ptr 0Ch
.text:00401020
.text:00401020 sub esp, 8 ;给局部变量拉开空间
.text:00401023 lea ecx, [esp+8+var_8] ;首地址放入ecx中
.text:00401027 mov dword_4098F8, 0Ah ;静态局部变量
.text:00401031 mov [esp+8+var_8], 2 ;第二个普通数据成员
.text:00401039 mov [esp+8+var_4], 0Bh ;第一个数据成员
.text:00401041 call sub_401000 ;成员函数
.text:00401046 push offset aHelloWorld ; "Hello World!\n"
.text:0040104B call sub_401060
.text:00401050 xor eax, eax
.text:00401052 add esp, 0Ch
.text:00401055 retn
.text:00401055 _main endp

对象做函数参数

普通对象

对象作为函数参数时,其传递过程较为复杂,传递方式比较独特。传参过程与数组不同;数组变量的名称代表数组首地址,而对象的变量名却不能代替对象的首地址。传参时先将对象中的所有数据备份,然后将复制好的数组作为形参传递到调用函数中使用

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
20:   int main(int argc, char* argv[])
21: {
00401060 push ebp
00401061 mov ebp,esp
00401063 sub esp,48h
00401066 push ebx
00401067 push esi
00401068 push edi
00401069 lea edi,[ebp-48h]
0040106C mov ecx,12h
00401071 mov eax,0CCCCCCCCh
00401076 rep stos dword ptr [edi]
22: CFunTest FunTest;
23: FunTest.m_nOne = 10;
00401078 mov dword ptr [ebp-8],0Ah ;数据成员m_nOne所在的地址为ebp-8
24: FunTest.m_nTwo = 123;
0040107F mov dword ptr [ebp-4],7Bh ;数据成员m_nOne所在的地址为ebp-4
25: ShowFunTest(FunTest);
00401086 mov eax,dword ptr [ebp-4] ;传入数据成员m_nOne
00401089 push eax
0040108A mov ecx,dword ptr [ebp-8] ;传入数据成员m_nTwo
0040108D push ecx
0040108E call @ILT+5(ShowFunTest) (0040100a)
00401093 add esp,8
26: return 0;
00401096 xor eax,eax
27: }



00401020 push ebp
00401021 mov ebp,esp
00401023 sub esp,40h
00401026 push ebx
00401027 push esi
00401028 push edi
00401029 lea edi,[ebp-40h]
0040102C mov ecx,10h
00401031 mov eax,0CCCCCCCCh
00401036 rep stos dword ptr [edi]
17: printf("m_nOne = %d m_nTwo = %d\n",FunTest.m_nOne,FunTest.m_nTwo);
00401038 mov eax,dword ptr [ebp+0Ch] ;取出数据成员m_nTwo作为printf的第三个参数
0040103B push eax
0040103C mov ecx,dword ptr [ebp+8] ;取出数据成员m_nOne作为printf的第二个参数
0040103F push ecx
00401040 push offset string "m_nOne = %d m_nTwo = %d\n" (0042201c)
00401045 call printf (004010b0)
0040104A add esp,0Ch
18: }
0040104D pop edi
0040104E pop esi
0040104F pop ebx
00401050 add esp,40h
00401053 cmp ebp,esp
00401055 call __chkesp (00401130)
0040105A mov esp,ebp
0040105C pop ebp
0040105D ret

类数据成员的传参顺序:最先定义的数据成员最后压栈,最后定义的数据成员最先压栈

当拥有多个数据成员或类的体积过大

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
21:   int main(int argc, char* argv[])
22: {
0040D790 push ebp
0040D791 mov ebp,esp
0040D793 sub esp,88h
0040D799 push ebx
0040D79A push esi
0040D79B push edi
0040D79C lea edi,[ebp-88h]
0040D7A2 mov ecx,22h
0040D7A7 mov eax,0CCCCCCCCh
0040D7AC rep stos dword ptr [edi]
23: CFunTest FunTest;
24: FunTest.m_nOne = 10;
0040D7AE mov dword ptr [ebp-48h],0Ah ;数据成员m_nOne所在地址为ebp-48
25: FunTest.m_nTwo = 123;
0040D7B5 mov dword ptr [ebp-44h],7Bh ;数据成员m_nTwo所在地址为ebp-44
26: strcpy(FunTest.szName,"sYstemk1t");
0040D7BC push offset string "m_nOne = %d m_nTwo = %d\n" (0042201c)
0040D7C1 lea eax,[ebp-40h] ;数据成员szName所在地址为ebp-40
0040D7C4 push eax
0040D7C5 call strcpy (00407050)
27: ShowFunTest(FunTest);
0040D7CA add esp,0C0h ;调整栈帧,抬高64字节
0040D7CD mov ecx,12h ;设置循环次数18次
0040D7D2 lea esi,[ebp-48h] ;获取对象的首地址并放入esi中
0040D7D5 mov edi,esp ;设置edi为栈顶
0040D7D7 rep movs dword ptr [edi],dword ptr [esi] ;执行18次4字节内存复制,将esi指向的数组复制到edi中,类似memcpy
0040D7D9 call @ILT+5(ShowFunTest) (0040100a)
0040D7DE add esp,48h
28: return 0;
0040D7E1 xor eax,eax
29: }



16: void ShowFunTest(CFunTest FunTest)
17: {
0040D740 push ebp
0040D741 mov ebp,esp
0040D743 sub esp,40h
0040D746 push ebx
0040D747 push esi
0040D748 push edi
0040D749 lea edi,[ebp-40h]
0040D74C mov ecx,10h
0040D751 mov eax,0CCCCCCCCh
0040D756 rep stos dword ptr [edi]
18: printf("m_nOne = %d m_nTwo = %d szName = %s\n",FunTest.m_nOne,FunTest.m_nTwo,FunTest.szName);
0040D758 lea eax,[ebp+10h] ;取数组成员szName的地址
0040D75B push eax ;做printf参数
0040D75C mov ecx,dword ptr [ebp+0Ch] ;取数据成员m_nTwo
0040D75F push ecx
0040D760 mov edx,dword ptr [ebp+8] ;取数据成员m_nOne
0040D763 push edx
0040D764 push offset string "m_nOne = %d m_nTwo = %d szName ="... (00422fbc)
0040D769 call printf (004010b0)
0040D76E add esp,10h
19: }


当对象作为函数的参数时,由于重新复制了对象,等同于又定义了一个对象,在某些情况会调用特殊的构造函数-拷贝构造函数。当函数退出时,复制的对象作为函数内的局部变量,将会被销毁。当析构函数存在时,则会调用析构函数;

问题代码:

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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
// Test08.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include <string.h>

class CMyString
{
public:

CMyString();
~CMyString();
char * GetString();

private:
char *m_PString;
};

CMyString::CMyString()
{
m_PString = new char[10];
if (m_PString == NULL)
{
return; //堆空间申请失败
}
strcpy(m_PString, "sYstemk1t");
}

CMyString::~CMyString()
{
if (m_PString != NULL)
{
delete[] m_PString;
m_PString = NULL;
}

}

char * CMyString::GetString()
{
return m_PString;
}


void ShowString(CMyString String)
{
printf(String.GetString());
}
int _tmain(int argc, _TCHAR* argv[])
{
CMyString MyString;
ShowString(MyString);
getchar();
return 0;
}




int _tmain(int argc, _TCHAR* argv[])
{
01131610 push ebp
01131611 mov ebp,esp
01131613 push 0FFFFFFFFh
01131615 push 1134E98h
0113161A mov eax,dword ptr fs:[00000000h]
01131620 push eax
01131621 sub esp,0D8h
01131627 push ebx
01131628 push esi
01131629 push edi
0113162A lea edi,[ebp-0E4h]
01131630 mov ecx,36h
01131635 mov eax,0CCCCCCCCh
0113163A rep stos dword ptr es:[edi]
0113163C mov eax,dword ptr ds:[01139000h]
01131641 xor eax,ebp
01131643 push eax
01131644 lea eax,[ebp-0Ch]
01131647 mov dword ptr fs:[00000000h],eax
CMyString MyString;
0113164D lea ecx,[MyString] ;获取对象首地址,装入ecx中作为this指针
01131650 call CMyString::CMyString (011310DCh) ;调用构造函数
01131655 mov dword ptr [ebp-4],0 ;记录同一作用域中该类对象个数
ShowString(MyString);
0113165C mov eax,dword ptr [MyString] ;MyString对象长度为4,一个寄存器单元存放
0113165F push eax ;eax获取对象首地址处4字节数据,即数据成员m_pString
01131660 call ShowString (0113112Ch) ;调用ShowString函数
01131665 add esp,4
getchar();
01131668 mov esi,esp
0113166A call dword ptr ds:[113A11Ch]
01131670 cmp esi,esp
01131672 call __RTC_CheckEsp (01131159h)
return 0;
01131677 mov dword ptr [ebp-0E0h],0
01131681 mov dword ptr [ebp-4],0FFFFFFFFh ;由于对象被释放,修改对象个数
01131688 lea ecx,[MyString] ;获取对象首地址,作为ecx传入this指针
0113168B call CMyString::~CMyString (01131172h) ;调用析构函数
01131690 mov eax,dword ptr [ebp-0E0h]
}



//ShowString函数讲解

void ShowString(CMyString String)
{
01131560 push ebp
01131561 mov ebp,esp
//异常处理
01131563 push 0FFFFFFFFh
01131565 push 1134E68h
0113156A mov eax,dword ptr fs:[00000000h]
01131570 push eax
//异常处理过程
01131571 sub esp,0C0h
01131577 push ebx
01131578 push esi
01131579 push edi
0113157A lea edi,[ebp-0CCh]
01131580 mov ecx,30h
01131585 mov eax,0CCCCCCCCh
0113158A rep stos dword ptr es:[edi]
0113158C mov eax,dword ptr ds:[01139000h]
01131591 xor eax,ebp
01131593 push eax
01131594 lea eax,[ebp-0Ch]
01131597 mov dword ptr fs:[00000000h],eax
0113159D mov dword ptr [ebp-4],0 ;作用域内对象的个数
printf(String.GetString());
011315A4 lea ecx,[String] ;获取对象首地址保存到ecx寄存器中作为this指针
011315A7 call CMyString::GetString (01131028h) ;调用成员方法
011315AC mov esi,esp
011315AE push eax ;返回eax的的值压栈
011315AF call dword ptr ds:[113A118h]
011315B5 add esp,4
011315B8 cmp esi,esp
011315BA call __RTC_CheckEsp (01131159h)
}
011315BF mov dword ptr [ebp-4],0FFFFFFFFh ;修改对象个数
011315C6 lea ecx,[String] ;获取对象首地址保存到ecx寄存器中,作为this指针
011315C9 call CMyString::~CMyString (01131172h) ;析构函数
011315CE mov ecx,dword ptr [ebp-0Ch] ;
011315D1 mov dword ptr fs:[0],ecx
011315D8 pop ecx
011315D9 pop edi
011315DA pop esi
011315DB pop ebx
011315DC add esp,0CCh
011315E2 cmp ebp,esp
011315E4 call __RTC_CheckEsp (01131159h)
011315E9 mov esp,ebp
011315EB pop ebp
011315EC ret

对象做返回值

对象作为函数的返回值时,与基本数据类型不同。基本数据类型大都通过eax寄存器来保存返回的数据。而对象属自定义类型,寄存器eax无法保存对象中的所有数据,所以在函数返回时,寄存器eax并不能满足需求;

对象作为返回值与对象作为参数的处理方式类似,对象作为参数时,进入函数前预先将对象使用的栈空间留出来,并将实参对象的数据复制到栈空间中。该栈空间作为函数参数,用于函数内部处理;同理,对象作为返回值,返回这个临时栈空间的首地址作为返回值;

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
01311490  push        ebp  
01311491 mov ebp,esp
01311493 sub esp,160h ;预留返回对象的栈空间
01311499 push ebx
0131149A push esi
0131149B push edi
0131149C lea edi,[ebp-160h]
013114A2 mov ecx,58h
013114A7 mov eax,0CCCCCCCCh
013114AC rep stos dword ptr es:[edi]
013114AE mov eax,dword ptr ds:[01318000h]
013114B3 xor eax,ebp
013114B5 mov dword ptr [ebp-4],eax
CReturn ObjA;
ObjA = GetCreturn();
013114B8 lea eax,[ebp-128h] ;获取返回对象的栈空间首地址
013114BE push eax ;将返回对象的首地址压入栈中,用于保存返回对象的数据
013114BF call GetCreturn (01311037h) ;调用函数GetCreturn
013114C4 add esp,4 ;函数调用结束后,eax中保存着地址ebp-128.即返回对象的首地址
013114C7 mov ecx,0Bh ;设置循环次数
013114CC mov esi,eax ;将返回对象的首地址放入esi中
013114CE lea edi,[ebp-15Ch] ;获取临时对象的首地址
013114D4 rep movs dword ptr es:[edi],dword ptr [esi] ;每次从返回对象中复制4字节数据到临时对象的地址中,共复制11次
013114D6 mov ecx,0Bh ;设置循环次数11次
013114DB lea esi,[ebp-15Ch] ;获取返回对象的首地址
013114E1 lea edi,[ObjA] ;获取局部对象的首地址
013114E4 rep movs dword ptr es:[edi],dword ptr [esi] ;将局部对象RetObj中的数据复制到返回对象中
getchar();
013114E6 mov esi,esp
013114E8 call dword ptr ds:[1319114h]
013114EE cmp esi,esp
013114F0 call __RTC_CheckEsp (01311145h)
return 0;
013114F5 xor eax,eax
}


CReturn GetCreturn()
{
013113C0 push ebp
013113C1 mov ebp,esp
013113C3 sub esp,104h ;调整栈空间,预留临时返回对象与局部变量的内存空间
013113C9 push ebx
013113CA push esi
013113CB push edi
013113CC lea edi,[ebp-104h]
013113D2 mov ecx,41h
013113D7 mov eax,0CCCCCCCCh
013113DC rep stos dword ptr es:[edi]
013113DE mov eax,dword ptr ds:[01318000h]
013113E3 xor eax,ebp
013113E5 mov dword ptr [ebp-4],eax
CReturn RetObj;
RetObj.m_nNumber = 0;
013113E8 mov dword ptr [RetObj],0 ;为对象首地址变量nNumber赋值为0
for (size_t i = 0; i < 10; i++)
013113EF mov dword ptr [ebp-40h],0 ;局部变量赋值
013113F6 jmp GetCreturn+41h (01311401h)
013113F8 mov eax,dword ptr [ebp-40h]
013113FB add eax,1
013113FE mov dword ptr [ebp-40h],eax
01311401 cmp dword ptr [ebp-40h],0Ah
01311405 jae GetCreturn+75h (01311435h)
{
RetObj.m_nArray[i] = i + 1;
01311407 mov eax,dword ptr [ebp-40h]
0131140A add eax,1
0131140D mov ecx,dword ptr [ebp-40h]
01311410 mov dword ptr [ebp+ecx*4-30h],eax
printf("%d\n", RetObj.m_nArray[i]);
01311414 mov esi,esp
01311416 mov eax,dword ptr [ebp-40h]
01311419 mov ecx,dword ptr [ebp+eax*4-30h]
0131141D push ecx
0131141E push 13158A8h
01311423 call dword ptr ds:[1319118h]
01311429 add esp,8
0131142C cmp esi,esp
0131142E call __RTC_CheckEsp (01311145h)
}
01311433 jmp GetCreturn+38h (013113F8h)
return RetObj;
01311435 mov ecx,0Bh ;设置循环次数11次
0131143A lea esi,[RetObj] ;获取局部对象首地址
0131143D mov edi,dword ptr [ebp+8] ;获取返回对象的首地址
01311440 rep movs dword ptr es:[edi],dword ptr [esi] ;将局部变量RetObj中的数据复制到返回对象中
01311442 mov eax,dword ptr [ebp+8] ;返回对象的首地址并保存到eax中
}

在调用GetCreturn前,编译器将在main函数中社区宁的返回对象的首地址作为参数压栈,在函数GetCReturn调用结束后进行了数据复制,将GetCreturn函数中定义的局部对象RetObj的数据复制到这个返回对象的空间中,再将这个返回地对象复制给目标对象objA,从而达到返回对象的目的;

虽然使用了临时对象对数据进行了复制,但还说存在出错的风险,这与对象作为参数时遇到的情况一样,由于使用了临时对象进行数据复制,但临时对象被销毁时,会调用析构函数。

通常情况下,VC++ 6.0编译的代码默认以thiscall方式调用成员函数,因此会使用ecx来保存this指针, 从而进行参数传递,但并非使用使用ecx传参的函数一定是成员函数。