---
title: Valgrind and Sanitizers in Practice
url: https://doc.liz6.com/en/systems-programming/08-performance-and-debugging/02-valgrind-and-sanitizers-in-practice
locale: en
area: systems-programming
tags:
- systems-programming
- performance-and-debugging
date: 2026-06-30
modified: 2026-07-16
description: 'Coverage: Valgrind (memcheck/helgrind/cachegrind/massif) → ASAN/UBSAN/TSAN → Comparison and Selection → Practical Debugging Cases Applicable to: Linux user-space, C/C++'
---

# Valgrind and Sanitizers in Practice

> Coverage: Valgrind (memcheck/helgrind/cachegrind/massif) → ASAN/UBSAN/TSAN → Comparison and Selection → Practical Debugging Cases
> Applicable to: Linux user-space, C/C++

## Valgrind: Dynamic Binary Translation

Valgrind is essentially a JIT compiler—it translates the program's machine code into an intermediate representation (VEX IR), instruments it for analysis on the IR, and then translates it back to machine code for execution. There is no need to recompile the target program, but the performance overhead is significant.

```bash
# Memory leaks + out-of-bounds detection:
valgrind --leak-check=full ./prog arg1 arg2

# Trace leak origins:
valgrind --leak-check=full --track-origins=yes ./prog

# Output interpretation:
#   ==12345== Invalid read of size 4          ← Out-of-bounds / use-after-free
#   ==12345==    at 0x4005A4: main (test.c:5)  ← Location where it occurred
#   ==12345==  Address 0x5204040 is 0 bytes after a block of size 40 alloc'd
#   ==12345==    at 0x4C2E80F: malloc (in /usr/lib/valgrind/vgpreload_memcheck.so)
#   ==12345==    by 0x40058F: main (test.c:3)  ← Allocation location
```

### Valgrind Tool Suite

```bash
# memcheck (default): Memory errors
#   Detects: UAF, double free, out-of-bounds, uninitialized values, memory leaks

# helgrind: Data races
valgrind --tool=helgrind ./prog

# cachegrind: CPU cache simulation
valgrind --tool=cachegrind ./prog
cg_annotate cachegrind.out.<pid>  # Source-level miss rate

# massif: Heap profiling
valgrind --tool=massif ./prog
ms_print massif.out.<pid>  # Timeline: who allocated how much memory
```

### Valgrind Limitations

```
Overhead:
  memcheck: ~20-30x slower
  helgrind: ~10-20x slower
  cachegrind: ~20-50x slower
  massif: ~10x slower

Not supported:
  - Statically linked programs (ld.so and libc must be replaceable)
  - Some uncommon system calls
  - Some instruction set extensions (partial support for AVX-512)
```

---

## Sanitizers: Compile-time Instrumentation

Sanitizers insert check code at compile time → detect errors at runtime. They are much faster than Valgrind but require recompilation.

```bash
# ASAN: Heap overflow, UAF, stack overflow, global overflow
gcc -fsanitize=address -g -O1 -o prog prog.c
./prog
# → On error: prints exact location + allocation location + deallocation location (colorful!)

# UBSAN: Undefined behavior
gcc -fsanitize=undefined -o prog prog.c
# Detects: integer overflow, shift out-of-bounds, null dereference, type alignment errors

# TSAN: Data races (64-bit only)
gcc -fsanitize=thread -g -O1 -o prog prog.c
# → Reports: "data race on variable x between thread T1 and T2"

# LSAN: Memory leaks (included in ASAN)
ASAN_OPTIONS=detect_leaks=1 ./prog

# MSAN: Uninitialized memory reads (Clang only)
clang -fsanitize=memory -g -o prog prog.c
```

### ASAN Principles

```
ASAN uses shadow memory:
  1 byte of shadow memory per 8 bytes of application memory
  Shadow values: 0=fully addressable, 1-7=partially valid, negative=unaddressable

Every memory access: compiler inserts shadow checks
  → If shadow value says unaddressable → ASAN report
  → "Red zones" placed around allocations → Out-of-bounds detection
  → "Quarantine" placed after deallocation → UAF detection

Memory overhead: ~2x (shadow memory + red zones + quarantine)
```

### Valgrind vs Sanitizers

| | Valgrind | Sanitizers |
|---|---|---|
| Requires recompilation | No (any binary) | Yes (-fsanitize=) |
| Speed | 20-50x slower | 1.5-5x slower |
| Memory overhead | No extra memory | 2x (ASAN) |
| Detection accuracy | High (binary translation sees everything) | High (compile-time instrumentation) |
| Multi-thread detection | helgrind | TSAN |
| Use cases | Black-box without source code | With source code, integrated into CI |

---

## Practical Cases

```bash
# Case 1: Heap overflow → ASAN
#   test.c:4: buf[100] = 0;  // buf is char[100]
gcc -fsanitize=address -g -o test test.c
./test
# ==ERROR: AddressSanitizer: heap-buffer-overflow
#     WRITE of size 1 at 0x602000000064 thread T0
#     #0 0x4007b3 in main test.c:4

# Case 2: Data race → TSAN
gcc -fsanitize=thread -g -o test test.c
./test
# WARNING: ThreadSanitizer: data race
#   Write of size 4 at 0x7b0400000000 by thread T2:
#     #0 counter_thread test.c:8
#   Previous write of size 4 at 0x7b0400000000 by thread T1:
#     #0 counter_thread test.c:8

# Case 3: Uninitialized read → Valgrind memcheck
valgrind ./prog
# Conditional jump or move depends on uninitialised value(s)
```

## References

- **Documentation**: https://github.com/google/sanitizers, https://valgrind.org/docs/manual
- **LWN**: "Address Sanitizer", "Thread Sanitizer"
- **Tools**: heaptrack (heap profiler, alternative to massif)

*Keywords: Valgrind, memcheck, helgrind, massif, ASAN, UBSAN, TSAN, MSAN, shadow memory, data race*
