凤清昶 发表于 2025-6-3 13:39:13

虚函数表里有什么?(一)——从一个普通类开始

前言

本系列文章,旨在探究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_ = '\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显然应该是一种只读的数据,在程序运行期间不应该被修改。那为什么需要重定位呢?
考虑下面这段代码。
1 // base.h 2 class Base 3 { 4 public: 5   virtual bool is_odd(int n); 6   virtual ~Base() {} 7 }; 89 // 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)
页: [1]
查看完整版本: 虚函数表里有什么?(一)——从一个普通类开始