逆向分析c++虚函数指针和虚函数表及其内存结构

1. 环境和工具

  • Ubuntu 18.04.4 LTS
  • g++ 7.5.0
  • objdump 静态反汇编
  • gdb + peda插件 动态调试

2. 从一个程序说起

#include <iostream>

class AA
{
public:
    virtual void print() {std::cout << "AA" <<std::endl;}
};

class BB
{
public:
    virtual void print() {std::cout << "BB" <<std::endl;}
};

int main(int argc,char *argv[])
{
    AA *p = (AA *)new BB();

    p->print();

    return 0;
}

输出结果:

BB

这段代码不是c++的标准写法,按照以往的认识BB和AA没有继承之间关系,应该没法出现多态的情况。但事实让我惊讶,这让我对c++多态的实现原理和虚函数表有了兴趣,最终决定逆向反汇编去看看其中的内存结构和虚函数调用流程

3. 无继承

3.1 虚函数指针的产生条件与初始化

  • 类里面有虚函数
  • 在构造函数中初始化

要验证很简单写两个类对照

案例1:

#include <iostream>

class AA
{
public:
    void print() {std::cout << "AA" << std::endl;};
    int a;
};

class BB
{
public:
    virtual void print() {std::cout << "BB" << std::endl;};
    int b;
};

int main(int argc,char *argv[])
{
    AA *pa = new AA();
    BB *pb = new BB();
    pa->print();
    pb->print();

    return 0;
}

使用AA和BB做对照,下面是使用objdump反汇编出来的部分代码。为大家更好理解写一些原始代码注释

00000000000009ba <main>:
 9ba:   55                      push   %rbp 
 9bb:   48 89 e5                mov    %rsp,%rbp
 9be:   53                      push   %rbx 
 9bf:   48 83 ec 28             sub    $0x28,%rsp
 9c3:   89 7d dc                mov    %edi,-0x24(%rbp)
 9c6:   48 89 75 d0             mov    %rsi,-0x30(%rbp)
 9ca:   bf 04 00 00 00          mov    $0x4,%edi
 9cf:   e8 9c fe ff ff          callq  870 <_Znwm@plt>		//分配内存 返回到rax
 9d4:   c7 00 00 00 00 00       movl   $0x0,(%rax)			//初始化第一个成员,成员变量a
 9da:   48 89 45 e0             mov    %rax,-0x20(%rbp)		//局部比例赋值,pa
 								AA *pa = (AA *)calloc(1,sizeof(AA));//类似于
 								
 9de:   bf 10 00 00 00          mov    $0x10,%edi
 9e3:   e8 88 fe ff ff          callq  870 <_Znwm@plt>		//分配内存
 9e8:   48 89 c3                mov    %rax,%rbx
 9eb:   48 c7 03 00 00 00 00    movq   $0x0,(%rbx)			//初始化第一个成员,虚函数指针
 9f2:   c7 43 08 00 00 00 00    movl   $0x0,0x8(%rbx)		//初始化第二个成员,成员变量b
 								void *p = calloc(1,sizeof(BB));//类似于
 
 9f9:   48 89 df                mov    %rbx,%rdi			//rdi = p,this指针的由来
 9fc:   e8 fd 00 00 00          callq  afe <_ZN2BBC1Ev>		//构造函数
 a01:   48 89 5d e8             mov    %rbx,-0x18(%rbp)		//局部比例赋值,pb
 								BB *pb = p;
.....

这是main函数的一些解析,再解析_ZN2BBC1Ev之前(BB的构造函数)先总结并了解一些东西

  • 如果类的没有虚函数并且成员变量没有一开始初始化,并不会有构造函数(AA()不存在)
  • 类成员函数的调用规则是,调用前rdi寄存器保存类的地址,在进入成员函数的时候用局部变量保存rdi的值,也就是传说中的this指针(调用约定64位下前6个参数由寄存器保存。rdi保存第一个参数,rsi保存第二个…)

