C++反汇编第三讲,反汇编中识别继承关系,父类,子类,成员对象

讲解目录:

   1.各类在内存中的表现形式   备注: 主要复习开发知识,和反汇编没有关系,但是是理解反汇编的前提.

      2.子类继承父类

     2.1 子类中有虚函数,父类中有虚函数    : 都有的情况下

       2.2 子类中没有虚函数,父类中有虚函数   : 子类没有,父类有的情况 2.1 2.2的情况都是一样的.

       2.3   子类中有虚函数,父类中没有虚函数  : 子有父没有的的情况下

       2.4 子类父类都没有虚函数的情况下

    第二专题大总结.    熟悉反汇编可以直接看这个总结,

   3.结合第二专题的成员对象有无虚表行为

     3.1成员对象有虚表的情况

     3.2成员对象没有虚表的情况

    第三专题大总结

    4.重载运算符的识别

   5.纯虚函数的反汇编  

   6.模版识别.

一丶各类在内存中的表现形式(复习开发知识)

讲解之前,我们首先要明白C/C++中的类的内存结构.继承之后的内存结构

普通类的内存结构:

class MyTest
{
public:
    MyTest();
    ~MyTest();
public:
    int m_int;
};

MyTest::MyTest(){}

MyTest::~MyTest(){}

int main(int argc, char* argv[])
{
    MyTest test;            //定义对象
    return 0;
}

对应内存结构图

  高级代码:

这是普通的一个类的内存结构图,因为我们只有一个成员,大小是一个4字节的,所以初始化为CC

总结: 普通类根据成员进行申请内存.

带有虚关键字的类(可能有虚函数或者虚构造)

PS: 类声明同上,但是析构前边加上了virtual 关键字,变为了虚析构

内存结构图:

可以看出,申请了八个字节,启动前4个字节是虚表指针,指向了虚表

后四个字节才是真正的为成员申请的内存.

总结: 带有虚函数(虚关键字)的时候,内存中会把前4个字节当做虚表指针,并且在构造的时候初始化.

子类继承父类,(都有虚函数的情况下)重要:

高级代码:

class MyFather
{
public:
    MyFather();
    virtual ~MyFather();
public:
    int m_int;
};

MyFather::MyFather(){}

MyFather::~MyFather(){}

class MyChild : public MyFather //继承
{
public:
    MyChild();
    virtual ~MyChild();
  float m_flt;

};

MyChild::MyChild(){}

MyChild::~MyChild(){}

int main(int argc, char* argv[])
{
    MyChild test;            //定义对象
    return 0;
}

内存结构图

总共申请了12个字节,前4个字节是虚表指针,后4个字节是父类的m_int成员,在后面才是子类的真正的成员.

说到这里我们就要说下复写虚表指针的操作.

首先我们知道:  子类构造的时候,会先构造父类,也就是说,父类的内存会先申请,并且把虚表指针填写到前4个字节位置,  而构造完毕父类之后,构造自己的时候,这时候虚表指针又写入子类的虚表指针了.产生了覆盖了.

流程图:

看上面图可以知道,我们子类继承父类,并且填写了虚表指针为子类的,此时 则可以写成  父类指针指向子类   例如:  Myfather *pFa = new MyChild;  pfa指向的位置就是父类区域的起始位置,

而且不会超过父类区域,所以是安全的,此时因为构造完毕,虚表指针是子类的,所以调用虚函数的时候,则是调用子类的虚函数了.

而且也说明了 为什么子类指针不能指向父类.这样会产生越界问题.

总结:

  子类继承父类时候,有虚函数的时候,会先把头4字节申请出来填写为虚表指针, 而且会产生复写(重复写入). 第一次, 构造父类,填写为父类指针,第二次构造完父类则会填写为子类的虚表指针.

  

二丶子类继承父类反汇编中的结构

2.1 子类中有虚函数,父类中有虚函数    : 都有的情况下

高级代码:

class MyFather
{
public:
    MyFather();
    virtual ~MyFather();
public:
    int m_int;
};

MyFather::MyFather(){}

MyFather::~MyFather(){}

class MyChild : public MyFather
{
public:
    MyChild();
    virtual ~MyChild();
    float m_flt;
};

MyChild::MyChild(){}

MyChild::~MyChild(){}

int main(int argc, char* argv[])
{
    MyChild test;            //定义对象
    return 0;
}

