欢迎光临
我们一直在努力

虚函数vtable,在链接时,linker把符号地址写入构造函数

源代码:

#include <iostream>
struct Base
{ virtual void f(); };
struct Derived:Base
{ void f() override; };
int main()
{ Base *p = new Derived; return 0; }

编译 / 链接 / 运行:vtable 与构造函数到底发生了什么

链接过程不包含构造对象,不会执行任何一行代码。

用 main.cpp 编译出的目标文件,把「编译 / 链接 / 运行」三个阶段各自做了什么实际看一遍。


链接是纯静态的过程,不执行任何代码,也没有任何对象被构造。有的说法是「构造函数的代码里引用了 vtable」和「构造函数运行时写 vptr」,不对。

下面按三个阶段拆开,每一步都用 main.cpp 的实际产物来看。

第一阶段:编译

g++ 先调用 cc1plus 把源码变成汇编,再汇编成 main.o。

编译器看到 new Derived,就隐式生成 Derived::Derived() 和 Base::Base() 的机器码。

Base::Base() 里有这样两条指令:

c: lea 0x0(%rip),%rdx # 取 vtable 地址
f: R_X86_64_PC32 vtable for Base+0xc
17: mov %rdx,(%rax) # 把它写进对象头部, 也就是写 vptr

编译器不知道 vtable 在哪,所以 lea 的操作数先填 0 占位,同时在重定位表里记一笔:偏移 0xf 处要填 _ZTV4Base 的地址。

符号表里 _ZTV4Base 标为 UND,意思是「我用到了,但定义不在我这」:

000000000000000f R_X86_64_PC32 _ZTV4Base + c ← .rela.text._ZN4BaseC2Ev
18: 0000000000000000 NOTYPE GLOBAL UND _ZTV4Base

第二阶段:链接

collect2 调用 ld,它做的是查表和填数:

把所有 .o 和库的 section 合并,然后逐条处理重定位项,每一条都去所有输入里找符号的定义,找到就把真实地址算出来填进占位处。

_ZTV4Base 在任何输入里都没有定义,查表失败,于是报错。

报错信息里出现 in function Base::Base(),只是因为这条没法填的重定位项位于 Base::Base() 的代码段里,
链接器在告诉你「引用发生在哪个函数的代码中」,而不是它运行了这个函数。

对比补了定义的 main_fixed.o,_ZTV4Base 是有定义的,落在一个自己的 section 里,长度 24 字节:

20: 0000000000000000 24 OBJECT WEAK DEFAULT 19 _ZTV4Base
[19] .data.rel.ro.local._ZTV4Base PROGBITS … 000018

链接后的 a.out 里,同一条 lea 的占位 0 已经被换成真实偏移,指向 vtable for Base+0x10,也就是跳过 offset-to-top 和 typeinfo 指针之后的第一个虚函数槽:

1256: lea 0x2b13(%rip),%rdx # 3d70 <vtable for Base+0x10>
1261: mov %rdx,(%rax)

重定位项里写的是 +0xc 而这里显示 +0x10,差的 4 字节是因为 PC32 相对寻址以指令结束处为基准,属于编码细节,不影响理解。

第三阶段:运行

只有 ./a.out 真正跑起来,

main 调用 new Derived,
Derived::Derived() 再调用 Base::Base(),
上面那条 mov %rdx,(%rax) 才被执行,vptr 才被写进堆上的对象。

这才是「构造对象」。

你的原始 main.cpp 在第二阶段就停了,根本没走到这一步。


更准确的说法

编译器在构造函数的机器码里放了一条「把 vtable 地址写进对象」的指令,
并给这条指令留了一个待填的地址。

链接器负责填这个地址时,在所有输入里都找不到 _ZTV4Base 的定义,
于是报错。

构造函数在这里只是「引用了 vtable 的那段代码」,并没有被执行。

赞(0)
未经允许不得转载:171主机测评 » 虚函数vtable,在链接时,linker把符号地址写入构造函数
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址