在C++中,结构体和类都具有构造函数、析构函数和成员函数,两者只有一个区别:结构体的访问控制默认为public,而类的默认访问控制是private。对于C++中的结构体而言,public、private、protected的访问控制都是在编译期进行检查,当越权访问时,编译过程中会检查此类错误并给予提示。编译成功后,程序在执行过程中不会在访问控制方面做任何检查和限制。因此,在反汇编中,C++的结构体与类没有分别,两者的原理相同,只是类型名称不同,本章使用的示例多为类。
9.1 对象的内存布局
结构体和类都是抽象的,在真实世界中它们只可以表示某个群体,无法确定这个群体中的某个独立个体,而对象则是群体中独立存在的个体。例如,地球上最智慧的生物是人,人便是抽象事物,可以看作是一个类。“人”只能描述这个类型的事物具有哪些特征,无法得知具体是哪一个人。在“人”这个类中,如关羽、张飞等都是独立存在的实体,可被看作“人”这个类中的实体对象。
人 → 类、结构,抽象的概念
关羽 → 实例对象,实际存在的事物
由于类是抽象概念,当两个类的特征相同时,它们之间就是相等的关系。而对象是实际存在的,即使它们之间包含的数据相同,也不能视为同一个对象。下面我们通过一个简单的示例(见代码清单9-1)来加深理解类与对象之间的关系。
代码清单9-1 类与对象的关系(C++源码)
//C++源码
#include <stdio.h>
class Person { //Person为抽象类名称,如同“人”这个名称
public:
Person() {
age = 18;
height = 180;
}
int getAge() { //类成员函数,如人类的行为,吃、喝、睡等
return age;
}
int getHeight() {
return height;
}
private:
int age; //类数据成员,如人类的身高、体重等
int height;
};
int main(int argc, char* argv[]) {
Person person;
return 0;
}
笔记--------------------------------------------start

笔记--------------------------------------------end
代码清单9-1中定义了自定义类型Person类以及该类的实例对象person。Person与C++中的int都属于数据类型。整型变量的数据大小为4字节,使用class关键字的自定义类型如何分配各数据成员呢?我们下面调试运行代码清单9-1,分析对象person的各成员在内存中的布局,如图9-1所示。

图9-1 对象内存布局
在图9-1中,对象person在内存中的地址为0x00D3FAA0,该地址处定义了对象person的各个数据成员,分别存放在地址0x00D3FAA0与0x00D3FAA4处。对象person中先定义的数据成员在低地址处,后定义的数据成员在高地址处,依次排列。对象的大小只包含数据成员,类成员函数属于执行代码,不属于类对象的数据。
根据图9-1可知,凡是属于Person类型的变量,在内存中都会占据8字节的空间。这8字节由类中的两个数据成员组成,它们都是int类型,数据长度为4字节。从内存布局上看,类与数组非常相似,都是由多个数据元素构成的,但类的能力要远远大于数组。类成员的数据类型定义非常广泛,除本身的对象外,任何已知的数据类型都可以在类中定义。
为什么在类中不能定义自身的对象呢?这是因为类需要在申请内存的过程中计算出自身的大小,以用于实例化。如果在类中定义了自身的对象,在计算各数据成员的长度时,又会回到自身,这样就形成了递归定义,而这个递归并没有出口,是一个无限循环的递归定义,所以不能定义自身对象作为类成员。但是,自身类型的指针除外,因为任何类型的指针在32位下占用的内存大小始终为4字节,64位下占8个字节,等同于一个常量值,因此将其作为类的数据成员不会影响长度的计算。根据以上知识,可以总结出如下对象长度计算公式。
对象长度=sizeof(数据成员1)+sizeof(数据成员2)+…+sizeof(数据成员n)
这个公式是否正确呢?
从表面上看,这个公式没有问题,但对象的大小计算远没有这么简单。即使类中没有继承和虚函数的定义,仍有三种特殊情况能推翻此公式:空类、内存对齐、静态数据成员。当出现这三种情况时,使用此公式得到的对象长度与实际情况不符。下面我们详细介绍一下为何该公式不适用这三种情况。
1. 空类
空类中没有任何数据成员,按照该公式计算得出的对象长度为0字节。类型长度为0,则此类的对象不占内存空间。而实际情况是,空类的长度为1字节。如果对象完全不占用内存空间,空类就无法取得实例对象的地址,this指针失效,因此不能被实例化。而类的定义是由成员数据和成员函数组成的,在没有成员数据的情况下,还可以有成员函数,因此仍然需要做实例化。分配1字节的空间用于类的实例化,这1字节的数据并没有被使用。
2. 内存对齐
在C++中,类和结构体中的数据成员是根据它们出现的顺序,依次申请内存空间的。由于内存对齐的原因,它们并不一定会像数组那样连续地排列;由于数据类型不同,占用的内存空间大小也会不同,在申请内存时,会遵守一定的规则。
在为结构体和类中的数据成员分配内存时,结构体中当前数据成员类型的长度为M,指定的对齐值为N,那么实际对齐值为q = min(M,N),成员的地址安排在q的倍数上,代码如下所示。
struct stTest{
short s; //应占2字节内存空间,假设所在地址为0x010FFB64
int n; //应占4字节内存空间
};
数据成员s的地址为0x010FFB64,类型为short,占2字节内存空间。C++中通常指定的对齐值默认为8,short的长度为2,实际对齐值取两者较小者,即2。因此short被分配在地址0x010FFB64处(此地址是2的倍数,可分配)。接下来,轮到为第二个数据成员分配内存了,如果分配在s后,第二个数据成员应在地址0x010FFB66处,但第二个数据成员为int类型,占4字节内存空间,与指定的对齐值进行比较后,实际对齐值取int类型的长度4,而地址0x010FFB66不是4的倍数,需要插入两个字节填充,以满足对齐条件,因此第二个数据成员被定义在地址0x010FFB68处,如图9-2所示。

图9-2 内存对齐说明
在图9-2中,内存监视窗口显示了test对象所在地址中的数据。在short类型变量占用的地址0x010FFB66处,数据成员s被赋值为1。在其后插入了两个0xCC数据,它们便是编译器用于对齐而插入的,实际运行中并没有使用这两个字节中的数据。
上述示例讲到了结构体成员对齐值的问题,接下来我们讨论对齐值对结构体整体大小的影响。如果按照C++默认的8字节对齐,那么结构体的整体大小要能被8整除,代码如下所示。
struct{
double d; //所在地址:0x0012FF00~0x0012FF08之间,占8字节
int n; //所在地址:0x0012FF08~0x0012FF0C之间,占4字节
short s; //所在地址:0x0012FF0C~0x0012FF10之间,占2字节
};
上例中结构体成员的总长度为8+4+2=14,按默认的对齐值设置要求,结构体的整体大小要能被8整除,于是编译器在最后一个成员s所占内存之后加入2字节空间填补到整个结构体中,使总大小变为8+4+2+2=16,这样就满足了对齐的要求。
但是,并非设定了默认对齐值就能将结构体的对齐值锁定。如果结构体中的数据成员类型最大值为M,指定的对齐值为N,那么实际对齐值就是min(M, N),代码如下所示。
struct{
char c; //应占1字节内存空间,如所在地址为0x0012FF00
int n; //应占4字节内存空间
short s; //应占2字节内存空间
};
以上结构如果还是按照8字节的方式对齐,布局格式如下所示。
c 所在地址:0x0012FF00~0x0012FF04之间,占4字节,对齐n
n 所在地址:0x0012FF04~0x0012FF08之间,占4字节
s 所在地址:0x0012FF08~0x0012FF0C之间,占2字节,另外填充2字节
随后定义的数据成员s应该使用6字节的空数据对齐。编译器通过检查发现,结构中最大的类型为n数据,占4字节空间。于是将对齐值由8调整为4,重新调整后,s只需要填充2字节的空白数据就可以实现对齐了。
既然有默认的对齐值,就可以在定义结构体时进行调整,在C++中可使用预编译指令#pragma pack(N)调整对齐大小。修改以上示例,调整对齐值为1,代码如下所示。
#pragma pack(1)
struct{
char c; // 应占1字节内存空间
int n; // 应占4字节内存空间
short s; // 应占2字节内存空间
};
调整对齐值后,根据对齐规则,在分配n时无须插入空白数据。对齐值为1,n占4字节大小,很明显,使用pack设定的对齐值更小,因此采用对齐值1的倍数计算分配内存空间的首地址,n紧靠在c之后即可,这样c只占用1字节内存空间。因为设定的对齐值小于、等于结构体中所有数据成员的类型长度,所以结构总长度只要是1的倍数即可。在这个例子中,结构总长度为7。
使用pack修改对齐值也并非一定会生效,与默认对齐值一样,都需要参考结构体中的数据成员类型。当设定的对齐值大于结构体中的数据成员类型时,此对齐值同样是无效的。对齐值的计算流程就是将设定的对齐值与结构体中最大的基本类型数据成员的长度进行比较,取两者之间的较小者。
当结构体中以数组作为成员时,计算对齐值是根据数组元素的长度,而不是数组的整体大小,代码如下所示。
struct {
char c; //应占1字节内存空间,如所在地址为0x0012FF00
char ary[4]; //应占4字节内存空间
short s; //应占2字节内存空间
};
按照对齐规定,c与ary的对齐没有缝隙,无须插入空白数据,当ary与s进行对齐时,c与ary在内存中将会占5字节,此时按照结构中当前的数据类型short进行对齐,插入1字节的数据,结构布局如下所示。
c 所在地址:0x0012FF00~0x0012FF01之间,占1字节
ary[4] 所在地址:0x0012FF01~0x0012FF06之间,占5字节
s 所在地址:0x0012FF06~0x0012FF08之间,占2字节
根据结构体中的各数据成员类型可知,最大类型的数据成员s占2字节,其余成员类型各为1字节。在默认的编译选项下,对齐值为8,而s长度为2,因此会按照short类型的长度(2字节)进行对齐,此时结构的大小为8字节,无须填入字节即可满足要求。当结构体中出现结构体类型的数据成员时,不会将嵌套的结构体类型的整体长度加入对齐值计算中,而是以嵌套定义的结构体使用的对齐值进行对齐,代码如下所示。
struct stOne{
char c; //占1字节内存空间
char ary[4]; //占5字节内存空间
short s; //占2字节内存空间
};
struct stTwo{
int n; //占4字节内存空间
stOne one; //占8字节内存空间
};
在以上结构中,虽然stOne结构占8字节,但由于其对齐值为2,因此stTwo结构体中的最大类型便是int,以4作为对齐值。所以,结构stTwo的总大小并非以8字节对齐的16字节,而是以4字节对齐的12字节。
因为存在内存对齐,数据的布局变化多端,所以在分析结构体和类的数据成员布局时,不能单纯地参考各数据成员的类型长度,按顺序进行排列,而应该按上述方法仔细观察和分析。另外,各编译器的实现效果可能有所不同,应详细阅读相关文档。
3. 静态数据成员
当类中的数据成员被修饰为静态时,对象的长度计算又会发生变化。虽然静态数据成员是在类中被定义的,但它与局部静态变量类似,存放的位置和全局变量一致。只是编译器增加了作用域的检查,在作用域之外不可见。同类对象将共同享有静态数据成员的空间,详细内容参见9.3节。
通过以上讲解我们发现,对象的内存布局并不简单。在类中定义了虚函数和类为派生类等情况下,对象的内存布局中将含有虚函数表和父类数据成员等信息,这将使长度计算变得更为复杂。我们要从简单的情况入手,先掌握最基本的类对象的内存结构分析方法,再深入学习。
当对象为全局对象时,其内存布局与局部对象相同,只是所在内存地址以及构造函数和析构函数的触发时机不同。全局对象所在的内存地址空间为全局数据区,而局部对象的内存地址空间在栈中。第10章将详细讲解全局对象构造函数初始化以及析构函数释放的全过程。
了解了类中数据成员的内存布局后,如何访问它们呢?在类方法中,又是如何知道数据成员在内存中的地址以及其中的数据信息呢?带着这些疑问,我们进入9.2节的学习,了解神秘的this指针。
笔记--------------------------------------------start
// list9_1_2.cpp 空类,内存对齐 示例
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
class Empty {
//空类, 点一个字节
};
struct stTest { //对齐后占8个字节
short s; //应占2字节内存空间
int n; //应占4字节内存空间
};
struct stTest2 { //4字节对齐 总占12字节
char c; //应占1字节内存空间 +3
int n; //应占4字节内存空间
short s; //应占2字节内存空间 +2
};
struct stTest3 {
char c; //应占1字节内存空间
char ary[4]; //应占5字节内存空间,实际要加最后一个结束符'\0'
short s; //应占2字节内存空间
};
//嵌套结构体数据
struct stOne {
char c; //占1字节内存空间
char ary[4]; //占5字节内存空间
short s; //占2字节内存空间
};
struct stTwo {
int n; //占4字节内存空间
stOne one; //占8字节内存空间
};
// stTest2 调整对齐大小为1
#pragma pack(1)
struct stTest4 { //1字节对齐 占7个字节
char c; //应占1字节内存空间
int n; //应占4字节内存空间
short s; //应占2字节内存空间
};
int main(int argc, char *argv[])
{
Empty empty;
printf("sizeof(empty) = %d\n", sizeof(empty));// 1
stTest test;
printf("sizeof(stTest) = %d\n", sizeof(test)); // 8
stTest2 test2;
printf("sizeof(stTest2) = %d\n", sizeof(test2)); //12
stTest3 test3;
printf("sizeof(stTest3) = %d\n", sizeof(test3)); //8
stTwo two;
printf("sizeof(stTwo) = %d\n", sizeof(two)); //12
stTest4 test4;
printf("sizeof(stTest4) = %d\n", sizeof(test4)); //7
return 0;
}


