0x02虚函数分析

#include <iostream>
using namespace std;

class Base1 {
public:
    virtual void fun()
    {
        cout << "Base1::fun" << endl;
    }
    virtual void foo()
    {
        cout << "Base1::foo" << endl;
    }
    Base1()
    {
        printf("Base1\n");
    }
    ~Base1()
    {
        printf("~Base1\n");
    }

};

class Base2 {
public:
    virtual void fun()
    {
        cout << "Base2::fun" << endl;
    }
    virtual void foo()
    {
        cout << "Base2::foo" << endl;
    }
    Base2()
    {
        printf("Base2\n");
    }
    ~Base2()
    {
        printf("~Base2\n");
    }
};

class Derive : public Base1, public Base2{
public:
    virtual void fun()
    {
        cout << "Derive::fun" << endl;
    }
    virtual void foo1()
    {
        cout << "Derive::foo1" << endl;
    }
    Derive()
    {
        printf("Derive\n");
    }
    ~Derive()
    {
        printf("~Derive\n");
    }
};


int main()
{

    Derive d;
    Base1 *b1 = &d;
    b1->fun();
    b1->foo();
    Base2 *b2 = &d;
    b2->fun();
    b2->foo();
    return 0;
}

这里写图片描述
根据继承关系的顺序,首先调用了基类Base1的构造函数。在调用另一个基类Base2时, 并不是直接将对象的首地址作为this指针传递,而是向后调整了基类Base1的大小,以调整后的地址值作为this指针,最后再调用基类Base2的构造函数。
由于有了两个基类,因此子类在继承时也将他们的虚表指针一起继承了过来,也就有了两个虚表指针。在多继承中,派生类虚表指针的个数取决于所继承的基类个数。
这里写图片描述

这里写图片描述

这里写图片描述

这里写图片描述
在转换Base2指针时,je short 001A65E5会调整首地址并跳过第一个基类所占用的空间。
这里写图片描述

这里写图片描述
由于具有多个同级的基类,因此早派生类中产生了多个虚表指针。在对基类进行析构时,需要设置this指针,用于调用基类的析构函数。由于具有多个基类,当在析构的过程中调用各个基类的析构函数时,传递的首地址将有所不同,编译器会根据每个基类在对象中占用的空间位置,对应地传入各个基类部分首地址作为this指针。

总结:

单继承:

  1. 在类对象占用的内存空间中,只保存一份虚表指针。
  2. 由于只有一个虚表指针,对应的也只有一个虚表。
  3. 虚表中各项保存了类中各虚函数的首地址。
  4. 构造时先构造基类,在构造自身,并且只调用一次基类构造函数。
  5. 析构时先析构自身,在析构基类,并且只调用一次基类析构函数。

多重继承类:

  1. 在类对象中所占的内存空间中,根据继承基类的个数保存对应的虚表指针。
  2. 根据所保存的虚表指针的个数,对应产生相应个数的虚表。
  3. 构造时需要调用多个基类构造函数。
  4. 转换基类指针时,需要调转到对应的首地址。
  5. 构造时先构造继承列表中第一个基类,然后依次调用到最后一个继承的基类构造函数
  6. 析构时先析构自身,然后以与构造函数相反的顺序调用所有基类的析构函数。
