在GDB中确认虚函数实际调用版本,需按vptr→vtable→函数指针→符号名四步链路验证:先p (void)obj得vptr,再p ((void)obj)得虚表首址,然后p ((void)((void)obj))[0]取首项地址,最后info symbol查对应函数名。

怎么在 GDB 里确认虚函数实际调用的是哪个版本
虚函数调用的目标在编译时不确定,GDB 默认 step 或 next 到 obj->show() 这类调用时,会直接跳进汇编或“看不见的分派逻辑”,而不是停在 Derived::show。关键不是“进不进函数”,而是要验证:当前指针指向的对象,其 vptr 指向的虚表里,第一条(或对应索引)函数指针到底填的是谁的地址。
- 先停在虚函数调用前:比如在
execute(b)和execute(d)后加断点,或直接b execute,再run - 运行到
obj->show()行后,用p (void**)obj查看对象起始地址处的值——这就是vptr - 再用
p *((void**)obj)得到虚表首地址,接着p ((void**)*((void**)obj))[0]提取第一个虚函数地址(show在本例中是第 0 项) - 最后用
info symbol <address></address>查这个地址属于哪个函数,比如输出Derived::show() at virtual_function_example.cpp:12就确认了多态生效
为什么 step 不进 Derived::show,但 stepi 可以看到跳转
GDB 的 step 基于源码行号,而虚函数调用的动态分派由几条汇编指令完成(load vptr → load vtable entry → indirect call),中间没有 C++ 源码对应行。所以它“跳过”了,不是 bug,是设计如此。
-
stepi是单步执行机器指令,能清楚看到类似mov rax,QWORD PTR [rdi](取 vptr)、call QWORD PTR [rax](跳转到虚表第一项)的过程 - 配合
x/3i $pc查看当前指令上下文,再结合info registers rax看寄存器值,就能把“跳哪去”和“为什么跳”对上 - 如果想强制让 GDB 在目标函数入口停住,可在
Derived::show上直接设断点:b Derived::show,比依赖step更可靠
用 -fdump-class-hierarchy 预先看虚表结构,避免猜偏移
手动算虚函数索引容易错——比如类有多个虚函数、有虚继承、或编译器插入了 RTTI 条目。GCC 的 -fdump-class-hierarchy 能生成人可读的虚表布局说明,比硬抠内存直观得多。
- 重新编译:
g++ -g -fdump-class-hierarchy virtual_function_example.cpp -o virtual_function_example - 生成文件名类似
virtual_function_example.cpp.002t.class,打开后找vtable for Derived段落 - 里面会明确列出:
0 Derived::show()、8 Derived::~Derived()等,数字是字节偏移,对应[0]、[1]索引 - 这个输出和 GDB 中
((void**)vptr)[0]的结果严格一致,是调试时查索引的权威依据
常见陷阱:基类指针指向子类对象,但 GDB 显示 Base::show
这不是虚函数失效,而是 GDB 在打印栈帧或变量类型时,仅根据静态类型(Base*)推断,不自动解引用虚表。你看到的 Base::show 往往只是符号名提示,不代表实际执行路径。
- 不要信
bt输出里显示的函数名,尤其当它写的是Base::show却你明明传了Derived*—— 这是 GDB 的静态推测,不可靠 - 真正可信的只有两件事:虚表地址里的函数指针值(用
p ((void**)obj)[0])、以及该地址对应的符号(用info symbol) - 如果发现虚表里填的是
Base::show地址,检查构造顺序:子类对象是否完整构造完毕?有没有在构造函数里提前调用了虚函数?
vptr → vtable → 函数指针 → 符号名 这四步链路上核对,其他都是干扰。


