笔记--------------------------------------------end
9.2 this指针
在学习C++的过程中,我们会接触到this指针。在类中没有对this指针的定义,而在成员函数中却可以使用this指针。许多C++程序员只知道在编码的过程中有this指针,却不知它从何而来、为何存在。
因为this指针的使用过程被编译器隐藏起来了,所以它显得格外神秘,本节我们来揭开它的庐山真面目。
根据字面含义,this指针应属于指针类型。this指针在32位环境下占4字节空间,在64位环境下占8字节空间,保存的数据为地址信息。“this”可翻译为“这个”,因此经过字面分析可认为this指针中保存了所属对象的首地址。9.1节介绍了对象的组成和它们在内存中的布局,但是并没有分析如何访问对象中的数据成员。接下来,我们从访问对象的数据成员和成员函数入手,分析this指针的使用过程。先来了解一下使用指针访问结构体或类成员的公式,假设type为某个正确定义的结构体或者类,member是type中可以访问的成员。
type *p;
// 此处略去p的赋值
// 以下是整型加法
p->member的地址=指针p的地址值+ member在type中的偏移量
举个例子,如果有以下定义。
struct A {
int n; // 在结构体内的偏移量为0
float f; // 在结构体内的偏移量为4
};
struct A a; //假设这个结构体变量a的地址为0x0012ff00
struct A *p = &a; //定义结构体指针并赋初值
printf("%p", &p->f); //结果
我们知道,p中保存的地址为0x0012ff00,f在结构体内的偏移量为4,于是可以得到:p-> f的地址=0x0012ff00+4=0x0012ff04。
思考题 接上例,见如下代码。
// 以下结果是什么?程序会崩溃吗?为什么?答案见本章小结
printf("%p", &((struct A*)NULL)->f);
明白结构体和类成员变量的寻址方法后,我们来看一个示例,如代码清单9-2所示。
代码清单9-2 访问类对象的数据成员(Debug版)
// C++ 源码
#include <stdio.h>
class Person {
public:
void setAge(int age) { //公有成员函数
this->age = age;
}
public:
int age; //公有数据成员
};
int main(int argc, char* argv[]) {
Person person;
person.setAge(5); //调用成员函数
printf("Person : %d\n", person.age); //获取数据成员
return 0;
}
//x86_vs对应汇编代码讲解
00401000 push ebp
00401001 mov ebp, esp
00401003 push ecx ;申请person对象空间
00401004 push 5 ;压入参数5
00401006 lea ecx, [ebp-4] ;传参,取出对象person的首地址存入ecx
00401009 call sub_401030 ;调用setAge成员函数
0040100E mov eax, [ebp-4] ;取出对象首地址处4字节的数据age存入eax
00401011 push eax ;将eax中保存的数据成员存入传参
00401012 push offset aPersonD ;"Person:%d\n"
00401017 call sub_401090 ;调用printf函数
0040101C add esp, 8
0040101F xor eax, eax
00401021 mov esp, ebp
00401023 pop ebp
00401024 retn
00401030 push ebp
00401031 mov ebp, esp
00401033 push ecx ;申请局部变量空间,
00401034 mov [ebp-4], ecx ;注意,ecx中保存了对象person的首地址
00401037 mov eax, [ebp-4] ;eax=this
0040103A mov ecx, [ebp+8] ;ecx=age
0040103D mov [eax], ecx ;this->age = age
0040103F mov esp, ebp
00401041 pop ebp
00401042 retn 4
//x86_gcc对应汇编代码相似,略
//x86_clang对应汇编代码相似,略
//x64_vs对应汇编代码讲解
0000000140001000 mov [rsp+10h], rdx
0000000140001005 mov [rsp+8], ecx
0000000140001009 sub rsp, 38h
000000014000100D mov edx, 5 ;传递参数,5
0000000140001012 lea rcx, [rsp+20h] ;传递参数,取出对象person的首地址存入rcx中
0000000140001017 call sub_140001040 ;调用setAge成员函数
000000014000101C mov edx, [rsp+20h] ;传递参数,person.age
0000000140001020 lea rcx, aPersonD ;"Person:%d\n"
0000000140001027 call sub_1400010C0 ;调用printf函数
000000014000102C xor eax, eax
000000014000102E add rsp, 38h
0000000140001032 retn
0000000140001040 mov [rsp+10h], edx
0000000140001044 mov [rsp+8], rcx ;注意,ecx中保存了对象person的首地址
0000000140001049 mov rax, [rsp+8] ;rax=this
000000014000104E mov ecx, [rsp+10h] ;ecx=age
0000000140001052 mov [rax], ecx ;this->age=age
0000000140001054 retn
//x64_gcc对应汇编代码相似,略
//x64_clang对应汇编代码相似,略
笔记--------------------------------------------start
//x86_vs对应汇编代码讲解 (Debug版)
00411650 var_4 = dword ptr -4
00411650 argc = dword ptr 8
00411650 argv = dword ptr 0Ch
00411650 envp = dword ptr 10h
00411650
00411650 push ebp
00411651 mov ebp, esp
00411653 sub esp, 44h ; 申请person对象空间
00411656 push ebx
00411657 push esi
00411658 push edi
00411659 push 5 ; 压入参数5
0041165B lea ecx, [ebp+var_4] ; 传参,取出对象person的首地址存入ecx
0041165E call j_Person__setAge ; 调用setAge成员函数
00411663 mov eax, [ebp+var_4] ; 取出对象首地址处4字节的数据age存入eax
00411666 push eax ; 将eax中保存的数据成员存入传参
00411667 push offset aPersonD ; "Person : %d\n"
0041166C call j_printf ; 调用printf函数
00411671 add esp, 8 ; 栈平衡
00411674 xor eax, eax
00411676 pop edi
00411677 pop esi
00411678 pop ebx
00411679 mov esp, ebp
0041167B pop ebp
0041167C retn
......
004112B2 j_Person__setAge proc near ; CODE XREF: main+E↓p
004112B2 jmp Person__setAge
004112B2 j_Person__setAge endp
.......
004115B0 var_4 = dword ptr -4
004115B0 arg_0 = dword ptr 8
004115B0
004115B0 push ebp
004115B1 mov ebp, esp
004115B3 sub esp, 44h ; 申请局部变量空间
004115B6 push ebx
004115B7 push esi
004115B8 push edi
004115B9 mov [ebp+var_4], ecx ; 注意,ecx中保存了对象person的首地址
004115BC mov eax, [ebp+var_4] ; eax=this
004115BF mov ecx, [ebp+arg_0] ; ecx=age
004115C2 mov [eax], ecx ; this->age = age
004115C4 pop edi
004115C5 pop esi
004115C6 pop ebx
004115C7 mov esp, ebp
004115C9 pop ebp
004115CA retn 4 ; 栈平衡

![]()

//x64_vs对应汇编代码讲解 (Debug版)
0000000140011910 mov [rsp-8+arg_8], rdx
0000000140011915 mov [rsp-8+arg_0], ecx
0000000140011919 push rbp
000000014001191A push rdi
000000014001191B sub rsp, 108h
0000000140011922 lea rbp, [rsp+20h]
0000000140011927 lea rdi, [rsp+110h+var_F0]
000000014001192C mov ecx, 0Ah
0000000140011931 mov eax, 0CCCCCCCCh
0000000140011936 rep stosd
0000000140011938 mov ecx, dword ptr [rsp+110h+arg_8]
000000014001193F mov rax, cs:__security_cookie
0000000140011946 xor rax, rbp
0000000140011949 mov [rbp+0F0h+var_18], rax
0000000140011950 lea rcx, unk_140021003
0000000140011957 call j___CheckForDebuggerJustMyCode
000000014001195C mov edx, 5 ; 传递参数,5
0000000140011961 lea rcx, [rbp+0F0h+var_EC] ; 传递参数,取出对象person的首地址存入rcx中
0000000140011965 call j_Person__setAge ; 调用setAge成员函数
000000014001196A mov edx, [rbp+0F0h+var_EC] ; 传递参数,person.age
000000014001196D lea rcx, Format ; "Person : %d\n"
0000000140011974 call j_printf ; 调用printf函数
0000000140011979 xor eax, eax
000000014001197B mov edi, eax
000000014001197D lea rcx, [rbp+0F0h+var_110] ; Esp
0000000140011981 lea rdx, Fd ; Fd
0000000140011988 call j__RTC_CheckStackVars
000000014001198D mov eax, edi
000000014001198F mov rcx, [rbp+0F0h+var_18]
0000000140011996 xor rcx, rbp ; StackCookie
0000000140011999 call j___security_check_cookie
000000014001199E lea rsp, [rbp+0E8h]
00000001400119A5 pop rdi
00000001400119A6 pop rbp
00000001400119A7 retn
......
00000001400113C0 j_Person__setAge proc near ; CODE XREF: main+55↓p
00000001400113C0 jmp Person__setAge
00000001400113C0 j_Person__setAge endp
.......
0000000140011C10 mov [rsp-8+arg_8], edx
0000000140011C14 mov [rsp-8+arg_0], rcx ; 注意,ecx中保存了对象person的首地址
0000000140011C19 push rbp
0000000140011C1A push rdi
0000000140011C1B sub rsp, 0E8h
0000000140011C22 lea rbp, [rsp+20h]
0000000140011C27 lea rcx, unk_140021003
0000000140011C2E call j___CheckForDebuggerJustMyCode
0000000140011C33 mov rax, [rbp+0D0h+arg_0] ; rax=this
0000000140011C3A mov ecx, [rbp+0D0h+arg_8] ; ecx=age
0000000140011C40 mov [rax], ecx ; this->age=age
0000000140011C42 lea rsp, [rbp+0C8h]
0000000140011C49 pop rdi
0000000140011C4A pop rbp
0000000140011C4B retn

![]()

笔记--------------------------------------------end
代码清单9-2中演示了对象调用成员的方法以及取出数据成员的过程。使用默认的调用约定时,在调用成员函数的过程中,编译器做了一个“小动作”:利用寄存器ecx保存了对象的首地址,并以寄存器传参的方式将其传递到成员函数中,这便是this指针的由来。由此可见,所有成员函数(非静态成员函数)都有一个隐藏参数,即自身类型的指针,这样的默认调用约定称为thiscall。
在成员函数中访问数据成员也是通过this指针间接访问的,这便是在成员函数内可以直接使用数据成员的原因。在类中使用数据成员和成员函数时,编译器隐藏了如下操作。
class Person {
public:
void show() {
//隐藏传递了this指针,这里实际为this->getAge()
printf("%d\n", getAge());
}
int getAge() {
//隐藏传递了this指针,这里实际为retrun this->age
return age;
}
public:
int age;
};
在C++的环境下,识别this指针的关键点是在函数的调用过程中使用ecx作为第一个参数,在ecx中保存的数据为对象的首地址,但并非所有this指针的传递都是如此。在代码清单9-2中,成员函数SetAge的调用方式为thiscall。thiscall的栈平衡方式与__stdcall相同,都是被调用方负责平衡。但是,两者传递参数的过程却不一样,声明为thiscall的函数,第一个参数使用寄存器ecx传递,而非通过栈顶传递。而且thiscall并不属于关键字,它是C++中成员函数特有的调用方式,在C语言中是没有这种调用方式的。由于在C++环境下,thiscall不属于关键字,因此函数无法显式声明为thiscall调用方式,而类的成员函数默认为thiscall调用方式。所以,在分析过程中,如果看到某函数使用ecx传递参数,且ecx中保留了对象的this指针以及在函数实现代码内,存在this指针参与的寄存器相对间接访问方式,如[reg+8],即可怀疑此函数为成员函数。因为64位程序本来就使用rcx传递参数,所以无此特征。
当使用其他调用方式(如__stdcall)时,this指针将不再使用ecx传递参数,而是改用栈传递参数。将代码清单9-2中的成员函数SetAge()修改为__stdcall调用方式,查看this指针的传递与使用过程,如代码清单9-3所示。
代码清单9-3 使用stdcall调用方式的成员函数(Debug版)
// C++ 源码
#include <stdio.h>
class Person {
public:
void _stdcall setAge(int age) { //修改调用方式
this->age = age;
}
public:
int age;
};
int main(int argc, char* argv[]) {
Person person;
person.setAge(5);
printf("Person : %d\n", person.age);
return 0;
}
//x86_vs对应汇编代码讲解
00401000 push ebp
00401001 mov ebp, esp
00401003 push ecx
00401004 push 5
00401006 lea eax, [ebp-4] ;获取对象首地址并存入eax
00401009 push eax ;将eax作为参数压栈
0040100A call sub_401030 ;调用setAge成员函数
0040100F mov ecx, [ebp-4]
00401012 push ecx
00401013 push offset aPersonD ; "Person : %d\n"
00401018 call sub_401080 ;调用printf函数
0040101D add esp, 8
00401020 xor eax, eax
00401022 mov esp, ebp
00401024 pop ebp
00401025 retn
00401030 push ebp
00401031 mov ebp, esp
00401033 mov eax, [ebp+8] ;取出this指针并存入eax
00401036 mov ecx, [ebp+0Ch] ;取出参数age并存入ecx
00401039 mov [eax], ecx ;使用eax取出成员并赋值
0040103B pop ebp
0040103C retn 8
//x86_gcc对应汇编代码相似,略
//x86_clang对应汇编代码相似,略
//x64_vs对应汇编代码讲解
0000000140001000 mov [rsp+10h], rdx
0000000140001005 mov [rsp+8], ecx
0000000140001009 sub rsp, 38h
000000014000100D mov edx, 5 ;传递参数, 5
0000000140001012 lea rcx, [rsp+20h] ;传递参数,取出对象person的首地址存入rcx
0000000140001017 call sub_140001040 ;调用setAge成员函数
000000014000101C mov edx, [rsp+20h] ;传递参数,person.age
0000000140001020 lea rcx, aPersonD ;"Person : %d\n"
0000000140001027 call sub_1400010C0 ;调用printf函数
000000014000102C xor eax, eax
000000014000102E add rsp, 38h
0000000140001032 retn
0000000140001040 mov [rsp+10h], edx
0000000140001044 mov [rsp+8], rcx
0000000140001049 mov rax, [rsp+8] ;取出this指针并存入rax
000000014000104E mov ecx, [rsp+10h] ;取出参数age并存入ecx
0000000140001052 mov [rax], ecx ;使用rax取出成员并赋值
0000000140001054 retn
//x64_gcc对应汇编代码相似,略
//x64_clang对应汇编代码相似,略
笔记--------------------------------------------start
//x86_vs对应汇编代码讲解 (Debug版)
00411640 push ebp
00411641 mov ebp, esp
00411643 sub esp, 44h
00411646 push ebx
00411647 push esi
00411648 push edi
00411649 push 5
0041164B lea eax, [ebp+var_4] ; 获取对象首地址并存入eax
0041164E push eax ; 将eax作为参数压栈
0041164F call j_Person__setAge ; 调用setAge成员函数
00411654 mov eax, [ebp+var_4] ; person.age存入eax
00411657 push eax ; person.age入栈
00411658 push offset aPersonD ; "Person : %d\n"
0041165D call j_printf ; 调用printf函数
00411662 add esp, 8 ; 栈平衡
00411665 xor eax, eax
00411667 pop edi
00411668 pop esi
00411669 pop ebx
0041166A mov esp, ebp
0041166C pop ebp
0041166D retn
......
0041123F j_Person__setAge proc near ; CODE XREF: main+F↓p
0041123F jmp Person__setAge
0041123F j_Person__setAge endp
......
004115B0 push ebp
004115B1 mov ebp, esp
004115B3 sub esp, 40h
004115B6 push ebx
004115B7 push esi
004115B8 push edi
004115B9 mov eax, [ebp+arg_0] ; 取出this指针并存入eax
004115BC mov ecx, [ebp+arg_4] ; 取出参数age并存入ecx
004115BF mov [eax], ecx ; 使用eax取出成员并赋值
004115C1 pop edi
004115C2 pop esi
004115C3 pop ebx
004115C4 mov esp, ebp
004115C6 pop ebp
004115C7 retn 8

