本文主要是介绍C++ 虚函数表 vtable,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
目录
- 1.测试代码
- 2.查看虚函数表
- Widget 虚函数表
- 虚函数表
- 参考
本篇文章通过小实验来了解了解虚函数表,测试环境 ubuntu16 gcc 5.4.0
1.测试代码
测试代码中定义了两个类,后面将会查看这两个类的虚函数表
#include <iostream>
using namespace std;class Object {
public:Object(){cout << "Object()" << endl;}virtual ~Object(){cout << "~Object()" << endl;}virtual void print() {cout << "Object::print()" << endl;}
private:
};class Widget : public Object {
public:Widget() {cout << "Widget()" << endl;}~Widget() {cout << "~Widget()" << endl;}void print() override {cout << "Widget::print()" << endl;}
private:
};int main() {Object *o = new Widget;// 打印虚函数表地址uintptr_t *vtbl = (uintptr_t*)o;cout << std::hex << *vtbl << endl;o->print();delete o;return 0;
}
执行结果
root@linux:~/code/module/modvma# g++ vtbl.cpp -o vtbl -std=c++11
root@linux:~/code/module/modvma# ./vtbl
Object()
Widget()
401020
Widget::print()
~Widget()
~Object()
初步看到 Widget 类的虚函数表地址是 0x401020
2.查看虚函数表
使用 objdump 来查看生成的可执行文件 vtbl
root@linux:~/code/module/modvma# objdump -s -d vtbl
-d, --disassemble Display assembler contents of executable sections
-s, --full-contents Display the full contents of all sections requested
mian 函数反汇编如下
0000000000400b36 <main>:
...
400b90: 48 8b 45 e0 mov -0x20(%rbp),%rax ; 将 o 的值给 rax
400b94: 48 8b 00 mov (%rax),%rax ; 对 o 的值解引用; 现在 rax 的值为虚函数表地址
400b97: 48 83 c0 10 add $0x10,%rax ; 将地址加 8 + 8 = 16; 现在 rax 指向了虚函数表中的一个条目
400b9b: 48 8b 00 mov (%rax),%rax ; 对这个条目解引用; 现在 rax 的值为虚函数的入口地址
400b9e: 48 8b 55 e0 mov -0x20(%rbp),%rdx
400ba2: 48 89 d7 mov %rdx,%rdi ; 将参数 o 给 rdi
400ba5: ff d0 callq *%rax ; 调用 print(o)
...
上面给出了 o->print();
这行代码对应的指令,可以看见对虚函数的调用的整个过程
测试代码运行结果显示,o 指向的对象的虚表指针 vptr 值为 0x401020,查看该地址内容如下
Contents of section .rodata:400fc0 01000200 004f626a 65637428 29007e4f .....Object().~O400fd0 626a6563 74282900 4f626a65 63743a3a bject().Object::400fe0 7072696e 74282900 57696467 65742829 print().Widget()400ff0 007e5769 64676574 28290057 69646765 .~Widget().Widge401000 743a3a70 72696e74 28290000 00000000 t::print()......401010 00000000 00000000 60104000 00000000 ........`.@.....401020 920e4000 00000000 ea0e4000 00000000 ..@.......@.....401030 100f4000 00000000 00000000 00000000 ..@.............401040 80104000 00000000 900d4000 00000000 ..@.......@.....401050 dc0d4000 00000000 020e4000 00000000 ..@.......@.....401060 20226000 00000000 78104000 00000000 "`.....x.@.....401070 80104000 00000000 36576964 67657400 ..@.....6Widget.401080 b0206000 00000000 90104000 00000000 . `.......@.....401090 364f626a 65637400 6Object.
0x401020 这个虚拟地址位于 .rodata 节中,所以虚函数表位于 .rodata 中
Widget 虚函数表
从 0x401020 开始的几个条目是
400e92
400eea
400f10
这三个条目的值为虚函数的地址,查看 Widget 类的函数地址如下
...
0000000000400e2e <_ZN6WidgetC1Ev>: ; ctor
...
0000000000400e92 <_ZN6WidgetD1Ev>: ; dtor
...
0000000000400eea <_ZN6WidgetD0Ev>: ; dtor
...
0000000000400f10 <_ZN6Widget5printEv>: ; Widget::print
左边是每个函数的入口地址,虚函数表中的条目正好对应了类中的虚函数
这里有两个虚析构函数,一个叫 complete object destructor, 另一个叫 deleting destructor,区别在于前者只执行析构函数不执行delete(),后面的在析构之后执行delete操作,也就是会 free。这是使用 gcc 得到的结果,使用 msvc 结果不一样,只有一个虚析构函数
所以,虚函数 Widget::print
的调用过程包括:首先通过对象的虚表指针 vptr 找到 Widget 类的虚函数表 vtable ,这里 vptr 指针(0x401020)指向是第一个虚析构函数,通过偏移 16 个字节找到 print
函数的项,这样就得到了 print
函数的入口地址 0x400f10
虚函数表
使用 compiler explorer 时给出了这些内容,可以看到这些才是真正的虚函数表内容(这里面使用的是 gcc 13 编译器,第一节中使用测试代码的是 ubuntu 进行演示的,实际内容有点不一样)
vtable for Widget:.quad 0.quad typeinfo for Widget.quad Widget::~Widget() [complete object destructor].quad Widget::~Widget() [deleting destructor].quad Widget::print()
vtable for Object:.quad 0.quad typeinfo for Object.quad Object::~Object() [complete object destructor].quad Object::~Object() [deleting destructor].quad Object::print()
typeinfo for Widget:.quad vtable for __cxxabiv1::__si_class_type_info+16.quad typeinfo name for Widget.quad typeinfo for Object
typeinfo name for Widget:.string "6Widget"
typeinfo for Object:.quad vtable for __cxxabiv1::__class_type_info+16.quad typeinfo name for Object
typeinfo name for Object:.string "6Object"
可以看到,对象中的 vptr 指向的并不是 vtable 的首地址,指向的是第一个虚函数条目,这些内容在可执行文件中都能看到
Contents of section .rodata:400fc0 01000200 004f626a 65637428 29007e4f .....Object().~O400fd0 626a6563 74282900 4f626a65 63743a3a bject().Object::400fe0 7072696e 74282900 57696467 65742829 print().Widget()400ff0 007e5769 64676574 28290057 69646765 .~Widget().Widge401000 743a3a70 72696e74 28290000 00000000 t::print()......401010 00000000 00000000 60104000 00000000 ........`.@.....401020 920e4000 00000000 ea0e4000 00000000 ..@.......@.....401030 100f4000 00000000 00000000 00000000 ..@.............401040 80104000 00000000 900d4000 00000000 ..@.......@.....401050 dc0d4000 00000000 020e4000 00000000 ..@.......@.....401060 20226000 00000000 78104000 00000000 "`.....x.@.....401070 80104000 00000000 36576964 67657400 ..@.....6Widget.401080 b0206000 00000000 90104000 00000000 . `.......@.....401090 364f626a 65637400 6Object.
0x401010 应该就是 Widget 类的虚函数表的首地址,每一项 8 个字节
- 第一项为 0
- 第二项为 0x401060,typeinfo for Widget
401060 20226000 00000000 78104000 00000000 "`.....x.@.....
401070 80104000 00000000 36576964 67657400 ..@.....6Widget.
typeinfo for Widget 中最后有一个类名 typeinfo name,内容为 6Widget,前面 8 个字节内容为 0x401080 (这里和 compiler explorer 中的内容不一样),是父类 Object 的 typeinfo
401080 b0206000 00000000 90104000 00000000 . `.......@.....
401090 364f626a 65637400 6Object.
- 第三项开始就是虚函数的地址了
而 Object 类的虚函数表从 0x406038 开始,下面是 Object 类的函数地址
...
0000000000400d58 <_ZN6ObjectC1Ev>:
...
0000000000400d90 <_ZN6ObjectD1Ev>:
...
0000000000400ddc <_ZN6ObjectD0Ev>:
...
0000000000400e02 <_ZN6Object5printEv>:
参考
更详细的继承、虚函数内容可以查看这篇文章 图说C++对象模型:对象内存布局详解
这篇关于C++ 虚函数表 vtable的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!