C++-逆向分析-结构体和类-内存布局-this指针-静态数据成员-对象作为参数和返回值

1.结构体和类

在C++中,结构体和类都具有构造函数、析构函数和成员函数,两者只有一个区别:结构体的访问控制默认为public,而类的默认访问控制是private。对于C++中的结构体而言,public、private、protected的访问控制都是在编译期进行检查,当越权访问时,编译过程中会检查出此类错误并给予提示。编译成功后,程序在执行的过程中不会在访问控制方面做任何检查和限制。因此,在反汇编中,C++中的结构体与类没有分别,两者的原理相同,只是类型名称不同,本章使用的示例多为类。

类成员:对象中先定义的数据成员在低地址处,后定义的数据成员在高地址处,依次排列。对象的大小只包含数据成员,类成员函数属于执行代码,不属于类对象的数据。

为什么在类中不能定义自身的对象呢?因为类需要在申请内存的过程中计算出自身的实际大小,以用于实例化。如果在类中定义了自身的对象,在计算各数据成员的长度时,又会回到自身,这样就形成了递归定义,而这个递归并没有出口,是一个无限的循环递归定义,所以不能定义自身对象作为类成员。但是,自身类型的指针除外,因为任何类型的指针在32位下所占用的内存大小始终为4字节,等同于一个常量值,因此将其作为类的数据成员不会影响长度的计算。

2.对象内存

空类。空类中没有任何数据成员,按照该公式计算得出的对象长度为0字节。类型长度为0,则此类的对象不占据内存空间。而实际情况是,空类的长度为1字节。如果对象完全不占用内存空间,那么空类就无法取得实例对象的地址,this指针失效,因此不能被实例化。而类的定义是由成员数据和成员函数组成,在没有成员数据的情况下,还可以有成员函数,因此仍然需要实例化,分配了1字节的空间用于类的实例化,这1字节的数据并没有被使用。
内存对齐。在VC++6.0中,类和结构体中的数据成员是根据它们在类或结构体中出现的顺序来依次申请内存空间的,由于内存对齐的原因,它们并不一定会像数组那样连续地排列。由于数据类型不同,因此占用的内存空间大小也会不同,在申请内存时,会遵守一定的规则。
在为结构体和类中的数据成员分配内存时,结构体中的当前数据成员类型长度为M,指定的对齐值为N,那么实际对齐值为q=min(M, N),其成员的地址安排在q的倍数上。

3.this指针

this指针应属于指针类型,在32位环境下占4字节大小,保存的数据为地址信息。在使用默认的调用约定时,在调用成员函数的过程中,编译器做了一个“小动作”:利用寄存器ecx保存了对象的首地址,并以寄存器传参的方式传递到成员函数中,这便是this指针的由来。由此可见,所有成员函数都有一个隐藏参数,即自身类型的指针,这便是this指针,将这样的默认调用约定称为thiscall。
在VC++的环境下,识别this指针的关键点是在函数的调用过程中使用了ecx作为第一个参数,并且在ecx中保存的数据为对象的首地址,但并非所有的this指针的传递都是如此。在默认模式下,成员函数的调用方式为thiscall。thiscall的栈平衡方式与__stdcall相同,都是由被调用方负责平衡。但是,两者在传参的过程中却不一样,声明为thiscall的函数,第一个参数使用寄存器ecx传递,而非通过栈顶传递。而且thiscall并不属于关键字,它是C++中成员函数特有的调用方式,在C语言中是没有这种调用方式的。由于在VC++环境下thiscall不属于关键字,因此函数无法显式声明为thiscall调用方式,而类的成员函数默认是thiscall调用方式。所以,在分析过程中,如果看到某函数使用ecx传参,且ecx中保留了对象的this指针,以及在函数实现代码内,存在this指针参与的寄存器相对间接访问方式,如[reg+8],即可怀疑此函数为成员函数

当使用其他调用方式(如__stdcall)时,this指针将不再使用ecx传递,而是改用栈传递。将代码清单中的成员函数SetNumber修改为__stdcall调用方式,查看this指针的传递与使用过程,代码如下所示:

class CTest
{
    publicvoid __stdcall SetNumber(int nNumber) //修改其调用方式
    {
        m_nInt = nNumber;
    }

    publicint m_nInt;//公有数据成员
}void main()
{
    CTest Test;
    Test.SetNumber(5);//调用__stdcall成员函数
    printf("CTest:%d\r\n",Test.m_nInt);//获取成员数据
}

在代码清单中,成员函数SetNumber在调用过程中没有通过ecx传递this指针,取而代之的是以栈方式传递参数。__cdecl调用方式和__stdcall调用方式只是在参数平衡时有所区别,这里就不详细讲解了。使用__cdecl和__stdcall声明的成员函数,this指针并不像thiscall那样容易识别。使用栈方式传递参数,并且第一个参数为对象首地址的函数很多,很难区分。
虽然难以区分,但如果能确定函数的第一个参数为this指针,并且在函数体内将this指针存入某寄存器,然后出现寄存器相对间接访问方式,那么将其还原为成员函数也是等价的。
在O2选项中,代码清单经过优化后,类对象将不复存在,只是使用printf函数,输出数字5。SetNumber函数完成的功能是将数据成员m_nInt赋值为常量5。其他代码没有再对此变量做任何修改,而类对象Test只有一个数据成员m_nInt,该对象除了为数据成员赋值外,并无其他操作,因此编译器作了减少变量的优化处理。

4.静态数据成员

当类中定义了静态数据成员时,由于静态数据成员和静态变量原理相同(是一个含有作用域的特殊全局变量),因此该静态数据成员的初值会被写入编译链接后的执行文件中。当程序被加载时,操作系统将执行文件中的数据读到对应的内存单元里,静态数据成员便已经存在,而这时类并没有实例对象。所以静态数据成员和对象之间的生命周期不同,并且静态数据成员也不属于某一对象,与对象之间是一对多的关系。静态数据成员仅仅和类相关,和对象无关,多个对象可以共同拥有同一个静态数据成员。

5.对象作为函数参数

对象作为函数的参数时,其传递过程较为复杂,传递方式比较独特。其传参过程与数组不同:数组变量的名称代表数组的首地址,而对象的变量名称却不能代表对象的首地址。传参时不会像数组那样以首地址作为参数传递,而是先将对象中的所有数据进行备份(复制),将复制的数据作为形参传递到调用函数中使用。
类对象中的数据成员的传参顺序为:最先定义的数据成员最后压栈,最后定义的数据成员最先压栈。

由于对象作为参数在传递过程中会制作一份对象的复制数据,当向对象分配内存时,如果有构造函数,编译器会再调用一次构造函数,并做一些初始化工作。当代码执行到作用域结束时,局部对象将被销毁,而对象中可能会涉及资源释放的问题,同样,编译器也会再调用一次局部对象的析构函数,从而完成资源数据的释放。有参考资料中提到,当类中没有定义构造函数和析构函数时,编译器会添加默认的构造函数和析构函数。而从编译器工作的结果看,在定义类对象时,编译器根本没有做任何处理,可见编译器并没有添加默认的构造函数。

6.对象作为返回值

对象作为函数的返回值时,与基本的数据类型不同。基本数据类型(双精度浮点类型以及非标准的__int64类型除外)作为返回值时,通过寄存器eax来保存返回的数据,而对象属于自定义类型,寄存器eax无法保存对象中的所有数据,所以在函数返回时,寄存器eax不能满足需求。
对象作为返回值与对象作为参数的处理方式非常类似。对象作为参数时,进入函数前预先将对象使用的栈空间保留出来,并将实参对象中的数据复制到栈空间中。该栈空间作为函数参数,用于函数内部使用。同理,对象作为返回值时,进入函数后将申请返回对象使用的栈空间,在退出函数时,将返回对象中的数据复制到临时的栈空间中,以这个临时栈空间的首地址作为返回值
当对象作为函数的参数时,可以传递指针;当对象作为返回值时,如果对象在函数内部被定义为局部变量,则不可返回此对象的首地址或引用,以避免返回已经被释放的局部变量

7.总结

我们看到,当对象结构简单、体积小时,函数间的对象传递直接使用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。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值