![]()

//x64_vs对应汇编代码讲解 (Debug版)
00000001400118B0 mov [rsp-8+arg_8], rdx
00000001400118B5 mov [rsp-8+arg_0], ecx
00000001400118B9 push rbp
00000001400118BA push rdi
00000001400118BB sub rsp, 108h
00000001400118C2 lea rbp, [rsp+20h]
00000001400118C7 lea rdi, [rsp+110h+var_F0]
00000001400118CC mov ecx, 0Ah
00000001400118D1 mov eax, 0CCCCCCCCh
00000001400118D6 rep stosd
00000001400118D8 mov ecx, dword ptr [rsp+110h+arg_8]
00000001400118DF mov rax, cs:__security_cookie
00000001400118E6 xor rax, rbp
00000001400118E9 mov [rbp+0F0h+var_18], rax
00000001400118F0 lea rcx, unk_140021003
00000001400118F7 call j___CheckForDebuggerJustMyCode
00000001400118FC mov edx, 5 ; 传递参数, 5
0000000140011901 lea rcx, [rbp+0F0h+var_EC] ; 传递参数,取出对象person的首地址存入rcx
0000000140011905 call j_Person__setAge ; 调用setAge成员函数
000000014001190A mov edx, [rbp+0F0h+var_EC] ; 传递参数,person.age
000000014001190D lea rcx, Format ; "Person : %d\n"
0000000140011914 call j_printf ; 调用printf函数
0000000140011919 xor eax, eax
000000014001191B mov edi, eax
000000014001191D lea rcx, [rbp+0F0h+var_110] ; Esp
0000000140011921 lea rdx, Fd ; Fd
0000000140011928 call j__RTC_CheckStackVars
000000014001192D mov eax, edi
000000014001192F mov rcx, [rbp+0F0h+var_18]
0000000140011936 xor rcx, rbp ; StackCookie
0000000140011939 call j___security_check_cookie
000000014001193E lea rsp, [rbp+0E8h]
0000000140011945 pop rdi
0000000140011946 pop rbp
0000000140011947 retn
.......
000000014001137A j_Person__setAge proc near ; CODE XREF: main+55↓p
000000014001137A jmp Person__setAge
000000014001137A j_Person__setAge endp
.......
0000000140011790 mov [rsp-8+arg_8], edx
0000000140011794 mov [rsp-8+arg_0], rcx
0000000140011799 push rbp
000000014001179A push rdi
000000014001179B sub rsp, 0E8h
00000001400117A2 lea rbp, [rsp+20h]
00000001400117A7 lea rcx, unk_140021003
00000001400117AE call j___CheckForDebuggerJustMyCode
00000001400117B3 mov rax, [rbp+0D0h+arg_0] ; 取出this指针并存入rax
00000001400117BA mov ecx, [rbp+0D0h+arg_8] ; 取出参数age并存入ecx
00000001400117C0 mov [rax], ecx ; 使用rax取出成员并赋值,this->age=age
00000001400117C2 lea rsp, [rbp+0C8h]
00000001400117C9 pop rdi
00000001400117CA pop rbp
00000001400117CB retn

![]()

笔记--------------------------------------------end
在代码清单9-3中,成员函数setAge()在调用过程中没有通过ecx传递this指针,取而代之的是以栈方式传递参数。#cdecl和#stdcall调用方式只是在参数平衡时有所区别,这里就不详细讲解了。使用#cdecl和#stdcall声明的成员函数,this指针并不像thiscall那样容易识别。使用栈方式传递参数,并且第一个参数为对象首地址的函数很多,很难区分。虽然难以区分,但如果能确定函数的第一个参数为this指针,并且在函数体内将this指针存入某寄存器,然后出现寄存器相对间接访问方式,那么将其还原为成员函数也是等价的。在02选项中,代码清单9-2和代码清单9-3经过优化后,类对象不复存在,只是使用printf函数输出数字5。setAge()函数完成的功能是将数据成员age赋值为常量5。其他代码没有再对此变量做任何修改,而类对象person只有一个数据成员age,该对象除了为数据成员赋值外,并无其他操作,因此编译器作了减少变量的优化处理。转换后代码如下所示。
int age = 5; // 可使用常量传播
printf("Person : %d\n", age);
// 减少变量后
printf("Person : %d\n", 5);
经过优化后,此程序中只有一句代码。使用02选项编译代码清单9-3,可通过图9-3验证分析结果。

笔记--------------------------------------------start
// list9_3_2.cpp 常量传播(Release版)
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
int main(int argc, char *argv[])
{
int age = 5; // 可使用常量传播
printf("Person : %d\n", age);
// 减少变量后
printf("Person : %d\n", 6);
return 0;
}
//x86_vs对应汇编代码讲解 (Release版)
00401040 push 5
00401042 push offset aPersonD ; "Person : %d\n"
00401047 call printf
0040104C push 6
0040104E push offset aPersonD ; "Person : %d\n"
00401053 call printf
00401058 add esp, 10h
0040105B xor eax, eax
0040105D retn

笔记--------------------------------------------end
图9-3 代码优化后的反汇编代码
使用thiscall调用方式的成员函数要点分析如下。
lea ecx, [mem] ; 取对象首地址并存入ecx,要注意观察内存
call FUN_ADDRESS ; 调用成员函数
; 在函数调用内,ecx 尚未重新赋值之前
mov XXX, ecx ; 发现函数内使用ecx中的数据,说明函数调用前对ecx的赋值
; 实际上是在传递参数
; 其后 ecx 中的内容会传递给其他寄存器
mov [reg+i], XXX ; 发现了寄存器相对间接寻址方式,如果能排除数组访问,说明reg中保存的是结构体或者类对象的首地址
符合以上特点,基本可判定这是调用类的成员函数。通过分析函数代码中访问ecx的方式,再结合内存窗口,以ecx中的值为地址观察其数据,可以进一步分析并还原对象中的各数据成员。
__stdcall与__cdecl调用方式的成员函数分析如下。
lea reg, [mem] ; 取出对象首地址并存入寄存器变量
push reg ; 将保存对象首地址的寄存器作为参数压栈
call FUN_ADDRESS ; 调用成员函数
; 在函数调用内,将第一个函数参数作为指针变量,以寄存器相对间接寻址方式进行访问
对于这种形式的代码,应重点分析压入的第一个参数是否为对象的首地址。如果是,则通过分析可知,该函数等价于此对象中的成员函数。根据第一个参数的使用以及它所指向的地址,可还原该结构中的各数据成员。
对于64位程序,应重点分析rcx是否为对象的首地址。如果是,则通过分析可知,该函数等价于该对象中的成员函数。根据rcx参数的使用以及它所指向的地址,可还原该结构中的各数据成员。
本节只简单讲解了类和对象相关的内容,并没有涉及复杂的案例。后续章节会逐步介绍较为复杂的对象结构。万丈高楼平地起,打下一个良好的基础,才能掌握更多的知识。
9.3 静态数据成员
9.1节简单介绍了静态数据成员。当类中定义了静态数据成员时,因为静态数据成员和静态变量原理相同(都是含有作用域的特殊全局变量),所以该静态数据成员的初值会被写入编译链接后的执行文件。当程序被加载时,操作系统将执行文件中的数据读到对应的内存单元里,静态数据成员已经存在,而这时类并没有实例对象。静态数据成员和对象之间的生命周期不同,并且静态数据成员也不属于某一对象,与对象之间是一对多的关系。静态数据成员仅仅和类相关,和对象无关,多个对象可以拥有同一个静态数据成员,如图9-4所示。
在图9-4中,定义了两个Static类对象obj1和obj2。Static类的定义如下面的代码所示。根据图9-4中监视器窗口的内容,两个对象各自的数据成员在内存中的地址不同,而静态数据成员的地址却相同。可见,类中的普通数据成员对于同类对象而言是独立存在的,而静态数据成员则是所有同类对象的共用数据,静态数据成员和对象是一对多的关系。

图9-4 普通数据成员与静态数据成员区别
因为静态数据成员有此特性,所以在计算类和对象的长度时,静态数据成员属于特殊的独立个体,不被计算在其中,代码如下所示。
#include <stdio.h>
class Static { //类CStatic的定义
public:
static int staticNum; //静态数据成员
int num; //普通数据成员
};
int Static::staticNum = 0; //静态数据成员初始化
int main() {
Static obj;
int size = sizeof(obj); //计算对象长度
//转换后的汇编代码,得到长度为4
//mov dword ptr [ebp-14h],4
printf("Static : %d\n", size); //显示对象长度
return 0;
}
通过sizeof获得对象obj占用的内存长度为4。静态数据成员staticNum没有参与对象obj的长度计算。staticNum为静态数据成员,num为普通数据成员,两者所属的内存地址空间不同,这也是静态数据成员不参与长度计算的原因之一,两者对比如下。
printf("0x%08x\n", &obj.staticNum); //使用对象直接调用静态数据成员
push 0A2A2D4h //静态成员所在地址为0x0A2A2D4
//部分printf代码分析略
printf("0x%08x\n", &obj.num); //获取普通数据成员地址
//获取对象的首地址并存入ecx,得到数据成员num的地址
lea eax,[ebp-0Ch]
//部分printf代码分析略
在以上代码分析中,静态数据成员所在的地址为0x0A2A2D4,而普通数据成员的地址在ebp-0Ch中,是一个栈空间地址。在使用的过程中,静态数据成员是常量地址,可通过立即数间接寻址的方式访问。普通数据成员只有在类对象产生后才出现,地址值无法确定,只能以寄存器相对间接寻址的方式进行访问。在成员函数中使用这两种数据成员时,因为静态数据成员属于全局变量,并且不属于任何对象,所以访问时无须this指针。而普通的数据成员属于对象所有,访问时需要使用this指针,如代码清单9-4所示。
代码清单9-4 在成员函数中使用静态数据成员与普通数据成员(Debug版)
// C++ 源码
#include <stdio.h>
class Person {
public:
void show();
static int count; //静态数据成员
int age; //普通数据成员
};
int Person::count = 0;
void Person::show() {
printf("age = %d , count = %d", age, count);
}
int main(int argc, char* argv[]) {
Person person;
person.age = 1;
person.count = 2;
person.show();
return 0;
}
//x86_vs对应汇编代码讲解
00401030 push ebp
00401031 mov ebp, esp
00401033 push ecx ;申请对象空间
00401034 mov dword ptr [ebp-4], 1 ;person.age=1,普通数据成员赋值
0040103B mov dword_4198B0, 2 ;person.count=2,静态数据成员赋值
00401045 lea ecx, [ebp-4] ;传递this指针
00401048 call sub_401000 ;调用show成员函数
0040104D xor eax, eax
0040104F mov esp, ebp
00401051 pop ebp
00401052 retn
00401000 push ebp
00401001 mov ebp, esp
00401003 push ecx
00401004 mov [ebp-4], ecx ;获取this指针
00401007 mov eax, dword_4198B0 ;直接访问静态数据成员count
0040100C push eax
0040100D mov ecx, [ebp-4] ;获取this指针
00401010 mov edx, [ecx] ;通过this指针访问数据成员age
00401012 push edx
00401013 push offset aAgeDCountD ;"age = %d , count = %d"
00401018 call sub_4010A0 ;调用printf函数
0040101D add esp, 0Ch
00401020 mov esp, ebp
00401022 pop ebp
00401023 retn
//x86_gcc对应汇编代码相似,略
//x86_clang对应汇编代码相似,略
//x64_vs对应汇编代码讲解
0000000140001030 mov [rsp+10h], rdx
0000000140001035 mov [rsp+8], ecx
0000000140001039 sub rsp, 38h
000000014000103D mov dword ptr [rsp+20h], 1 ;person.age=1,普通数据成员赋值
0000000140001045 mov cs:dword_14001CA40, 2 ;person.count=2,静态数据成员赋值
000000014000104F lea rcx, [rsp+20h] ;传递this指针
0000000140001054 call sub_140001000 ;调用show成员函数
0000000140001059 xor eax, eax
000000014000105B add rsp, 38h
000000014000105F retn
0000000140001000 mov [rsp+8], rcx ;获取this指针
0000000140001005 sub rsp, 28h
0000000140001009 mov r8d, cs:dword_14001CA40 ;直接访问静态数据成员count
0000000140001010 mov rax, [rsp+30h] ;获取this指针
0000000140001015 mov edx, [rax] ;通过this指针访问数据成员age
0000000140001017 lea rcx, aAgeDCountD ;"age=%d,count=%d"
000000014000101E call sub_1400010C0 ;调用printf函数
0000000140001023 add rsp, 28h
0000000140001027 retn
//x64_gcc对应汇编代码相似,略
//x64_clang对应汇编代码相似,略
笔记--------------------------------------------start

