gdb与objdump显示的C++析构函数不一致问题排查
G++析构函数调用差异问题解析
测试代码
#include <stdlib.h> #include <stdint.h> #include <string.h> class Base { public: Base(uint32_t len) : len_(len) { buf_ = (char *)malloc(len_ * sizeof(char)); } virtual ~Base() { if (nullptr != buf_) { free(buf_); buf_ = nullptr; } } void set_buf(const char *str) { if (nullptr != str && nullptr != buf_ && len_ > 0) { strncpy(buf_, str, len_); buf_[len_ - 1] = '\0'; } } private: uint32_t len_; char *buf_; }; int main(int argc, char *argv[]) { Base base(8); base.set_buf("hello"); return 0; }
编译命令
g++ -g -O0 -fno-inline -std=c++11 -Wall main.cpp -o main
objdump输出的汇编结果
执行objdump -S -t main得到:
0000000000001250 <_ZN4BaseD0Ev>: virtual ~Base() 1250: f3 0f 1e fa endbr64 1254: 55 push %rbp 1255: 48 89 fd mov %rdi,%rbp } 1258: e8 d3 ff ff ff call 1230 <_ZN4BaseD1Ev>
此处显示删除析构函数_ZN4BaseD0Ev调用了完整对象析构函数_ZN4BaseD1Ev。
GDB调试结果
反汇编_ZN4BaseD0Ev
(gdb) disassemble _ZN4BaseD0Ev Dump of assembler code for function _ZN4BaseD0Ev: 0x0000555555555250 <+0>: endbr64 0x0000555555555254 <+4>: push %rbp 0x0000555555555255 <+5>: mov %rdi,%rbp 0x0000555555555258 <+8>: call 0x555555555230 <_ZN4BaseD2Ev> 0x000055555555525d <+13>: mov %rbp,%rdi 0x0000555555555260 <+16>: mov $0x18,%esi 0x0000555555555265 <+21>: pop %rbp 0x0000555555555266 <+22>: jmp 0x5555555550a0 <_ZdlPvm@plt> End of assembler dump.
此处显示_ZN4BaseD0Ev调用的是基类对象析构函数_ZN4BaseD2Ev,且后续会调用operator delete。
查看虚表
(gdb) x/4ag 0x555555557d78 - 16 0x555555557d68 <_ZTV4Base>: 0x0 0x555555557d88 <_ZTI4Base> 0x555555557d78 <_ZTV4Base+16>: 0x555555555230 <_ZN4BaseD2Ev> 0x555555555250 <_ZN4BaseD0Ev>
虚表中存放的是_ZN4BaseD2Ev而非_ZN4BaseD1Ev。
核心问题
- 为什么objdump和GDB显示的调用目标不一致?
_ZN4BaseD0Ev实际调用的是_ZN4BaseD1Ev还是_ZN4BaseD2Ev?
问题解析
首先明确G++为带虚析构的类生成的三个析构函数的作用:
_ZN4BaseD2Ev(基类对象析构函数):负责执行用户编写的析构逻辑,是所有析构函数的基础实现,无论对象是作为基类子对象还是完整对象,最终都会调用这个函数完成资源释放。_ZN4BaseD1Ev(完整对象析构函数):仅针对完整对象(无继承场景下,和D2逻辑完全一致;有子类时,会先调用子类析构,再调用D2)。_ZN4BaseD0Ev(删除析构函数):除了执行析构逻辑,还会调用operator delete释放对象内存,通常在delete表达式中被调用。
差异原因
你的代码中Base没有子类,此时D1和D2的实现逻辑完全相同。G++会做COMDAT符号折叠优化:将逻辑完全一致的D1和D2合并为同一个函数体,只是保留两个不同的符号名。
- objdump直接解析可执行文件的静态符号表,显示的是编译时生成的调用目标符号
D1,但实际该符号对应的地址和D2完全相同。 - GDB在动态调试时,会根据内存地址对应的符号名显示,此时地址指向的符号是
D2,所以显示调用D2。
实际调用情况
_ZN4BaseD0Ev实际调用的是同一个函数体——因为D1和D2被合并了,无论显示的是哪个符号,执行的都是你编写的析构函数逻辑(释放buf_),之后D0再调用operator delete完成内存释放。
你可以通过nm main | grep _ZN4BaseD验证:会发现_ZN4BaseD1Ev和_ZN4BaseD2Ev的地址完全相同,证明它们是同一个函数的不同符号别名。
环境信息
$ uname -a Linux xxx 5.15.0-127-generic #137-Ubuntu SMP Fri Nov 8 15:21:01 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux $ g++ --version g++ (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0 $ gdb --version GNU gdb (Ubuntu 12.1-0ubuntu1~22.04.2) 12.1
内容的提问来源于stack exchange,提问作者xxllxx666
相关产品推荐
相关产品推荐