下面来分析_ZN2BBC1Ev(BB的构造函数)

0000000000000afe <_ZN2BBC1Ev>:                                    
 afe:   55                      push   %rbp 
 aff:   48 89 e5                mov    %rsp,%rbp
 b02:   48 89 7d f8             mov    %rdi,-0x8(%rbp)		//在调用之前rdi保存的类地址
     							this = rdi;
     
 b06:   48 8d 15 63 12 20 00    lea    0x201263(%rip),%rdx  //传说中的虚表
 b0d:   48 8b 45 f8             mov    -0x8(%rbp),%rax		//rax = this
 b11:   48 89 10                mov    %rdx,(%rax)			//保存虚表
     							this->ptr = rdx				//虚表指针保存虚表
     
 b14:   90                      nop  
 b15:   5d                      pop    %rbp 
 b16:   c3                      retq

这个函数并不难,由于我们并没有直接对成员变量b赋值,所以构造函数中只对第一个成员(虚表指针)做了赋值

了解一下构造函数对成员变量赋值的模式

3.1.1 构造函数对成员变量的初始化模式

mov    -0x8(%rbp),%rax
mov    %rdx,xxx(%rax)

-0x8(%rbp) 表示this指针
xxx是偏移偏移量,第一个成员就是0 =》 mov    %rdx,(%rax)

为了验证”lea 0x201263(%rip),%rdx“中保存的是虚表,我们来用gdb动态调试一下

在这里插入图片描述

运行到这里我们用x/20gz 查看rdx的值,然后我们使用disassemble查看

在这里插入图片描述

使用disassemble 查看反汇编代码。可以看到”lea 0x201263(%rip),%rdx“确实保存了虚表

总结:

  • 构造函数中使用”lea 0xxxxx(%rip),%rdx“的方式拿到虚表
  • 一个单独的类(无父类)虚表指针是成员变量的第一个成员

下面来通过下面的代码证实结论

案例2:

#include <iostream>

class AA
{
public:
    virtual void print() {std::cout << "AA" << std::endl;};
};

void print_aaa()
{
    printf("aaa\n");
}

int main(int argc,char *argv[])
{
    AA *p = new AA();
	//申请一块内存,用来作为函数地址表
    unsigned long *pfunctions = (unsigned long *)calloc(1,8);
    pfunctions[0] = (unsigned long)print_aaa;
	//拿到第一个成员的地址 虚表指针
    unsigned long *ptr = (unsigned long *)p;
    //改变虚表指针的指向
    *ptr = (unsigned long)pfunctions;

    //调用函数print函数
    p->print();

    return 0;
}

程序输出:
aaa

在这里插入图片描述

这里我们改变了虚函数的指针,指向一块填写的内存然后调用print函数,结果成功的执行到了写入内存块的函数。(注意普通函数和成员函数的调用约定是不同的会导致参数的乱序,所以只使用了无参函数)

下面分析一下执行函数的汇编代码(有点长删减了一些代码)

0000000000000a5d <main>:
......
 a72:   e8 79 fe ff ff          callq  8f0 <_Znwm@plt>	//申请内存,内存地址保存在rax
 a77:   48 89 c3                mov    %rax,%rbx		//rbx = rax
 a7a:   48 c7 03 00 00 00 00    movq   $0x0,(%rbx)		//第一个成员清0 (虚函数指针)
 a81:   48 89 df                mov    %rbx,%rdi  //rdi保存内存地址(调用约定,函数内生成this指针)
 a84:   e8 ed 00 00 00          callq  b76 <_ZN2AAC1Ev> //构造函数
 a89:   48 89 5d d8             mov    %rbx,-0x28(%rbp) //内存地址赋值给局部变量
 								AA *p = new AA();