//x86_vs对应汇编代码讲解 (Debug版)
00411660 push ebp
00411661 mov ebp, esp
00411663 sub esp, 44h
00411666 push ebx
00411667 push esi
00411668 push edi
00411669 mov [ebp+var_4], 5 ; person.age=1,普通数据成员赋值
00411670 mov Person__count, 2 ; person.count=2,静态数据成员赋值
0041167A lea ecx, [ebp+var_4] ; 传递this指针
0041167D call j_Person__show ; 调用show成员函数
00411682 xor eax, eax
00411684 pop edi
00411685 pop esi
00411686 pop ebx
00411687 mov esp, ebp
00411689 pop ebp
0041168A retn
........
00411299 j_Person__show proc near ; CODE XREF: main+1D↓p
00411299 jmp Person__show
00411299 j_Person__show endp
........
004115B0 push ebp
004115B1 mov ebp, esp
004115B3 sub esp, 44h
004115B6 push ebx
004115B7 push esi
004115B8 push edi
004115B9 mov [ebp+var_4], ecx ; 获取this指针
004115BC mov eax, Person__count ; 直接访问静态数据成员count
004115C1 push eax ; 参数3: count
004115C2 mov ecx, [ebp+var_4] ; 获取this指针
004115C5 mov edx, [ecx] ; 通过this指针访问数据成员age
004115C7 push edx ; 参数2: age
004115C8 push offset aAgeDCountD ; 参数1: "age = %d, count = %d\n"
004115CD call j_printf ; 调用printf函数
004115D2 add esp, 0Ch ; 栈平衡
004115D5 pop edi
004115D6 pop esi
004115D7 pop ebx
004115D8 mov esp, ebp
004115DA pop ebp
004115DB retn

![]()

//x64_vs对应汇编代码讲解 (Debug版)
00000001400118C0 mov [rsp-8+arg_8], rdx
00000001400118C5 mov [rsp-8+arg_0], ecx
00000001400118C9 push rbp
00000001400118CA push rdi
00000001400118CB sub rsp, 108h
00000001400118D2 lea rbp, [rsp+20h]
00000001400118D7 lea rdi, [rsp+110h+var_F0]
00000001400118DC mov ecx, 0Ah
00000001400118E1 mov eax, 0CCCCCCCCh
00000001400118E6 rep stosd
00000001400118E8 mov ecx, dword ptr [rsp+110h+arg_8]
00000001400118EF mov rax, cs:__security_cookie
00000001400118F6 xor rax, rbp
00000001400118F9 mov [rbp+0F0h+var_18], rax
0000000140011900 lea rcx, unk_140021003
0000000140011907 call j___CheckForDebuggerJustMyCode
000000014001190C mov [rbp+0F0h+var_EC], 5 ; person.age=1,普通数据成员赋值
0000000140011913 mov cs:Person__count, 2 ; person.count=2,静态数据成员赋值
000000014001191D lea rcx, [rbp+0F0h+var_EC] ; 传递this指针
0000000140011921 call j_Person__show ; 调用show成员函数
0000000140011926 xor eax, eax
0000000140011928 mov edi, eax
000000014001192A lea rcx, [rbp+0F0h+var_110] ; Esp
000000014001192E lea rdx, Fd ; Fd
0000000140011935 call j__RTC_CheckStackVars
000000014001193A mov eax, edi
000000014001193C mov rcx, [rbp+0F0h+var_18]
0000000140011943 xor rcx, rbp ; StackCookie
0000000140011946 call j___security_check_cookie
000000014001194B lea rsp, [rbp+0E8h]
0000000140011952 pop rdi
0000000140011953 pop rbp
0000000140011954 retn
........
0000000140011316 j_Person__show proc near ; CODE XREF: main+61↓p
0000000140011316 jmp Person__show
0000000140011316 j_Person__show endp
........
0000000140011790 mov [rsp-8+arg_0], rcx ; 获取this指针
0000000140011795 push rbp
0000000140011796 push rdi
0000000140011797 sub rsp, 0E8h
000000014001179E lea rbp, [rsp+20h]
00000001400117A3 lea rcx, unk_140021003
00000001400117AA call j___CheckForDebuggerJustMyCode
00000001400117AF mov r8d, cs:Person__count ; 参数3: 直接访问静态数据成员count
00000001400117B6 mov rax, [rbp+0D0h+arg_0] ; 获取this指针
00000001400117BD mov edx, [rax] ; 通过this指针访问数据成员age
00000001400117BF lea rcx, Format ; "age = %d, count = %d\n"
00000001400117C6 call j_printf ; 调用printf函数
00000001400117CB lea rsp, [rbp+0C8h]
00000001400117D2 pop rdi
00000001400117D3 pop rbp
00000001400117D4 retn

![]()

笔记--------------------------------------------end
静态数据成员在反汇编代码中很难被识别,这是因为其展示形态与全局变量相同,很难被还原成对应的高级代码。可参考其代码的功能,酌情处理。
9.4 对象作为函数参数
对象作为函数的参数时,其传递过程较为复杂,传递方式也比较独特。对象的传参过程与数组不同,数组变量的名称代表数组的首地址,而对象的变量名称却不能代表对象的首地址。传参时不会像数组那样以首地址作为参数传递,而是先将对象中的所有数据进行备份(复制),将备份的数据作为形参传递到调用函数中使用。
在基本的数据类型中,32位程序下,除双精度浮点和long long类型外,其他所有数据类型的大小都不超过4字节,64位程序类型不超过8字节,使用一个栈元素即可完成数据的复制和传递。而类对象是自定义数据类型,是除自身外的所有数据类型的集合,各个对象的长度不定。对象在传参的过程中是如何被复制和传递的呢?我们来分析一下代码清单9-5。
代码清单9-5 对象作为函数的参数(Debug版)
// C++ 源码
#include <stdio.h>
class Person {
public:
int age;
int height;
};
void show(Person person) { //参数为类Person的对象
printf("age = %d , height = %d\n", person.age, person.height);
}
int main(int argc, char* argv[]) {
Person person;
person.age = 1;
person.height = 2;
show(person);
return 0;
}
//x86_vs对应汇编代码讲解
00401020 push ebp
00401021 mov ebp, esp
00401023 sub esp, 8
00401026 mov dword ptr [ebp-8], 1 ;person.age=1,对象首地址为ebp-8
0040102D mov dword ptr [ebp-4], 2 ;person.height=2
00401034 mov eax, [ebp-4]
00401037 push eax ;传递参数2,person.height
00401038 mov ecx, [ebp-8]
0040103B push ecx ;传递参数1,person.age
0040103C call sub_401000 ;调用show函数
00401041 add esp, 8
00401044 xor eax, eax
00401046 mov esp, ebp
00401048 pop ebp
00401049 retn
00401000 push ebp
00401001 mov ebp, esp
00401003 mov eax, [ebp+0Ch] ;对象首地址为ebp+8
00401006 push eax ;传递参数3,person.height
00401007 mov ecx, [ebp+8]
0040100A push ecx ;传递参数2,person.age
0040100B push offset aAgeDHeightD ;参数1,"age = %d , height = %d\n"
00401010 call sub_401090 ;调用printf函数
00401015 add esp, 0Ch
00401018 pop ebp
00401019 retn
//x86_gcc对应汇编代码相似,略
//x86_clang对应汇编代码相似,略
//x64_vs对应汇编代码讲解
0000000140001030 mov [rsp+10h], rdx
0000000140001035 mov [rsp+8], ecx
0000000140001039 sub rsp, 38h
000000014000103D mov dword ptr [rsp+20h],1;person.age=1,对象首地址为rsp+20h
0000000140001045 mov dword ptr [rsp+24h],2;person.height =1,
000000014000104D mov rcx, [rsp+20h] ;rcx 8字节大小,直接存放age+height的值
0000000140001052 call sub_140001000 ;调用show函数
0000000140001057 xor eax, eax
0000000140001059 add rsp, 38h
000000014000105D retn
0000000140001000 mov [rsp+8], rcx
0000000140001005 sub rsp, 28h ;对象首地址为rsp+30h
0000000140001009 mov r8d, [rsp+34h] ;传递参数3,person.height
000000014000100E mov edx, [rsp+30h] ;传递参数2,person.age
0000000140001012 lea rcx, aAgeDHeightD ;参数1,"age = %d , height = %d\n"
0000000140001019 call sub_1400010C0 ;调用printf函数
000000014000101E add rsp, 28h
0000000140001022 retn
//x64_gcc对应汇编代码相似,略
//x64_clang对应汇编代码相似,略
笔记--------------------------------------------start
//x86_vs对应汇编代码讲解 (Debug版)
00411650 push ebp
00411651 mov ebp, esp
00411653 sub esp, 48h
00411656 push ebx
00411657 push esi
00411658 push edi
00411659 mov [ebp+var_8], 1 ; person.age=1,对象首地址为ebp-8
00411660 mov [ebp+var_4], 2 ; person.height=2
00411667 mov eax, [ebp+var_4] ; eax=2
0041166A push eax ; 传递参数2,person.height
0041166B mov ecx, [ebp+var_8] ; ecx=1
0041166E push ecx ; 传递参数1,person.age
0041166F call j_show ; 调用show函数
00411674 add esp, 8 ; 栈平衡
00411677 xor eax, eax
00411679 pop edi
0041167A pop esi
0041167B pop ebx
0041167C mov esp, ebp
0041167E pop ebp
0041167F retn
.......
0041106E j_show proc near ; CODE XREF: main+1F↓p
0041106E jmp show
0041106E j_show endp
........
004115B0 push ebp
004115B1 mov ebp, esp
004115B3 sub esp, 40h
004115B6 push ebx
004115B7 push esi
004115B8 push edi
004115B9 mov eax, [ebp+arg_4] ; eax=height
004115BC push eax ; 传递参数3,person.height
004115BD mov ecx, [ebp+arg_0] ; ecx=age
004115C0 push ecx ; 传递参数2,person.age
004115C1 push offset aAgeDHeightD ; 参数1: "age = %d, height = %d\n"
004115C6 call j_printf ; 调用printf函数
004115CB add esp, 0Ch ; 栈平衡
004115CE pop edi
004115CF pop esi
004115D0 pop ebx
004115D1 mov esp, ebp
004115D3 pop ebp
004115D4 retn

![]()

//x64_vs对应汇编代码讲解 (Debug版)
00000001400118C0 mov [rsp-8+arg_8], rdx
00000001400118C5 mov [rsp-8+arg_0], ecx
00000001400118C9 push rbp
00000001400118CA push rdi
00000001400118CB sub rsp, 108h
00000001400118D2 lea rbp, [rsp+20h]
00000001400118D7 lea rdi, [rsp+110h+var_F0]
00000001400118DC mov ecx, 0Ah
00000001400118E1 mov eax, 0CCCCCCCCh
00000001400118E6 rep stosd
00000001400118E8 mov ecx, dword ptr [rsp+110h+arg_8]
00000001400118EF lea rcx, unk_140021003
00000001400118F6 call j___CheckForDebuggerJustMyCode
00000001400118FB mov dword ptr [rbp+0F0h+var_E8], 1 ; person.age=1,对象首地址为rsp+20h
0000000140011902 mov dword ptr [rbp+0F0h+var_E8+4], 2 ; person.height =2,
0000000140011909 mov rcx, [rbp+0F0h+var_E8] ; rcx 8字节大小,直接存放age+height的值
000000014001190D call j_show ; 调用show函数
0000000140011912 xor eax, eax
0000000140011914 mov edi, eax
0000000140011916 lea rcx, [rbp+0F0h+var_110] ; Esp
000000014001191A lea rdx, Fd ; Fd
0000000140011921 call j__RTC_CheckStackVars
0000000140011926 mov eax, edi
0000000140011928 lea rsp, [rbp+0E8h]
000000014001192F pop rdi
0000000140011930 pop rbp
0000000140011931 retn
......
000000014001109B j_show proc near ; CODE XREF: main+4D↓p
000000014001109B jmp show
000000014001109B j_show endp
......
0000000140011790 mov [rsp-8+arg_0], rcx
0000000140011795 push rbp
0000000140011796 push rdi
0000000140011797 sub rsp, 0E8h ; 对象的首地址为rbp + 0E0h
000000014001179E lea rbp, [rsp+20h]
00000001400117A3 lea rcx, unk_140021003
00000001400117AA call j___CheckForDebuggerJustMyCode
00000001400117AF mov r8d, dword ptr [rbp+0D0h+arg_0+4] ; 传递参数3,person.height
00000001400117B6 mov edx, dword ptr [rbp+0D0h+arg_0] ; 传递参数2,person.age
00000001400117BC lea rcx, Format ; 参数1,"age = %d , height = %d\n"
00000001400117C3 call j_printf ; 调用printf函数
00000001400117C8 lea rsp, [rbp+0C8h]
00000001400117CF pop rdi
00000001400117D0 pop rbp
00000001400117D1 retn

