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

若没有另外说明,都是运行在VS2022,x86,Debug下。

结构体和类都有构造函数、析构函数和成员函数,两者只有一个区别:结构体的默认访问控制是public,类的默认访问控制是private。访问控制是在编译期进行检查,所以在反汇编中,结构体与类没有分别,两者原理相同,只是类型名称不同。

9.1对象的内存布局
人->类、结构体,抽象的概念
关羽->实例对象,实际存在的事物
1.对象的地址存放对象的各数据成员(顺序)。对象的大小只包含数据成员,类成员函数属于执行代码,不属于类对象的数据。
2.类中不能定义自身类型的对象,是因为类需要在申请内存的过程中计算出自身的大小,以用于实例化,若在类中定义自身类型的对象,在计算各数据成员的长度时,会形成无限循环的递归定义。
但自身类型的指针除外,因为指针占4字节。
3.对象长度等于各数据成员的长度之和,有3种情况例外:
(1)空类。空类的长度为1字节,是因为如果对象完全不占用内存空间,空类就无法取得实例对象的地址,this指针失效,因此不能被实例化。而类的定义是由数据成员和成员函数组成的,在没有数据成员情况下,还可以有成员函数,因此仍然需要实例化。
(2)内存对齐。首先,对齐值计算就是将设定的对齐值(默认对齐值8或使用#pragma pack(N)修改对齐值)与类中最大的基本类型数据成员的长度进行比较,取两者之间较小值)。【注意1】当数据成员是数组时,计算对齐值是根据数组元素的长度,而不是数组的整体大小。【注意2】当数据成员是类类型时,计算对齐值是根据嵌套的类使用的对齐值,而不是嵌套的类的整体大小。然后,为类中数据成员分配内存时,类中当前数据成员的长度为M,对齐值为N,那么实际对齐值q=min(M,N),成员的地址安排在q的倍数上。例子如下。


(3)静态数据成员。它与局部静态变量类似,存放的位置和全局变量一致,只是编译器增加了作用域的检查,在作用域外不可见。同类对象将共享静态数据成员的空间,即静态数据成员和对象是一对多的关系,所以不计入对象长度。

9.2this指针
1.指针访问类成员,例子如下。


2.this指针保存了所属对象的首地址,例子如下。分析:
(1)利用寄存器ecx保存了对象首地址,并以寄存器传参的方式将其传递到成员函数中(作为函数第一个参数),这便是this指针的由来。所有成员函数(非静态成员函数)都有一个隐藏参数,即自身类型的指针,这样的默认调用约定称为thiscall,它是C++成员函数特有的调用方式。
(2)在成员函数中访问数据成员也是通过this指针间接访问的,这便是在成员函数内可以直接使用数据成员的原因。
(3)thiscall与_stdcall都是被调用方平衡栈,不同在于this指针传递方式,前者使用寄存器ecx传递,后者使用栈传递。


3.符合以下特点,基本上可判定这是调用类的成员函数。通过分析函数代码中访问ecx的方式,再结合内存窗口,以ecx中的值为地址观察其数据,可以进一步分析并还原对象中的各数据成员。

9.3静态数据成员
1.例子如下。分析:静态数据成员属于全局变量,并不属于任何对象,所以访问时无须this指针。而普通的数据成员属于对象所有,访问时需要使用this指针。


2.静态数据成员必须初始化,是因为静态数据成员在类中仅仅是声明,没有定义,在类外定义实际上是给静态数据成员分配内存。否则会如下。


访问该静态数据成员时会报错【无法解析的外部符号 "public: static int Person::staticNum" (?staticNum@Person@@2HA)】

9.4对象作为函数参数
1.对象的传参过程与数组不同,数组变量名称代表数组的首地址,而对象变量名称却不能代表对象的首地址,所以是先将对象中的所有数据进行备份(复制),将备份的数据作为形参传递到调用函数中使用。类对象中数据成员传参顺序为最先定义的数据最后压栈。
2.例子如下。


在64位程序中,因为栈顶为栈预留空间,所以无法将对象的数据成员复制到栈顶,编译器将对象的数据成员先复制到临时对象,再将临时对象的地址传递给show函数,在show函数内部使用this指针间接访问对象的数据成员。


3.隐患,如当数据成员是指针时,分析:
由于没有编写拷贝构造函数,因此在传递参数时没有被调用,编译器以浅拷贝处理,复制对象的数据成员即指针,指针保存是堆地址(对象person和局部对象obj的指针都指向同一个堆空间)。当show函数退出时,调用析构函数,释放对象obj的指针指向的堆空间。当main函数退出时,调用析构函数,释放对象person的指针指向的堆空间【会因重复释放同一堆空间而触发中断】。解决:深拷贝数据或设置引用计数。


解决后如下,即加上拷贝构造函数。

#include<iostream>
#include<string>
using namespace std;

class Person
{
private:
	char* name;	
public:
	Person():name(nullptr){}

	Person(const Person& other)
	{ 
		int len = strlen(other.name);
		this->name = new char[len +1];//加上'\0'
		if (this->name != nullptr)
			strcpy(this->name, other.name);	
	}

	~Person()
	{
		if (name != nullptr)
		{
			delete[] name;
			name = nullptr;
		}
	}
public:
	void setName(const char* name)
	{
		int len = strlen(name);
		if (this->name != nullptr)
			delete[] this->name;

		this->name = new char[len + 1];
		if (this->name != nullptr)
			strcpy(this->name, name);
	}

	const char* getName() { return name; }

};

void show(Person obj) { printf(obj.getName());}


int main()
{
	Person person;
	person.setName("Tom");
	show(person);

	return 0;
}

4.当参数为对象的指针类型时,则不存在这种错误。直接把对象的首地址传递过去了,因为没有副本,所以在函数进入和退出时不会调用拷贝构造函数和析构函数,也就不存在资源释放的错误隐患,而且避免复制提升了效率。所以在类对象作为参数时,若无特殊需求,应尽量使用指针或引用。

9.5对象作为返回值
1.基本数据类型(浮点类型、非标准类型除外)作为返回值时,通过寄存器eax/rax返回。而对象属于自定义类型,寄存器eax/rax无法保存对象中所有数据,所以它的处理方式与对象作为参数时非常相似。在退出函数时,将返回对象的数据复制到临时的栈空间,以这个临时栈空间的首地址作为返回值。
2.例子如下。


3.与对象作为参数时的隐患一样, 如当数据成员是指针时,函数内的局部对象和返回对象指向同一个栈空间,函数退出时执行析构函数释放局部对象,当返回对象被销毁时执行析构函数就【会重复释放同一堆空间而触发中断】,解决方案也相同。
4.当对象作为参数时,可以传递指针。而当对象作为返回值时,如果对象在函数内被定义为局部变量,则不可以返回此对象的首地址或引用,以避免返回已经释放的局部变量。例子如下,VS可能会警告【返回局部变量或临时变量的地址: test】

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值