Debug下的反汇编

PS: 代码太多,只说明这个反汇编在哪个函数中

1.main函数中找到构造

2.构造中生成的反汇编

可以看出,构造中又有一个Call,这个Call是构造父类的,构造完毕之后填写自己的虚表指针.

3.父类构造

父类构造填写虚表指针,也就是对象的前4个字节修改为父类的虚表指针.而后通过第二步,得出,当构造完父类之后,其前4个字节会被子类重新写入.也就产生了复写过程

总结

  1.子类构造的时候会先构造父类,父类构造中先填写虚表指针.

  2.父类构造完成之后,子类会重新写入虚表指针.

  3..子类继承父类,都有虚函数的情况下,会产生复写行为, 对象首地址4个字节处填写虚表.

 2.2 子类中没有虚函数,父类中有虚函数 : 子类没有,父类有的情况

PS: 高级代码中,子类类声明去掉了虚函数

Debug下的反汇编代码:

 1.main函数下构造的反汇编

 2.构造内部反汇编

看到这一步我们明白了,首先构造父类,因为父类有虚函数,所以肯定会有虚表指针填写,而下方也填写了一次虚表指针.由此得出

父类有虚函数,子类没有虚函数则子类也会有虚表.也会产生复写行为.

总结:

  父有,子没有,子类也会有虚表,而且也会产生虚表指针复写行为.

  且只要父类有虚函数,不管子类有没有虚函数,子类都会产生虚表,且会复写虚表指针.

2.3 子类有虚函数,父类没有虚函数

高级代码子类中定义了虚函数,父类则把虚函数去掉了.

Debug下的反汇编代码

  1.main函数下构造

  2.构造内部

看其内部得出,父类没有虚函数的情况下,其对象 +4位置,跳过前边的4个字节,来构造父类,构造完毕之后填写子类虚表指针.

  3.父类构造内部

父类构造内部没有产生虚表指针填写行为

总结:

  子类有虚表,父类没有,则会跳过虚表指针的位置来构造父类,当构造完毕父类之后前4个字节填写子类的虚表指针.

2.4 子类,父类都没有虚函数的情况下

直接构造内存,没有虚表,也不会产生虚表指针复写,可以当做结构体还原.

第二专题大总结

    1.父类有虚函数,子类不管有没有虚函数,都会有虚表

    2.父类有虚函数构造的时候会填写虚表指针,且子类也会填写虚表指针,两者会产生虚表指针复写行为

    3.子类中有虚函数,父类没有,则会跳过虚表指针来构造父类,其子类会在构造完毕父类之后填写虚表指针,不会产生虚表指针复写行为.

三丶结合第二专题的成员对象有无虚表行为

3.1成员对象没有虚表的情况下

高级代码: 

class MyMemberObj           //成员对象
{
public:
    MyMemberObj(){}
    ~MyMemberObj(){}
};

class MyFather              //父类
{
public:
    MyFather();
     ~MyFather();
public:
    int m_int;
};

MyFather::MyFather(){}

MyFather::~MyFather(){}

class MyChild : public MyFather //子类继承父类
{
public:
    MyChild();
     virtual ~MyChild();
    MyMemberObj m_memberobj;    //成员对象
    float m_flt;
};

MyChild::MyChild(){}

MyChild::~MyChild(){}

int main(int argc, char* argv[])
{
    MyChild test;            //定义对象
    return 0;
}

Debug下的反汇编

1.main函数下的构造

2.构造内部

1.构造父类,因为父类没有虚函数,所以+4构造一下,且父类有一个成员,所以申请了4个字节空间

2.成员变量的构造+8的位置开始构造,父类构造完毕之后构造,且此时成员对象没有虚函数.

3.子类在自己的头4个字节位置处填写虚表指针.

3.成员对象构造内部

成员对象内部不会产生写虚表的行为.

总结:

  成员对象没有虚函数的情况下,会在合适偏移位置处进行构造,注意合适位置处的用语,如果你是子类的成员对象,肯定会先构造父类,父类成员很多,则你的偏移位置则不固定.

3.2成员对象有虚表的情况下.

Debug下的汇编代码:

  因为其类之加了一个虚关键字,析构变为了虚析构,产生了虚表的动作.所以其汇编代码1,2步没有改变,同上.

  不同的是构造的时候,成员对象有了虚函数,构造的时候则会填写虚表.

