Ir al contenido

Valgrind (memcheck, helgrind) y perf

🎯 Al terminar podrás

  1. Leer un reporte de memcheck y decir qué error es, en qué línea está y cómo se corrige.
  2. Distinguir los tipos de fuga (definitely, indirectly, possibly lost, still reachable).
  3. Saber qué no detecta memcheck (desbordes en el stack) y qué usar en su lugar.
  4. Explicar por qué helgrind detecta una race aunque el resultado haya salido bien, y medir page faults con perf.

Antes necesitas: malloc y el heap

  • Detective escribe tu hipótesis del bug antes de abrir el diagnóstico
  • Laboratorio sube la escalera: resuelto → completa → solo → variante
Ventana de terminal
gcc -g -O0 prog.c -o prog # -g: números de línea en los reportes
valgrind --tool=memcheck ./prog # memcheck es la herramienta por defecto
valgrind --leak-check=full ./prog # detalle de cada fuga
valgrind --track-origins=yes ./prog # de dónde salió un valor sin inicializar
valgrind --tool=helgrind ./prog_con_hilos

Compila con -g: sin eso, los reportes muestran solo direcciones y no archivo.c:línea.

Cada caso es un programa de prueba propio (errores.c <caso>), ejecutado con valgrind --leak-check=full:

#ErrorCódigoLo que dice memcheck
1escritura fuera del bloque (heap)a = malloc(10*sizeof(int)); a[10] = 7;Invalid write of size 4 … Address … is 0 bytes after a block of size 40 alloc'd
2usar memoria ya liberadafree(a); printf("%d", a[0]);Invalid read of size 4 … Address … is 0 bytes inside a block of size 40 free'd
3doble freefree(a); free(a);Invalid free() / delete / delete[] / realloc()
4fuga de memorias = malloc(64); s = NULL;64 bytes in 1 blocks are definitely lost
5valor sin inicializarint x; *p = x + 1; if (*p > 3) …Conditional jump or move depends on uninitialised value(s)
6free de algo que no es del heapint pila[4]; free(pila);Invalid free() … Address … is on thread 1's stack
malloc(40)a[0]a[1]a[2]a[3]a[4]a[5]a[6]a[7]a[8]a[9]zona rojaa[10] = 7Invalid write of size 4 … 0 bytes after a block of size 40tras free(a)bloque liberado (memcheck lo recuerda)leer a[0]: Invalid read … 0 bytes inside a block of size 40 free'd

Cómo leerlo: memcheck rodea cada bloque de malloc con zonas rojas (bytes prohibidos). a[10] cae en la zona roja de la derecha, justo después del bloque: por eso dice 0 bytes after. Después de free, memcheck recuerda el bloque liberado: leer a[0] cae inside un bloque free’d (casos 1 y 2 de la tabla).

Detalles que suelen preguntar:

  • En el caso 5, el error aparece en la línea del if, no en la de x + 1. Memcheck solo protesta cuando un valor no inicializado cambia el comportamiento: un salto condicional, una dirección o una syscall. Copiarlo o hacer aritmética con él no se reporta. Con --track-origins=yes te dice dónde nació el valor.
  • Tipos de fuga:
    • definitely lost: no queda ningún puntero al bloque;
    • indirectly lost: solo se llega a él desde un bloque perdido;
    • possibly lost: queda un puntero al interior del bloque;
    • still reachable: sigue habiendo un puntero al terminar, que no es grave.
raícesstack, globales,registrosstill reachablehay un puntero al inicio al terminar: no es gravepossibly lostsolo un puntero al interior del bloquedefinitely lostningún puntero llegaindirectly lostsolo desde un bloque perdidoEjemplo del caso 4: s = malloc(64); s = NULL; deja el bloque sin ningún puntero → definitely lost.

Cómo leerlo: al terminar el programa, memcheck busca qué bloques se alcanzan desde las raíces. La categoría depende de qué flecha llega al bloque: al inicio (still reachable), al interior (possibly lost), ninguna (definitely lost) o solo desde otro bloque perdido (indirectly lost).

  • ERROR SUMMARY: N errors from M contexts cuenta errores distintos (M) y sus apariciones (N).

«¿Qué errores tiene el programa? ¿En qué líneas? Corregir y validar con memcheck.»

