找回密码
 立即注册
首页 业界区 业界 虚函数表里有什么?(一)——从一个普通类开始 ...

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

凤清昶 4 天前
前言

本系列文章,旨在探究C++虚函数表中除函数地址以外的条目,以及这些条目的设计意图和作用,并介绍与此相关的C++类对象内存布局,最后将两者用图解的形式结合起来,给读者带来全局性的视角。
这是本系列的第一篇文章,让我们从一个简单的类开始。
本系列文章的实验环境如下:

  • OS: Ubuntu 22.04.1 LTS x86_64 (virtual machine)
  • g++: 11.4.0
  • gdb: 12.1
对象与虚函数表内存布局

我们的探究基于下面这段代码。
  1. 1 #include <stdlib.h>
  2. 2 #include <stdint.h>
  3. 3 #include <string.h>
  4. 4
  5. 5 class Base
  6. 6 {
  7. 7 public:
  8. 8     Base(uint32_t len)
  9. 9         : len_(len)
  10. 10     {
  11. 11         buf_ = (char *)malloc(len_ * sizeof(char));
  12. 12     }
  13. 13     virtual ~Base()
  14. 14     {
  15. 15         if (nullptr != buf_)
  16. 16         {
  17. 17             free(buf_);
  18. 18             buf_ = nullptr;
  19. 19         }
  20. 20     }
  21. 21     void set_buf(const char *str)
  22. 22     {
  23. 23         if (nullptr != str && nullptr != buf_ && len_ > 0)
  24. 24         {
  25. 25             strncpy(buf_, str, len_);
  26. 26             buf_[len_ - 1] = '\0';
  27. 27         }
  28. 28     }
  29. 29
  30. 30 private:
  31. 31     uint32_t len_;
  32. 32     char *buf_;
  33. 33 };
  34. 34
  35. 35 int main(int argc, char *argv[])
  36. 36 {
  37. 37     Base base(8);
  38. 38     base.set_buf("hello");
  39. 39     return 0;
  40. 40 }
复制代码
通过Compiler Explorer,可以看到生成的虚函数表的布局以及typeinfo相关内容(这个后文会详细介绍):
1.png

接下来,让我们通过gdb调试更深入地探究虚函数表和对象内存布局。
首先,执行下列命令:
  1. g++ -g -O2 -fno-inline -std=c++20 -Wall main.cpp -o main  # 编译代码,假设示例代码命名为main.cpp
  2. gdb main  # gdb调试可执行文件,此后进入gdb
  3. b 38  # 在38行处打断点<br>r # run
复制代码
接下来,打印对象和虚函数表的内存布局
2.png

 x 命令显示的符号是经过Name Mangling的,可以使用 c++filt 命令将其还原。
3.png

整体的内存布局如下。
4.png

 可以看出:

  •  Base 对象的虚表指针并没有指向vtable的起始位置,而是指向了偏移了16个字节的位置,即第一个虚函数地址的位置。
  • 为了内存对齐,  Base 对象中插入了4个字节的padding,它的值无关紧要。
到这里, 可能有些读者会有疑问,比如,什么是top_offset?为什么会有两个析构函数?别急,往下看。
深入探索

vtable在哪个segment?

我们知道,Linux下可执行文件采用ELF (Executable and Linkable Format) 格式,那么,vtable存放在哪个段 (segment)呢?
要回答这个问题,我们可以在gdb调试中使用 info files 命令打印可执行程序的段信息,然后看看vtable的首地址 0x555555557d68 在哪个段。
5.png

可以看到,是存储在.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)
您需要登录后才可以回帖 登录 | 立即注册