......
		调用流程
 ac1:   48 8b 45 d8             mov    -0x28(%rbp),%rax //rax拿到类地址 (在a89行保存了地址)
 ac5:   48 8b 00                mov    (%rax),%rax		//拿第一个成员的值(虚函数表指针)
 ac8:   48 8b 00                mov    (%rax),%rax		//拿虚函数表的第一个成员的值
 acb:   48 8b 55 d8             mov    -0x28(%rbp),%rdx //rdx拿到类地址
 acf:   48 89 d7                mov    %rdx,%rdi		//rdi拿到类地址(调用约定)
 ad2:   ff d0                   callq  *%rax			//执行函数
 								p->print();
......

调用流程的代码是固定的,也就是**“p->print();”生成的代码是固定的模式**

3.1.2 虚函数的调用模式

mov    -0xxx(%rbp),%rax //rax拿到类地址
mov    (%rax),%rax		//拿第一个成员的值(虚函数表指针)
add    $0x0,%rax		//+偏移(如果是第一个虚函数则没有)
mov    (%rax),%rax		//拿虚函数表成员的值
mov    -0xxx(%rbp),%rdx //rdx拿到类地址
mov    %rdx,%rdi		//rdi拿到类地址(调用约定)用于生成this指针
callq  *%rax			//执行函数

上面的代码案例总结:

  1. 虚函数指针的生成方法(有虚函数的存在),初始化在构造函数中
  2. this指针的生成和调用约定
  3. 一个单独的类(无父类)虚表指针是成员变量的第一个成员
  4. 虚函数的调用流程

有了这些基础我们来分析最开始的代码

#include <iostream>

class AA
{
public:
    virtual void print() {std::cout << "AA" <<std::endl;}
};

class BB
{
public:
    virtual void print() {std::cout << "BB" <<std::endl;}
};

int main(int argc,char *argv[])
{
    AA *p = (AA *)new BB();
    p->print();
    return 0;
}

分析者个代码只要上面的1,4基础

首先是 “new BB()” 分配内存并执行构造函数,这里初始化了虚函数指针(明白虚函数表里存的是BB::print)

然后是 “p->print();” 虚函数的调用流程

mov    -0xxx(%rbp),%rax //rax拿到类地址
mov    (%rax),%rax		//拿第一个成员的值(虚函数表指针)
mov    (%rax),%rax		//拿虚函数表的第一个成员的值
mov    -0xxx(%rbp),%rdx //rcx拿到类地址
mov    %rdx,%rdi		//rdi拿到类地址(调用约定)
callq  *%rax			//执行函数

由于模板是固定的所以就是执行虚函数表的第一个函数(而这个虚函数表是在BB初始化的,所以是BB::print)

所以多态的实现最关键的地方在于构造函数初始化虚表指针

为了加深印象更好的理解这几个规则。在写一个案例

案例3:

#include <iostream>

class AA
{
public:
    virtual void print() {std::cout << "AA" << std::endl;};
    virtual void print_t() {}; 
};

void print_aaa()
{
    printf("aaa\n");
}

void print_bbb()
{
    printf("bbb\n");
}

int main(int argc,char *argv[])
{
    //构建函数表
    unsigned long *pfunctions = (unsigned long *)calloc(1,16);
    pfunctions[0] = (unsigned long)print_aaa;
    pfunctions[1] = (unsigned long)print_bbb;
	
    AA *p = (AA *)&pfunctions;
    p->print();
    p->print_t();

    return 0;
}

案例输出:

aaa

bbb

这里直接没有类,手动去构造一个函数表,让类AA的指针去执行两个虚函数。(借助虚函数的调用模式实现)

这个代码也不是c++的标准写法,但这个确实是理解虚函数调用模式的好案例

3.2 探究虚函数表

案例4:

#include <iostream>

class AA
{
public:
    virtual void print() {std::cout << "AA" << std::endl;};
    virtual void print_t() {std::cout << "AA_t" <<std::endl;};
};

int main(int argc,char *argv[])
{
    AA *p = new AA();
    
    p->print();
    p->print_t();

    return 0;
}

