Ir para o conteúdo

Ú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

Comentários