总结:

  1.有成员对象的时候其成员对象内部没有虚表产生,则会在合适位置构造成员对象.

  2.有成员对象的时候,其成员对象内部有虚表产生,则在合适位置填写虚表指针,并且构造成员对象.

四丶反汇编中重载运算符的识别

在说重载运算符的时候,我们首先熟悉一下运算符重载的高级代码:

简单的运算符重载

函数类型 operator 运算符名称 (形参表列)
    {
        // 对运算符的重载处理
    }

高深一点的可以参考博客,这里不再重复讲解.复习开发知识可以参考博客链接 http://c.biancheng.net/cpp/biancheng/view/215.html

高级代码:

int operator+(MyChild& a,MyFather& b)
{
    return (int)a.m_flt + b.m_int;
}
int main(int argc, char* argv[])
{
    MyChild a;            //定义对象
    MyFather b;
    cout << a + b << endl;
    return 0;
}

在反汇编中,其实运算符重载就是调用函数.只不过换了一种函数的认知方式.

其实不难.当做函数还原就好.

说道这里,我们可以说下运算符重载的额外认知.

比如我们熟悉的

1.数学中的中缀式   a + b / c - d * e 这种表达式就是中缀表达式

2.波兰式     -+a/bc*de  中缀转化为了波兰式,我们学习数据结构的树的时候就学习过这种方式,这个是编译原理中的.适用于计算机的识别.

怎么转换的