直接objdump -sd 程序名 找到AA的构造函数

0000000000000b0e <_ZN2AAC1Ev>:
 b0e:   55                      push   %rbp
 b0f:   48 89 e5                mov    %rsp,%rbp
 b12:   48 89 7d f8             mov    %rdi,-0x8(%rbp)
 b16:   48 8d 15 4b 12 20 00    lea    0x20124b(%rip),%rdx
 b1d:   48 8b 45 f8             mov    -0x8(%rbp),%rax
 b21:   48 89 10                mov    %rdx,(%rax)
 b24:   90                      nop 
 b25:   5d                      pop    %rbp
 b26:   c3                      retq

找到b16拿到虚函数指针。地址为rip + 0x20124b 也就是 b1d + 0x20124b = 0x201d68

搜索201d68(虚函数表地址)

在这里插入图片描述

找到201d68,可以看到保存的值。由于linux系统使用的是小端(高位高地址,低位低地址),这是的地址是从右往左递增,所以第一个虚函数地址是 0xa9e

搜索0xa9e地址

在这里插入图片描述

这就是AA::print函数**(由于c++要支持函数重载所以编译的符号会加上前后缀以区分同名函数**)

我们找到了虚函数表的位置位于“.data.rel.ro”段。所以可以确定我们的程序中没有填写虚函数表的过程,虚函数表是编译器生成,然后在构造函数中给虚函数指针初始化

4. 单继承

探究继承跟虚函数表的关系

案例5:

#include <iostream> 

class AA
{
public:
    AA(){
        m_a1 = 1;
    };  

    virtual void print() {std::cout << "AA" << std::endl;};
    virtual void print_t() {std::cout << "AA_t" <<std::endl;};

    int m_a1;
};

class BB : public AA
{
public:
    BB(){
        m_b1 = 11; 
        m_b2 = 12; 
    }   

    virtual void print() {std::cout << "BB" << std::endl;};
    virtual void print_b() {std::cout << "BB_b" << std::endl;};

    int m_b1;
    int m_b2;
};

int main(int argc,char *argv[])
{
    AA *p = new BB();

    p->print();
    p->print_t();

    return 0;
}

在BB中重写一个虚函数,一个不重写,另外在写一个虚函数

之前测试过,虚函数指针在构造函数中初始化因此我们直接反汇编BB的构造

0000000000000b88 <_ZN2AAC1Ev>:	//AA的构造函数                                       
 b88:   55                      push   %rbp
 b89:   48 89 e5                mov    %rsp,%rbp
 b8c:   48 89 7d f8             mov    %rdi,-0x8(%rbp)			//this指针
 b90:   48 8d 15 b9 11 20 00    lea    0x2011b9(%rip),%rdx      //虚函数表
 
 b97:   48 8b 45 f8             mov    -0x8(%rbp),%rax
 b9b:   48 89 10                mov    %rdx,(%rax)				//虚函数指针指向虚函数表
 
 b9e:   48 8b 45 f8             mov    -0x8(%rbp),%rax
 ba2:   c7 40 08 01 00 00 00    movl   $0x1,0x8(%rax)			//初始化m_a1
 								this->m_a1 = 1;
 		
 ba9:   90                      nop
 baa:   5d                      pop    %rbp
 bab:   c3                      retq

