CPP(十一)——继承,重写,虚函数,多态
这一篇也有点硬,慢慢食用。
- CPP(十一)——继承,重写,虚函数,多态
一、继承
#include <iostream>
class Animal {public: int age;
void eat() { std::cout << "Animal eat\n"; }};
class Dog : public Animal {public: int weight;
void bark() { std::cout << "Dog bark\n"; }};这里:
Animal是父类(基类)。Dog是子类(派生类)。Dog继承了Animal的成员。
因此,Dog 对象可以访问 age,也可以访问自己的 weight。
来看看子类的内存布局。
Dog对象总共8字节,4字节的age,4字节的weight。
这里最重要的一点:
继承不会让子类对象额外保存一份父类对象指针。
父类的非静态成员通常直接成为子类对象布局的一部分。
所以就会出现
Dog dog;
Animal* p = &dog;p 指向的是 dog 对象中作为基类子对象的那一部分。在这个简单的单继承例子中,基类子对象通常位于偏移 0。
二、虚函数,重写与多态
先来看没有虚函数的情况
#include <iostream>
class Animal {public: void speak() { std::cout << "Animal\n"; }};
class Dog : public Animal {public: void speak() { std::cout << "Dog\n"; }};
int main() { Dog dog;
Animal* p = &dog; p->speak();}你可能会认为:
p 指向 Dog,所以调用 Dog 的 speak()。
但实际输出是:Animal。
为什么?
因为 speak() 不是虚函数。
编译器根据指针的静态类型 Animal* 决定调用哪个函数,而不是根据对象的实际类型决定。
使用 virtual 实现多态
现在给父类函数加上 virtual:
class Animal {public: virtual void speak() { std::cout << "Animal\n"; }};
class Dog : public Animal {public: void speak() override { std::cout << "Dog\n"; }};
int main() { Dog dog;
Animal* p = &dog; p->speak();}
//输出Dog这就是运行时多态。
它的核心是:
- 编译时:
p的静态类型是Animal*。 - 运行时:
p实际指向一个Dog对象。 - 最终:调用
Dog::speak()。
override 是 C++11 提供的关键字,用于明确告诉编译器:
这个函数应该重写父类的虚函数。
注意:
virtual:声明虚函数。override:检查是否成功重写。final:禁止进一步重写(也可用于禁止类被继承)。
三、vptr和vtable:虚函数背后的实现
先记住两个概念:
| 名称 | 含义 |
|---|---|
| vtable | 虚函数表,保存虚函数相关的函数入口 |
| vptr | 虚表指针,通常保存在对象内部,指向对应的虚表 |
一个典型的实现思路是:
- 含虚函数的类会有一张虚函数表。
- 对象内部通常会有一个 vptr。
- vptr 指向该对象当前动态类型对应的虚函数表。
- 调用虚函数时,通过 vptr 找到表,再从表中找到对应函数入口。
class Animal {public: int age;
virtual void speak() {} virtual void run() {}};
class Dog : public Animal {public: int weight;
void speak() override {}};这个时候Dog对象的内存布局可以变成下面的样子
offset0 vptr8 age12 weightDog的虚函数表(可能不是这么简单的排列,gpt说和架构也有点关系)
表项0: Dog::speak表项1: Animal::run这里发生了一个重要变化:
原本 Animal 对象中只有 age,现在因为存在虚函数,典型实现中对象还多了一个 vptr。
Dog 继承 Animal 后,通常沿用这套虚函数机制,并在自己的虚表中将 speak 对应的入口替换为 Dog::speak。
虚函数调用时发生了什么
Animal* p = new Dog();p->speak();可以理解成以下过程
p保存Dog对象地址->拿到vptr指针->查表->调用函数。
// 仅用于理解的伪代码,不是标准 C++ 实现vptr = *(void***)p;func = vptr[0];func(p);四、基类子对象
class Animal {public: int age;
virtual void speak() {}};
class Dog : public Animal {public: int weight;
void speak() override {}};创建对象:Dog dog;从 C++ 语言的角度看,Dog 对象内部包含一个 Animal 基类子对象,以及 Dog 自己声明的成员。
这里的「基类子对象」非常重要。
它不是一个单独通过 new 申请的对象,也不是一个指向父类的指针,而是完整 Dog 对象的一部分。
基类指针为什么可以指向子类
Dog dog;
Animal* p = &dog;这是一种隐式向上转型(upcast)。
因为 Dog 是 Animal,所以 Dog 对象中包含 Animal 基类子对象。
在简单的单继承中,基类子对象通常位于完整对象的起始位置。
在这个例子中,p 保存的地址与 &dog 相同。
但是,记住:
基类指针不等于完整子类对象的全部信息。
通过 Animal*,你只能直接访问 Animal 提供的接口,不能直接访问 Dog 独有的 weight。
p->age; // 正确p->speak(); // 正确
// p->weight; // 编译错误五、多重继承
单继承只有一个直接基类。
多重继承则允许一个类同时继承多个类。
class A {public: int a;
virtual void funcA() {}};
class B {public: int b;
virtual void funcB() {}};
class C : public A, public B {public: int c;
void funcA() override {} void funcB() override {}};
C obj;obj包含:
- 一个 A 基类子对象;
- 一个 B 基类子对象;
- C 自己的成员
c。
此时的obj的内存布局如下
A 基类子对象A 的 vptr · offset 0A::a · offset 8
B 基类子对象B 的 vptr · offset 16B::b · offset 24
C::c · offset 28你会发现一个重要现象:
一个完整对象中可能存在多个 vptr。
单继承的简单例子中,我们只看到一个 vptr。
但多重继承中,A 和 B 的基类子对象都可能有各自的 vptr。
这也是 Native 逆向中很重要的识别线索。
多重继承的this调整
C obj;
B* pb = &obj;pb->funcB();pb 指向的是 B 子对象,而 C::funcB() 是 C 的成员函数。
如果函数内部需要访问 C::c,就必须知道完整 C 对象的位置。
因此,编译器可能生成一个 this 调整 thunk:
B* pb | vB 子对象地址 | vthis 调整(减去 B 子对象偏移) | v完整 C 对象地址 | vC::funcB()六、RTTI:运行时类型识别
python有个叫自省的东西,不知道和这个有没有啥渊源。
RTTI 全称是:
Run-Time Type Information,运行时类型信息。
它解决的问题是:
当我手里只有一个父类指针时,能不能在运行时知道它实际指向哪个子类?
class Animal {public: virtual ~Animal() {}};
class Dog : public Animal {public: void bark() {}};
class Cat : public Animal {public: void meow() {}};
Animal* p = new Dog();编译时,你只知道它是 Animal*。
但运行时,它实际指向的是 Dog。
RTTI 就可以帮助程序识别这个实际类型。
typeid:获取运行时类型信息
#include <typeinfo>#include <iostream>
Animal* p = new Dog();
std::cout << typeid(*p).name() << std::endl;这里有一个细节:
typeid(p)得到的是指针的静态类型 Animal*。
而:
typeid(*p)在 Animal 是多态类型(至少有一个虚函数)的情况下,会获取对象的动态类型,也就是 Dog。
如果类没有虚函数,typeid(*p) 通常只能得到表达式的静态类型
RTTI 和 vtable 的关系
在常见的 Itanium C++ ABI 实现中,虚表附近通常会关联 RTTI 类型信息。
概念上:
Dog 对象 | vptr | vtable | +---- offset-to-top | +---- typeinfo 指针 | +---- Dog::speak() | +---- Dog::run()不过要注意:
- RTTI 不是虚函数表本身。
- RTTI 是运行时类型信息机制。
- 虚表是常见 ABI 实现虚函数动态派发的一种方式。
- 两者在常见 ABI 中有关联,但不是同一个概念。
七、dynamic_cast:运行时安全类型转换
这是 RTTI 最常见的应用之一。
假设:
Animal* p = new Dog();现在想把 Animal* 转换成 Dog*。
可以使用:
Dog* dog = dynamic_cast<Dog*>(p);比如
Animal* p = new Cat();
Dog* dog = dynamic_cast<Dog*>(p);
if (dog == nullptr) { std::cout << "不是 Dog\n";}dynamic_cast 的几个特点
| 转换类型 | 失败时表现 |
|---|---|
指针转换 dynamic_cast<Dog*>(p) | 返回 nullptr |
引用转换 dynamic_cast<Dog&>(*p) | 抛出 std::bad_cast |
还有一个非常重要的条件:
对于通常的向下转型(基类指针转派生类指针),基类必须是多态类型,也就是至少有一个虚函数。
例如:
class Animal {public: virtual ~Animal() {}};这个虚析构函数使 Animal 成为多态类型。
dynamic_cast 在多重继承中尤其有用
看下面的代码:
class A {public: virtual ~A() {}};
class B {public: virtual ~B() {}};
class C : public A, public B {public: int value;};
C obj;
B* pb = &obj;
C* pc = dynamic_cast<C*>(pb);转换成功。
这里比较有意思:
pb 指向 C 对象中的 B 基类子对象,并不一定等于 C 对象的起始地址。
但 dynamic_cast 能够利用运行时类型信息,识别完整对象,并完成必要的地址调整。
其他几种类型转换
这四种类型转换需要区分清楚。
| 转换 | 主要用途 | 是否运行时检查 |
|---|---|---|
static_cast | 常规类型转换、继承体系中的向上转型和某些向下转型 | 否 |
dynamic_cast | 多态类型的安全运行时转换 | 是 |
reinterpret_cast | 按另一种类型解释底层值或地址 | 否 |
const_cast | 增加或移除 const/volatile 限定 | 否 |
1. static_cast
Dog dog;
Animal* p = static_cast<Animal*>(&dog);这是向上转型,安全且常见。
也可以:
Animal* p = new Dog();
Dog* d = static_cast<Dog*>(p);但是这里要小心:
static_cast 不会在运行时验证 p 是否真的指向 Dog。
如果 p 实际指向 Cat,却强行转换成 Dog*,再通过这个指针访问对象,会产生未定义行为。
2. dynamic_cast
Animal* p = new Cat();
Dog* d = dynamic_cast<Dog*>(p);会检查运行时类型,失败返回 nullptr。
3. reinterpret_cast
int value = 100;
int* p = &value;
uintptr_t address = reinterpret_cast<uintptr_t>(p);常用于底层地址和数据表示的处理。
但不要误认为 reinterpret_cast 可以把一个对象真的变成另一种对象。
Animal* p = new Cat();
Dog* d = reinterpret_cast<Dog*>(p);这不会让 Cat 变成 Dog,也不会做 RTTI 检查。
4. const_cast
const int value = 10;
const int* p = &value;
int* q = const_cast<int*>(p);它只改变指针类型上的 const 限定。
如果原对象本身就是 const,之后通过去掉 const 的指针修改它,会产生未定义行为。
八、纯虚函数与抽象类
class Animal {public: virtual void speak() = 0;};上面这样带了个=0表示纯虚函数。
它表达的是:
Animal 规定所有派生类都需要提供 speak 接口,但 Animal 本身不要求提供一个可直接使用的具体实现。
抽象类
至少包含一个纯虚函数的类就是抽象类
// Animal a; // 编译错误但可以创建派生类对象:
class Dog : public Animal {public: void speak() override { std::cout << "Dog\n"; }};
Dog dog; // 正确如果子类没有实现父类的纯虚函数,那么子类也仍然是抽象类。
纯虚函数与虚表
class Animal {public: virtual void speak() = 0; virtual void run() = 0;};
class Dog : public Animal {public: void speak() override {} void run() override {}};Animal 是抽象类。
在常见 ABI 实现中,Animal 仍然可能有自己的虚表相关数据。
Dog 则有自己的虚表,其中对应的表项指向:
Dog::speakDog::run而不是简单地认为抽象类没有虚表。
如果纯虚函数没有实现,某些 ABI 的虚表表项可能指向纯虚函数处理入口,例如 Itanium ABI 中可能出现:
__cxa_pure_virtual这是一个很有用的逆向识别线索。
但注意:纯虚函数也允许在类外提供函数定义,只是它仍然保持纯虚属性,派生类仍然必须重写它才能成为具体类。
纯虚函数的析构函数则是例外
class Animal {public: virtual ~Animal() = 0;};
Animal::~Animal() {}纯虚析构函数必须提供定义,因为销毁派生类对象时,最终仍然需要执行基类析构函数。
九、反编译实战
源代码
#include <iostream>
class Animal{public: int age;
Animal(int a) : age(a) {}
virtual void speak() = 0;
virtual int getAge() { return age; }
virtual ~Animal() {}};
class Flyable{public: int speed;
Flyable(int s) : speed(s) {}
virtual void fly() = 0;
virtual ~Flyable() {}};
class Dog : public Animal, public Flyable{public: int weight;
Dog(int a, int s, int w) : Animal(a), Flyable(s), weight(w) {}
void speak() override { age += 1; }
int getAge() override { return age; }
void fly() override { speed += 1; }
~Dog() override {}};
int test_cast(Animal *p){ Dog *d = dynamic_cast<Dog *>(p);
if (d == nullptr) { return -1; }
return d->weight;}
int test_virtual(Animal *p){ p->speak(); return p->getAge();}
int test_multi(){ Dog dog(3, 100, 20);
Animal *a = &dog; Flyable *f = &dog;
test_virtual(a); f->fly();
return test_cast(a);}
int main(){ Dog d(1,2,3); Animal* p = &d;
test_cast(p); test_virtual(p); test_multi();}RTTI元数据定义
注意!这里不是在定义类的实现,而是在定义类的typeinfo等信息对象!
我们来看看RTTI的具体实现,非常的有意思!

