虚函数vtable,链接时看到构造函数,linker把符号地址写入vtable(三)

深入理解 C++ 链接错误:vtable、名字修饰与 relocation

本文基于一次真实的编译报错,系统解释 _ZTV、_ZN、relocation 以及"为什么没有定义虚函数却报 vtable 未定义"等问题。


问题现象

编译时报错如下:

bash 复制代码
g++ main.cpp
/usr/bin/ld: /tmp/ccWhQak0.o: warning: relocation against `_ZTV4Base' in read-only section `.text._ZN4BaseC2Ev[_ZN4BaseC5Ev]'
/usr/bin/ld: /tmp/ccWhQak0.o: in function `Base::Base()':
main.cpp:(.text._ZN4BaseC2Ev[_ZN4BaseC5Ev]+0xf): undefined reference to `vtable for Base'
/usr/bin/ld: /tmp/ccWhQak0.o: in function `Derived::Derived()':
main.cpp:(.text._ZN7DerivedC2Ev[_ZN7DerivedC5Ev]+0x1f): undefined reference to `vtable for Derived'
/usr/bin/ld: warning: creating DT_TEXTREL in a PIE
collect2: error: ld returned 1 exit status

根因非常直接:Base::f() 和 Derived::f() 都只有声明,没有定义。

下面结合实际目标文件中的符号与重定位记录,逐条解释这条链接消息,并纠正一些常见误解。


1. _ZTV4Base 是什么?

Linux 上 GCC 通常遵循 Itanium C++ ABI 的名字修饰规则,把 C++ 名称编码成链接器使用的符号名:

复制代码
_Z TV 4 Base
│  │  │ └── 类名
│  │  └──── 后面的名称长度为 4
│  └─────── 虚函数表的特殊名称编码
└────────── C++ 修饰名称的前缀

因此:

修饰名 含义
_ZTV4Base vtable for Base
_ZTV7Derived vtable for Derived

_ZTV4Base 表示 Base 的虚函数表这个符号,而不是某个对象内部的虚表指针。

对象内部的虚表指针保存的是指向相应虚函数表中某个位置的地址。


2. _ZN 和 _ZTV 有什么区别?

它们属于同一套名字编码规则,但描述的对象不同:

编码 含义 示例
_ZTV... 虚函数表的特殊符号名 _ZTV4Base → Base 的虚函数表
_ZN...E... 使用嵌套名称编码的符号(类成员、命名空间成员等) _ZN4BaseC2Ev → Base::Base() 的一个构造函数版本

以 _ZN4BaseC2Ev 为例,拆解如下:

复制代码
_Z  N  4Base  C2  E  v
    │         │  │  └── 无参数
    │         │  └───── 嵌套名称结束
    │         └──────── 基类子对象构造函数版本
    └────────────────── 嵌套名称开始

这里的 C2 有具体含义。ABI 区分不同用途的构造函数:

  • C1:构造完整对象的版本
  • C2:构造基类子对象的版本

在这个简单例子中,两种版本可以共用同一份机器码。

报错中的:

复制代码
.text._ZN4BaseC2Ev[_ZN4BaseC5Ev]

前半部分是构造函数的代码节名。方括号里的 _ZN4BaseC5Ev 是 GCC 使用的构造函数 COMDAT 节组标识 ,供链接器组织、去重相关代码使用;它不是 另一个报错函数,也不是某种调用顺序。

注意 :_ZN 中的 N 表示"嵌套名称"(Nested name),不是 "函数"的标记;末尾的 v 表示无参数,不表示 构造函数返回 void。


3. relocation 到底是什么意思?

relocation 通常译为 重定位,可以理解为:

根据符号的最终位置,修正机器码或数据中与地址有关的字段。

比如构造函数需要取得 Base 虚函数表的地址,但生成 .o 文件时,编译器还不知道虚函数表最终会放在哪里,因此留下:

  • 机器指令中的待填字段
  • 重定位记录:这个字段需要根据 _ZTV4Base 的位置计算

链接器安排好代码和数据的位置后,再根据记录修正这个字段。

修正的值不一定是完整地址,也可能是相对距离。你的程序实际用的就是后者。Base 构造函数中的指令类似:

asm 复制代码
c:  48 8d 15 00 00 00 00    lea 0x0(%rip), %rdx
        f: R_X86_64_PC32    _ZTV4Base+0xc

含义如下:

  • lea 用于计算虚表相关地址
  • 00 00 00 00 所在字段需要链接器修正
  • R_X86_64_PC32 表示 32 位 PC 相对重定位
  • _ZTV4Base 是计算时需要的目标符号
  • f: 表示待修正字段位于该代码节偏移 0xf 的位置

这恰好对应报错中的:

