《C++反汇编与逆向分析技术揭秘》学习之第9章 结构体和类

 在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的官方定义。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值