0000000000000c1c <_ZN2BBC1Ev>:	//BB的构造函数
 c1c:   55                      push   %rbp
 c1d:   48 89 e5                mov    %rsp,%rbp
 c20:   48 83 ec 10             sub    $0x10,%rsp
 c24:   48 89 7d f8             mov    %rdi,-0x8(%rbp)			//this指针
 c28:   48 8b 45 f8             mov    -0x8(%rbp),%rax
 c2c:   48 89 c7                mov    %rax,%rdi				//类地址传给AA的构造
 c2f:   e8 54 ff ff ff          callq  b88 <_ZN2AAC1Ev>			//调用AA的构造
 c34:   48 8d 15 ed 10 20 00    lea    0x2010ed(%rip),%rdx      //虚函数表
 
 c3b:   48 8b 45 f8             mov    -0x8(%rbp),%rax
 c3f:   48 89 10                mov    %rdx,(%rax)				//虚函数指针指向虚函数表
 
 c42:   48 8b 45 f8             mov    -0x8(%rbp),%rax
 c46:   c7 40 0c 0b 00 00 00    movl   $0xb,0xc(%rax)			//初始化m_b1
 								this->m_b1 = 11;
 								
 c4d:   48 8b 45 f8             mov    -0x8(%rbp),%rax
 c51:   c7 40 10 0c 00 00 00    movl   $0xc,0x10(%rax)			//初始化m_b2
 								this->m_b2 = 12;
 								
 c58:   90                      nop 
 c59:   c9                      leaveq 
 c5a:   c3                      retq

在调用BB的构造函数时,会在其构造函数中调用AA构造函数并初始化AA的内存部分,调用完之后在初始化BB的内存部分。可以看到两个构造函数都初始化了虚函数指针(c3f,b9b)最后以c3f为最后的值 (后写入),继承后虚函数指针也只有一份

内存图:

在这里插入图片描述

用之前找虚函数表的方法找虚函数表的位置

BB 201d28

AA 201d50

在这里插入图片描述

可以看到BB的三个虚函数地址: c5c、be4、c94 (不太清楚以什么终止,但是0地址不可能是函数)

可以看到AA的三个虚函数地址: bac、be4

从数据可以看出单继承虚函数是由本生所有虚函数 + 未重写父类的虚函数合集(这个结论其实初学c++就是知道的,这个案例主要是想看继承后的内存模型)

5. 多继承

探究继承跟虚函数表的关系

案例6:

#include <iostream>

class AA
{
public:
    AA(){
        m_a1 = 1;
        m_a2 = 2;
    }   

    virtual void print() {std::cout << "AA" << std::endl;}
    virtual void print_t() {std::cout << "AA_t" << std::endl;}

    int m_a1;
    int m_a2;
};

class CC
{
public:
    CC(){
        m_c1 = 11; 
        m_c2 = 12; 
    }   

    virtual void print() {std::cout << "CC" << std::endl;}
    virtual void print_t() {std::cout << "CC_t" << std::endl;}

    int m_c1;
    int m_c2;
};

class DD
{
public:
    DD(){
        m_d1 = 21;
        m_d2 = 22;
    }

    virtual void print_d() {std::cout << "DD" << std::endl;}
    virtual void print_dd() {std::cout << "DD_t" << std::endl;}

    int m_d1;
    int m_d2;
};

class BB : public AA , public CC , public DD
{
public:
    BB(){
        m_b1 = 31;
        m_b2 = 32;
    }

    virtual void print() {std::cout << "BB" << std::endl;}
    void print_t() {std::cout << "BB_t" << std::endl;}

    int m_b1;
    int m_b2;
};

int main(int argc,char *argv[])
{
    BB *pb = new BB();
    printf("pb =  %p\n",pb);
    AA *pa = (AA *)pb;
    printf("pa =  %p\n",pa);
    CC *pc = (CC *)pb;
    printf("pc =  %p\n",pc);
    DD *pd = (DD *)pb;
    printf("pd =  %p\n",pd);

    return 0;
}

程序输出:

pb = 0x559d11172e70
pa = 0x559d11172e70
pc = 0x559d11172e80
pd = 0x559d11172e90

说实话一开始看到这个输出我觉得很纳闷,指针赋值地址居然变了。要弄清楚这个问题我们先来探究多继承的内存结构是怎么样的。

我们从BB的构造函数入手去探究其内存结构

