C++面向对象逆向笔记

笔记参考自黄色那本C++逆向揭秘

对象的识别

所有的类都有构造函数吗

  1. 情况一:编译器会为类内有虚函数父类有虚函数,或者本类中的对象成员有虚函数的情况下,编译器会添加默认的构造函数,用于完成虚表的初始化工作
  2. 情况二:编译器会为父类或者本类中定义的成员对象带有构造函数时。派生类的构造顺序是先构造父类再构造自身,需要添加默认的构造函数调用父类的构造函数。

构造函数的识别

  1. 该函数的调用是某对象在作用域内的第一次成员函数的调用,通过this指针判断是哪个对象的成员函数。
  2. 使用thiscall调用方式,使用ecx传递this指针
  3. 返回值为this指针,与传入的第一个参数相同。

析构函数的识别

  1. 该函数的调用时某对象在作用域内的最后一次成员函数调用,通过this指针来分辨是那个对象的成员函数。
  2. 使用thiscal调用方式,使用ecx寄存器来传递this指针。
  3. 没有返回值。也没有参数

全局构造函数和全局析构函数的调用时机

1._cinit()函数的第二个_initterm()函数内完成,在_initterm()中。会先执行__onexitinit()函数初始化函数指针数组。在执行每个全局对象构造代理函数时都会先执行对象的构造函数,然后使用atexit()函数注册析构代理函数。
2. 编译器会为每个全局对象和静态对象建立一个中间代理的析构函数用于传入全局对象的this指针。(那局部对象、参数对象和返回对象呢?)

函数调用时对象是否调用拷贝构造函数

1.编译器默认采用的是浅拷贝,即所有拷贝的对象和原对象引用的是同一资源,比如指针会简单地复制,而不是申请新的内存空间,将指针所指向的内容进行拷贝然后复制。这种浅拷贝在对象退出其作用域时释放资源可能引发错误,将同一份资源重复释放
2.函数调用时,如果以对象为参数而非对象指针或引用为参数,并且返回一个对象。调用之前,栈中除了局部对象的内存空间外,还有一份内存空间用于保存返回对象。调用函数之后在该栈帧中生成一个对象,退出函数时,该对象会被拷贝到返回对象中,最后回到调用者的栈帧后,再将该对象拷贝到引用该对象的变量中。
3. 设置了返回对象的原因是,如果调用者的作用域某变量要引用被调用函数中的对象,而此时已经退出该函数栈帧,即退出了该局部变量的作用域。栈中的数据(包括作为参数传入的返回对象的地址)可能被覆盖从而产生错误,所以需要在调用者的栈帧中用一块内存空间保存返回对象和临时对象,将返回对象拷贝到临时对象中,方便后续的引用。拷贝顺序:被调用者中的对象–>返回对象–>临时对象–>调用者中的对象。
4. 如果使用指针作为参数和返回值,函数内就没有对拷贝构造函数的调用。这也是一个判断参数或返回值时对象还是对象指针的方法。

虚函数的识别

  1. 找到对象的虚函数表
  2. 利用IDA的交叉引用,找到引用虚函数表的位置(构造函数和析构函数会设置虚函数表)
  3. 根据前面的条件,判断这些函数是否是构造函数和析构函数。
  4. 如果满足条件,这说明这是一个虚函数。否则则可能是用户自定义的函数指针数组

对象的析构和释放

  1. 如果析构函数是虚函数,也会有多态行为。表现在汇编指令中,会发现,调用虚表中的析构函数前,也会重复对虚表进行写入。但是写入顺序与构造函数相反,先写入子类自身的虚表,再写入父类的
  2. 析构函数释放堆空间是两回事。显式调用了析构函数不一定会释放该对象的堆空间。通过C++的放置语法,可以在已析构对象的对内存中创建一个新的对象,从而节约内存空间。如下:
    void main(int argc, char* argv[]){
    	CPerson *pPerson = new CChinese;
    	pPerson->ShowSpeak();
    	pPerson->~CPerson();	//显式调用析构函数
    	
    	//C++放置语法:将pPerson指向的堆内存作为CChinese类新对象的首地址,并调用其构造函数。从而重复使用一个堆内存。
    	pPerson = new(pPerson) CChinese();
    	delete pPerson;
    }
    
    在汇编指令中,会通过传递给析构函数的参数判断是否需要释放堆空间,如果不需要,则会跳过delete的调用。

判断继承关系的方法

  1. 在虚表的地址处利用交叉引用功能“Chart of xrefs to”得到所有直接引用这个地址的位置。
  2. 即可得到所有构造函数和析构函数。
  3. 找到每一次引用的前面最后一次写入虚表的地方。并确定函数范围。
  4. 构造函数中先写入父类的虚表,再写入子类的虚表;析构函数中则相反。

一些函数可能被编译器内联,从而没有this指针的传递和使用。

单继承类与多继承类虚表指针的区别

  1. 单继承类在类对象占用的内存空间中,只保存一份虚表指针,对应一个虚表。构造时先构造父类函数,再构造自身,析构时相反,都只调用一次父类的构造函数和析构函数。
  2. 多重继承类保存的虚表指针的数量等于父类的个数(如果父类没有虚函数呢?)。从子类对象转换为父类指针时,会跳转到对象的父类部分的首地址并赋值。构造时,按照继承列表中的顺序依次调用父类的构造函数构造对应的内存部分,析构时先析构自身,然后按照相反顺序析构父类对象部分
  3. 对象作为成员时,类对象的内存结构和多重继承很相似。类中无虚函数时,整个类对象的内存结构和多重继承一样。父类或成员对象存在虚函数时,通过虚表指针的位置和构造函数、析构函数中填写虚表指针的数目及目标地址,来还原继承货成员关系。
    1. 假如某对象为第一个成员,且该类无虚函数,那么类对象的首地址就是该成员对象的首地址,如果该成员对象有虚函数,则首地址就是该成员对象的虚表指针,与多重继承的表现相似
    2. 假如某对象为第一个成员,且该类有虚函数,那么类对象的首地址时该类的虚表指针,然后才是该成员对象的内存部分的首地址。
    3. 假如对象不是定义的第一个成员,则该成员对象的偏移处才是该成员对象的成员或虚表指针,此时以该成员首地址为this指针的函数,即为该成员对象的成员函数</font color=“#0000ff”>(交叉引用判断)。

虚基类和虚继承

  1. 虚基类也称为抽象类。
  2. 对于纯虚函数,由于没有实现。其函数地址指向的事void __cdecl _purecall(void),该函数中会结束程序,并发出错误编码信息0x19。Release版本中,编译器会进行优化,纯虚函数将会被优化掉。

问题

  1. 子类对象构造完成时,虚表指针已经是子类的虚表了。为什么编译器在析构函数中再次将虚表设置为子类的虚表?
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值