Última actualización:2026-10-12
Investigación práctica de fugas de memoria en C++¶
Al investigar un problema de memoria en un kernel de base de datos (familia Greenplum/PostgreSQL), me encontré con una cadena de WARNING muy clásica: problem in alloc set ExecutorState: bad single-chunk. El significado de este registro: el pool de memoria (alloc set) descubrió, al validar, que la cabecera de un chunk estaba corrupta — nueve de cada diez veces, algún código escribió fuera de rango y pisó los metadatos del chunk vecino.
La dificultad de este tipo de problemas: el lugar donde salta el error no es el lugar del fallo. El WARNING se imprime cuando la memoria se libera y se valida, pero la escritura fuera de rango pudo ocurrir en cualquier trozo de código mucho antes. Abajo dejo el proceso completo de aquella investigación, para ver juntos cómo recuperar la «escena del crimen» con gdb.
Primero, leer la información de la escena:
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)
De los WARNING se extraen varios datos clave:
(1) la dirección del chunk corrupto es conocida: 0x7cfdd48 (bad single-chunk la señala);
(2) el block que lo contiene es 0x7cfc580, y el «found inconsistent memory block» del mismo block indica que la validación en cadena de todo el block también falló;
(3) que varios segmentos (seg0/seg1/seg2) lo reporten a la vez descarta las fluctuaciones del entorno: es un bug lógico determinista.
Con una dirección exacta en la mano, el plan es muy directo: poner un breakpoint condicional en el asignador de memoria (AllocSetAlloc de aset.c) para interceptar la llamada que «devuelve esta dirección al asignar», y luego poner un watchpoint por hardware sobre esa dirección: quien la escriba es el culpable.
La sesión de gdb:
Depuración:
(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¶
El guion de este caso se traslada tal cual a la investigación de corrupción de memoria en cualquier proyecto C/C++:
(1) primero extraer del registro de errores la dirección exacta: es el único ancla fiable;
(2) el breakpoint condicional b aset.c:1109 if chunk=0x7cfdd48 atrapa el momento de la asignación y confirma a quién fue a parar esta dirección;
(3) watch (*(Size*)0x7cfdd48), un watchpoint por hardware, espera a que aparezca el culpable — los breakpoints por hardware no reescriben memoria ni afectan al comportamiento del programa, y son lo más fiable para los problemas de corrupción de memoria;
(4) una vez en ejecución, la pila de llamadas en el momento del hit del watchpoint es la escena completa de la escritura fuera de rango.
Cabe señalar que en el primer paso p chunk->size imprimió 9187201950435737471 (0x7F7F7F7F7F7F7F7F) — un montón de 0x7F, la firma típica de memoria sobrescrita: el código que pisó la memoria estaba escribiendo un puñado de flags o valores de relleno. Buscar ese valor globalmente en el código muchas veces encuentra al culpable directamente.
Dejo una pregunta: si la escritura fuera de rango ocurre en un proceso hijo nacido de un fork y tú depuras el proceso padre, ¿cómo habría que ajustar el flujo anterior? Bienvenido a discutirlo en los comentarios.
Temas relacionados¶
- Construir proyectos C++ con Bazel | C++那些事
- Depuración conjunta de Bazel y LLDB | C++那些事
- CMake: introducción y práctica | C++那些事
- Introducción a Conan, gestor de paquetes de C++ | C++那些事
- C++ Insights: observación en tiempo de compilación | C++那些事
- C++ Jupyter Notebook interactivo | C++那些事
- Convertir string a int en C++ | C++那些事
- dbg-macro, macro de depuración para C++ | C++那些事
- Explicación detallada de las opciones de compilación de GCC | C++那些事
- Depurar programas C++ con GDB | C++那些事