0000000000000dda <_ZN2AAC1Ev>: 
     dda:   55                      push   %rbp
     ddb:   48 89 e5                mov    %rsp,%rbp
     dde:   48 89 7d f8             mov    %rdi,-0x8(%rbp)
     de2:   48 8d 15 0f 0f 20 00    lea    0x200f0f(%rip),%rdx

     de9:   48 8b 45 f8             mov    -0x8(%rbp),%rax
     ded:   48 89 10                mov    %rdx,(%rax)		//初始化虚函数指针,最后会在BB覆盖
         
     df0:   48 8b 45 f8             mov    -0x8(%rbp),%rax
     df4:   c7 40 08 01 00 00 00    movl   $0x1,0x8(%rax)
         							this->m_a1 = 1;
         
     dfb:   48 8b 45 f8             mov    -0x8(%rbp),%rax
     dff:   c7 40 0c 02 00 00 00    movl   $0x2,0xc(%rax)
         							this->m_a2 = 2;

     e06:   90                      nop
     e07:   5d                      pop    %rbp
     e08:   c3                      retq

0000000000000e7a <_ZN2CCC1Ev>:
     e7a:   55                      push   %rbp
     e7b:   48 89 e5                mov    %rsp,%rbp
     e7e:   48 89 7d f8             mov    %rdi,-0x8(%rbp)   //this指针(注意是类地址偏移16)     
     e82:   48 8d 15 4f 0e 20 00    lea    0x200e4f(%rip),%rdx
         
     e89:   48 8b 45 f8             mov    -0x8(%rbp),%rax
     e8d:   48 89 10                mov    %rdx,(%rax)		//初始化虚函数指针,最后会在BB覆盖
                
     e90:   48 8b 45 f8             mov    -0x8(%rbp),%rax
     e94:   c7 40 08 0b 00 00 00    movl   $0xb,0x8(%rax)
         							this->m_c1 = 11;
         
     e9b:   48 8b 45 f8             mov    -0x8(%rbp),%rax
     e9f:   c7 40 0c 0c 00 00 00    movl   $0xc,0xc(%rax)
         							this->m_c2 = 12;
         
     ea6:   90                      nop
     ea7:   5d                      pop    %rbp
     ea8:   c3                      retq

0000000000000f1a <_ZN2DDC1Ev>:  
     f1a:   55                      push   %rbp
     f1b:   48 89 e5                mov    %rsp,%rbp
     f1e:   48 89 7d f8             mov    %rdi,-0x8(%rbp)	//this指针(注意是类地址偏移32)
     f22:   48 8d 15 8f 0d 20 00    lea    0x200d8f(%rip),%rdx
         
     f29:   48 8b 45 f8             mov    -0x8(%rbp),%rax
     f2d:   48 89 10                mov    %rdx,(%rax)		//初始化虚函数指针,最后会在BB覆盖
         
     f30:   48 8b 45 f8             mov    -0x8(%rbp),%rax
     f34:   c7 40 08 15 00 00 00    movl   $0x15,0x8(%rax)
         							this->m_d1 = 21;
         
     f3b:   48 8b 45 f8             mov    -0x8(%rbp),%rax
     f3f:   c7 40 0c 16 00 00 00    movl   $0x16,0xc(%rax)
         							this->m_d2 = 22;
         
     f46:   90                      nop
     f47:   5d                      pop    %rbp
     f48:   c3                      retq
         