![]()

笔记--------------------------------------------end
在代码清单9-5中,类Person的体积不大,只有两个数据成员,编译器在调用函数传参的过程中分别将对象的两个成员依次压栈,也就是直接将两个数据成员当成两个int类型数据,并将它们当作printf函数的参数。64位程序中直接使用一个寄存器存储类的两个数据成员。同理,它们也是一份复制数据,除数据相同外,与对象中的两个数据成员没有关系。
类对象中数据成员的传参顺序为最先定义的数据成员最后压栈,最后定义的数据成员最先压栈。当类的体积过大,或者其中定义有数组类型的数据成员时,会将数组的首地址作为参数压栈吗?我们来看看代码清单9-6。
代码清单9-6 含有数组数据成员的对象传参(Debug版)
// C++ 源码
#include <stdio.h>
#include <string.h>
class Person {
public:
int age;
int height;
char name[32]; //定义数组类型的数据成员
};
void show(Person person) {
printf("age = %d , height = %d name:%s\n", person.age, person.height, person.name);
}
int main(int argc, char* argv[]) {
Person person;
person.age = 1;
person.height = 2;
strcpy(person.name, "tom"); //赋值数据成员数组
show(person);
return 0;
}
//x86_vs对应汇编代码讲解
00401020 push ebp
00401021 mov ebp, esp
00401023 sub esp, 2Ch
00401026 mov eax, ___security_cookie
0040102B xor eax, ebp
0040102D mov [ebp-4], eax
00401030 push esi
00401031 push edi
00401032 mov dword ptr [ebp-2Ch], 1 ;person.age=1,对象首地址ebp-2Ch
00401039 mov dword ptr [ebp-28h], 2 ;person.height=2
00401040 push offset aTom ;参数2,"tom"
00401045 lea eax, [ebp-24h] ;eax=person.name地址
00401048 push eax ;参数1,person.name
00401049 call sub_404610 ;调用strcpy函数
0040104E add esp, 0FFFFFFE0h ;调整栈顶,抬高32字节。
00401051 mov ecx, 0Ah ;设置循环次数为10
00401056 lea esi, [ebp-2Ch] ;获取对象的首地址并保存到esi
00401059 mov edi, esp ;设置edi为当前栈顶
0040105B rep movsd ;执行10次4字节内存复制,将esi所指向的数据复
;制到edi中,类似memcpy的内联方式
0040105D call sub_401000 ;调用show函数
00401062 add esp, 28h
00401065 xor eax, eax
00401067 pop edi
00401068 pop esi
00401069 mov ecx, [ebp-4]
0040106C xor ecx, ebp
0040106E call @__security_check_cookie@4 ; __security_check_cookie(x)
00401073 mov esp, ebp
00401075 pop ebp
00401076 retn
//x86_gcc对应汇编代码相似,略
//x86_clang对应汇编相似,略
//x64_vs对应汇编代码讲解
0000000140001040 mov [rsp+arg_8], rdx
0000000140001045 mov [rsp+arg_0], ecx
0000000140001049 push rsi
000000014000104A push rdi
000000014000104B sub rsp, 88h
0000000140001052 mov rax, cs:__security_cookie
0000000140001059 xor rax, rsp
000000014000105C mov [rsp+78h], rax
0000000140001061 mov dword ptr [rsp+50h], 1 ;person.age=1,对象首地址rsp+50h
0000000140001069 mov dword ptr [rsp+54h], 2 ;person.height=2
0000000140001071 lea rdx, aTom ;参数2,"tom"
0000000140001078 lea rcx, [rsp+58h] ;参数1,rcx=person.name地址
000000014000107D call sub_140004530 ;调用strcpy
0000000140001082 lea rax, [rsp+20h] ;rax=临时对象
0000000140001087 lea rcx, [rsp+50h] ;rcx=&person
000000014000108C mov rdi, rax
000000014000108F mov rsi, rcx
0000000140001092 mov ecx, 28h ;设置循环次数为40
0000000140001097 rep movsb ;复制person对象的数据成员到临时对象
0000000140001099 lea rcx, [rsp+20h] ;rcx=this,传递临时对象的this指针
000000014000109E call sub_140001000 ;调用show函数
00000001400010A3 xor eax, eax
00000001400010A5 mov rcx, [rsp+78h]
00000001400010AA xor rcx, rsp
00000001400010AD call __security_check_cookie
00000001400010B2 add rsp, 88h
00000001400010B9 pop rdi
00000001400010BA pop rsi
00000001400010BB retn
//x64_gcc对应汇编代码相似,略
//x64_clang对应汇编代码相似,略
笔记--------------------------------------------start
//x86_vs对应汇编代码讲解 (Debug版)
00411660 push ebp
00411661 mov ebp, esp
00411663 sub esp, 68h
00411666 push ebx
00411667 push esi
00411668 push edi
00411669 mov [ebp+var_28], 1 ; person.age=1,对象首地址ebp-28h
00411670 mov [ebp+var_24], 2 ; person.height=2
00411677 push offset Source ; 参数2: "tom"
0041167C lea eax, [ebp+Dest] ; eax=person.name地址
0041167F push eax ; Dest 参数1:parson.name
00411680 call j__strcpy ; 调用strcpy函数
00411685 add esp, 8 ; 栈平衡
00411688 sub esp, 28h ; 调整栈顶
0041168B mov ecx, 0Ah ; 设置循环次数为10
00411690 lea esi, [ebp+var_28] ; 获取对象的首地址并保存到esi
00411693 mov edi, esp ; 设置edi为当前栈顶
00411695 rep movsd ; 执行10次4字节内存复制,将esi所指向的数据复
00411695 ; 制到edi中,类似memcpy的内联方式
00411697 call j_show ; 调用show函数
0041169C add esp, 28h
0041169F xor eax, eax
004116A1 pop edi
004116A2 pop esi
004116A3 pop ebx
004116A4 mov esp, ebp
004116A6 pop ebp
004116A7 retn

//x64_vs对应汇编代码讲解 (Debug版)
00000001400118D0 mov [rsp-8+arg_8], rdx
00000001400118D5 mov [rsp-8+arg_0], ecx
00000001400118D9 push rbp
00000001400118DA push rsi
00000001400118DB push rdi
00000001400118DC sub rsp, 170h
00000001400118E3 lea rbp, [rsp+20h]
00000001400118E8 lea rdi, [rsp+180h+var_160]
00000001400118ED mov ecx, 24h
00000001400118F2 mov eax, 0CCCCCCCCh
00000001400118F7 rep stosd
00000001400118F9 mov ecx, dword ptr [rsp+180h+arg_8]
0000000140011900 mov rax, cs:__security_cookie
0000000140011907 xor rax, rbp
000000014001190A mov [rbp+160h+var_18], rax
0000000140011911 lea rcx, unk_140021007
0000000140011918 call j___CheckForDebuggerJustMyCode
000000014001191D mov [rbp+160h+var_158], 1 ; person.age=1,对象首地址rbp+8h
0000000140011924 mov [rbp+160h+var_154], 2 ; person.height=2
000000014001192B lea rdx, Source ; 参数2,"tom"
0000000140011932 lea rcx, [rbp+160h+Dest] ; Dest 参数1,rcx=person.name地址
0000000140011936 call j_strcpy_0 ; 调用strcpy函数
000000014001193B lea rax, [rbp+160h+var_50] ; rax=临时对象
0000000140011942 lea rcx, [rbp+160h+var_158] ; rcx=&person
0000000140011946 mov rdi, rax
0000000140011949 mov rsi, rcx
000000014001194C mov ecx, 28h ; 设置循环次数为40
0000000140011951 rep movsb ; 复制person对象的数据成员到临时对象
0000000140011953 lea rcx, [rbp+160h+var_50] ; rcx=this,传递临时对象的this指针
000000014001195A call j_show ; 调用show函数
000000014001195F xor eax, eax
0000000140011961 mov edi, eax
0000000140011963 lea rcx, [rbp+160h+var_180] ; Esp
0000000140011967 lea rdx, Fd ; Fd
000000014001196E call j__RTC_CheckStackVars
0000000140011973 mov eax, edi
0000000140011975 mov rcx, [rbp+160h+var_18]
000000014001197C xor rcx, rbp ; StackCookie
000000014001197F call j___security_check_cookie
0000000140011984 lea rsp, [rbp+150h]
000000014001198B pop rdi
000000014001198C pop rsi
000000014001198D pop rbp
000000014001198E retn


笔记--------------------------------------------end
如代码清单9-6所示,在传递类对象的过程中使用了“add esp,0FFFFFFE0h”调整栈顶指针esp,0FFFFFFE0h是补码,转换后为-20h,等同于esp-20h。6.1节介绍过,参数变量在传递时,需要向低地址调整栈顶指针esp,此处申请的32字节栈空间,加上strcpy未平衡的8字节参数空间,都用于存放参数对象person的数据。将对象person中的数据依次复制到申请的栈空间中,对象person的内存布局如图9-5所示。

