zoukankan      html  css  js  c++  java
  • 《C++反汇编与逆向分析技术揭秘》之12——继承

    •  识别类和类之间的关系

    在父类中声明为私有的成员,虽然子类对象无法直接访问,但是在子类对象的内存结构中,父类私有的成员数据依然存在。

    在没有提供构造函数的时候,系统会尝试提供默认的构造函数:

    当子类中没有构造函数或析构函数,而它的父类却需要构造函数与析构函数是,编译器会为这个子类提供默认的构造函数与析构函数。这是为了帮助父类对象完成初始化。同理,如果是子类对象的成员对象需要构造函数,那么系统也会为这个子类对象提供一个默认的构造函数。

    如果子类中存在构造函数,而父类中不存在构造函数。那么,如果父类中没有虚函数,就不存在初始化虚表的任务,那么编译器就不会为父类提供默认的构造函数。反之,如果父类中存在虚函数,需要初始化虚表指针,那么就会为父类提供默认的构造函数。

    子类对象被销毁时,为了能够调用父类的析构函数,编译器会为子类提供默认的析构函数。

    编译器提供默认的构造函数是基于两种需要:1、父类、成员对象、本类需要初始化虚表;2、父类、成员对象定义了构造函数,需要调用。

    子类对象在内存中的数据排列为:先安排父类数据,后安排子类新定义的数据。构造顺序为:先构造父类,后按照声明顺序构造成员对象和初始化列表中指定的成员,最后才是执行自身构造函数中的内容。

    • 构造函数内部虚函数失效问题

    举例:

    子类的构造函数会先调用父类的构造函数,并以子类对象的首地址作为this指针传递给父类的构造函数

    父类的构造函数中会有一步,把子类的虚表指针赋值为父类的虚表地址(目的是避免调用到子类的ShowSpeak,而是确保调用到父类的ShowSpeak):

    编译器发现这时虚表是父类的虚表,而这时的作用域也是在父类构造函数之内,所以就转换成了直接调用

    当你重新回到子类构造函数作用域中的时候,会重新给子类对象的虚表指针赋值:

     以上是构造函数中出现虚函数的调用。如果是成员函数中出现虚函数的调用呢?

     举例:

     两个Test中的ShowSpeak都是通过间接查找虚表来调用到虚函数的:

    这也解释了,为什么借助成员函数调用到的虚函数都是自身的那个虚函数了。

     如果是在构造函数中调用了一个成员函数,而这个成员函数中又调用了虚函数呢?

    举例:

    一上来调用CChinese的构造函数:

    调用父类的构造函数:

    调用父类构造函数中的Test时仍然使用的是子类对象的地址,但是会把子类对象的虚表指针的值赋值为父类的虚表地址:

    所以,尽管Test中的ShowSpeak是间接查虚表调用,但是仍调用到的是父类的虚函数:

     考虑另外一种情况,类对象直接调用虚函数:

    这如同调用普通 的成员函数,不会出现间接调用的情况,因为很明确是调用的哪个对象的函数:

    但是如果是类对象调用了一个虚函数,而虚函数中又调用了虚函数呢?比如:

    直接调用虚函数:

    虚函数内部间接调用另外一个虚函数:

    这时的Test()并没有传递指针参数,内部也不是通过指针调用的ShowSpeak(),所以就直接通过ecx查找到虚表并进行间接调用。否则,会设置一个虚表指针,根据虚表指针间接调用到正确的ShowSpeak。

    •  多重继承

    举例:

     构建CSofaBed的时候,先后构建两个父类:

    构建CSofaBed时前两个数据分别是CSofa的虚表地址和m_nColor,后三个数据分别是CBed的虚表地址和m_nLength、m_nWidth:

    两个虚表指向的内容分别如下:

    构建完两个父类之后,会重写这两个虚表地址的内容,并写入CSofaBed中独有的数据成员:

     三个数据被更改了:

    因为CSofaBed中覆写了CSofa中的两个虚函数,所以0x00d8cad4中所指的函数地址数组中有两个地址与0x00d8ca58中所指的三个函数地址有所不同,而第三个红线下边的函数是CSofaBed自定义的虚函数GetHeight:

    再来看0x00d8caec所指向的内容

     

     

    其实这里也是跳转到0x00d83a60.和第一个虚表的第一个虚函数(CSofaBed的虚析构函数)最终要执行的位置是一样的。

     再来看对CSofaBed进行析构的过程:

    先填写了CSofaBed中的两个虚表地址:

    然后再调用两个虚函数:

    ~CBed和~CSofa中又各自重新填写了虚表:

    提问,作为成员的类对象和继承下来的父类对象,在内存中的表现形式有什么区别?

    如果类没有虚函数,那么作为成员和作为父类,在内存中的表现形式几乎没有区别。但是如果有虚函数存在,就会有区别。举例:

    调用完两个父类的构造函数之后,会有两个虚表指针的写入:

    而随后的成员对象的构造函数被调用之后,没有虚表指针写入的步骤:

    •  抽象类

    在构建成员对象MemberSon的时候会先构建其父类Member:

    写入的虚表指针的第一项就是纯虚函数purefunc:

    来到了纯虚函数调用处:

    • 菱形继承

    举例:

    1、构造过程:

    构造CSofaBed会先压入一个1:

    进入CSofaBed的构造函数中发现,这里会有一个判断,这里的判断是为了防止重复构造Furniture。判断后,写入两个offset域到CSofaBed结构中,然后调用Furniture的构造函数:

    101DB60:

    101DB6C:

    调用完Furniture的构造函数之后,祖父类的内容在CSofaBed的内存结构中显现出来:

    随后构造Sofa:

    由于这里push的是0,所以Sofa的构造函数中会跳过对Furniture的构造,避免了重复构造:

    随后Sofa写入了自己的虚表地址和数据内容,同时借助offset更改了Furniture位置上的虚表指针的值(毕竟Sofa也重写了Furniture的虚函数):

    随后调用的CBed的构造函数同理,写入了自己的虚表地址,写入了自己的数据,覆写了Furniture位置上的虚表指针和数据:

     调用完CSofa和CBed的构造函数之后,写入自己的虚表指针,毕竟SofaBed也覆写了Sofa和Bed类中的虚函数:

    当然也要修改Furniture位置上的虚表指针的值:

     2、执行过程

    借助offset找到Furniture的位置:

    直接定位到Sofa的位置:

    同样也是直接定位到Bed的位置:

    借助offset调用到Furniture位置上的虚表中的虚函数:

    3、析构函数的调用

    现在CSofaBed中析构CSofa和CBed,然后再在后边析构CFurniture:

    • 虚继承多个父类的情况

    举例:

    内存布局如下:

    而且offset处会记录多个父类的偏移

     但是,如果有一个父类不是虚继承而来的,比如:

    那么存放offset的位置也会变化(不再是紧接在虚表指针之后):

     先构建虚基类CFurniture:

    随后构建CSofa:

    这里可以看出,offset实际上是跟在CSofa的虚表以及父类CFurniture2的数据成员7之后的。因为实际上这个虚表指针0eda74和数据成员7是其父类Furniture2的成员,只不过CSofa覆写了这个虚表指针而已。

    随后会构造CBed:

    同样,offset是排在CBed中的父类CFurniture2的内容(一个Furniture2的虚表指针和一个Furniture2的数据成员)的后边。

     最后写入CSofaBed的数据成员,构建完毕:

    而此时查看offset中的内容:

    只记录了一个父类的偏移。所以我们可以得出结论:offset中只记录虚基类的偏移,不记录父类的偏移

  • 相关阅读:
    LeetCode 453 Minimum Moves to Equal Array Elements
    LeetCode 112 Path Sum
    LeetCode 437 Path Sum III
    LeetCode 263 Ugly Number
    Solutions and Summay for Linked List Naive and Easy Questions
    AWS–Sysops notes
    Linked List
    All About Linked List
    datatable fix error–Invalid JSON response
    [转]反编译c#的相关问题
  • 原文地址:https://www.cnblogs.com/predator-wang/p/8078421.html
Copyright © 2011-2022 走看看