跳转至

最后更新:2026-10-13

C++ 内存泄漏排查实战

最近在排查一个数据库内核(Greenplum/PostgreSQL 系)的内存问题时,拿到了一串非常经典的 WARNING:problem in alloc set ExecutorState: bad single-chunk。这条日志的含义是:内存池(alloc set)在校验时发现某个 chunk 的头部信息被破坏了——十有八九是某处代码越界写,把隔壁 chunk 的元数据踩了。

这类问题的难点在于:报错的地方不是出错的地方。WARNING 是在内存被释放、校验时才打出来的,而越界写可能发生在很久之前的任意一段代码里。下面就把当时的完整排查过程贴出来,一起看看怎么用 gdb 把"案发现场"找回来。

现场信息先读一遍:

regression=# SELECT t1.a, t1.c, t2.b, t2.c FROM prt1_e t1, prt2_e t2 WHERE (t1.a + t1.b)/2 = (t2.b + t2.a)/2 AND t1.c = 0 ORDER BY t1.a, t2.b;
WARNING:  problem in alloc set ExecutorState: bad single-chunk 0x7cfdd48 in block 0x7cfc580  (seg1 slice1 127.0.1.1:7003 pid=4295)
WARNING:  problem in alloc set ExecutorState: found inconsistent memory block 0x7cfc580  (seg1 slice1 127.0.1.1:7003 pid=4295)
WARNING:  problem in alloc set ExecutorState: bad single-chunk 0x7cfdd48 in block 0x7cfc580  (seg1 slice1 127.0.1.1:7003 pid=4295)
WARNING:  problem in alloc set ExecutorState: found inconsistent memory block 0x7cfc580  (seg1 slice1 127.0.1.1:7003 pid=4295)
WARNING:  problem in alloc set ExecutorState: bad single-chunk 0x87f85c8 in block 0x87f6e00  (seg0 slice1 127.0.1.1:7002 pid=4294)
WARNING:  problem in alloc set ExecutorState: found inconsistent memory block 0x87f6e00  (seg0 slice1 127.0.1.1:7002 pid=4294)
WARNING:  problem in alloc set ExecutorState: bad single-chunk 0x7d3c5c8 in block 0x7d3ae00  (seg2 slice1 127.0.1.1:7004 pid=4296)
WARNING:  problem in alloc set ExecutorState: found inconsistent memory block 0x7d3ae00  (seg2 slice1 127.0.1.1:7004 pid=4296)
WARNING:  problem in alloc set ExecutorState: bad single-chunk 0x87f85c8 in block 0x87f6e00  (seg0 slice1 127.0.1.1:7002 pid=4294)
WARNING:  problem in alloc set ExecutorState: bad single-chunk 0x7d3c5c8 in block 0x7d3ae00  (seg2 slice1 127.0.1.1:7004 pid=4296)
WARNING:  problem in alloc set ExecutorState: found inconsistent memory block 0x87f6e00  (seg0 slice1 127.0.1.1:7002 pid=4294)
WARNING:  problem in alloc set ExecutorState: found inconsistent memory block 0x7d3ae00  (seg2 slice1 127.0.1.1:7004 pid=4296)

从 WARNING 里能拿到几个关键信息:

(1)坏 chunk 的地址是确定的:0x7cfdd48(bad single-chunk 指向它);

(2)它所在的 block 是 0x7cfc580,同一 block 里"found inconsistent memory block"说明整个 block 的链式校验也挂了;

(3)多个 segment(seg0/seg1/seg2)同时报,说明不是环境抖动,是确定性的逻辑 bug。

有了确切地址,思路就非常直接了:在内存分配器(aset.c 的 AllocSetAlloc)上打**条件断点**,拦下"分配时返回这个地址"的那一次调用,然后对这个地址下**硬件 watchpoint**,谁写它,谁就是凶手。

gdb 会话如下:

调试:

(gdb) b aset.c:1109 if chunk=0x7cfdd48
Breakpoint 1 at 0x105d420: file aset.c, line 1109.
(gdb) c
Continuing.

Thread 1 "postgres" hit Breakpoint 1, AllocSetAlloc (context=0x787ac50, size=4096) at aset.c:1109
1109        block->freeptr += (chunk_size + ALLOC_CHUNKHDRSZ);
(gdb) p chunk
$1 = (AllocChunk) 0x7cfdd48
(gdb) p chunk->size
$2 = 9187201950435737471
(gdb) p &chunk->size
$3 = (Size *) 0x7cfdd48
(gdb) watch (*((Size *) 0x7cfdd48))
Hardware watchpoint 2: (*((Size *) 0x7cfdd48))
(gdb) i b
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x000000000105d420 in AllocSetAlloc at aset.c:1109
    stop only if chunk=0x7cfdd48
    breakpoint already hit 1 time
2       hw watchpoint  keep y                      (*((Size *) 0x7cfdd48))
(gdb) d 1
(gdb) c

复盘

这个 case 的套路可以直接迁移到任何 C/C++ 项目的内存破坏排查:

(1)先从报错日志里提取**确切的地址**,这是唯一可靠的锚点;

(2)条件断点 b aset.c:1109 if chunk=0x7cfdd48 拦住分配现场,确认这个地址给了谁;

(3)watch (*(Size*)0x7cfdd48) 硬件 watchpoint 等凶手现身——硬件断点不改写内存、不影响程序行为,对内存破坏类问题最可靠;

(4)跑起来之后 watchpoint 命中时的调用栈就是越界写入的完整现场。

值得注意的是第一步里 p chunk->size 打出来是 9187201950435737471(0x7F7F7F7F7F7F7F7F)——一堆 0x7F,这是被写花的典型特征,说明踩内存的代码在写一堆标志位/填充值,顺着这个值在代码里全局搜,往往还能直接搜到元凶。

留一个问题:如果越界写发生在 fork 出的子进程、而你在父进程调试,上面的流程要怎么调整?欢迎留言区讨论。

相关主题

评论