ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

虚函数vtable,链接时看到构造函数,linker去查询符号地址并写入(一)

虚函数vtable,链接时看到构造函数,linker去查询符号地址并写入(一) 摘要编译器编译时看到了构造函数需要虚函数表此时把地址留空让链接器去找虚函数表和地址并填入。如果链接器找不到虚函数没有定义只有声明就报错编译器在构造函数的机器码里放了一条「把 vtable 地址写进对象」的指令并给这条指令留了一个待填的地址。链接器负责填这个地址时在所有输入里都找不到 _ZTV4Base 的定义于是报错。构造函数在这里只是「引用了 vtable 的那段代码」并没有被执行。源代码#includeiostreamstructBase{virtualvoidf();};structDerived:Base{voidf()override;};intmain(){Base*pnew Derived;return0;}编译 / 链接 / 运行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 Base0xc 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 Base0x10也就是跳过 offset-to-top 和 typeinfo 指针之后的第一个虚函数槽1256: lea 0x2b13(%rip),%rdx # 3d70 vtable for Base0x10 1261: mov %rdx,(%rax)重定位项里写的是0xc而这里显示0x10差的 4 字节是因为 PC32 相对寻址以指令结束处为基准属于编码细节不影响理解。第三阶段运行只有./a.out真正跑起来main调用new DerivedDerived::Derived()再调用Base::Base()上面那条mov %rdx,(%rax)才被执行vptr 才被写进堆上的对象。这才是「构造对象」。你的原始main.cpp在第二阶段就停了根本没走到这一步。更准确的说法编译器在构造函数的机器码里放了一条「把 vtable 地址写进对象」的指令并给这条指令留了一个待填的地址。链接器负责填这个地址时在所有输入里都找不到_ZTV4Base的定义于是报错。构造函数在这里只是「引用了 vtable 的那段代码」并没有被执行。
返回列表