Última atualização:2026-10-12
Investigação prática de vazamentos de memória em C++¶
Investigando um problema de memória num kernel de banco de dados (família Greenplum/PostgreSQL), recebi uma sequência de WARNINGs clássicos: problem in alloc set ExecutorState: bad single-chunk. O que esse log significa: o pool de memória (alloc set) descobriu, ao validar, que o cabeçalho de um chunk estava corrompido — nove em cada dez vezes, algum código escreveu fora dos limites e pisou nos metadados do chunk vizinho.
A dificuldade desses problemas: onde o erro aparece não é onde ele nasceu. O WARNING só é impresso quando a memória é liberada e validada, mas a escrita fora dos limites pode ter acontecido em qualquer trecho de código muito antes. Abaixo está o processo completo daquela investigação, para vermos juntos como recuperar a «cena do crime» com o gdb.
Primeiro, leia as informações da cena:
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)
Dos WARNINGs dá para extrair várias informações-chave:
(1) o endereço do chunk corrompido é conhecido: 0x7cfdd48 (o bad single-chunk aponta para ele);
(2) o block em que ele está é 0x7cfc580, e o «found inconsistent memory block» do mesmo block mostra que a validação encadeada do block inteiro também quebrou;
(3) vários segmentos (seg0/seg1/seg2) reportam ao mesmo tempo, então não é instabilidade do ambiente — é um bug lógico determinístico.
Com um endereço exato em mãos, o plano é bem direto: coloque um breakpoint condicional no alocador de memória (AllocSetAlloc do aset.c) para interceptar a chamada que «devolve esse endereço na alocação», e então coloque um watchpoint por hardware nesse endereço — quem escrever nele é o culpado.
A sessão do gdb:
Depuração:
(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
Retrospectiva¶
O roteiro deste caso transfere direto para investigações de corrupção de memória em qualquer projeto C/C++:
(1) primeiro extraia do log de erros o endereço exato — é a única âncora confiável;
(2) o breakpoint condicional b aset.c:1109 if chunk=0x7cfdd48 prende o momento da alocação e confirma para quem esse endereço foi;
(3) watch (*(Size*)0x7cfdd48), um watchpoint por hardware, espera o culpado aparecer — breakpoints por hardware não reescrevem a memória nem afetam o comportamento do programa, sendo os mais confiáveis para problemas de corrupção de memória;
(4) em execução, a pilha de chamadas no instante do hit do watchpoint é a cena completa da escrita fora dos limites.
Vale notar que no primeiro passo p chunk->size imprimiu 9187201950435737471 (0x7F7F7F7F7F7F7F7F) — uma pilha de 0x7F, o traço típico de memória sobrescrita: o código que pisou na memória estava gravando um monte de flags ou valores de preenchimento. Buscar esse valor globalmente no código muitas vezes acha o culpado direto.
Deixo uma pergunta: se a escrita fora dos limites acontece num processo filho criado por fork e você depura o processo pai, como ajustar o fluxo acima? Fique à vontade para discutir nos comentários.
Tópicos relacionados¶
- Construindo projetos C++ com Bazel | C++那些事
- Depuração conjunta de Bazel e LLDB | C++那些事
- CMake: introdução e prática | C++那些事
- Introdução ao gerenciamento de pacotes C++ com Conan | C++那些事
- C++ Insights: observação em tempo de compilação | C++那些事
- C++ interativo com Jupyter Notebook | C++那些事
- Converter string para int em C++ | C++那些事
- Macro de depuração dbg-macro em C++ | C++那些事
- Opções de compilação do GCC em detalhes | C++那些事
- Depuração de programas C++ com GDB | C++那些事