Valgrind (memcheck, helgrind) y perf
🎯 Al terminar podrás
- Leer un reporte de memcheck y decir qué error es, en qué línea está y cómo se corrige.
- Distinguir los tipos de fuga (definitely, indirectly, possibly lost, still reachable).
- Saber qué no detecta memcheck (desbordes en el stack) y qué usar en su lugar.
- 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
gcc -g -O0 prog.c -o prog # -g: números de línea en los reportesvalgrind --tool=memcheck ./prog # memcheck es la herramienta por defectovalgrind --leak-check=full ./prog # detalle de cada fugavalgrind --track-origins=yes ./prog # de dónde salió un valor sin inicializarvalgrind --tool=helgrind ./prog_con_hilosCompila con -g: sin eso, los reportes muestran solo direcciones y no archivo.c:línea.
Qué detecta memcheck (con su mensaje real)
Sección titulada «Qué detecta memcheck (con su mensaje real)»Cada caso es un programa de prueba propio (errores.c <caso>), ejecutado con valgrind --leak-check=full:
| # | Error | Código | Lo que dice memcheck |
|---|---|---|---|
| 1 | escritura 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 |
| 2 | usar memoria ya liberada | free(a); printf("%d", a[0]); | Invalid read of size 4 … Address … is 0 bytes inside a block of size 40 free'd |
| 3 | doble free | free(a); free(a); | Invalid free() / delete / delete[] / realloc() |
| 4 | fuga de memoria | s = malloc(64); s = NULL; | 64 bytes in 1 blocks are definitely lost |
| 5 | valor sin inicializar | int x; *p = x + 1; if (*p > 3) … | Conditional jump or move depends on uninitialised value(s) |
| 6 | free de algo que no es del heap | int pila[4]; free(pila); | Invalid free() … Address … is on thread 1's stack |
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 dex + 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=yeste 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.
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 contextscuenta errores distintos (M) y sus apariciones (N).
La actividad: mem1.c, mem2.c, mem3.c
Sección titulada «La actividad: mem1.c, mem2.c, mem3.c»«¿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 allocatedAll heap blocks were freed -- no leaks are possibleERROR SUMMARY: 0 errors from 0 contextsNo 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 initializedif (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
numbersin inicializar en la línea 6, la delif. Memcheck indica que el valor nace en el marco demain(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ínea | Mensaje de memcheck | Bug | Corrección |
|---|---|---|---|
| 8 | Invalid write of size 4 … 0 bytes after a block of size 20 alloc'd | el for llega a i <= n: escribe notas[5], fuera del bloque | i < n |
| 20 | Conditional jump or move depends on uninitialised value(s) | suma sin inicializar. El error sale en el if, no en el += | int suma = 0; |
| 15 | 16 bytes in 1 blocks are definitely lost | nunca se libera nombre | free(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.
Lo que memcheck no ve
Sección titulada «Lo que memcheck no ve»- Desbordes en el stack o en globales.
memcpy1.ccopia 1024 bytes a un arreglo local de 20 y memcheck reporta 0 errores. Memcheck conoce los límites de cada bloque del heap porque interceptamalloc, pero no los de cada arreglo del stack. Para eso existe AddressSanitizer (gcc -fsanitize=address) o-D_FORTIFY_SOURCE=2.
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.
helgrind: condiciones de carrera
Sección titulada «helgrind: condiciones de carrera»Dos hilos incrementan un contador global 1000 veces cada uno (race.c):
== sin lockPossible 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 = 2000ERROR SUMMARY: 0 errors from 0 contextshelgrind 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.
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.
perf: contar page faults
Sección titulada «perf: contar page faults»La guía pide:
getconf PAGESIZE # 4096gcc -O0 arreglo.c -o arreglo # sin optimizaciones, para que el recorrido no desaparezcasudo perf stat -e page-faults ./arregloperf 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 respondidass = malloc(64); s = NULL; produce…📈 ¿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)