前言
本系列文章,旨在探究C++虚函数表中除函数地址以外的条目,以及这些条目的设计意图和作用,并介绍与此相关的C++类对象内存布局,最后将两者用图解的形式结合起来,给读者带来全局性的视角。
这是本系列的第一篇文章,让我们从一个简单的类开始。
本系列文章的实验环境如下:
- OS: Ubuntu 22.04.1 LTS x86_64 (virtual machine)
- g++: 11.4.0
- gdb: 12.1
对象与虚函数表内存布局
我们的探究基于下面这段代码。- 1 #include <stdlib.h>
- 2 #include <stdint.h>
- 3 #include <string.h>
- 4
- 5 class Base
- 6 {
- 7 public:
- 8 Base(uint32_t len)
- 9 : len_(len)
- 10 {
- 11 buf_ = (char *)malloc(len_ * sizeof(char));
- 12 }
- 13 virtual ~Base()
- 14 {
- 15 if (nullptr != buf_)
- 16 {
- 17 free(buf_);
- 18 buf_ = nullptr;
- 19 }
- 20 }
- 21 void set_buf(const char *str)
- 22 {
- 23 if (nullptr != str && nullptr != buf_ && len_ > 0)
- 24 {
- 25 strncpy(buf_, str, len_);
- 26 buf_[len_ - 1] = '\0';
- 27 }
- 28 }
- 29
- 30 private:
- 31 uint32_t len_;
- 32 char *buf_;
- 33 };
- 34
- 35 int main(int argc, char *argv[])
- 36 {
- 37 Base base(8);
- 38 base.set_buf("hello");
- 39 return 0;
- 40 }
复制代码 通过Compiler Explorer,可以看到生成的虚函数表的布局以及typeinfo相关内容(这个后文会详细介绍):
接下来,让我们通过gdb调试更深入地探究虚函数表和对象内存布局。
首先,执行下列命令:- g++ -g -O2 -fno-inline -std=c++20 -Wall main.cpp -o main # 编译代码,假设示例代码命名为main.cpp
- gdb main # gdb调试可执行文件,此后进入gdb
- b 38 # 在38行处打断点<br>r # run
复制代码 接下来,打印对象和虚函数表的内存布局
x 命令显示的符号是经过Name Mangling的,可以使用 c++filt 命令将其还原。
整体的内存布局如下。
可以看出:
- Base 对象的虚表指针并没有指向vtable的起始位置,而是指向了偏移了16个字节的位置,即第一个虚函数地址的位置。
- 为了内存对齐, Base 对象中插入了4个字节的padding,它的值无关紧要。
到这里, 可能有些读者会有疑问,比如,什么是top_offset?为什么会有两个析构函数?别急,往下看。
深入探索
vtable在哪个segment?
我们知道,Linux下可执行文件采用ELF (Executable and Linkable Format) 格式,那么,vtable存放在哪个段 (segment)呢?
要回答这个问题,我们可以在gdb调试中使用 info files 命令打印可执行程序的段信息,然后看看vtable的首地址 0x555555557d68 在哪个段。
可以看到,是存储在.data.rel.ro段。这是一个什么段呢?.data表示数据段,.rel表示重定位 (relocation),.ro表示只读 (readonly)。.data和.ro都好理解,毕竟vtable显然应该是一种只读的数据,在程序运行期间不应该被修改。那为什么需要重定位呢?
考虑下面这段代码。
[code] 1 // base.h 2 class Base 3 { 4 public: 5 virtual bool is_odd(int n); 6 virtual ~Base() {} 7 }; 8 9 // base.cpp10 #include "base.h"11 12 bool Base::is_odd(int n)13 {14 return 0 == n % 2 ? false : true;15 }16 17 // derived.h18 #include "base.h"19 20 class Derived : public Base21 {22 public:23 virtual bool is_even(int n);24 virtual ~Derived() {}25 };26 27 // derived.cpp28 #include "derived.h"29 30 bool Derived::is_even(int n)31 {32 return !is_odd(n);33 }34 35 // main.cpp36 #include "derived.h"37 #include 38 39 int main()40 {41 Derived *p = new Derived;42 std::cout is_even(10) |