图9-5 对象person的内存布局
在图9-5中,0x00AFF8A8为对象person的首地址,第一个4字节的数据为数据成员age。由此向后,第二个4字节的数据为数据成员height,以0x00AFF8B0为起始地址,后面的第一个32字节数据为数组成员name。对象person占用的内存大小为40字节,而代码清单9-6却只为栈空间申请了32字节。以对象的首地址为起始点,使用指令“rep movs”复制了40字节的数据,比栈空间申请的大小多出了8字节。为什么申请栈空间时少了8字节呢?数据复制完成后会不会造成越界访问呢?
我们先看一下之前调用的函数strcpy,该函数的调用方式为__cdecl,当函数调用结束后,并没有平衡参数使用的栈顶。函数strcpy有两个参数,正好使用了8字节的栈空间。在函数show的调用过程中,重新利用这8字节的栈空间,完成了对对象person中数据的复制。当函数show调用结束后,调用指令“add esp,28h”平衡了该函数参数使用的40字节栈空间。
在64位程序中,因为栈顶为栈预留空间,所以无法将对象的数据成员复制到栈顶,编译器将对象的数据成员先复制到临时对象,再将临时对象的地址传递给show函数,在show函数内部使用this指针间接访问对象的数据成员。
代码清单9-4和代码清单9-5中定义的类都没有定义构造函数和析构函数。对象作为参数在传递过程中会制作一份对象的复制数据,当向对象分配内存时,如果有构造函数,编译器会再调用一次构造函数,并做一些初始化工作。当代码执行到作用域结束时,局部对象将被销毁,而对象中可能会涉及资源释放的问题,同样,编译器也会再调用一次局部对象的析构函数,从而完成资源数据的释放。
有参考资料中提到,当类中没有定义构造函数和析构函数时,编译器会添加默认的构造函数和析构函数。根据代码清单9-4和代码清单9-5中的分析可知,在定义类对象时,编译器根本没有做任何处理,可见编译器并没有添加默认的构造函数。其原因涉及更多构造函数与析构函数的知识,详见第10章。
当对象作为函数的参数时,因为重新复制了对象,所以等同于又定义了一个对象,在某些情况下会调用特殊的构造函数——复制构造函数,详见第10章。当函数退出时,复制的对象作为函数内的局部变量,将会被销毁。当存在析构函数时,则会调用析构函数,这时问题就会出现了,如代码清单9-7所示。
代码清单9-7 对象作为参数的资源释放错误(Debug版)
// C++ 源码说明:涉及资源申请与释放的类对象
#include <stdio.h>
#include <string.h>
class Person {
public:
Person() {
name = new char[32]; //申请堆空间,只要不释放,进程退出前将一直存在
if (name != NULL) { //堆空间申请成功与否
strcpy(name, "tom");
}
}
~Person() {
if (name != NULL) { //检查资源
delete[] name; //释放堆空间
name = NULL;
}
}
const char* getName() {
return name; //获取数据成员
}
private:
char *name; //数据成员定义,保存堆的首地址
};
//参数为Person类对象的函数
void show(Person obj) {
printf(obj.getName());
}
int main(int argc, char* argv[]) {
Person person; //类对象定义
show(person);
return 0;
}
//x86_vs对应汇编代码讲解
00401020 push ebp
00401021 mov ebp, esp
00401023 sub esp, 8
00401026 lea ecx, [ebp-4] ;传递this指针,ecx=&person
00401029 call sub_401050 ;调用构造函数
0040102E mov eax, [ebp-4] ;person对象长度为4,一个寄存器单元刚好能存放
;于是eax获取对象首地址处4字节的数据,即数据成员name
00401031 push eax ;传递对象参数
00401032 call sub_401000 ;调用show函数
00401037 add esp, 4
0040103A mov dword ptr [ebp-8], 0
00401041 lea ecx, [ebp-4] ;传递this指针,ecx=&person
00401044 call sub_401090 ;调用析构函数
00401049 mov eax, [ebp-8]
0040104C mov esp, ebp
0040104E pop ebp
0040104F retn
00401000 push ebp ;show函数
00401001 mov ebp, esp
00401003 lea ecx, [ebp+8] ;传递this指针,ecx=&obj
00401006 call sub_4010D0 ;调用getName函数
0040100B push eax ;参数1, eax=obj.name
0040100C call sub_401120 ;调用printf函数
00401011 add esp, 4
00401014 lea ecx, [ebp+8] ;传递this指针,ecx=&obj
00401017 call sub_401090 ;调用析构函数
0040101C pop ebp
0040101D retn
//x86_gcc对应汇编代码相似,略
//x86_clang对应汇编代码相似,略
//x64_vs对应汇编代码讲解
0000000140001030 mov [rsp+10h], rdx
0000000140001035 mov [rsp+8], ecx
0000000140001039 sub rsp, 38h
000000014000103D lea rcx, [rsp+28h] ;传递this指针,rcx=&person
0000000140001042 call sub_140001070 ;调用构造函数
0000000140001047 mov rcx, [rsp+28h] ;传递this指针,rcx=&person
000000014000104C call sub_140001000 ;调用show函数
0000000140001051 mov dword ptr [rsp+20h], 0
0000000140001059 lea rcx, [rsp+28h] ;传递this指针,rcx=&person
000000014000105E call sub_1400010C0 ;调用析构函数
0000000140001063 mov eax, [rsp+20h]
0000000140001067 add rsp, 38h
000000014000106B retn
0000000140001000 mov [rsp+8], rcx ;show函数
0000000140001005 sub rsp, 28h
0000000140001009 lea rcx, [rsp+30h] ;传递this指针,rcx=&obj
000000014000100E call sub_140001100 ;调用getName函数
0000000140001013 mov rcx, rax ;参数1, rcx=obj.name
0000000140001016 call sub_140001170 ;调用printf函数
000000014000101B lea rcx, [rsp+30h] ;传递this指针,rcx=&obj
0000000140001020 call sub_1400010C0 ;调用析构函数
0000000140001025 add rsp, 28h
0000000140001029 retn
//x64_gcc对应汇编代码相似,略
//x64_clang对应汇编代码相似,略
笔记--------------------------------------------start
//x86_vs对应汇编代码讲解 (Debug版)
004118E0 push ebp
004118E1 mov ebp, esp
004118E3 push 0FFFFFFFFh
004118E5 push offset main_SEH ; 压入异常处理函数地址 main_SEH
004118EA mov eax, large fs:0
004118F0 push eax
004118F1 mov large fs:0, esp ; 使用SEH(结构化异常处理)
004118F8 sub esp, 48h
004118FB push ebx
004118FC push esi
004118FD push edi
004118FE lea ecx, [ebp+var_10] ; 传递this指针,ecx=&person
00411901 call j_Person__Person ; 调用构造函数
00411906 mov [ebp+var_4], 0
0041190D mov eax, [ebp+var_10] ; person对象长度为4,一个寄存器单元刚好能存放
0041190D ; 于是eax获取对象首地址处4字节的数据,即数据成员name
00411910 push eax ; 传递对象参数
00411911 call j_show ; 调用show函数
00411916 add esp, 4 ; 栈平衡
00411919 mov [ebp+var_54], 0
00411920 mov [ebp+var_4], 0FFFFFFFFh
00411927 lea ecx, [ebp+var_10] ; 传递this指针,ecx=&person
0041192A call j_Person___Person ; 调用析构函数
0041192F mov eax, [ebp+var_54]
00411932 mov ecx, [ebp+var_C]
00411935 mov large fs:0, ecx ; 恢复SEH链表并返回
0041193C pop edi
0041193D pop esi
0041193E pop ebx
0041193F mov esp, ebp
00411941 pop ebp
00411942 retn
.......
00411082 j_show proc near ; CODE XREF: main+31↓p
00411082 jmp show
00411082 j_show endp
.......
00411800 push ebp
00411801 mov ebp, esp
00411803 push 0FFFFFFFFh
00411805 push offset show_SEH ; 结构化异常处理
0041180A mov eax, large fs:0
00411810 push eax
00411811 mov large fs:0, esp
00411818 sub esp, 40h
0041181B push ebx
0041181C push esi
0041181D push edi
0041181E mov [ebp+var_4], 0
00411825 lea ecx, [ebp+arg_0] ; 传递this指针,ecx=&obj
00411828 call j_Person__getName ; 调用getName函数
0041182D push eax ; 参数1, eax=obj.name
0041182E call j_printf ; 调用printf函数
00411833 add esp, 4 ; 栈平衡
00411836 mov [ebp+var_4], 0FFFFFFFFh
0041183D lea ecx, [ebp+arg_0] ; 传递this指针,ecx=&obj
00411840 call j_Person___Person ; 调用析构函数
00411845 mov ecx, [ebp+var_C]
00411848 mov large fs:0, ecx ; 恢复结构化异常处理
0041184F pop edi
00411850 pop esi
00411851 pop ebx
00411852 mov esp, ebp
00411854 pop ebp
00411855 retn

![]()

//x64_vs对应汇编代码讲解 (Debug版)
0000000140011BF0 mov [rsp-8+arg_8], rdx
0000000140011BF5 mov [rsp-8+arg_0], ecx
0000000140011BF9 push rbp
0000000140011BFA push rdi
0000000140011BFB sub rsp, 128h
0000000140011C02 lea rbp, [rsp+20h]
0000000140011C07 lea rdi, [rsp+130h+var_110]
0000000140011C0C mov ecx, 12h
0000000140011C11 mov eax, 0CCCCCCCCh
0000000140011C16 rep stosd
0000000140011C18 mov ecx, dword ptr [rsp+130h+arg_8]
0000000140011C1F mov rax, cs:__security_cookie
0000000140011C26 xor rax, rbp
0000000140011C29 mov [rbp+110h+var_18], rax
0000000140011C30 lea rcx, unk_140024007
0000000140011C37 call j___CheckForDebuggerJustMyCode
0000000140011C3C mov edx, 8
0000000140011C41 lea rcx, [rbp+110h+var_108] ; 传递this指针,rcx=&person
0000000140011C45 call j_Person____autoclassinit2
0000000140011C4A lea rcx, [rbp+110h+var_108]
0000000140011C4E call j_Person__Person ; 调用构造函数
0000000140011C53 nop
0000000140011C54 mov rcx, [rbp+110h+var_108] ; 传递this指针,rcx=&person
0000000140011C58 call j_show ; 调用show函数
0000000140011C5D mov [rbp+110h+var_2C], 0
0000000140011C67 lea rcx, [rbp+110h+var_108] ; 传递this指针,rcx=&person
0000000140011C6B call j_Person___Person ; 调用析构函数
0000000140011C70 mov eax, [rbp+110h+var_2C]
0000000140011C76 mov edi, eax
0000000140011C78 lea rcx, [rbp+110h+var_130] ; Esp
0000000140011C7C lea rdx, Fd ; Fd
0000000140011C83 call j__RTC_CheckStackVars
0000000140011C88 mov eax, edi
0000000140011C8A mov rcx, [rbp+110h+var_18]
0000000140011C91 xor rcx, rbp ; StackCookie
0000000140011C94 call j___security_check_cookie
0000000140011C99 lea rsp, [rbp+108h]
0000000140011CA0 pop rdi
0000000140011CA1 pop rbp
0000000140011CA2 retn
.......
00000001400110AF j_show proc near ; CODE XREF: main+68↓p
00000001400110AF jmp show
00000001400110AF j_show endp
.......
0000000140011AC0 mov [rsp-8+arg_0], rcx
0000000140011AC5 push rbp
0000000140011AC6 push rdi
0000000140011AC7 sub rsp, 0E8h
0000000140011ACE lea rbp, [rsp+20h]
0000000140011AD3 lea rcx, unk_140024007
0000000140011ADA call j___CheckForDebuggerJustMyCode
0000000140011ADF lea rcx, [rbp+0D0h+arg_0] ; 传递this指针,rcx=&obj
0000000140011AE6 call j_Person__getName ; 调用getName函数
0000000140011AEB mov rcx, rax ; 参数1, rcx=obj.name
0000000140011AEE call j_printf ; 调用printf函数
0000000140011AF3 nop
0000000140011AF4 lea rcx, [rbp+0D0h+arg_0] ; 传递this指针,rcx=&obj
0000000140011AFB call j_Person___Person ; 调用析构函数
0000000140011B00 lea rsp, [rbp+0C8h]
0000000140011B07 pop rdi
0000000140011B08 pop rbp
0000000140011B09 retn


![]()

笔记--------------------------------------------end
在代码清单9-7中,当对象作为参数传递时,参数obj复制了对象person中的数据成员name,产生了两个Person类的对象。由于没有编写复制构造函数,因此在传递参数的时候就没有被调用,这时候编译器以浅拷贝处理,它们的数据成员name都指向了同一个堆地址,如图9-6所示。