mem1.c: 100 malloc y 100 free
char *ptr[100];
for (size_t i = 0; i < 100; i++) ptr[i] = malloc(100);
for (size_t i = 0; i < 100; i++) free(ptr[i]);

Ejecución de prueba:

total heap usage: 100 allocs, 100 frees, 10,000 bytes allocated
All heap blocks were freed -- no leaks are possible
ERROR SUMMARY: 0 errors from 0 contexts

No tiene errores. Cada bloque se libera exactamente una vez. Es el caso de control: así se ve un reporte limpio. Una respuesta completa también menciona que no se revisa si malloc devolvió NULL, aunque memcheck no lo reporta.

mem2.c: puntero a una variable global
int a;
int main() { int *ptr = &a; *ptr = 73; return 0; }

Ejecución de prueba: 0 allocs, 0 frees … ERROR SUMMARY: 0 errors.

No tiene errores. a es una global en bss, inicializada en 0 y válida durante toda la ejecución. ptr apunta a ella y escribir ahí es legal. No hay heap involucrado, así que tampoco hay fugas.

mem3.c: variable sin inicializar (el único con error)
int number; // línea 4: Should be initialized
if (number == 0) { // línea 6
printf("Number is zero");
}

Ejecución de prueba (con --track-origins=yes):

Conditional jump or move depends on uninitialised value(s)
at 0x109159: main (mem3.c:6)
Uninitialised value was created by a stack allocation
at 0x109149: main (mem3.c:3)
ERROR SUMMARY: 1 errors from 1 contexts
  • Error: se usa number sin inicializar en la línea 6, la del if. Memcheck indica que el valor nace en el marco de main (línea 3).
  • Corrección: int number = 0;. Con eso, memcheck da 0 errores.
  • GCC también lo advierte con -Wall: 'number' is used uninitialized.
hello.c y los «2 allocs» misteriosos

hello.c hace un malloc(sizeof(int)) y su free, pero memcheck reporta 2 allocs, 2 frees, 4,100 bytes allocated. El segundo bloque, de 4096 B, es el buffer de stdout que printf reserva la primera vez. Ojo: 4100 = 4 + 4096. glibc lo libera al terminar.

Peldaño 3 · Solosin ayuda; luego compara

Solo: ¿qué reporta memcheck y cómo lo corriges?

Antes de abrir la respuesta, escribe qué mensajes dará valgrind --leak-check=full, en qué líneas y la corrección de cada bug.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int *leer_notas(int n) {
int *notas = malloc(n * sizeof(int));
for (int i = 0; i <= n; i++)
notas[i] = 10 + i;
return notas;
}
int main(void) {
int n = 5;
int *notas = leer_notas(n);
char *nombre = malloc(16);
strcpy(nombre, "Ana");
int suma;
for (int i = 0; i < n; i++)
suma += notas[i];
if (suma > 50)
printf("%s aprobó con %d\n", nombre, suma);
free(notas);
return 0;
}
Comparar (ejecución de prueba con Valgrind 3.22)
LíneaMensaje de memcheckBugCorrección
8Invalid write of size 4 … 0 bytes after a block of size 20 alloc'del for llega a i <= n: escribe notas[5], fuera del bloquei < n
20Conditional jump or move depends on uninitialised value(s)suma sin inicializar. El error sale en el if, no en el +=int suma = 0;
1516 bytes in 1 blocks are definitely lostnunca se libera nombrefree(nombre);

El resumen dice ERROR SUMMARY: 9 errors from 7 contexts, porque el valor no inicializado también «contamina» a printf (varios contextos dentro de glibc). Lo traicionero: el programa imprimió Ana aprobó con 60, que es correcto, porque la basura de suma justo valía 0 en esa ejecución. Con las tres correcciones, memcheck da 0 errors y 3 allocs, 3 frees.

  • Desbordes en el stack o en globales. memcpy1.c copia 1024 bytes a un arreglo local de 20 y memcheck reporta 0 errores. Memcheck conoce los límites de cada bloque del heap porque intercepta malloc, pero no los de cada arreglo del stack. Para eso existe AddressSanitizer (gcc -fsanitize=address) o -D_FORTIFY_SOURCE=2.
heapstackbloque de mallocmemcheck intercepta malloc: sabe dóndeempieza y termina cada bloque✓ detecta el desbordedest[20]otra localretornosin zonas rojas entre variables: el stackparece un solo bloque válido✗ no lo detecta (usa ASan o FORTIFY)Zonas rojas: bytes que memcheck marca como prohibidos alrededor de cada bloque.