<think>我们被要求通过汇编分析C++虚函数表的存储结构,并且要参考前面提供的引用内容。根据引用,虚函数表(vtable)的实现原理可以通过汇编代码来观察。 关键点: 1. 每个包含虚函数的类(或从包含虚函数的类派生)都有一个虚函数表。 2. 每个对象在创建时,会在对象的起始位置(即对象内存布局的开头)存储一个指向虚函数表的指针(通常称为vptr)。 3. 虚函数表本身是一个函数指针数组,其中每个元素指向该类的一个虚函数的实际实现。 4. 当通过基类指针或引用调用虚函数时,会通过对象的vptr找到虚函数表,然后根据函数在表中的偏移量来调用正确的函数。 通过汇编分析步骤: 1. 创建对象:观察对象的内存布局,特别是对象开头是否有一个指针被设置。 2. 调用虚函数:观察调用过程中如何通过对象地址找到虚函数表,如何通过偏移量找到函数地址,并进行调用。 根据引用中的内容: - 引用[2]提到:指向虚函数表的指针被存放在虚类的最开头。 - 引用[5]提到:初始化b的vptr,将虚表地址存入对象首地址。 - 引用[4]提到:对象的前四个字节(在32位系统)存储虚表地址,虚表内容就是虚函数的地址。 下面我们通过一个例子,结合汇编代码逐步分析。 示例C++代码: ```cpp class Animal { public: virtual void speak() { /* ... */ } }; class Dog : public Animal { public: void speak() override { /* ... */ } }; int main() { Animal* animal = new Dog(); animal->speak(); delete animal; return 0; } ``` 我们关注`animal->speak();`这一行对应的汇编。假设我们使用x86-64架构,编译器为GCC。 反汇编可能如下(关键部分): ```asm ; 创建Dog对象,假设使用new mov edi, 8 ; 分配8字节(假设指针大小8字节,Animal只有一个虚函数指针) call operator new(unsigned long) mov rbx, rax ; 保存返回的地址到rbx mov rdi, rbx ; 将地址作为this指针传递给构造函数 call Dog::Dog() ; 调用Dog的构造函数 ; 将animal指针指向新创建的对象 mov QWORD PTR [rbp-8], rbx ; 将对象地址存入animal变量 ; 调用animal->speak(); mov rax, QWORD PTR [rbp-8] ; 将animal指针(对象地址)加载到rax mov rax, QWORD PTR [rax] ; 取对象的前8个字节(即虚函数表指针)到rax mov rdi, QWORD PTR [rbp-8] ; 将this指针(即对象地址)作为第一个参数(隐含参数)传递 call QWORD PTR [rax] ; 调用虚函数表中的第一个函数(即speak) ; 后续省略... ``` 解释: 1. 在创建Dog对象时,首先分配内存,然后调用Dog的构造函数。在构造函数内部,会初始化虚函数表指针,使其指向Dog类的虚函数表。 2. 当调用`animal->speak()`时: - 首先从`animal`变量(即对象指针)所指向的内存中取出前8个字节(这就是虚函数表指针)。 - 然后准备调用:将对象指针作为this指针传递给函数(在x86-64上,通常第一个参数是rdi)。 - 接着从虚函数表中取出第一个函数指针(因为speak是第一个虚函数,所以它在虚函数表的第一项)并调用。 虚函数表的存储结构: - 虚函数表是一个静态数组,在编译时由编译器生成,存放在程序的只读数据段(如.rodata)。 - 每个类(而不是每个对象)都有自己的虚函数表。同一个类的所有对象共享同一个虚函数表。 - 虚函数表中的每一项是一个函数指针,指向该类的虚函数的实现。 - 派生类的虚函数表会继承基类的虚函数表,如果派生类覆盖了某个虚函数,则在相应位置替换为派生类的函数地址;如果派生类新增了虚函数,则追加在虚函数表末尾。 通过汇编如何验证虚函数表的存储结构? 1. 查看对象内存:在调试器中,打印对象地址处的内存,应该可以看到一个指针(在64位系统下是8字节)。这个指针指向虚函数表。 2. 查看虚函数表内存:然后打印该指针(虚函数表地址)处的内存,应该是一系列函数指针。每个函数指针对应一个虚函数的入口地址。 例如,假设对象地址为0x123400,那么: - 地址0x123400处存储的值(8字节)就是虚函数表地址,假设为0x567800。 - 然后,地址0x567800处开始的8字节就是第一个虚函数的地址。 - 地址0x567808处的8字节就是第二个虚函数的地址(如果有的话)。 引用[4]中描述:在dog对象的内存中,前四个字节(32位)或八个字节(64位)就是虚表地址,虚表的内容就是虚函数的地址。 总结方法: 1. 通过反汇编观察对象的创建过程,特别是构造函数中对虚函数表指针的初始化。 2. 通过反汇编观察虚函数调用时的寻址过程:先取对象地址,再取虚表指针,再计算偏移量,最后调用。 3. 使用调试器查看对象的内存布局,验证虚函数表指针的位置和虚函数表的内容。 注意:不同编译器、不同平台(x86/ARM)、不同调用约定可能会有差异,但基本原理相同。 相关问题: 1. 如何通过调试器查看C++对象的虚函数表? 2. 虚函数表在多重继承下的结构是怎样的? 3. 虚析构函数在虚函数表中是如何存储的? 4. 虚函数表在程序内存的哪个段? </think>### 通过汇编分析C++虚函数表存储结构的方法 #### 一、核心分析原理 虚函数表(vtable)在汇编层的本质是**连续的函数指针数组**,其存储结构遵循以下原则: 1. **虚表指针位置**:每个对象实例的首地址存储指向虚函数表的指针(vptr)[[2]](#引用2)[[5]](#引用5) 2. **虚表内存布局**:虚函数表本身是编译器生成的静态数据结构,包含按声明顺序排列的虚函数地址[[3]](#引用3)[[4]](#引用4) 3. **动态绑定机制**:虚函数调用通过`vptr → offset → call`三级寻址实现运行时绑定[[1]](#引用1) #### 二、具体分析步骤(x86-64架构示例) ##### 步骤1:定位虚表指针 ```asm ; 对象创建时的虚表指针初始化 lea rax, [vtable_Dog] ; 加载虚表地址到rax [^5] mov QWORD PTR [rbp-0x18], rax ; 存入对象首地址(vptr位置)[^5] ``` - 验证方法:通过调试器查看对象内存首8字节(64位系统)的值即为vptr地址 ##### 步骤2:解析虚表内容 ```asm ; 虚函数调用过程 mov rax, QWORD PTR [obj] ; 获取对象首地址 → vptr [^4] mov rax, QWORD PTR [rax] ; 获取虚表首地址 mov rdx, QWORD PTR [rax+8] ; 获取第二项虚函数地址(偏移8字节) call rdx ; 调用虚函数 ``` - 虚表存储结构示意: ``` 虚表地址+0x0: &Animal::vfunc1 虚表地址+0x8: &Dog::vfunc2 ; 派生类覆盖的函数 虚表地址+0x10: &Dog::vfunc3 ; 派生类新增虚函数 ``` ##### 步骤3:验证多重继承场景 ```asm ; 多重继承的虚表跳转 mov rax, QWORD PTR [obj+16] ; 访问第二个基类的vptr call QWORD PTR [rax+24] ; 调用偏移24字节的虚函数 ``` - 特征:每个基类对应独立的虚表指针,派生类对象包含多个vptr[[4]](#引用4) #### 三、关键验证技术 1. **内存查看器验证**: - 使用GDB的`x/xg obj_addr`查看对象首地址值 → vptr - 用`x/8g vptr_addr`查看虚表内容(每8字节一个函数指针) 2. **编译器布局检查**: ```bash g++ -fdump-class-hierarchy source.cpp # 输出类内存布局 objdump -d -j .rodata a.out # 查看只读段的虚表数据 ``` 3. **运行时调试技巧**: ```gdb (gdb) p *obj # 查看对象 (gdb) p /a *(void**)obj # 获取vptr (gdb) p /a ((void***)obj)[0][0] # 首个虚函数地址 (gdb) disas /m main # 反汇编关联源码 ``` #### 四、存储结构核心特征 1. **层级关系**: ```mermaid graph TD A[对象实例] --> B[vptr] B --> C[虚函数表] C --> D[&Base::func1] C --> E[&Derived::func2] C --> F[...] ``` 2. **跨平台差异**: | 架构 | vptr偏移 | 虚表项大小 | 调用约定 | |------------|----------|------------|---------------| | x86-64 | +0 | 8字节 | rdi传this指针[[2]](#引用2) | | ARM64 | +0 | 8字节 | x0传this指针 | | x86 | +0 | 4字节 | ecx传this指针 | 3. **动态特性证据**: - 同一虚函数在不同派生类中对应不同地址 - RTTI(类型信息)通常存储在虚表前的负偏移位置[[4]](#引用4) #### 五、典型误区的汇编验证 1. **虚表非"表"**: 汇编显示虚表是连续内存块,而非抽象数据结构 ```asm section .rodata vtable_Dog: dq 0 ; 类型信息偏移 dq _ZN3Dog5barkEv ; 函数1地址 dq _ZN3Dog5jumpEv ; 函数2地址 ``` 2. **非单继承优化**: 多重继承时,汇编显示派生类包含多个独立虚表指针: ```asm mov rax, [obj] ; 第一个基类vptr mov rbx, [obj+32] ; 第二个基类vptr ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值