应使用 info vtbl 或 stepi 配合寄存器观察来调试虚函数调用,而非依赖 step;info vtbl 直接显示虚函数表,stepi 逐条执行汇编并检查 rax 等寄存器值以确认实际跳转目标。

直接用 step 会跳进错误的函数体
虚函数调用不是静态绑定,step(或 s)在遇到 obj->func() 时,GDB 默认按汇编指令单步,但往往直接跳进基类实现(甚至内联展开后的代码),而不是你期望的派生类重写版本。这不是 GDB 的 bug,而是它没主动解析 vtable 跳转目标。
真正起作用的是运行时从 vptr 查表再跳转——这个过程跨了至少两次内存读取:mov rax, [obj] → mov rax, [rax] → call rax。GDB 不会自动把这三步“合成”为一次逻辑单步。
- 必须先停在虚函数调用语句上(比如
break main.cpp:15),再用stepi(单条汇编指令)逐条走,观察rax/rdx等寄存器如何被填入 vtable 地址和最终函数地址 - 若想确认当前调用目标,执行
info registers rax后,再用x/a $rax查看该地址是否指向你预期的Derived::func - 不要依赖
step进入虚函数体——它可能带你进Base::func,哪怕对象实际是Derived实例
info vtbl 是最省力的验证方式
GDB 内置命令 info vtbl 能直接打印当前对象指针所指向的虚函数表结构,包括每个虚函数名和对应地址。它不依赖源码符号是否完整,只要编译带 -g 就能工作。
- 前提:断点停在虚函数调用前,且有合法对象指针,例如
Base* ptr = new Derived();,然后执行info vtbl ptr - 输出中会列出类似
0: Base::func、1: Derived::func的条目(具体索引取决于虚函数声明顺序);注意看哪一行地址和你obj->func()实际跳转的一致 - 多重继承下
info vtbl ptr可能只显示第一个基类的 vtable;要查第二个基类部分,得先做指针偏移,比如info vtbl ((Base2*)ptr) - 如果提示
Can't find virtual table for type,大概率是调试信息缺失或对象指针已悬空,检查print ptr和print *ptr是否可读
用 x/3a *(void**)obj 手动解包 vtable 内容
当 info vtbl 不可用(如旧版 GDB)或你想绕过符号层直看原始数据时,这条命令组合最可靠:它先把对象首地址转成 void**,解引用得 vptr,再解引用得 vtable 起始地址,最后以地址格式打印前三项。
- 典型操作链:
p (void**)obj→ 得到类似$1 = (void **) 0x5555557562a0;然后x/3a 0x5555557562a0→ 显示三个函数地址 - 若看到某地址是
0x0或明显非法(如0x1),说明 vtable 未初始化或对象构造失败;若地址落在.text段但符号名对不上,可能是 LTO 或链接时优化干扰了符号表 - 注意字长:64 位系统用
x/3a,32 位系统用x/3aw(w表示 word=4 字节),否则可能错位读出半个地址
反汇编调用点,对照 call 指令的目标寄存器
虚函数调用在汇编里必有明确的间接跳转指令,定位它就能确认运行时真实目标。关键不是看 C++ 行号,而是看 call 指令操作数是否来自寄存器(如 call rax),然后回溯该寄存器值来源。
- 用
disassemble /m查看源码与汇编映射,找到obj->func()对应的call行;再用stepi停在那条call前,执行info registers rax(或对应寄存器) - 若寄存器值是
0x5555557562b8,立刻x/i 0x5555557562b8反汇编该地址,看第一条指令是否属于Derived::func的入口 - 常见陷阱:GCC 在 -O2 下可能将简单虚函数内联,此时汇编里根本没
call,而是直接展开函数体——这时step确实能进对地方,但已失去“虚调用”语义,调试需切回-O0
虚函数调用路径不是线性的,它的分支发生在内存里而非代码行间。最易被忽略的是:你以为 step 进了 Derived::func,其实只是进了编译器生成的某个中间跳板(thunk),而真正的函数体还在别处——这时候必须看寄存器值,而不是依赖 GDB 的源码高亮位置。