Cómo leerlo: en el heap cada bloque tiene zonas rojas a los lados, así que salirse se nota. En el stack las variables están pegadas sin nada entre ellas: escribir de más en dest cae en memoria «válida» del propio stack y memcheck no lo puede distinguir.

  • Errores de lógica que no tocan memoria inválida.
  • Caminos que no se ejecutaron: Valgrind analiza una ejecución, no el programa completo.

Dos hilos incrementan un contador global 1000 veces cada uno (race.c):

== sin lock
Possible data race during read of size 8 at 0x10C040 by thread #3
at 0x10921B: f (race.c:11)
This conflicts with a previous write of size 8 by thread #2
at 0x109226: f (race.c:11)
Address 0x10c040 is 0 bytes inside data symbol "contador"
== con pthread_mutex alrededor de contador++
contador = 2000
ERROR SUMMARY: 0 errors from 0 contexts

helgrind detecta la race aunque esa ejecución haya dado el resultado correcto: no depende de la suerte del entrelazado, sino de que no hay un lock común que ordene esos dos accesos (algoritmo happens-before). También detecta los mal usos de la API de pthreads (unlock de un mutex que no tienes) y las inversiones en el orden de los locks, que son posibles deadlocks.

sin lockcon mutexT1T2lee 5escribe 6lee 5escribe 6resultado 6, no 7: se perdió un incrementolock · 5 → 6 · unlocklock · 6 → 7 · unlockespera el lockhappens-beforeresultado 7helgrind marca la race de la izquierda aunque esa ejecución haya dado bien: no hay un lock común que ordene los accesos.

Cómo leerlo: cada fila es un hilo y el tiempo avanza hacia la derecha. Sin lock, los dos leen 5 antes de que el otro escriba, y uno de los incrementos se pierde (líneas rojas: accesos en conflicto). Con mutex, el unlock de T1 ocurre antes que el lock de T2 (flecha verde): ese orden es lo que helgrind busca, y si falta, avisa. Ejemplo propio con valores chicos.

La guía pide:

Ventana de terminal
getconf PAGESIZE # 4096
gcc -O0 arreglo.c -o arreglo # sin optimizaciones, para que el recorrido no desaparezca
sudo perf stat -e page-faults ./arreglo

perf stat -e page-faults muestra cuántos fallos de página tuvo el proceso. La guía escribe sudo perf -e page-faults, pero falta el subcomando stat. Si perf no está disponible (por ejemplo, en máquinas virtuales con un kernel sin linux-tools), el mismo número se obtiene desde el programa con getrusage (ru_minflt + ru_majflt), o con /usr/bin/time -v ./arreglo (líneas Minor/Major page faults).

Resultado esperado, medido con getrusage: los fallos crecen linealmente con N, uno por página en la primera escritura y ninguno al volver a leer. La tabla y la explicación están en paginación por demanda medida.

Otros eventos útiles: perf stat -e cycles,instructions,cache-misses,dTLB-load-misses ./prog. Por ejemplo, recorrer una matriz por columnas dispara los fallos de caché y de TLB frente a recorrerla por filas (es la pregunta 5 de ejercicios de memoria virtual).

Autoevaluación

0/7 correctas · 0 respondidas
1. ¿Por qué compilar con -g antes de usar Valgrind?
2. mem1.c (100 malloc + 100 free) según memcheck…
3. En mem3.c, ¿en qué línea reporta memcheck el error?
4. s = malloc(64); s = NULL; produce…
5. Un desborde de un arreglo LOCAL (en el stack)…
6. helgrind reporta una data race cuando…
7. ¿Qué comando cuenta page faults?

📈 ¿Qué tan bien lo dominas?

Elige pensando en lo que hiciste, no en lo que leíste. Para marcar 3 tienes que haber resuelto un ejercicio nuevo sin pistas y haberlo justificado. Si solo lo leíste y sientes que «ya entendí», todavía es un 1.

Fuente ·sem_9/10-OSL-Valgrind.pdf (actividad de laboratorio) · Código del curso sem9/valgrind/: hello.c, mem1.c, mem2.c, mem3.c (ejecuciones de prueba con memcheck) · Pruebas propias: errores.c (6 tipos de error), race.c (helgrind) · Valgrind User Manual (memcheck, helgrind)