0000000000000fba <_ZN2BBC1Ev>:
     fba:   55                      push   %rbp
     fbb:   48 89 e5                mov    %rsp,%rbp
     fbe:   48 83 ec 10             sub    $0x10,%rsp
     fc2:   48 89 7d f8             mov    %rdi,-0x8(%rbp)		//this指针
         
     fc6:   48 8b 45 f8             mov    -0x8(%rbp),%rax
     fca:   48 89 c7                mov    %rax,%rdi			//调用约定,rdi保存类地址
     fcd:   e8 08 fe ff ff          callq  dda <_ZN2AAC1Ev>		//AA的构造
         
     fd2:   48 8b 45 f8             mov    -0x8(%rbp),%rax
     fd6:   48 83 c0 10             add    $0x10,%rax			//类的基地址偏移this + 16
     fda:   48 89 c7                mov    %rax,%rdi
     fdd:   e8 98 fe ff ff          callq  e7a <_ZN2CCC1Ev>		//CC的构造
         
     fe2:   48 8b 45 f8             mov    -0x8(%rbp),%rax
     fe6:   48 83 c0 20             add    $0x20,%rax			//类的基地址偏移32
     fea:   48 89 c7                mov    %rax,%rdi
     fed:   e8 28 ff ff ff          callq  f1a <_ZN2DDC1Ev>		//DD的构造
         
     ff2:   48 8d 15 5f 0c 20 00    lea    0x200c5f(%rip),%rdx	//第一个虚函数表
     ff9:   48 8b 45 f8             mov    -0x8(%rbp),%rax
     ffd:   48 89 10                mov    %rdx,(%rax)			//第一个虚函数表给类的起始地址
         
    1000:   48 8d 15 71 0c 20 00    lea    0x200c71(%rip),%rdx	//第二个虚函数表
    1007:   48 8b 45 f8             mov    -0x8(%rbp),%rax
    100b:   48 89 50 10             mov    %rdx,0x10(%rax)		//第二个虚函数表给this+16
        
    100f:   48 8d 15 82 0c 20 00    lea    0x200c82(%rip),%rdx	//第三个虚函数表
    1016:   48 8b 45 f8             mov    -0x8(%rbp),%rax
    101a:   48 89 50 20             mov    %rdx,0x20(%rax)		//第三个虚函数表给this+32
        
    101e:   48 8b 45 f8             mov    -0x8(%rbp),%rax
    1022:   c7 40 30 1f 00 00 00    movl   $0x1f,0x30(%rax)
        							this->m_b1 = 31;
        
    1029:   48 8b 45 f8             mov    -0x8(%rbp),%rax
    102d:   c7 40 34 20 00 00 00    movl   $0x20,0x34(%rax)
        							this->m_b2 = 32;
        
    1034:   90                      nop
    1035:   c9                      leaveq 
    1036:   c3                      retq  

在BB的构造函数中按照继承顺序先后调用AA,CC,DD的构造函数,最后初始化了三个虚函数表指针。

在调用构造函数前,rdi的值是用来生成this指针的。根据这一点和各个构造函数中初始化的值我们可以推断多继承的内存结构

内存图:

在这里插入图片描述

现在我们再看一下输出:

pb = 0x559d11172e70
pa = 0x559d11172e70
pc = 0x559d11172e80
pd = 0x559d11172e90

pa = pb + 0,pc = pb + 16,pd = pb + 32

  • 我们可以看到指针都指向了属于他们类的内存部分,并且BB本身没有虚函数指针(其实是合一了,和单继承一样。可以理解为只有一个亲爹?)
  • 属于BB的内存叠在最后

为什么指针要指向属于他们的内存部分?可以看看之前分析的虚函数调用流程

mov    -0xxx(%rbp),%rax //rax拿到类地址
mov    (%rax),%rax		//拿第一个成员的值(虚函数表指针)
add    $0x0,%rax		//+偏移(如果是第一个虚函数则没有)
mov    (%rax),%rax		//拿虚函数表的第一个成员的值

由于调用流程的模式一直是这样,默认第一个成员是虚函数指针。

6. 总结

  • 了解虚表指针产生条件和初始化
  • 虚函数的调用模式
  • 继承下的虚表指针和内存模型

下面是我逆向之后,改变对c++的一些认识误区

  • 默认构造函数不存在(没有虚函数和对成员的初始化的条件下。)
  • 继承下的构造函数调用先调用父类的构造,然后调用子类的构造(其实是子类构造先调用,只不过在子类构造函数中会先调用父类构造)
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值