图9-6 复制对象与原对象对比
如图9-6所示,两个对象中的数据成员name指向了相同地址,当函数show调用结束后,便会释放对象obj,以对象obj的首地址作为this指针调用析构函数。在析构函数中,调用delete函数释放对象obj的数据成员name保存的堆空间的首地址。但对象obj是person的复制品,真正的person仍存在,而数据成员name保存的堆空间的首地址却被释放了,如果出现以下代码便会产生错误。
Person person;
// 当该函数调用结束后,对象person中的数据成员name保存的堆空间已经
// 被释放,再次使用此对象中的数据成员name便无法得到堆空间的数据
show(person);
show(person); ; 显示地址中为错误数据
这个错误在VS的编译中被触发,因为使用delete函数后,堆空间被置为某个标记值;而在02选项中,并不会对释放堆中的数据进行检查。如果没有再次申请堆空间,此地址中的数据仍然存在,会导致错误被隐蔽,为程序埋下隐患。
有两种解决方案可以修正这个错误:深拷贝数据和设置引用计数,这两种解决方案都需要复制构造函数的配合。本节中只做简单的讲解,详见第10章。
1. 深拷贝数据
在复制对象时,编译器会调用一次该类的复制构造函数,给编码者一次机会。深拷贝利用这次机会原对象的数据成员保存的资源信息制作一份副本。
这样,当销毁复制对象时,销毁的资源是复制对象在复制构造函数中制作的副本,而非原对象中保存的资源信息。
2. 设置引用计数
在进入复制构造函数时,记录类对象被复制引用的次数。当对象被销毁时,检查这个引用计数中保存的引用复制次数是否为0。如果是,则释放申请的资源,否则引用计数减1。
当参数为对象的指针类型时,则不存在这种错误。传递的数据是指针类型,在函数内的操作都是针对原对象的,不存在对象被复制的问题。因为没有副本,所以在函数进入和退出时不会调用构造函数和析构函数,也就不存在资源释放的错误隐患。在使用类对象作为参数时,如无特殊需求,应尽量使用指针或引用。这样做不但可以避免资源释放的错误隐患,还可以在函数调用过程中避免复制对象的过程,提升程序运行的效率。
由于我们目前所学知识还无法修正这个错误,因此暂且将其搁置。虽然错误没有解决,但并不影响后面的学习。学习了构造函数和析构函数的相关知识后,这个问题便会迎刃而解。
笔者并不赞成在设计软件时将申请资源的工作交给构造函数来完成,此处仅仅为了讲解实例。
9.5 对象作为返回值
对象作为函数的返回值时,与基本的数据类型不同。基本数据类型(浮点类型、非标准类型除外)作为返回值时,32位程序不超过4字节的数据通过eax返回,在64位程序不超过8字节的数据使用rax返回。而对象属于自定义类型,寄存器eax/rax无法保存对象中的所有数据,所以在函数返回时,寄存器eax/rax不能满足需求。
对象作为返回值与对象作为参数的处理方式非常相似。对象作为参数时,进入函数前预先保留对象使用的栈空间并将实参对象中的数据复制到栈空间中。该栈空间作为函数参数,用于函数内部使用。同理,对象作为返回值时,进入函数后将申请返回对象使用的栈空间,在退出函数时,将返回对象中的数据复制到临时的栈空间中,以这个临时栈空间的首地址作为返回值。
我们先由简单的类对象作为返回值入手,由浅入深地学习。先来看一个例子,如代码清单9-8所示。
代码清单9-8 对象作为返回值(Debug版)
// C++ 源码
#include <stdio.h>
class Person {
public:
int count;
int buffer[10]; //定义两个数据成员,该类的大小为44字节
};
Person getPerson() {
Person person;
person.count = 10;
for (int i = 0; i < 10; i++){
person.buffer[i] = i+1;
}
return person;
}
int main(int argc, char* argv[]) {
Person person;
person = getPerson();
printf("%d %d %d", person.count, person.buffer[0], person.buffer[9]);
return 0;
}
//x86_vs对应汇编代码讲解
00401060 push ebp
00401061 mov ebp, esp
00401063 sub esp, 88h ;预留返回对象的栈空间
00401069 mov eax, ___security_cookie
0040106E xor eax, ebp
00401070 mov [ebp-4], eax
00401073 push esi
00401074 push edi
00401075 lea eax, [ebp-88h] ;获取返回对象的栈空间首地址
0040107B push eax ;将返回对象的首地址压入栈中,用于保存返回对象的数据
0040107C call sub_401000 ;调用getPerson函数
00401081 add esp, 4 ;函数调用结束后,eax中保存着地址ebp-88h,即返回对
;象的首地址
00401084 mov ecx, 0Bh ;设置循环次数
00401089 mov esi, eax ;将返回对象的首地址存入esi中
0040108B lea edi, [ebp-5Ch] ;获取临时对象的首地址
0040108E rep movsd ;每次从返回对象中复制4字节数据到临时对象的
;地址中,共复制11次
00401090 mov ecx, 0Bh ;重新设置复制次数
00401095 lea esi, [ebp-5Ch] ;获取临时对象的首地址
00401098 lea edi, [ebp-30h] ;获取对象person的首地址
0040109B rep movsd ;将数据复制到对象person中
0040109D mov ecx, 4
004010A2 imul edx, ecx, 9
004010A5 mov eax, [ebp+edx-2Ch] ;eax=[ebp-30h+4+9*4]
004010A9 push eax ;参数4,person.buffer[9]
004010AA mov ecx, 4
004010AF imul edx, ecx, 0
004010B2 mov eax, [ebp+edx-2Ch] ;eax=[ebp-30h+4+0*4]
004010B6 push eax ;参数3,person.buffer[0]
004010B7 mov ecx, [ebp-30h]
004010BA push ecx ;参数2,person.count
004010BB push offset aDDD ;参数1,"%d %d %d"
004010C0 call sub_401120 ;调用printf函数
004010C5 add esp, 10h
004010C8 xor eax, eax
004010CA pop edi
004010CB pop esi
004010CC mov ecx, [ebp-4]
004010CF xor ecx, ebp
004010D1 call @__security_check_cookie@4 ; __security_check_cookie(x)
004010D6 mov esp, ebp
004010D8 pop ebp
004010D9 retn
00401000 push ebp
00401001 mov ebp, esp
00401003 sub esp, 34h
00401006 mov eax, ___security_cookie
0040100B xor eax, ebp
0040100D mov [ebp-4], eax
00401010 push esi
00401011 push edi
00401012 mov dword ptr [ebp-30h], 0Ah ;person.count = 10
00401019 mov dword ptr [ebp-34h], 0 ;i=0
00401020 jmp short loc_40102B ;for循环
00401022 mov eax, [ebp-34h]
00401025 add eax, 1
00401028 mov [ebp-34h], eax ;i++
0040102B cmp dword ptr [ebp-34h], 0Ah
0040102F jge short loc_401040 ;如果i>=10,则结束循环
00401031 mov ecx, [ebp-34h]
00401034 add ecx, 1
00401037 mov edx, [ebp-34h]
0040103A mov [ebp+edx*4-2Ch], ecx ;person.buffer[i] = i+1;
0040103E jmp short loc_401022
00401040 mov ecx, 0Bh ;设置循环次数为11次
00401045 lea esi, [ebp-30h] ;获取局部对象的首地址,&person
00401048 mov edi, [ebp+8] ;获取返回对象的首地址
0040104B rep movsd ;将局部对象person中的数据复制到返回对象中
0040104D mov eax, [ebp+8] ;获取返回对象的首地址并保存到eax中,作为
;返回值
00401050 pop edi
00401051 pop esi
00401052 mov ecx, [ebp-4]
00401055 xor ecx, ebp
00401057 call @__security_check_cookie@4 ; __security_check_cookie(x)
0040105C mov esp, ebp
0040105E pop ebp
0040105F retn
//x86_gcc对应汇编代码相似,略
//x86_clang对应汇编代码相似,略
//x64_vs对应汇编代码讲解
0000000140001080 mov [rsp+10h], rdx
0000000140001085 mov [rsp+8], ecx
0000000140001089 push rsi
000000014000108A push rdi
000000014000108B sub rsp, 0C8h ;预留返回对象的栈空间
0000000140001092 mov rax, cs:__security_cookie
0000000140001099 xor rax, rsp
000000014000109C mov [rsp+0B0h], rax
00000001400010A4 lea rcx, [rsp+20h] ;参数1,返回对象的首地址
00000001400010A9 call sub_140001000 ;调用getPerson函数
00000001400010AE lea rcx, [rsp+80h]
00000001400010B6 mov rdi, rcx ;获取临时对象的首地址
00000001400010B9 mov rsi, rax ;返回对象的首地址存入rsi中
00000001400010BC mov ecx, 2Ch ;循环次数
00000001400010C1 rep movsb ;复制数据到临时对象的地址中,共复制44字节
00000001400010C3 lea rax, [rsp+50h] ;获取对象person的首地址
00000001400010C8 lea rcx, [rsp+80h] ;获取临时对象的首地址
00000001400010D0 mov rdi, rax
00000001400010D3 mov rsi, rcx
00000001400010D6 mov ecx, 2Ch ;重新设置复制次数
00000001400010DB rep movsb ;将数据复制到对象person中
00000001400010DD mov eax, 4
00000001400010E2 imul rax, 9
00000001400010E6 mov ecx, 4
00000001400010EB imul rcx, 0
00000001400010EF mov r9d, [rsp+rax+54h] ;参数4,person.buffer[9]
00000001400010F4 mov r8d, [rsp+rcx+54h] ;参数3,person.buffer[0]
00000001400010F9 mov edx, [rsp+50h] ;参数2,person.count
00000001400010FD lea rcx, aDDD ;参数1,"%d %d %d"
0000000140001104 call sub_140001190 ;调用printf函数
0000000140001109 xor eax, eax
000000014000110B mov rcx, [rsp+0B0h]
0000000140001113 xor rcx, rsp ; StackCookie
0000000140001116 call __security_check_cookie
000000014000111B add rsp, 0C8h
0000000140001122 pop rdi
0000000140001123 pop rsi
0000000140001124 retn
0000000140001000 mov [rsp+8], rcx
0000000140001005 push rsi
0000000140001006 push rdi
0000000140001007 sub rsp, 48h
000000014000100B mov rax, cs:__security_cookie
0000000140001012 xor rax, rsp
0000000140001015 mov [rsp+38h], rax
000000014000101A mov dword ptr [rsp+8], 0Ah ;person.count = 10
0000000140001022 mov dword ptr [rsp], 0 ;i=0
0000000140001029 jmp short loc_140001033 ;for循环
000000014000102B mov eax, [rsp]
000000014000102E inc eax
0000000140001030 mov [rsp], eax ;i++
0000000140001033 cmp dword ptr [rsp], 0Ah
0000000140001037 jge short loc_140001048 ;如果i>=10,则结束循环
0000000140001039 mov eax, [rsp]
000000014000103C inc eax
000000014000103E movsxd rcx, dword ptr [rsp]
0000000140001042 mov [rsp+rcx*4+0Ch], eax ;person.buffer[i] = i+1;
0000000140001046 jmp short loc_14000102B
0000000140001048 lea rax, [rsp+8]
000000014000104D mov rdi, [rsp+60h] ;获取返回对象的首地址
0000000140001052 mov rsi, rax ;获取局部对象的首地址,&person
0000000140001055 mov ecx, 2Ch ;设置循环次数为44次
000000014000105A rep movsb ;将局部对象person中的数据复制到返回对象中
000000014000105C mov rax, [rsp+60h] ;获取返回对象的首地址并保存到rax中,作为返回值
0000000140001061 mov rcx, [rsp+38h]
0000000140001066 xor rcx, rsp ; StackCookie
0000000140001069 call __security_check_cookie
000000014000106E add rsp, 48h
0000000140001072 pop rdi
0000000140001073 pop rsi
0000000140001074 retn
//x64_gcc对应汇编代码相似,略
//x64_clang对应汇编代码相似,略
笔记--------------------------------------------start
//x86_vs对应汇编代码讲解 (Debug版)
00411650 push ebp
00411651 mov ebp, esp
00411653 sub esp, 0C4h ; 预留返回对象的栈空间
00411659 push ebx
0041165A push esi
0041165B push edi
0041165C lea eax, [ebp+var_C4] ; 获取返回对象的栈空间首地址
00411662 push eax ; 将返回对象的首地址压入栈中,用于保存返回对象的数据
00411663 call j_getPerson ; 调用getPerson函数
00411668 add esp, 4 ; 函数调用结束后,eax中保存着地址ebp-0C4h,
00411668 ; 即返回对象的首地址
0041166B mov ecx, 0Bh ; 设置循环次数
00411670 mov esi, eax ; 将返回对象的首地址存入esi中
00411672 lea edi, [ebp+var_98] ; 获取临时对象的首地址
00411678 rep movsd ; 每次从返回对象中复制4字节数据到临时对象的
00411678 ; 地址中,共复制11次
0041167A mov ecx, 0Bh ; 重新设置复制次数
0041167F lea esi, [ebp+var_98] ; 获取临时对象的首地址
00411685 lea edi, [ebp+var_2C] ; 获取对象person的首地址
00411688 rep movsd ; 将数据复制到对象person中
0041168A mov eax, 4
0041168F imul ecx, eax, 9 ; ecx=4*9
00411692 mov edx, [ebp+ecx+var_28] ; eax=[ebp+4*9-2Ch+4]
00411696 push edx ; 参数4,person.buffer[9]
00411697 mov eax, 4
0041169C imul ecx, eax, 0
0041169F mov edx, [ebp+ecx+var_28] ; eax=[ebp+0*9-2Ch+4]
004116A3 push edx ; 参数3,person.buffer[0]
004116A4 mov eax, [ebp+var_2C]
004116A7 push eax ; 参数2,person.count
004116A8 push offset aDDD ; "%d %d %d\n"
004116AD call j_printf ; 调用printf函数
004116B2 add esp, 10h
004116B5 xor eax, eax
004116B7 pop edi
004116B8 pop esi
004116B9 pop ebx
004116BA mov esp, ebp
004116BC pop ebp
004116BD retn
......
004111C7 j_getPerson proc near ; CODE XREF: main+13↓p
004111C7 jmp getPerson
004111C7 j_getPerson endp
.......
00411750 push ebp
00411751 mov ebp, esp
00411753 sub esp, 70h
00411756 push ebx
00411757 push esi
00411758 push edi
00411759 mov [ebp+var_2C], 0Ah ; person.count = 10
00411760 mov [ebp+var_30], 0 ; i=0
00411767 jmp short loc_411772 ; 跳转到for循环
00411769 ; ---------------------------------------------------------------------------
00411769
00411769 loc_411769: ; CODE XREF: getPerson+35↓j
00411769 mov eax, [ebp+var_30]
0041176C add eax, 1
0041176F mov [ebp+var_30], eax ; i++
00411772
00411772 loc_411772: ; CODE XREF: getPerson+17↑j
00411772 cmp [ebp+var_30], 0Ah
00411776 jge short loc_411787 ; 如果i>=10,则结束循环
00411778 mov eax, [ebp+var_30]
0041177B add eax, 1
0041177E mov ecx, [ebp+var_30]
00411781 mov [ebp+ecx*4+var_28], eax ; person.buffer[i] = i+1;
00411785 jmp short loc_411769 ; 继续for循环
00411787 ; ---------------------------------------------------------------------------
00411787
00411787 loc_411787: ; CODE XREF: getPerson+26↑j
00411787 mov ecx, 0Bh ; 设置循环次数为11次
0041178C lea esi, [ebp+var_2C] ; 获取局部对象的首地址,&person
0041178F mov edi, [ebp+arg_0] ; 获取返回对象的首地址
00411792 rep movsd ; 将局部对象person中的数据复制到返回对象中
00411794 mov eax, [ebp+arg_0] ; 获取返回对象的首地址并保存到eax中,作为返回值
00411797 pop edi
00411798 pop esi
00411799 pop ebx
0041179A mov esp, ebp
0041179C pop ebp
0041179D retn


![]()