复制代码
main.cpp:(...+0xf): undefined reference to `vtable for Base'

所以 +0xf 是代码节中的字节偏移,不是源代码行号,也不是虚表中的第几个条目。


4. 如何完整理解这句 warning?

复制代码
/usr/bin/ld: /tmp/ccfsaz2x.o: warning:
relocation against `_ZTV4Base' in read-only section
`.text._ZN4BaseC2Ev[_ZN4BaseC5Ev]'

逐项解释:

部分 含义
/usr/bin/ld 输出消息的程序是链接器
/tmp/ccfsaz2x.o g++ 编译源文件时生成的临时目标文件
warning 这条消息本身是警告
relocation against '_ZTV4Base' 存在一个引用 Base 虚函数表符号的重定位
in read-only section 需要修正的位置位于只读节中
.text._ZN4BaseC2Ev[...] 这个位置在 Base 构造函数的机器码里

这里特别容易混淆:

"只读节"说的是引用虚表的机器指令所在的节,而不是在描述虚表所在的节。

至于为什么警告,需要区分两种情况:

  1. 链接时重定位:链接器修改正在生成的文件里的指令字段。这很正常,不违反程序运行时的只读权限。
  2. 运行时动态重定位:程序加载时还需要修改那些字段。如果修改位置在只读代码区域,就涉及 text relocation,链接器会对此发出警告。

你的环境默认生成 PIE(位置无关可执行文件)。在这次链接失败过程中,未能解析的虚表引用触发了上述警告,并伴随:

复制代码
warning: creating DT_TEXTREL in a PIE

DT_TEXTREL 是 ELF 动态信息中的一个标记,表示存在需要修改只读区域的动态重定位。

但这次链接最终已经失败,不能据此认为成功生成了一个带 DT_TEXTREL 的可执行文件。真正让链接失败的是随后两条 undefined reference。

补充纠正:目标文件的只读代码节里存在重定位,本身很正常。这里的警告涉及链接器在生成 PIE 时对动态重定位的处理,不能理解为"只读代码里有地址需要修正就一定报错"。


5. 为什么没有定义 f(),却报"没有虚函数表"?

这里需要引入 key function(关键虚函数) 规则。

在你这个例子里:

cpp 复制代码
struct Base {
    virtual void f();
};

struct Derived : Base {
    void f() override;
};

Base::f() 和 Derived::f() 分别是各自类的关键虚函数 :它们是该类中第一个在类定义处非内联、非纯虚的虚函数。

按照这里使用的 ABI 规则,编译器通常在定义关键虚函数的翻译单元中生成该类的虚函数表。

但你没有定义这两个函数,因此:

  • 没有 Base::f() 的定义 → 没有相应的 Base 虚表定义
  • 没有 Derived::f() 的定义 → 没有相应的 Derived 虚表定义

同时,new Derived 又使构造函数需要引用这两个虚表。于是出现"有引用、没有定义"的链接错误。

实际检查目标文件也印证了这一点:

复制代码
U vtable for Base
U vtable for Derived

U 表示该目标文件引用了符号,但没有定义它。

更准确地说:编译器按规则生成虚表,链接器负责解析对虚表的引用;不是链接器发现虚函数缺失之后,才尝试创建虚表。


6. 是不是先发现 Base,再发现 Derived?

从这次诊断的打印顺序看,先报告了 Base,随后报告了 Derived。

但这不能进一步推断为"链接器先检查基类,再检查派生类"。

链接器主要处理节、符号和重定位记录,并不是在按照 C++ 继承关系逐个检查类。

另外,需要把两个顺序分开:

顺序类型 说明
程序的构造顺序 构造 Derived 对象时,先构造 Base 子对象,再执行 Derived 自身的构造部分
链接器的报错顺序 取决于它遍历目标文件、节和重定位记录的顺序,不保证与构造顺序一致

这次两个构造函数分别引用各自的虚表,因此两个缺失的符号都被报告出来。首条 warning 只提到 Base,也不表示 Derived 没有同类引用。


如何修复

如果只解决当前链接错误,在类定义之后补上:

cpp 复制代码
void Base::f() {}
void Derived::f() {}

即可提供缺失的定义。

另外还有一个独立问题:

cpp 复制代码
delete p;   // p 的类型是 Base*

通过 Base* 删除 Derived 对象时,需要给 Base 加上虚析构函数:

cpp 复制代码
virtual ~Base() = default;

否则即使修好了链接错误,这个删除操作仍然是未定义行为。


总结

概念 核心含义
_ZTV4Base Base 类的虚函数表符号
_ZN... 嵌套名称编码(类成员、命名空间成员等)
C1 / C2 完整对象构造函数 / 基类子对象构造函数
relocation 根据符号最终位置修正机器码或数据中的地址相关字段
key function 决定虚表在哪个翻译单元生成的关键虚函数
DT_TEXTREL 表示存在需要修改只读区域的动态重定位
相关推荐
ss2731 小时前
AI全栈实战 | 3.3-02 Python 并发:有 GIL 为什么还用多线程,asyncio 和 JS 事件循环同源不同味
开发语言·javascript·python
菜鸟~noob2331 小时前
【电子战】第20篇:凸优化与对数障碍法【含matlab代码】
开发语言·matlab
Wang's Blog1 小时前
Java框架 SpringCloud 快速入门: 服务拆分案例 Demo
java·开发语言·spring cloud
小小龙学IT1 小时前
Python SQLAlchemy 2.0 深度解析:从 Core 到 ORM 的数据访问双引擎
开发语言·数据库·python
2601_962885721 小时前
如何用 Python 计算 ROC 变动率指标做动量分析?
开发语言·python·算法
潼心1412o2 小时前
C++初阶(长期更新)第9讲:string(上)
开发语言·c++
朝朝辞暮i2 小时前
C++ 第 39 章: 阶段性总复习——类、对象、构造、继承、this、指针与智能指针
开发语言·c++·算法·ros2
wuminyu2 小时前
Virtual Thread重投递至ForkJoinPool任务队列过程解析
java·linux·c语言·jvm·c++
All for pursuit.2 小时前
【回溯-6】79.单词搜索
数据结构·c++·算法·leetcode