静态类型
概述
- 静态类型是一个表达式在编译期就确定的类型,它由声明决定,永远不会改变
- 示例
- 无论运行时
p实际指向什么,p的静态类型始终是Animal*,这是编译器在词法分析阶段就锁死的
- 无论运行时
|
1 2 3 4 5 6 |
class Animal { ... }; class Dog : public Animal { ... }; Animal* p; // p 的静态类型永远是 Animal* Animal& ref = *p; // ref 的静态类型永远是 Animal& Dog d; // d 的静态类型永远是 Dog |
什么时候确定
- 编译期,词法/语义分析阶段
- 编译器看到变量声明时,立刻将类型信息记入符号表(
Symbol Table)
- 编译器看到变量声明时,立刻将类型信息记入符号表(
存在哪里
- 静态类型不存在于运行时的任何内存中,它只活在编译阶段
- 编译完成后,静态类型信息被用于:
- 生成正确的函数调用指令(非虚函数直接绑定地址)
- 类型检查(不合法的赋值/转换在此报错)
- 决定非虚函数调用走哪个版本
动态类型
概述
- 动态类型是指针或引用在运行时实际指向(或绑定)的那个完整对象的类型,只对指针和引用有意义
|
1 2 |
Animal* p = new Dog(); // 静态类型:Animal*,动态类型:Dog p = new Animal(); // 静态类型:Animal*,动态类型:Animal(变了) |
- 对于非指针非引用的普通对象,静态类型 == 动态类型,不存在区别:
|
1 |
Dog d; // 静态类型 Dog == 动态类型 Dog,永远如此 |
什么时候确定
- 确定时机:运行时,对象构造时
- 动态类型通过
vptr(虚函数表指针) 来携带,在构造函数执行期间被写入对象
- 动态类型通过
存在哪里
- 对象内存的头部(
vptr)- 每个含有虚函数的类的对象,内存布局的最开头(通常)有一个隐藏的指针
vptr,它指向该类的vtable(虚函数表)
- 每个含有虚函数的类的对象,内存布局的最开头(通常)有一个隐藏的指针
|
1 2 3 4 5 6 7 8 |
Dog 对象的内存布局: ┌─────────────────────────────┐ │ vptr ──────────────────────┼──► Dog::vtable ├─────────────────────────────┤ │ │ Animal 部分的数据成员 │ ├─ Dog::speak() ├─────────────────────────────┤ ├─ Dog::move() │ Dog 自己的数据成员 │ └─ ... └─────────────────────────────┘ |
vptr就是动态类型的"运行时身份证",它指向哪张vtable,对象的动态类型就是哪个类
vtable 存在哪里
vtable是一张函数指针数组,由编译器在编译期为每个多态类生成,存放在程序的只读数据段(.rodata)中- 每个类只有一份,所有该类的对象共享
|
1 2 3 4 5 6 7 8 9 10 11 |
内存分区示意: ───────────────────────── .text │ 代码 ───────────────────────── .rodata │ Animal::vtable ← 编译期生成,运行时只读 │ Dog::vtable ───────────────────────── .heap │ new Dog() 的对象 ← vptr 在这里,指向上面的 vtable ───────────────────────── .stack │ 局部变量 ───────────────────────── |
运行时确定动态类型的详细过程
- 编译器的准备工作(运行前)
- 编译器为每个多态类生成
vtable,并在每个构造函数中插入vptr赋值指令 - 这一步是自动的,程序员看不到,但可以从汇编中观察到
- 编译器为每个多态类生成
|
1 2 3 4 5 6 7 8 9 |
class Animal { public: virtual void speak() { } }; class Dog : public Animal { public: void speak() override { } }; |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 编译器悄悄将构造函数改写为(伪代码): // 编译器生成的 Animal 构造函数(伪代码) Animal::Animal() { this->vptr = &Animal::vtable; // ← 编译器插入,你看不到 // ... 你写的构造函数体 } // 编译器生成的 Dog 构造函数(伪代码) Dog::Dog() { Animal::Animal(this); // 先调用基类构造 this->vptr = &Dog::vtable; // ← 再覆盖 vptr!关键! // ... 你写的构造函数体 } |
- 对象构造时
vptr的写入过程- 这就是为什么不应该在构造函数中调用虚函数:
- 基类构造阶段,
vptr还指向基类的vtable,虚函数不会多态分发
|
1 |
Animal* p = new Dog(); |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
// 这一行触发的完整序列: 1. operator new 在堆上分配 sizeof(Dog) 字节的内存 ┌─────────────────────┐ │ ???? │ ???? │ │ ← 全是未初始化数据 └─────────────────────┘ 2. 调用 Animal::Animal() → 编译器插入:this->vptr = &Animal::vtable ┌─────────────────────┐ │ Animal │ Animal │ ← vptr 指向 Animal::vtable │ vptr │ data │ └─────────────────────┘ 此刻如果在基类构造函数中调用虚函数,动态类型是 Animal! 3. 调用 Dog::Dog() → 编译器插入:this->vptr = &Dog::vtable ← 覆盖! ┌─────────────────────┐ │ Dog │ Animal │ Dog │ ← vptr 现在指向 Dog::vtable │ vptr │ data │ data │ └─────────────────────┘ 构造完成,动态类型确立为 Dog 4. 返回指针,p(静态类型 Animal*)指向这个对象 |
- 运行时虚函数调用的分发过程
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
p->speak(); // p 的静态类型是 Animal* // 编译器看到这是虚函数调用,生成如下逻辑(对应的汇编伪代码): 1. 读取对象头部的 vptr mov rax, [p] ; rax = p 指向的对象的第一个字段,即 vptr 2. 根据 speak() 在 vtable 中的偏移量,取出函数地址 mov rax, [rax + 0] ; 偏移量由编译器在编译期确定,speak() 是第0项 ; rax 现在是 Dog::speak 的地址 3. 调用 call rax ; 动态分发,实际执行 Dog::speak() |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// 整个过程用一张图总结: p(Animal*,静态类型) │ │ 运行时解引用 ▼ ┌──────────────────────────┐ │ vptr ────────────────────┼──► Dog::vtable(.rodata) │ Animal::data │ ├─[0] Dog::speak ◄── 取这里 │ Dog::data │ ├─[1] Dog::move └──────────────────────────┘ └─ ... ↑ 动态类型就是 "vptr 指向哪张 vtable" 这张表属于 Dog, 所以动态类型是 Dog |
- 析构时
vptr的逆向复原- 析构的顺序和构造相反,
vptr也会被逐步还原:
- 析构的顺序和构造相反,
|
1 2 3 4 5 6 7 8 9 10 11 12 |
delete p; 1. 调用 Dog::~Dog() → 编译器插入:this->vptr = &Dog::vtable (析构体执行期间) → 执行 Dog 的析构体 → 编译器插入:this->vptr = &Animal::vtable (即将调用基类析构前) 2. 调用 Animal::~Animal() → vptr 已经是 Animal::vtable → 执行 Animal 的析构体 3. operator delete 释放内存 |
问题
在构造函数中调用普通虚函数
- 调用的虚函数,基类有这个虚函数
- 输出
Animal::speak(),不是Dog::speak() - 在
Animal::Animal()执行期间,对象的动态类型就是Animal,因为此时vptr指向Animal::vtable Dog的部分尚未构造,编译器通过vptr赋值时机保证了这个行为的一致性
- 输出
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Animal { public: Animal() { speak(); // 在基类构造函数中调用虚函数 } virtual void speak() { cout << "Animal::speak()" << endl; } }; class Dog : public Animal { public: Dog() { } void speak() override { cout << "Dog::speak()" << endl; } }; // 执行: Animal* p = new Dog(); |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
// 执行过程详解 new Dog() 触发的构造序列: ┌─────────────────────────────────────────────────┐ │ Step 1:分配内存 │ │ 堆上分配 sizeof(Dog) 字节 │ │ ┌──────────────────────┐ │ │ │ ???? │ ???? │ │ ← 未初始化 │ │ └──────────────────────┘ │ └─────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────┐ │ Step 2:调用 Animal::Animal() │ │ 编译器插入:this->vptr = &Animal::vtable │ │ ┌──────────────────────┐ │ │ │ Animal::vptr │ data │ │ │ └──────┬───────────────┘ │ │ └──► Animal::vtable │ │ [0] Animal::speak ← 此刻表里 │ │ 只有这个 │ │ │ │ ⚡ 执行 speak(): │ │ 读 vptr → Animal::vtable │ │ 取 [0] → Animal::speak 地址 │ │ 调用 → Animal::speak() ✓ 调用的是基类版本│ └─────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────┐ │ Step 3:调用 Dog::Dog() │ │ 编译器插入:this->vptr = &Dog::vtable ← 覆盖! │ │ ┌──────────────────────┐ │ │ │ Dog::vptr │ data │ │ │ └──────┬──────────────-┘ │ │ └──► Dog::vtable │ │ [0] Dog::speak ← 现在变了 │ └─────────────────────────────────────────────────┘ |
- 调用的虚函数,基类没有这个虚函数,是派生类的虚函数
- 先明确:基类根本调不到派生类的独有虚函数
- 基类不知道派生类的存在,所以基类构造函数中无法直接调用派生类独有的虚函数:
- 因此,这种情况真正有意义的场景是(见下面的场景:三层继承):
派生类自己在构造函数中调用自己新增的虚函数,而这个虚函数可能又被更下一级的派生类重写
|
1 2 3 4 5 6 7 8 9 10 11 |
class Animal { public: Animal() { fetch(); // ❌ 编译错误:Animal 根本没有 fetch() } }; class Dog : public Animal { public: virtual void fetch() { } // Dog 独有,Animal 不知道 }; |
- 场景设定:三层继承
- 输出
Dog::fetch(),不是Labrador::fetch()
- 输出
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class Animal { public: Animal() { } virtual void speak() { } // Animal 有 }; class Dog : public Animal { public: Dog() { fetch(); // Dog 在自己的构造函数中调用自己新增的虚函数 } virtual void fetch() { // Dog 新增,Animal 没有 cout << "Dog::fetch()" << endl; } }; class Labrador : public Dog { public: Labrador() { } void fetch() override { // Labrador 重写了 fetch cout << "Labrador::fetch()" << endl; } }; |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
// 执行: Dog* p = new Labrador(); new Labrador() 触发的完整构造序列: ┌──────────────────────────────────────────────────────────┐ │ Step 1:Animal::Animal() │ │ this->vptr = &Animal::vtable │ │ ┌────────────────────────────────────────────┐ │ │ │ vptr │ Animal data │ │ │ └────┬───────────────────────────────────────┘ │ │ └──► Animal::vtable │ │ [0] Animal::speak │ │ (fetch 根本不在这张表里) │ └──────────────────────────────────────────────────────────┘ ↓ ┌──────────────────────────────────────────────────────────┐ │ Step 2:Dog::Dog() │ │ this->vptr = &Dog::vtable ← 覆盖为 Dog 的表 │ │ ┌────────────────────────────────────────────┐ │ │ │ vptr │ Animal data │ Dog data │ │ │ └────┬───────────────────────────────────────┘ │ │ └──► Dog::vtable │ │ [0] Animal::speak (继承来的) │ │ [1] Dog::fetch ← fetch 在这里 │ │ │ │ ⚡ 执行 fetch(): │ │ 读 vptr → Dog::vtable │ │ 取 [1] → Dog::fetch 地址 │ │ 调用 → Dog::fetch() ← 调用的是 Dog 版本! │ │ 不是 Labrador::fetch() │ └──────────────────────────────────────────────────────────┘ ↓ ┌──────────────────────────────────────────────────────────┐ │ Step 3:Labrador::Labrador() │ │ this->vptr = &Labrador::vtable ← 再次覆盖 │ │ ┌────────────────────────────────────────────┐ │ │ │ vptr │ Animal data │ Dog data │ Lab data │ │ │ └────┬───────────────────────────────────────┘ │ │ └──► Labrador::vtable │ │ [0] Animal::speak │ │ [1] Labrador::fetch ← 现在才变成这个 │ └──────────────────────────────────────────────────────────┘ |
- 表现
- 对于普通虚函数,确实只是多态不生效,退化为调用当前类的版本,不会崩溃
- 总结
- 在某个类的构造函数执行期间,对象的动态类型就是那个类本身,
vptr指向那个类的vtable,虚函数分发只到那个类为止,不会穿透到尚未构造的子类
- 在某个类的构造函数执行期间,对象的动态类型就是那个类本身,
在构造函数中调用纯虚函数
- 基类构造函数调用了纯虚函数
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Animal { public: Animal() { speak(); // ← 调用了纯虚函数! } virtual void speak() = 0; // 纯虚函数,Animal 自己没有实现 }; class Dog : public Animal { public: void speak() override { cout << "Dog::speak()" << endl; } }; Animal* p = new Dog(); // 💥 崩溃 |
- 为什么会崩溃:
vtable里放的是什么- 编译器对纯虚函数的处理是:
- 在
Animal::vtable的对应槽位,不放真实函数地址,而是放一个特殊的错误处理函数的地址
|
1 2 3 4 5 6 7 8 9 |
Animal::vtable(运行时实际内容): ┌────────────────────────────────────────────────────┐ │ [0] __cxa_pure_virtual ← 不是真实函数,是"陷阱" │ └────────────────────────────────────────────────────┘ Dog::vtable(构造完成后): ┌────────────────────────────────────────────────────┐ │ [0] Dog::speak ← 正常的函数地址 │ └────────────────────────────────────────────────────┘ |
__cxa_pure_virtual是运行时库提供的一个函数,它的实现只做一件事:
|
1 2 3 4 |
// libstdc++ 的实现本质上就是这样 extern "C" void __cxa_pure_virtual() { abort(); // 直接终止程序 } |
- 完整崩溃过程
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// new Dog() 的执行序列: Step 1:Animal::Animal() this->vptr = &Animal::vtable ┌──────────────────────────────────────────┐ │ vptr ──► Animal::vtable │ │ [0] __cxa_pure_virtual │ └──────────────────────────────────────────┘ ⚡ 执行 speak(): 读 vptr → Animal::vtable 取 [0] → __cxa_pure_virtual 的地址 call → __cxa_pure_virtual() → abort() 💥 程序终止,到不了 Step 2 Step 2:Dog::Dog() ← 永远执行不到 this->vptr = &Dog::vtable ... |
vtable里面存的是函数地址
vtable里存的就是函数在代码段(.text)的地址vtable在.rodata(只读数据段),它里面的每一项是一个函数指针,指向.text里对应函数的机器码起始地址
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
内存分区: ───────────────────────────────────────────── .text │ Animal::speak 的机器码 ← 地址:0x401020 │ Dog::speak 的机器码 ← 地址:0x401080 │ Dog::fetch 的机器码 ← 地址:0x4010F0 ───────────────────────────────────────────── .rodata │ Animal::vtable │ [0] 0x401020 ← 指向 Animal::speak 的代码 │ │ Dog::vtable │ [0] 0x401080 ← 指向 Dog::speak 的代码 │ [1] 0x4010F0 ← 指向 Dog::fetch 的代码 ───────────────────────────────────────────── .heap │ Dog 对象 │ vptr → Dog::vtable ───────────────────────────────────────────── |
虚函数在 vtable 中的偏移是怎么确定的
- 结论
- 偏移量不是运行时计算的,是编译器在编译期静态分配好的,编译器为每个虚函数分配一个固定的槽位索引,调用时直接用这个索引 × 指针大小作为偏移
- 编译器如何分配槽位
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 规则一:基类虚函数,按声明顺序分配槽位 class Animal { virtual void speak() { } // 槽位 0 virtual void move() { } // 槽位 1 virtual void eat() { } // 槽位 2 }; Animal::vtable: ┌─────────────────────────────┐ │ [0] &Animal::speak │ 偏移 0 × 8 = 0 字节 │ [1] &Animal::move │ 偏移 1 × 8 = 8 字节 │ [2] &Animal::eat │ 偏移 2 × 8 = 16 字节 └─────────────────────────────┘ |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 规则二:派生类重写的虚函数,继承基类的槽位,索引不变 class Dog : public Animal { void speak() override { } // 继承槽位 0,覆盖地址 void move() override { } // 继承槽位 1,覆盖地址 // eat() 没重写,槽位 2 继续用 Animal::eat 的地址 }; Animal::vtable: Dog::vtable: ┌──────────────────┐ ┌──────────────────┐ │ [0] Animal::speak│ │ [0] Dog::speak │ ← 地址换了,索引没变 │ [1] Animal::move │ │ [1] Dog::move │ ← 地址换了,索引没变 │ [2] Animal::eat │ │ [2] Animal::eat │ ← 没重写,地址不变 └──────────────────┘ └──────────────────┘ |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// 规则三:派生类新增的虚函数,追加到末尾 class Dog : public Animal { void speak() override { } // 槽位 0(继承) void move() override { } // 槽位 1(继承) // eat() 未重写 // 槽位 2(继承) virtual void fetch() { } // 槽位 3(新增,追加到末尾) }; Dog::vtable: ┌──────────────────┐ │ [0] Dog::speak │ 偏移 0 × 8 = 0 字节 │ [1] Dog::move │ 偏移 1 × 8 = 8 字节 │ [2] Animal::eat │ 偏移 2 × 8 = 16 字节 │ [3] Dog::fetch │ 偏移 3 × 8 = 24 字节 ← 新增槽位 └──────────────────┘ |
-
多态正确工作的关键:
speak()无论在哪个类的vtable里,都在槽位0- 所以编译器生成
p->speak()时,只需写死"取偏移0的函数指针",运行时换不同的vtable就自动得到不同的实现
-
偏移量在哪里体现:看汇编
|
1 2 3 |
Animal* p = new Dog(); p->speak(); // 槽位 0 p->move(); // 槽位 1 |
|
1 2 3 4 5 6 7 8 9 10 11 12 |
; p->speak():槽位 0,偏移 = 0 * 8 = 0 mov rax, [p] ; rax = vptr(对象头部) mov rax, [rax + 0] ; 取 vtable[0] 的函数地址 call rax ; 调用 ; p->move():槽位 1,偏移 = 1 * 8 = 8 mov rax, [p] ; rax = vptr mov rax, [rax + 8] ; 取 vtable[1] 的函数地址 call rax ; 调用 // [rax + 0]、[rax + 8] 这两个偏移量是编译器在编译期写死在指令里的 // 运行时不需要任何计算 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
// 完整的调用全景图 源码:p->speak() │ │ 编译期: │ 1. p 的静态类型是 Animal* │ 2. speak() 是虚函数,查 Animal 的虚函数表定义 │ 3. 确定 speak() 的槽位是 0,偏移 = 0 * 8 = 0 │ 4. 生成指令:取 vptr,再取 vptr[+0],call │ ▼ 运行期: p(Animal*) │ │ 1. 读对象头部 ▼ ┌────────────────────────────┐ │ vptr = 0x602040 │ → Dog::vtable 的地址 │ ... │ └────────────────────────────┘ │ │ 2. 去 vtable,偏移 +0(编译期写死) ▼ Dog::vtable(0x602040) ┌────────────────────────────┐ │ [+0] 0x401080 ← 取这里 │ → Dog::speak 在 .text 的地址 │ [+8] 0x4010A0 │ │ [+16] 0x401020 │ └────────────────────────────┘ │ │ 3. 拿到 0x401080,call ▼ .text(0x401080) ┌────────────────────────────┐ │ Dog::speak() 的机器码 │ ← 实际执行这里 └────────────────────────────┘ |
示例
代码
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class Animal { public: virtual void speak() { } // 槽位 0 virtual void move() { } // 槽位 1 virtual void eat() { } // 槽位 2 }; class Dog : public Animal { public: void speak() override { } // 槽位 0(继承) void move() override { } // 槽位 1(继承) // eat() 未重写 // 槽位 2(继承) virtual void fetch() { } // 槽位 3(新增,追加到末尾) }; int tet() { Animal* pt = new Dog(); pt->speak(); return 0; } |
汇编
x86-64 gcc 15.2环境下编出的汇编https://godbolt.org
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
Animal::speak(): push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi nop pop rbp ret Animal::move(): push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi nop pop rbp ret Animal::eat(): push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi nop pop rbp ret Dog::speak(): push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi nop pop rbp ret Dog::move(): push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi nop pop rbp ret Dog::fetch(): push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi nop pop rbp ret Animal::Animal() [base object constructor]: push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi mov edx, OFFSET FLAT:vtable for Animal+16 mov rax, QWORD PTR [rbp-8] mov QWORD PTR [rax], rdx nop pop rbp ret .set Animal::Animal() [complete object constructor],Animal::Animal() [base object constructor] Dog::Dog() [base object constructor]: push rbp mov rbp, rsp sub rsp, 16 mov QWORD PTR [rbp-8], rdi mov rax, QWORD PTR [rbp-8] mov rdi, rax call Animal::Animal() [base object constructor] mov edx, OFFSET FLAT:vtable for Dog+16 mov rax, QWORD PTR [rbp-8] mov QWORD PTR [rax], rdx nop leave ret .set Dog::Dog() [complete object constructor],Dog::Dog() [base object constructor] tet(): push rbp mov rbp, rsp push rbx sub rsp, 24 mov edi, 8 call operator new(unsigned long) mov rbx, rax mov QWORD PTR [rbx], 0 mov rdi, rbx call Dog::Dog() [complete object constructor] mov eax, 0 mov QWORD PTR [rbp-24], rbx test al, al je .L10 mov esi, 8 mov rdi, rbx call operator delete(void*, unsigned long) .L10: mov rax, QWORD PTR [rbp-24] mov rax, QWORD PTR [rax] mov rdx, QWORD PTR [rax] mov rax, QWORD PTR [rbp-24] mov rdi, rax call rdx mov eax, 0 mov rbx, QWORD PTR [rbp-8] leave ret vtable for Dog: .quad 0 .quad typeinfo for Dog .quad Dog::speak() .quad Dog::move() .quad Animal::eat() .quad Dog::fetch() vtable for Animal: .quad 0 .quad typeinfo for Animal .quad Animal::speak() .quad Animal::move() .quad Animal::eat() typeinfo for Dog: .quad vtable for __cxxabiv1::__si_class_type_info+16 .quad typeinfo name for Dog .quad typeinfo for Animal typeinfo name for Dog: .string "3Dog" typeinfo for Animal: .quad vtable for __cxxabiv1::__class_type_info+16 .quad typeinfo name for Animal typeinfo name for Animal: .string "6Animal" |
vtable 的真实布局
vtable开头有两个隐藏字段(各占8字节),所以vptr不是指向vtable的第0字节,而是指向vtable+16,也就是第一个函数指针的位置
|
1 2 3 4 5 6 7 |
vtable for Dog: .quad 0 ; [vtable+0] offset-to-top(多继承用) .quad typeinfo for Dog ; [vtable+8] RTTI 类型信息指针 .quad Dog::speak() ; [vtable+16] ← vptr 实际指向这里!槽位0 .quad Dog::move() ; [vtable+24] 槽位1 .quad Animal::eat() ; [vtable+32] 槽位2(未重写,用基类的) .quad Dog::fetch() ; [vtable+40] 槽位3(新增) |
构造函数写入 vptr
|
1 2 3 4 5 6 7 8 9 10 |
Animal::Animal() [base object constructor]: mov edx, OFFSET FLAT:vtable for Animal+16 ; edx = Animal::vtable 第一个函数槽的地址 mov rax, QWORD PTR [rbp-8] ; rax = this mov QWORD PTR [rax], rdx ; this->vptr = &Animal::vtable[0] Dog::Dog() [base object constructor]: call Animal::Animal() [base object constructor] ; 先调基类构造,vptr=Animal::vtable+16 mov edx, OFFSET FLAT:vtable for Dog+16 ; edx = Dog::vtable 第一个函数槽的地址 mov rax, QWORD PTR [rbp-8] ; rax = this mov QWORD PTR [rax], rdx ; this->vptr = &Dog::vtable[0] ← 覆盖! |
调用 pt->speak()
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
tet(): ; ── new Dog() 部分 ── mov edi, 8 call operator new(unsigned long) ; 堆上分配 8 字节(只有 vptr,无其他成员) mov rbx, rax ; rbx = 对象地址 mov QWORD PTR [rbx], 0 ; 对象内存清零 mov rdi, rbx call Dog::Dog() [complete object constructor] ; 调构造函数,写入 vptr mov QWORD PTR [rbp-24], rbx ; pt = rbx(存到栈上的 pt 变量) ; ── pt->speak() 部分 ── mov rax, QWORD PTR [rbp-24] ; ① rax = pt(从栈取出 Animal* 指针) mov rax, QWORD PTR [rax] ; ② rax = *pt = vptr = Dog::vtable+16 mov rdx, QWORD PTR [rax] ; ③ rdx = *(vptr+0) = Dog::speak 的地址 mov rax, QWORD PTR [rbp-24] ; ④ rax = pt(重新取,用作 this 指针) mov rdi, rax ; ⑤ rdi = this(x86-64 第一个参数用 rdi) call rdx ; ⑥ call Dog::speak() |
解析
- 关于
vtableOFFSET的意思是:把 (vtable起始地址 + 16) 这个地址值本身装入edxFLAT指的是平坦内存模型(Flat Memory Model),x86-64下所有地址都在同一个线性地址空间里,不需要段寄存器来寻址- 这里只是
GAS汇编语法的一个修饰符,表示"用平坦地址空间",实际上就是普通的内存地址,可以直接忽略这个词
|
1 |
mov edx, OFFSET FLAT:vtable for Animal+16 |
+16是什么- 跳过
vtable的两个隐藏字段,让vptr直接指向第一个虚函数槽,这样后续取槽位时偏移计算更干净: vptr + 0 → speak()槽位0vptr + 8 → move()槽位1vptr + 16 → eat()槽位2
- 跳过
|
1 2 3 4 5 6 7 8 9 10 |
vtable for Animal 在 .rodata 的布局: 地址 内容 ───────────────────────────────────────────────────── vtable + 0 │ 0 (offset-to-top,8字节) vtable + 8 │ &typeinfo for Animal (RTTI指针,8字节) vtable + 16 │ &Animal::speak() ← vptr 指向这里 vtable + 24 │ &Animal::move() vtable + 32 │ &Animal::eat() ───────────────────────────────────────────────────── |
OFFSET FLAT:vtable for Animal+16- 是一个编译期地址常量,值是
vtable起始地址加16字节,跳过了offset-to-top和typeinfo两个隐藏字段,直接指向vtable中第一个虚函数指针的存放位置 vptr存的就是这个地址
- 是一个编译期地址常量,值是
new返回的堆空间的地址是存在rax里面的- 之所以又要从
rbx复制到rdi,是因为接下来要调用函数了 rdi到时候可以作为参数去读取
- 之所以又要从
|
1 2 3 4 5 |
call operator new(unsigned long) mov rbx, rax mov QWORD PTR [rbx], 0 ;清0 mov rdi, rbx call Dog::Dog() [complete object constructor] |
关于嵌套函数调用过程中寄存器的分配问题
callee-saved寄存器的约定rbx是callee-saved,意思是:如果xy函数内部要用rbx,必须在用之前先把rbx的旧值压栈保存,函数返回前再恢复tet()的rbx值在xy()执行期间被压到栈上"冬眠",xy()返回后自动恢复,tet()完全感知不到
|
1 2 3 4 5 6 7 8 9 10 11 |
xy(): push rbp push rbx ← 我要用 rbx 了,先把调用者的 rbx 保存到栈上 mov rbx, ... ← 现在可以放心用 rbx 了 call 其他函数() ... pop rbx ← 返回前恢复 rbx,tet() 的 rbx 不受影响 pop rbp ret |
x86-64全部callee-saved寄存器
|
1 |
rbx, rbp, r12, r13, r14, r15 |
vtable 的两个隐藏字段
概述
|
1 2 3 4 5 |
vtable for Dog: .quad 0 ; [vtable+0] offset-to-top .quad typeinfo for Dog ; [vtable+8] RTTI 类型信息指针 .quad Dog::speak() ; [vtable+16] ← vptr 指向这里 ... |
offset-to-top
- 单继承时这个字段始终是
0,它真正发挥作用是在多继承场景下
|
1 2 3 4 5 6 7 8 9 10 11 12 |
class A { virtual void funcA() { } }; class B { virtual void funcB() { } }; class C : public A, public B { void funcA() override { } void funcB() override { } }; |
|
1 2 3 4 5 6 7 8 9 10 11 12 |
// C 的对象内存布局: C 对象的内存: ┌──────────────────────────────┐ ← 对象起始地址,也是 A 子对象起始 │ vptr_A → C的A部分vtable │ 偏移 0 │ A 的数据成员 │ ├──────────────────────────────┤ │ vptr_B → C的B部分vtable │ 偏移 16(假设)← B* 指针指向这里 │ B 的数据成员 │ ├──────────────────────────────┤ │ C 自己的数据成员 │ └──────────────────────────────┘ |
- 问题
dynamic_cast需要从pb(指向偏移16处)找回到C对象的起始地址,就需要知道往回退多少字节,这就是offset-to-top的作用:
|
1 2 3 4 5 6 |
C* pc = new C(); B* pb = pc; // pb 指向 C 对象内部的 B 子对象部分,偏移 16 处 // pb 的值 = pc 的值 + 16 // 现在要做向下转型: C* pc2 = dynamic_cast<C*>(pb); |
|
1 2 3 4 5 6 7 8 9 10 11 |
C 的 B 部分的 vtable: ┌─────────────────────────────┐ │ offset-to-top = -16 │ ← 从 B 子对象退回到对象顶部需要 -16 字节 │ typeinfo for C │ │ C::funcB() │ └─────────────────────────────┘ dynamic_cast 的计算: C对象起始地址 = pb + offset-to-top = pb + (-16) = pc ✓ |
- 这里的
-8是怎么算出来的
|
1 2 3 4 5 6 7 8 |
vtable for C: .quad 0 .quad typeinfo for C .quad C::funcA() .quad C::funcB() .quad -8 .quad typeinfo for C .quad non-virtual thunk to C::funcB() |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
offset-to-top = 对象起始地址 - 当前子对象地址 也就是:从当前 vptr 所在的子对象位置,回退到整个对象起始地址,需要多少字节 代入 C 对象的内存布局 地址 内容 ───────────────────────────── +0 ┌──────────────┐ ← C对象起始地址,A子对象在这里 │ vptr_A │ vptr_A 的 offset-to-top = 起始 - 当前 = 0 - 0 = 0 +8 ├──────────────┤ ← B子对象在这里 │ vptr_B │ vptr_B 的 offset-to-top = 起始 - 当前 = 0 - 8 = -8 └──────────────┘ |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
B 子对象的地址比 C 对象起始地址高 8 字节,所以要退回起始地址需要 -8 这个值怎么确定 编译器在编译期就能算出来,步骤如下: 1. 确定 A 子对象的大小: A 只有一个 vptr,sizeof(A子对象) = 8 字节 2. C 的内存布局按继承顺序排列: class C : public A, public B ↑第一 ↑第二 A 子对象 → 偏移 0 B 子对象 → 偏移 0 + sizeof(A部分) = 0 + 8 = 8 3. 计算 B 部分的 offset-to-top: offset-to-top = C对象起始(0) - B子对象起始(8) = -8 |
|
1 2 3 4 5 6 7 8 9 10 11 |
dynamic_cast 用这个值 dynamic_cast<C*>(pb) 的计算: pb 指向 B 子对象(偏移 +8 处) ↓ 读 vptr_B[-2] = offset-to-top = -8 ↓ C对象起始 = pb + (-8) = (C对象起始 + 8) + (-8) = C对象起始 ✓ |
RTTI 类型信息指针(typeinfo)
- 指向一个
type_info对象,这个对象描述了这个类的运行时类型信息,看汇编里的实际内容:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
typeinfo for Dog: .quad vtable for __cxxabiv1::__si_class_type_info+16 ; Dog 是单继承类 .quad typeinfo name for Dog ; 指向类名字符串 .quad typeinfo for Animal ; 指向父类的 typeinfo typeinfo name for Dog: .string "3Dog" ; "3" 是名字长度,"Dog" 是类名 typeinfo for Animal: .quad vtable for __cxxabiv1::__class_type_info+16 ; Animal 是基类 .quad typeinfo name for Animal typeinfo name for Animal: .string "6Animal" |
typeinfo记录了三件事:
|
1 2 3 4 5 6 |
typeinfo for Dog: ┌────────────────────────────────────────────┐ │ 继承关系类型 │ 单继承?多继承?虚继承? │ 类名字符串 "Dog" │ 类的名字 │ 父类的 typeinfo 指针 → typeinfo for Animal │ 继承链 └────────────────────────────────────────────┘ |
- 被谁使用:
|
1 2 3 4 5 6 7 8 9 10 11 |
// dynamic_cast Animal* pa = new Dog(); Dog* pd = dynamic_cast<Dog*>(pa); // 运行时过程: // 1. 读 pa 对象的 vptr // 2. 从 vptr[-1] 取 typeinfo for Dog // 3. 检查 typeinfo for Dog 的继承链里是否有 Dog // 4. 有 → 转型成功,返回正确地址 // 没有 → 返回 nullptr |
|
1 2 3 4 5 6 7 8 9 |
// typeid Animal* pa = new Dog(); cout << typeid(*pa).name(); // 输出 "3Dog" // 运行时过程: // 1. 读 vptr // 2. 从 vptr[-1] 取 typeinfo // 3. 返回这个 type_info 对象的引用 |
|
1 2 3 |
// std::type_info::operator== typeid(*pa) == typeid(Dog) // 比较两个 typeinfo 指针是否相同 |
vptr[-1]是怎么取到typeinfo的
|
1 2 3 4 5 6 7 8 9 10 |
vtable 内存: ┌─────────────────────────┐ │ offset-to-top │ vptr - 16 │ typeinfo for Dog │ vptr - 8 ← vptr[-1] │ Dog::speak() │ vptr + 0 ← vptr 指向这里 │ Dog::move() │ vptr + 8 └─────────────────────────┘ 取 typeinfo: mov rax, [vptr - 8] ; 直接往前偏移 8 字节就拿到了 |
- 怎么从
typeinfo的第一个字段看出继承关系__cxxabiv1命名空间下有一套专门描述继承关系的类,不同继承情况用不同的类:__class_type_info→ 没有基类(独立类)__si_class_type_info→single inheritance,单继承 ←si__vmi_class_type_info→virtual/multiple inheritance,多继承或虚继承 ←vmi
|
1 2 3 4 5 6 |
// 单继承(Dog : Animal) typeinfo for Dog: .quad vtable for __cxxabiv1::__si_class_type_info+16 ; ^^ ; si = single inheritance |
|
1 2 3 4 5 6 7 8 9 10 11 |
// 多继承(C : A, B) typeinfo for C: .quad vtable for __cxxabiv1::__vmi_class_type_info+16 ; 多继承/虚继承 .quad typeinfo name for C .long 0 ; flags .long 2 ; base_count = 2,有两个基类 .quad typeinfo for A ; 基类A的typeinfo .quad 2 ; A的偏移信息 .quad typeinfo for B ; 基类B的typeinfo .quad 2050 ; B的偏移信息 |
|
1 2 3 4 5 6 |
// 无基类(Animal) typeinfo for Animal: .quad vtable for __cxxabiv1::__class_type_info+16 ; 无基类 .quad typeinfo name for Animal ; 没有任何基类指针 |
多继承向下转型
代码
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
class A { virtual void funcA() { } }; class B { virtual void funcB() { } }; class C : public A, public B { void funcA() override { } void funcB() override { } }; C* pc = new C(); B* pb = pc; C* pc2 = dynamic_cast<C*>(pb); |
汇编
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
A::funcA(): push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi nop pop rbp ret B::funcB(): push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi nop pop rbp ret C::funcA(): push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi nop pop rbp ret C::funcB(): push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi nop pop rbp ret .set .LTHUNK0,C::funcB() non-virtual thunk to C::funcB(): sub rdi, 8 jmp .LTHUNK0 A::A() [base object constructor]: push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi mov edx, OFFSET FLAT:vtable for A+16 mov rax, QWORD PTR [rbp-8] mov QWORD PTR [rax], rdx nop pop rbp ret .set A::A() [complete object constructor],A::A() [base object constructor] B::B() [base object constructor]: push rbp mov rbp, rsp mov QWORD PTR [rbp-8], rdi mov edx, OFFSET FLAT:vtable for B+16 mov rax, QWORD PTR [rbp-8] mov QWORD PTR [rax], rdx nop pop rbp ret .set B::B() [complete object constructor],B::B() [base object constructor] C::C() [base object constructor]: push rbp mov rbp, rsp sub rsp, 16 mov QWORD PTR [rbp-8], rdi mov rax, QWORD PTR [rbp-8] mov rdi, rax call A::A() [base object constructor] mov rax, QWORD PTR [rbp-8] add rax, 8 mov rdi, rax call B::B() [base object constructor] mov edx, OFFSET FLAT:vtable for C+16 mov rax, QWORD PTR [rbp-8] mov QWORD PTR [rax], rdx mov edx, OFFSET FLAT:vtable for C+48 mov rax, QWORD PTR [rbp-8] mov QWORD PTR [rax+8], rdx nop leave ret .set C::C() [complete object constructor],C::C() [base object constructor] pc: .zero 8 pb: .zero 8 pc2: .zero 8 vtable for C: .quad 0 .quad typeinfo for C .quad C::funcA() .quad C::funcB() .quad -8 .quad typeinfo for C .quad non-virtual thunk to C::funcB() vtable for B: .quad 0 .quad typeinfo for B .quad B::funcB() vtable for A: .quad 0 .quad typeinfo for A .quad A::funcA() typeinfo for C: .quad vtable for __cxxabiv1::__vmi_class_type_info+16 .quad typeinfo name for C .long 0 .long 2 .quad typeinfo for A .quad 2 .quad typeinfo for B .quad 2050 typeinfo name for C: .string "1C" typeinfo for B: .quad vtable for __cxxabiv1::__class_type_info+16 .quad typeinfo name for B typeinfo name for B: .string "1B" typeinfo for A: .quad vtable for __cxxabiv1::__class_type_info+16 .quad typeinfo name for A typeinfo name for A: .string "1A" __static_initialization_and_destruction_0(): push rbp mov rbp, rsp push rbx sub rsp, 8 mov edi, 16 call operator new(unsigned long) mov rbx, rax mov QWORD PTR [rbx], 0 mov QWORD PTR [rbx+8], 0 mov rdi, rbx call C::C() [complete object constructor] mov eax, 0 mov QWORD PTR pc[rip], rbx test al, al je .L9 mov esi, 16 mov rdi, rbx call operator delete(void*, unsigned long) .L9: mov rax, QWORD PTR pc[rip] test rax, rax je .L10 mov rax, QWORD PTR pc[rip] add rax, 8 jmp .L11 .L10: mov eax, 0 .L11: mov QWORD PTR pb[rip], rax mov rax, QWORD PTR pb[rip] test rax, rax je .L12 mov ecx, 8 mov edx, OFFSET FLAT:typeinfo for C mov esi, OFFSET FLAT:typeinfo for B mov rdi, rax call __dynamic_cast jmp .L13 .L12: mov eax, 0 .L13: mov QWORD PTR pc2[rip], rax nop mov rbx, QWORD PTR [rbp-8] leave ret _GLOBAL__sub_I_pc: push rbp mov rbp, rsp call __static_initialization_and_destruction_0() pop rbp ret |
解析
new C()分配内存并构造rax保存了分配的地址- 使用
rbx保存地址,可以避免rax被破坏
|
1 2 3 4 5 6 7 |
mov edi, 16 call operator new(unsigned long) ; 分配 16 字节(两个 vptr,各 8 字节) mov rbx, rax ; rbx = 对象地址 mov QWORD PTR [rbx], 0 ; 对象前 8 字节清零 mov QWORD PTR [rbx+8], 0 ; 对象后 8 字节清零 mov rdi, rbx call C::C() [complete object constructor] |
|
1 2 3 4 5 6 7 8 |
// 分配 16 字节是因为 C 对象有两个 vptr: C 对象内存(16字节): ┌─────────────────────┐ ← rbx,偏移 0 │ vptr_A(8字节) │ ├─────────────────────┤ ← rbx+8,偏移 8 │ vptr_B(8字节) │ └─────────────────────┘ |
C::C()构造函数,三阶段写入vptr
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
C::C() [base object constructor]: ; ── 阶段1:调用 A::A() ── mov rax, QWORD PTR [rbp-8] ; rax = this(C对象起始) mov rdi, rax call A::A() [base object constructor] ; A::A() 内部:this->vptr = &A::vtable+16 ; 此刻对象:[+0]=A::vptr [+8]=0 ; ── 阶段2:调用 B::B() ── mov rax, QWORD PTR [rbp-8] add rax, 8 ; rax = this+8,B子对象的起始地址 mov rdi, rax ; 传给 B::B() 的 this 是偏移+8处 call B::B() [base object constructor] ; B::B() 内部:(this+8)->vptr = &B::vtable+16 ; 此刻对象:[+0]=A::vptr [+8]=B::vptr ; ── 阶段3:覆盖两个 vptr 为 C 的 vtable ── mov edx, OFFSET FLAT:vtable for C+16 ; C的A部分vtable mov rax, QWORD PTR [rbp-8] mov QWORD PTR [rax], rdx ; [+0] = C的A部分vtable mov edx, OFFSET FLAT:vtable for C+48 ; C的B部分vtable mov rax, QWORD PTR [rbp-8] mov QWORD PTR [rax+8], rdx ; [+8] = C的B部分vtable |
vtable for C+48为什么是48,看vtable布局:
|
1 2 3 4 5 6 7 8 |
vtable for C: .quad 0 ; [C::vtable+0] A部分 offset-to-top .quad typeinfo for C ; [C::vtable+8] A部分 typeinfo .quad C::funcA() ; [C::vtable+16] ← vptr_A 指向这里 .quad C::funcB() ; [C::vtable+24] .quad -8 ; [C::vtable+32] B部分 offset-to-top = -8 .quad typeinfo for C ; [C::vtable+40] B部分 typeinfo .quad non-virtual thunk to C::funcB() ; [C::vtable+48] ← vptr_B 指向这里 |
|
1 2 3 4 |
偏移计算: A部分:vtable+16(跳过 offset-to-top 和 typeinfo) B部分:vtable+48(再跳过 A部分3个槽×8=24,再跳过 B部分的 offset-to-top 和 typeinfo) 16 + 8(funcA) + 8(funcB) + 8(offset) + 8(typeinfo) = 48 ✓ |
|
1 2 3 4 5 6 7 |
构造完成后对象状态: ┌─────────────────────────────────┐ ← pc,偏移 0 │ vptr_A = &C::vtable+16 │ → 指向 C::funcA() 槽 ├─────────────────────────────────┤ 偏移 8 │ vptr_B = &C::vtable+48 │ → 指向 thunk to C::funcB() 槽 └─────────────────────────────────┘ |
B* pb = pc的指针调整
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
mov rax, QWORD PTR pc[rip] ; rax = pc(C对象起始地址) test rax, rax ; pc 是否为 nullptr je .L10 mov rax, QWORD PTR pc[rip] add rax, 8 ; ← 关键!pb = pc + 8 jmp .L11 .L10: mov eax, 0 ; pc 为 nullptr 则 pb = nullptr .L11: mov QWORD PTR pb[rip], rax ; 存入 pb pc = 0x55aa00 → 指向 C 对象起始(A子对象) pb = 0x55aa08 → 指向 C 对象内部偏移+8处(B子对象) C 对象: 0x55aa00 ┌──────────────┐ ← pc(A*) 指向这里 │ vptr_A │ 0x55aa08 ├──────────────┤ ← pb(B*) 指向这里 │ vptr_B │ └──────────────┘ |
dynamic_cast<C*>(pb)调用__dynamic_cast
|
1 2 3 4 5 6 |
mov ecx, 8 ; 第4个参数:pb 相对于 C 对象起始的偏移 = 8 mov edx, OFFSET FLAT:typeinfo for C ; 第3个参数:目标类型的 typeinfo(转成 C*) mov esi, OFFSET FLAT:typeinfo for B ; 第2个参数:源类型的 typeinfo(pb 是 B*) mov rdi, rax ; 第1个参数:pb 的值(B子对象地址) call __dynamic_cast mov QWORD PTR pc2[rip], rax ; pc2 = 返回值 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
`__dynamic_cast` 内部用 `offset-to-top` 找回 C 对象起始: __dynamic_cast 的运行过程: 1. 拿到 pb = 0x55aa08(B子对象地址) 2. 读 pb 的 vptr: vptr_B = *(0x55aa08) = &C::vtable+48 3. 读 offset-to-top: *(vptr_B - 16) = C::vtable+32 处的值 = -8 4. 计算 C 对象起始: C对象起始 = pb + offset-to-top = 0x55aa08 + (-8) = 0x55aa00 ✓ 5. 读 typeinfo 验证类型: *(vptr_B - 8) = typeinfo for C 目标是 C*,typeinfo 匹配 ✓ 6. 返回 0x55aa00 → pc2 = pc ✓ |
thunk是什么- 汇编里出现了一个特殊的东西
|
1 2 3 |
non-virtual thunk to C::funcB(): sub rdi, 8 ; this -= 8,把 B子对象地址调整回 C对象起始地址 jmp .LTHUNK0 ; 跳转到真正的 C::funcB() |
|
1 2 3 4 5 6 7 8 9 10 11 |
这是因为通过 `B*` 调用 `funcB()` 时,传入的 `this` 是 B 子对象地址(偏移+8处),但 `C::funcB()` 内部需要的是 C 对象的起始地址,thunk 负责把 `this` 修正: pb->funcB() 的执行路径: pb(B*,指向偏移+8) ↓ 读 vptr_B C::vtable+48 ↓ 取槽位0 non-virtual thunk to C::funcB() ↓ sub rdi, 8(this 从 +8 调整回 +0) C::funcB()(拿到正确的 this,是 C 对象起始) |
- 总结全景图
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
C 对象内存: ┌──────────────────────┐ ← 0x55aa00,pc/A* 指向这里 │ vptr_A=C::vtable+16 │──► [C::funcA(), C::funcB(), ...] ├──────────────────────┤ ← 0x55aa08,pb/B* 指向这里 │ vptr_B=C::vtable+48 │──► offset-to-top=-8 │ │ typeinfo for C │ │ [thunk to C::funcB()] └──────────────────────┘ dynamic_cast<C*>(pb): 读 vptr_B[-2] = offset-to-top = -8 pc2 = pb + (-8) = 0x55aa00 = pc ✓ pb->funcB(): vptr_B[0] → thunk → sub rdi,8 → C::funcB(this=pc) ✓ |
声明:本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ Boost 程序库完全开发指南:工具与字符串08/22
- ♥ Spdlog记述:二07/09
- ♥ C++标准库_cfenv02/14
- ♥ C++14_第一篇12/14
- ♥ 51CTO:C++编程技巧与规范08/01
- ♥ C++_volatile10/08