这里定义了两个基类的typeinfo,可以看到Animal是一个一个指向 __class_type_info 虚表的指针,一个指向类型名字符串 "Animal" 的指针。Flyable同理。
它是全局唯一的、编译期生成的类型描述符,每个类只有一份,放在 .data.rel.ro 里。它描述的是”Animal 这个类型长什么样”,供 typeid、dynamic_cast、异常匹配使用。
而 Dog 对象里的基类子对象是运行时在栈/堆上构造出来的实例,布局是 vptr + 成员数据。子对象里只有一个 vptr 指向 vtable,vtable 里再指向 typeinfo。typeinfo 本身不在 Dog 对象里,是独立的全局数据。
_ZTVN10__cxxabiv117__class_type_infoE+0x10 这个值表示:
_ZTVN10__cxxabiv117__class_type_infoE是__class_type_info类的 vtable 符号+0x10跳过 vtable 头部的 offset-to-top 和 typeinfo 两个槽,指向第一个虚函数槽- 把 typeinfo 对象的 vptr 设成这个值,就说明”我是一个
__class_type_info实例”
所以 _ZTI6Animal 这个对象本质是:
// 编译器生成的等价结构struct __class_type_info_Animal { void* vptr = &__class_type_info_vtable[2]; // +0x10 后的地址 const char* name = "Animal";};接着来看看Dog
.data.rel.ro:0000000000003308 ; public Dog :.data.rel.ro:0000000000003308 ; public /* offset 0x0 */ Animal,.data.rel.ro:0000000000003308 ; public /* offset 0x10 */ Flyable可以看到在Dog类里,两个基类所处的偏移地址.data.rel.ro:0000000000003308 WEAK _ZTI3Dog.data.rel.ro:0000000000003308 ; `typeinfo for'Dog.data.rel.ro:0000000000003308 _ZTI3Dog DCQ _ZTVN10__cxxabiv121__vmi_class_type_infoE+0x10.data.rel.ro:0000000000003308 ; DATA XREF: LOAD:0000000000000450↑o.data.rel.ro:0000000000003308 ; test_cast(Animal *)+1C↑o ....data.rel.ro:0000000000003308 ; reference to RTTI's type class.data.rel.ro:0000000000003310 DCQ _ZTS3Dog ; reference to type's name.data.rel.ro:0000000000003318 DCD 0 ; attribute.data.rel.ro:000000000000331C DCD 2 ; count of base classes.data.rel.ro:0000000000003320 DCQ _ZTI6Animal ; base class' type descriptor.data.rel.ro:0000000000003328 DCQ 2 ; base class' attributes.data.rel.ro:0000000000003330 DCQ _ZTI7Flyable ; base class' type descriptor.data.rel.ro:0000000000003338 DCQ 0x1002 ; base class' attributes.data.rel.ro:0000000000003340 WEAK _ZTV3Dog.data.rel.ro:0000000000003340 ; `vtable for'Dog.data.rel.ro:0000000000003340 _ZTV3Dog DCQ 0 ; DATA XREF: LOAD:00000000000005A0↑o.data.rel.ro:0000000000003340 ; test_multi(void)+24↑o ....data.rel.ro:0000000000003340 ; offset to this.data.rel.ro:0000000000003348 DCQ _ZTI3Dog ; `typeinfo for'Dog.data.rel.ro:0000000000003350 off_3350 DCQ _ZN3Dog5speakEv ; Dog::speak(void).data.rel.ro:0000000000003358 DCQ _ZN3Dog6getAgeEv ; Dog::getAge(void).data.rel.ro:0000000000003360 DCQ _ZN3DogD2Ev ; Dog::~Dog().data.rel.ro:0000000000003368 DCQ _ZN3DogD0Ev ; Dog::~Dog().data.rel.ro:0000000000003370 DCQ _ZN3Dog3flyEv ; Dog::fly(void).data.rel.ro:0000000000003378 DCQ -16 ; offset to this.data.rel.ro:0000000000003380 DCQ _ZTI3Dog ; `typeinfo for'Dog.data.rel.ro:0000000000003388 off_3388 DCQ _ZThn16_N3Dog3flyEv ; `non-virtual thunk to'Dog::fly(void).data.rel.ro:0000000000003390 DCQ _ZThn16_N3DogD1Ev ; `non-virtual thunk to'Dog::~Dog().data.rel.ro:0000000000003398 DCQ _ZThn16_N3DogD0Ev ; `non-virtual thunk to'Dog::~Dog().data.rel.ro:00000000000033A0 WEAK _ZTV6Animal.data.rel.ro:00000000000033A0 ; `vtable for'Animal.data.rel.ro:00000000000033A0 _ZTV6Animal DCQ 0 ; DATA XREF: LOAD:00000000000003D8↑o.data.rel.ro:00000000000033A0 ; offset to this.data.rel.ro:00000000000033A8 DCQ _ZTI6Animal ; `typeinfo for'Animal.data.rel.ro:00000000000033B0 DCQ __cxa_pure_virtual.data.rel.ro:00000000000033B8 DCQ _ZN6Animal6getAgeEv ; Animal::getAge(void).data.rel.ro:00000000000033C0 DCQ _ZN6AnimalD2Ev ; Animal::~Animal().data.rel.ro:00000000000033C8 DCQ _ZN6AnimalD0Ev ; Animal::~Animal().data.rel.ro:00000000000033D0 WEAK _ZTV7Flyable.data.rel.ro:00000000000033D0 ; `vtable for'Flyable.data.rel.ro:00000000000033D0 _ZTV7Flyable DCQ 0 ; DATA XREF: LOAD:00000000000003F0↑o.data.rel.ro:00000000000033D0 ; offset to this.data.rel.ro:00000000000033D8 DCQ _ZTI7Flyable ; `typeinfo for'Flyable.data.rel.ro:00000000000033E0 DCQ __cxa_pure_virtual.data.rel.ro:00000000000033E8 DCQ _ZN7FlyableD2Ev ; Flyable::~Flyable().data.rel.ro:00000000000033F0 DCQ _ZN7FlyableD0Ev ; Flyable::~Flyable().data.rel.ro:00000000000033F0 ; .data.rel.ro ends.data.rel.ro:00000000000033F0我们将上表简化一下
_ZTI3Dog DCQ _ZTVN10__cxxabiv121__vmi_class_type_infoE+0x10 DCQ _ZTS3Dog DCD 0 DCD 2
DCQ _ZTI6Animal DCQ 2
DCQ _ZTI7Flyable DCQ 0x1002先回顾一下符号:
| 符号 | 含义 |
|---|---|
_ZTI6Animal | Animal 的 typeinfo |
_ZTI7Flyable | Flyable 的 typeinfo |
_ZTI3Dog | Dog 的 typeinfo |
_ZTS3Dog | Dog 的类型名称字符串 |
_ZTV3Dog | Dog 的虚表 |
这里:
_ZTI3Dog DCQ _ZTVN10__cxxabiv121__vmi_class_type_infoE+0x10表示 Dog 使用的是:
__vmi_class_type_info为什么不是普通的 __class_type_info?
因为 Dog 有多个直接基类。
在 Itanium C++ ABI 中,RTTI 类型描述大致分为:
__class_type_info:普通类。__si_class_type_info:符合条件的单继承类。__vmi_class_type_info:多重继承或虚继承相关的类。
所以你看到 __vmi_class_type_info,就应该想到:这个类的 RTTI 中需要记录基类关系。
再来根据上面简化的代码拆一下_ZTI3Dog 的定义
| 字段 | 值 | 含义 |
|---|---|---|
| flags | 0 | 类继承属性标志 |
| base_count | 2 | 有两个直接基类 |
| base[0].type | _ZTI6Animal | 第一个基类是 Animal |
| base[0].offset_flags | 2 | Animal 子对象偏移 0,public 继承 |
| base[1].type | _ZTI7Flyable | 第二个基类是 Flyable |
| base[1].offset_flags | 0x1002 | Flyable 子对象偏移 0x10,public 继承 |
这里最重要的是 offset_flags。
它不是简单的偏移,而是一个打包字段:
高位:基类子对象偏移低 8 位:继承属性标志2 = 0x00000002低 8 位是 0x02,表示 public 继承;偏移为 0。
再看:
0x1002拆开:
0x1002>>8得到0x10所以:
- 偏移:
0x10 - 属性:public 继承
这就直接验证了类定义:
class Dog : public Animal, public FlyableAnimal 位于偏移 0,Flyable 位于偏移 0x10。
后面就是Dog的两端虚表
_ZTV3Dog
3340 DCQ 03348 DCQ _ZTI3Dog
3350 DCQ Dog::speak3358 DCQ Dog::getAge3360 DCQ Dog::~Dog()3368 DCQ Dog::~Dog()3370 DCQ Dog::fly
3378 DCQ -163380 DCQ _ZTI3Dog
3388 DCQ thunk to Dog::fly3390 DCQ thunk to Dog::~Dog()3398 DCQ thunk to Dog::~Dog()先理解一个重要概念:
_ZTV3Dog 是一个虚表组(vtable group),里面可以包含主虚表和次要虚表。
1. 主虚表
3340 DCQ 03348 DCQ _ZTI3Dog
3350 DCQ Dog::speak3358 DCQ Dog::getAge3360 DCQ Dog::~Dog()3368 DCQ Dog::~Dog()3370 DCQ Dog::fly这里有两个容易混淆的概念:
_ZTV3Dog的起始地址是0x3340。- 对象的 vptr 指向的是虚表的 address point(地址点),这里是
0x3350。
也就是说:
vptr = 0x3350;而不是:
vptr = 0x3340;为什么?
因为在 address point 之前还有 ABI 定义的元数据:
0x3340:offset-to-top0x3348:typeinfo0x3350:虚函数入口开始你可以记住:
vptr 通常指向虚表地址点,而不一定指向虚表符号的开头。
所以特喵的虚表里都藏了本类的元数据,牛皮!???
2. 次要虚表
再看:
3378 DCQ -163380 DCQ _ZTI3Dog
3388 DCQ thunk to Dog::fly3390 DCQ thunk to Dog::~Dog()3398 DCQ thunk to Dog::~Dog()这就是 Flyable 对应的次要虚表。
为什么需要它?
因为:
class Dog : public Animal, public FlyableDog 对象中存在两个基类子对象:
Dog ├── Animal └── Flyable当通过:
Flyable* f = &dog;f->fly();调用虚函数时,f 指向的是 Flyable 子对象。
因此,Flyable 子对象需要有自己的 vptr,指向与 Flyable 接口对应的次要虚表。
在main代码中:
v6 = &off_3388;这就对应了第二个 vptr。
3. 为什么次要虚表的 offset-to-top 是 -16?
这是一个非常重要的细节。
次要虚表开头:
3378 DCQ -16这个字段叫:
offset-to-top(this)意思是:
从当前虚表所对应的基类子对象位置,回到完整对象起始位置,需要移动多少字节。
Flyable 子对象在 Dog 中的偏移是:
+0x10所以反过来回到 Dog 起始地址:
-0x10也就是-16
内存布局Dog起始地址 | | 偏移0x10Flyable 子对象地址4、thunk:为什么 Flyable 的虚表不直接指向 Dog::fly?
次要虚表中有:
_ZThn16_N3Dog3flyEvIDA 注释:
non-virtual thunk to Dog::fly(void)这个符号可以拆成:
_ZThn16_...表示一个需要对 this 做负 16 字节调整的非虚 thunk。
为什么要调整?
先看:
Dog dog(3, 100, 20);
Flyable* f = &dog;f->fly();假设 Dog 对象起始地址为:
0x1000那么:
Dog* = 0x1000Flyable* = 0x1010调用:
f->fly();传入函数的 this 是:
0x1010但是 Dog::fly() 要访问:
speed += 1;speed 位于完整 Dog 对象偏移 0x18。
如果直接将 0x1010 当作 Dog 起始地址,那么访问 this + 0x18 就会访问错误位置。
所以需要先调整:
0x1010 - 0x10 = 0x1000然后才能执行真正的:
Dog::fly();典型 thunk 可能长这样:
SUB X0, X0, #0x10B Dog_fly这两条指令的含义:
- 将
this从 Flyable 子对象地址调整为完整 Dog 对象地址。 - 跳转到真正的
Dog::fly()。
这就是 thunk。
在 IDA 中,如果你看到一个短函数只做 this 加减,然后跳转到另一个函数,要优先考虑它是不是 C++ ABI 生成的 thunk。
同理,你看到的:
_ZThn16_N3DogD1Ev_ZThn16_N3DogD0Ev也是用于从 Flyable 子对象视角调用 Dog 析构函数时进行 this 调整的 thunk。
补充一下析构函数的 ABI 名称:
D0:Deleting Destructor,删除析构函数。D1:Complete Object Destructor,完整对象析构函数。D2:Base Object Destructor,基类子对象析构函数。
主虚表中显示了 D2 和 D0,而次要虚表显示了 D1 和 D0 的 thunk。这里可能涉及编译器对析构函数实现的合并或符号别名,不能只根据 IDA 的注释就断定它们是完全独立的函数实现。
所以Dog的内存布局如下
偏移 内容 说明0x00 vptr (主表) 指向 _ZTV3Dog @0x3340,Animal 子对象的 vptr0x08 Animal::age int,Animal 的数据成员0x0C padding0x10 vptr (次表) 指向 _ZTV3Dog 第二块 @0x3378,Flyable 子对象的 vptr0x18 Flyable::speed int,Flyable 的数据成员0x1C padding0x20 Dog::weight int,Dog 自己的数据成员0x24 paddingmain函数里的类型转换
v5 = 1; 对应dog.age = 1;v4 = off_3350; 第一个 vptrv6 = &off_3388; 第二个 vptrv7 = 0x300000002LL; //转成小端序 02 00 00 00 03 00 00 00 也就是对应的2和3参数所以上面这段代码实际上是在栈上构造一个 Dog 对象的内存布局__dynamic_cast( &v4, &typeinfo for Animal, &typeinfo for Dog, 0);参数含义:
| 参数 | 你的值 | 含义 |
|---|---|---|
| sub | &v4 | 当前 Animal 子对象地址 |
| src | Animal typeinfo | 源类型 |
| dst | Dog typeinfo | 目标类型 |
| src2dst_offset | 0 | 编译器提供的源基类在目标类中的偏移提示 |
最后一个参数特别容易误解。
这里的 0 表示:
Animal 基类子对象在 Dog 对象中的偏移为 0。
它不是说转换后指针需要加 0,也不是动态转换失败的标志。
在 Itanium ABI 中,这个参数还可能使用负值表示不同的提示状态,例如没有可用偏移提示等。
转换成功后,返回的是完整 Dog 对象的地址。
最后则是纯虚函数的实现,这里不多说了。
_ZTV6Animal DCQ 0 DCQ _ZTI6Animal DCQ __cxa_pure_virtual DCQ Animal::getAge DCQ Animal::~Animal() DCQ Animal::~Animal()最后一个小问题:为什么Dog里只有两张表?
先给结论:
Dog 有两个多态基类:Animal 和 Flyable,因此在这个典型 ABI 布局下,Dog 对象中有两个 vptr,分别指向 Dog 的主虚表和次级虚表。
三个概念
- 类的虚表(vtable)。
- 对象里的虚表指针(vptr)。
- 基类自己的虚表与派生类的虚表。
它们不是一回事。
1. Animal、Flyable、Dog 都有自己的虚表吗?
是的,作为独立的多态类,它们都可以有自己的虚表。
例如:
class Animal {public: virtual void speak() = 0; virtual int getAge() { return 0; } virtual ~Animal() {}};
class Flyable {public: virtual void fly() = 0; virtual ~Flyable() {}};
class Dog : public Animal, public Flyable {public: void speak() override {} int getAge() override { return 1; } void fly() override {} ~Dog() override {}};可以把它们理解成:
Animal 的虚表
Animal 类型对象使用
Animal vtable├── offset-to-top├── typeinfo: Animal├── speak → __cxa_pure_virtual├── getAge → Animal::getAge└── destructorFlyable 的虚表
Flyable 类型对象使用
Flyable vtable├── offset-to-top├── typeinfo: Flyable├── fly → __cxa_pure_virtual└── destructorDog 的虚表组(vtable group)
Dog 类型对象使用,包含主表和次表
Dog vtable group├── 主虚表(Animal 子对象对应)│ ├── Dog::speak│ ├── Dog::getAge│ └── Dog destructor│└── 次级虚表(Flyable 子对象对应) ├── thunk → Dog::fly └── thunk → Dog destructor在 .data.rel.ro 里看到 Animal、Flyable、Dog 各自的虚表数据是正常的。
但是,Dog 对象本身不需要同时保存 Animal、Flyable 和 Dog 三套独立的 vptr。
Dog 对象只需要保存两个 vptr:
- 第一个 vptr:指向 Dog 的主虚表。
- 第二个 vptr:指向 Dog 的次级虚表。
因为 Dog 的主虚表已经覆盖了 Animal 子对象的虚函数调用,次级虚表则负责 Flyable 子对象的虚函数调用。
vtable 是按子对象分配的,不是按类。一个子对象只要”有虚函数且是某个继承链里最派生视图的起点”,就带一个 vptr。
只有 Animal 和 Flyable 这两个基类子对象带 vptr。Dog 自己没有独立的子对象——Dog 的成员(weight)直接挂在对象末尾,不单独引入 vptr。
| 是什么 | 谁有 | 数量 | |
|---|---|---|---|
| vtable | 函数指针数组,全局静态数据 | 每个有虚函数的类一张 | Animal 1 张、Flyable 1 张、Dog 2 张(按子对象) |
| vptr | 对象里的一个指针,指向某张 vtable | 每个含 vptr 的子对象一个 | Animal 对象 1 个、Flyable 对象 1 个、Dog 对象 2 个 |
注意看,是类和对象的区别,类里有表,对象有指针。只要是有虚函数的类就会有虚函数表。
部分信息可能已经过时