Sub(add(a,Div(b,c),Imul(d,e); 转为汇编代码,比如a + b /c 我们则写成  add(a,div(b,c),然后转为汇编表达式即可.最终的结果则是上面写的波兰式.只不过按照语义,变为符号化了.

五丶纯虚函数的反汇编 

我们知道,纯虚函数是为了子类实现了,自己不能实现,但是反汇编代码中其实实现了,只不过里面调用了提示错误的API.就是为了你不小心调用的时候提示不能创建xxx对象的实例.等等一些列的错误.

高级代码:

class MyFather              //父类
{
public:
    MyFather();
     ~MyFather();
    virtual void show() = 0;    //纯虚函数
};

MyFather::MyFather(){}

MyFather::~MyFather(){}

class MyChild : public MyFather //子类继承父类
{
public:
    MyChild();
     virtual ~MyChild();
    virtual void show();
};

MyChild::MyChild(){}

MyChild::~MyChild(){}

void MyChild::show()
{
    cout << 1 << endl;
}

int main(int argc, char* argv[])
{
    MyChild a;            //定义对象
    a.show();
    return 0;
}

Debug下反汇编

我们直接看纯虚函数内部了,在子类构造的时候父类会构造,父类构造自己的时候会填写虚表指针,我们直接找父类的虚表指针即可.然后定位虚表中的第二项.

第一项是父类的虚析构,第二项才是我们的.

纯虚函数在低版本就是19h,并且调用__amsg_exit,且如果弄了签名,则是__purecall

高版本不太一样,高版本不是简单的这样调用了(vs系列)它会保存当时的寄存器信息啊,什么的,然后写日志用的.反正结果是一样的.

高版本自己可以试试看一看有什么不同.

六丶模版识别.

模版和运算符重载一样,都是函数,编译为反汇编的代码都是函数调用.而且函数和函数的重载不同,它生成的反汇编代码有多处.

高级代码:

template <typename T>
T MySub(T a,T b)
{
    return a  - b;
}

int main(int argc, char* argv[])
{
    printf("%d\r\n",MySub(1,2));
   printf("%f\r\n",MySub(3.0f,1.0f));
  printf("%lf\r\n",MySub(8.3,4.3));

   return 0;
}

运行结果:

Debug下反汇编.

虽然都是一样调用,但是其内部是不同的.每个函数都有自己的汇编代码.

转载于:

作者:IBinary
出处:http://www.cnblogs.com/iBinary/

原文地址:https://www.cnblogs.com/gd-luojialin/p/11219933.html

时间: 2024-10-09 17:06:58

C++反汇编第三讲,反汇编中识别继承关系,父类,子类,成员对象的相关文章

在Entity Framework 中实现继承关系映射到数据库表

继承关系映射到数据库表中有多种方式: 第一种:TPH(table-per-hiaerachy) 每一层次一张表 (只有一张表) 仅使用名为父类的类型名的一张表,它包含了各个子类的所有属性信息,使用区分列(Disciriminator column)(通常内容为子类的类型名)来区分哪一行表示什么类型的数据. 第二种:TPT(Table-per-type) 每种类型都有一张表(父类及每个子类都有表) 父类.各子类各自都有一张表.父类的表中只有共同的数据,子类表中有子类特定的属性.TPT很像类的继承结

hibernate中的继承关系

  1.       hibernate中的继承关系   代码:   <1>.POJO类:(Person类)   package qau.edu; import java.util.Date; public class Person { //  成员变量: private int id ; private String name ; private Date date ; //  相应的getter和setter方法: public int getId() { return id; } pub

java中的继承关系

1.定义 java中的继承是单一的,一个子类只能拥有一个父类:java中所有类的父类是java.lang.Object,除了这个类之外,每个类只能有一个父类: 而一个父类可以有多个子类,可以被多个子类继承: Java只支持单继承,也就是说,一个类不能继承多个类. Java只支持单继承(继承基本类和抽象类),但是我们可以用接口来实现(多继承{实现}接口来实现),脚本结构如: public class One extends Parent implements Two,Three,Four{} 2.

初步学习C++中的继承关系

 继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能.这样产生新的类,称派生类.继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程. #include<<span style="font-size:14px;">iostream</span>> using namespace std; class Base { public: Base() {} ~Base() {} p

Java中的继承关系的加载顺序

/* 在加载类时,Java虚拟机先加载父类再加载子类,再对静态初始化块. 静态成员变量(类变量).静态方法进行一次初始化. 只有调用new方法时才会创建类的对象.按照父子继承关系进行初始化, 首先执行父类的初始化块部分,然后是父类的构造方法,再执行子类的 初始化块,最后是子类的构造方法. 销毁对象的过程是:首先销毁子类部分,再销毁父类部分. */ public class InheritanceLoadOrder { public static void main(String[] args)

Java中包含继承关系时对象的创建与销毁顺序详解(附源码)

前言 通过组合和继承方法来创建新类时,永远不必担心对象的清理问题,子对象通常都会留给垃圾回收器进行处理.如果确实遇到清理的问题,那么必须用心为新类创建dispose()方法(在这里我选用此名称,读者可以提出更好的).并且由于继承的缘故,如果我们有其他作为垃圾回收一部分的特殊清理动作,就必须在导出类中覆盖dispose()方法.当覆盖被继承类的dispose()方法时,务必记住调用基类版本dispose()方法:否则,基类的清理动作就不会发生.下例就证明了这一点: 示例源码 package com

Java中的继承:父类和子类的关系

一.父类引用指向子类对象时 1.若子类覆盖了某方法,则父类引用调用子类重新定义的新方法 2.若子类未覆盖某方法,则父类引用调用父类本身的旧方法 3.若子类覆盖了某属性,但父类引用仍调用父类本身的旧属性 4.若子类未覆盖某属性,则父类引用调用父类本身的旧属性 5.父类引用不能访问子类新定义的方法 二.子类引用指向自身对象时 1.若子类覆盖了某方法,则子类引用调用子类重新定义的新方法 2.若子类未覆盖某方法,则子类引用调用父类本身的旧方法 3.若子类覆盖了某属性,则子类引用调用子类重新定义的新属性

java 类继承,父类子类方法调用的过程d

1.Mytank类继承tank,则Mytank里面的公有部分即public 成员和方法在Mytank中是含有的可以调用和赋值,且在MyTank中不要有新的成员变量与tank中的公有成员名称一样,这样会发生调用紊乱,如: Tank tank = new MyTank(); int c = tank.a; 若 tank类中public a= 3: Mytank子类中新定义public a = 2: 则上述代码中的调用将会使得c = 3: 用了父类中的a.

java 类继承,父类子类方法调用的过程

1.Mytank类继承tank,则Mytank里面的公有部分即public 成员和方法在Mytank中是含有的可以调用和赋值,且在MyTank中不要有新的成员变量与tank中的公有成员名称一样,这样会发生调用紊乱,如: Tank tank = new MyTank(); int c = tank.a; 若 tank类中public a= 3: Mytank子类中新定义public a = 2: 则上述代码中的调用将会使得c = 3: 用了父类中的a.