//x64_vs对应汇编代码讲解 (Debug版)
0000000140011930 mov [rsp-8+arg_8], rdx
0000000140011935 mov [rsp-8+arg_0], ecx
0000000140011939 push rbp
000000014001193A push rsi
000000014001193B push rdi
000000014001193C sub rsp, 1D0h ; 预留返回对象的栈空间
0000000140011943 lea rbp, [rsp+20h]
0000000140011948 lea rdi, [rsp+1E0h+var_1C0]
000000014001194D mov ecx, 3Ch
0000000140011952 mov eax, 0CCCCCCCCh
0000000140011957 rep stosd
0000000140011959 mov ecx, dword ptr [rsp+1E0h+arg_8]
0000000140011960 lea rcx, unk_140021007
0000000140011967 call j___CheckForDebuggerJustMyCode
000000014001196C lea rcx, [rbp+1C0h+var_58] ; 参数1,返回对象的首地址
0000000140011973 call j_getPerson ; 调用getPerson函数
0000000140011978 lea rcx, [rbp+1C0h+var_A8] ; 获取临时对象的首地址
000000014001197F mov rdi, rcx
0000000140011982 mov rsi, rax ; 返回对象的首地址存入rsi中
0000000140011985 mov ecx, 2Ch ; 循环次数
000000014001198A rep movsb ; 复制数据到临时对象的地址中,共复制44字节
000000014001198C lea rax, [rbp+1C0h+var_1B8] ; 获取对象person的首地址
0000000140011990 lea rcx, [rbp+1C0h+var_A8] ; 获取临时对象的首地址
0000000140011997 mov rdi, rax
000000014001199A mov rsi, rcx
000000014001199D mov ecx, 2Ch ; 重新设置复制次数
00000001400119A2 rep movsb ; 将数据复制到对象person中
00000001400119A4 mov eax, 4
00000001400119A9 imul rax, 9
00000001400119AD mov ecx, 4
00000001400119B2 imul rcx, 0
00000001400119B6 mov r9d, [rbp+rax+1C0h+var_1B4] ; 参数4,person.buffer[9]
00000001400119BB mov r8d, [rbp+rcx+1C0h+var_1B4] ; 参数3,person.buffer[0]
00000001400119C0 mov edx, [rbp+1C0h+var_1B8] ; 参数2,person.count
00000001400119C3 lea rcx, Format ; 参数1,"%d %d %d\n"
00000001400119CA call j_printf ; 调用printf函数
00000001400119CF xor eax, eax
00000001400119D1 mov edi, eax
00000001400119D3 lea rcx, [rbp+1C0h+var_1E0] ; Esp
00000001400119D7 lea rdx, stru_140019D00 ; Fd
00000001400119DE call j__RTC_CheckStackVars
00000001400119E3 mov eax, edi
00000001400119E5 lea rsp, [rbp+1B0h]
00000001400119EC pop rdi
00000001400119ED pop rsi
00000001400119EE pop rbp
00000001400119EF retn
.......
0000000140011262 j_getPerson proc near ; CODE XREF: main+43↓p
0000000140011262 jmp getPerson
0000000140011262 j_getPerson endp
.......
0000000140011790 mov [rsp-8+arg_0], rcx
0000000140011795 push rbp
0000000140011796 push rsi
0000000140011797 push rdi
0000000140011798 sub rsp, 150h
000000014001179F lea rbp, [rsp+20h]
00000001400117A4 lea rdi, [rsp+160h+var_140]
00000001400117A9 mov ecx, 1Ch
00000001400117AE mov eax, 0CCCCCCCCh
00000001400117B3 rep stosd
00000001400117B5 mov rcx, [rsp+160h+arg_8]
00000001400117BD lea rcx, unk_140021007
00000001400117C4 call j___CheckForDebuggerJustMyCode
00000001400117C9 mov [rbp+140h+var_138], 0Ah ; person.count = 10
00000001400117D0 mov [rbp+140h+var_EC], 0 ; i=0
00000001400117D7 jmp short loc_1400117E1 ; 跳转到for循环
00000001400117D9 loc_1400117D9: ; CODE XREF: getPerson+64↓j
00000001400117D9 mov eax, [rbp+140h+var_EC] ; eax=i
00000001400117DC inc eax ; eax=i+1
00000001400117DE mov [rbp+140h+var_EC], eax ; i++
00000001400117E1
00000001400117E1 loc_1400117E1: ; CODE XREF: getPerson+47↑j
00000001400117E1 cmp [rbp+140h+var_EC], 0Ah
00000001400117E5 jge short loc_1400117F6 ; 如果i>=10,则结束循环
00000001400117E7 mov eax, [rbp+140h+var_EC]
00000001400117EA inc eax
00000001400117EC movsxd rcx, [rbp+140h+var_EC]
00000001400117F0 mov [rbp+rcx*4+140h+var_134], eax ; person.buffer[i] = i+1;
00000001400117F4 jmp short loc_1400117D9 ; 继续for循环
00000001400117F6 loc_1400117F6: ; CODE XREF: getPerson+55↑j
00000001400117F6 lea rax, [rbp+140h+var_138]
00000001400117FA mov rdi, [rbp+140h+arg_0] ; 获取返回对象的首地址
0000000140011801 mov rsi, rax ; 获取局部对象的首地址,&person
0000000140011804 mov ecx, 2Ch ; 设置循环次数为44次
0000000140011809 rep movsb ; 将局部对象person中的数据复制到返回对象中
000000014001180B mov rax, [rbp+140h+arg_0] ; 获取返回对象的首地址并保存到rax中,
000000014001180B ; 作为返回值
0000000140011812 mov rdi, rax
0000000140011815 lea rcx, [rbp+140h+var_160] ; Esp
0000000140011819 lea rdx, Fd ; Fd
0000000140011820 call j__RTC_CheckStackVars
0000000140011825 mov rax, rdi
0000000140011828 lea rsp, [rbp+130h]
000000014001182F pop rdi
0000000140011830 pop rsi
0000000140011831 pop rbp
0000000140011832 retn


![]()


笔记--------------------------------------------end
代码清单9-8演示了函数返回对象的全过程。在调用getPerson前,编译器将在main()函数中申请的返回对象的首地址作为参数压栈,在函数getPerson调用结束后进行数据复制,将getPerson函数中定义的局部对象person的数据复制到这个返回对象的空间中,再将这个返回的对象复制给目标对象person,从而达到返回对象的目的。因为在这个示例中不存在函数返回后为对象的引用赋值,所以这里的返回对象是临时存在的,也就是C++中的临时对象,作用域仅限于单条语句。
为什么会产生这个临时对象呢?因为调用返回对象的函数时,C++程序员可能采用这类写法,如getPerson().count,这只是针对返回对象的操作,而此时函数已经退出,其栈帧也被关闭了。函数退出后去操作局部对象显然不合适,因此只能由函数的调用方准备空间,建立临时对象,然后将函数中的局部对象复制给临时对象,再把这个临时对象交给调用方去操作。本例中的person =getPerson();是个赋值运算,因为赋值时getPerson函数已经退出,所以栈空间也关闭了。同理,person不能直接和函数内局部对象做赋值运算,因此需要临时对象记录返回值之后再参与赋值。
虽然使用临时对象进行了数据复制,但是同样存在出错的风险。这与对象作为参数时遇到的情况一样,因为使用了临时对象进行数据复制,所以当临时对象被销毁时,会执行析构函数。如果析构函数中有对资源释放的处理,就有可能产生同一个资源多次释放的错误。
这个错误与对象作为函数参数时的错误在原理上是一样的,也是临时对象被析构造成的,因此两者的解决方案也相同。对于复制对象的资源释放错误,我们会在第10章分析错误的处理过程并给出详细的解决方案。
当对象作为函数的参数时,可以传递指针;当对象作为返回值时,如果对象在函数内部被定义为局部变量,则不可返回此对象的首地址或引用,以避免返回已经被释放的局部变量,代码如下所示。
class Test{
public:
int mem1;
int mem2;
};
// 错误1:返回局部对象的首地址
Test* getTest(){
Test test;
return &test;
}
//错误2:返回局部对象的引用,等同于返回局部对象的首地址
Test& getTest(){
Test test;
return test;
}
由于函数退出后栈内对象的空间将被释放,因此无法保证返回值指向地址的数据的正确性。引用返回值后,如果运气好,会导致数据访问错误和程序当场出错。如果运气再好一点,程序就会直接崩溃,这样就能在调试的时候发现错误。如果运气实在很差,在开发时数据访问正常,程序也工作正常,这个问题可能会成为一个隐藏很深的错误。不过不用太担心,只要你在VS工程中设置的警告级别(warning level)不是None,这个问题在编译检查时就会被警告,只要你不漠视编译器的每个警告就行,最好把Warnings as errors打上勾。要解决此类错误,只能避免返回函数内局部变量的地址,但可以返回堆地址,还可以使用返回对象的办法来代替。由此可见,使用返回值为类对象的情况具有特殊的意义。
编译器在处理简单的结构体和类结构时,二者经过02选项的编译优化后,将难以区分它们和局部变量,但仍可根据数据的访问过程还原相应的数据,如代码清单9-9所示。
代码清单9-9 还原对象数据(Release版)
//x86_vs对应汇编代码讲解
00401010 push 0Ah ;常量传播优化
00401012 push 1 ;常量传播优化
00401014 push 0Ah ;常量传播优化
00401016 push offset aDDD ;"%d %d %d"
0040101B call sub_401030 ;调用printf函数
00401020 add esp, 10h
00401023 xor eax, eax
00401025 retn
//x86_gcc对应汇编代码讲解
004025A0 push ebp
004025A1 mov ebp, esp
004025A3 and esp, 0FFFFFFF0h
004025A6 sub esp, 40h
004025A9 call ___main
004025AE xor eax, eax ;i=0
004025B0 add eax, 1 ;i++
004025B3 mov [esp+eax*4+14h], eax ;person.buffer[i] = i
004025B7 cmp eax, 0Ah
004025BA jnz short loc_4025B0 ;如果i == 10结束循环
004025BC mov eax, [esp+3Ch] ;eax=person.buffer[9]
004025C0 mov dword ptr [esp+4], 0Ah ;参数2,person.count
004025C8 mov dword ptr [esp], offset aDDD ;参数1,"%d %d %d"
004025CF mov [esp+0Ch], eax ;参数4,person.buffer[9]
004025D3 mov eax, [esp+18h]
004025D7 mov [esp+8], eax ;参数3,person.buffer[0]
004025DB call _printf ;调用printf函数
004025E0 xor eax, eax
004025E2 leave
004025E3 retn
//x86_clang对应汇编代码相似,略
//x64_vs对应汇编代码讲解
0000000140001010 sub rsp, 58h
0000000140001014 mov edx, 0Ah ;常量传播优化
0000000140001019 mov dword ptr [rsp+20h], 0Ah ;常量传播优化
0000000140001021 mov dword ptr [rsp+24h], 1 ;常量传播优化
0000000140001029 lea rcx, aDDD ;"%d %d %d"
0000000140001030 mov r8, [rsp+58h+var_38]
0000000140001035 mov r9d, edx
0000000140001038 shr r8, 20h
000000014000103C call sub_140001050 ;调用printf函数
0000000140001041 xor eax, eax
0000000140001043 add rsp, 58h
0000000140001047 retn
//x64_gcc对应汇编代码讲解
0000000000402C10 sub rsp, 58h
0000000000402C14 call __main
0000000000402C19 lea rdx, [rsp+20h]
0000000000402C1E mov eax, 1 ;i=1
0000000000402C23 mov [rdx+rax*4], eax ;person.buffer[i] = i
0000000000402C26 add rax, 1 ;i++
0000000000402C2A cmp rax, 0Bh
0000000000402C2E jnz short loc_402C23 ;如果I == 11结束循环
0000000000402C30 mov r9d, [rsp+48h] ;参数4,person.buffer[9]
0000000000402C35 mov edx, 0Ah ;参数2,person.count
0000000000402C3A mov r8d, [rsp+24h] ;参数3,person.buffer[0]
0000000000402C3F lea rcx, aDDD ;参数1,"%d %d %d"
0000000000402C46 call printf ;调用printf函数
0000000000402C4B xor eax, eax
0000000000402C4D add rsp, 58h
0000000000402C51 retn
//x64_clang对应汇编代码相似,略
笔记--------------------------------------------start
//x86_vs对应汇编代码讲解 (Release版)
00401040 push 0Ah ; 常量传播优化
00401042 push 1 ; 常量传播优化
00401044 push 0Ah ; 常量传播优化
00401046 push offset aDDD ; "%d %d %d\n"
0040104B call printf ; 调用printf函数
00401050 add esp, 10h ; 栈平衡
00401053 xor eax, eax
00401055 retn

//x64_vs对应汇编代码讲解 (Release版)
0000000140001070 sub rsp, 58h ; 分配 0x58 字节的栈空间
0000000140001074 mov edx, 0Ah ; 常量传播优化
0000000140001079 mov dword ptr [rsp+58h+var_38], 0Ah ; 常量传播优化
0000000140001081 mov dword ptr [rsp+58h+var_38+4], 1 ; 常量传播优化
0000000140001089 lea rcx, Format ; "%d %d %d\n"
0000000140001090 mov r8, [rsp+58h+var_38]
0000000140001095 mov r9d, edx ; r9d表示低32位
0000000140001098 shr r8, 20h
000000014000109C call printf ; 调用printf函数
00000001400010A1 xor eax, eax
00000001400010A3 add rsp, 58h ; 栈平衡
00000001400010A7 retn

笔记--------------------------------------------end
代码清单9-9中main()函数的代码编译器进行了函数内联优化,无法区分是对象还是变量。相对而言,复杂对象的分析过程更为复杂,但可找到的特征信息也更多。在函数的调用过程中,当第一个参数为该对象首地址时,可怀疑这是this指针,按此函数的功能酌情还原为此类对象的成员函数。
在通常情况下,VS编译的代码默认以thiscall方式调用成员函数,因此会使用ecx保存this指针,从而进行参数传递。但并非具有ecx传参的函数就一定是成员函数,当使用__fastcall方式时,同样可以在反汇编代码中体现ecx传参。因此,在分析时不可将ecx传参作为识别this指针的唯一特征。那么类对象还具备哪些特征呢?通过下一章的学习,我们将发现它更多的与众不同的特性。
9.6 本章小结
本章首先讨论了结构体和类的内存结构,然后讨论了函数间对象传递的相关问题以及这些问题在编译器内部的实现原理。我们可以看到,当对象结构简单、体积小时,函数间的对象传递直接使用eax和edx保存对象中的内容。当对象体积过大,结构复杂时,寄存器就明显不够用了,于是编译器在开发人员不知情的情况下,偷偷给函数加上一个参数,将其作为返回值。传递参数对象时,存在一次复制过程,简单的对象直接按成员顺序执行push指令传参,复杂的对象则使用重复前缀的串操作指令rep movs,其edi被设置为栈顶。
在访问对象成员时,寻址方式颇为特别,使用的是寄存器相对间接访问方式。这种访问方式可以作为识别对象的必要条件,但是还须考察成员类型。如果类型一致,则应优先考虑是数组的访问。因为在数组的下标访问时,编译器也可能采用寄存器相对间接访问方式,如a[i],当i为常量时就会出现寄存器相对间接访问方式。当对象在栈内时,其首地址表示为ebp±n或者esp + n,其中n为立即数,而编译器计算对象成员的地址为对象首地址+成员偏移量,这个偏移量值是编译器在编译过程中确定的,视为常量值,结合上式,对象成员的地址表达为ebp ± n + offset或者esp + n + offset,其中n和offset(成员偏移量)皆为常量,符合常量折叠的优化条件,于是在编译时可计算出N = n±offset,所以在分析的时候,我们只能看到ebp±N或者esp + N。
思考题答案
&((struct A*)NULL)->f不会崩溃,这时求f的地址,根据前面提出的结构体寻址公式:
p->member的地址 = 指针p的地址值+ member在type中的偏移量代入得&((struct A*)NULL)->f = 0 + 4 = 4。
这个表达式实际上是求结构体内成员的偏移量。
可以定义如下宏,用于在不产生对象的情况下取得成员偏移量。
#define offsetof(s,m) (size_t)&(((s *)0)->m)
大家不用自行定义,在VS的stddef.h中有offsetof的官方定义。

1519

被折叠的 条评论
为什么被折叠?



