Debugging and analysis

Memory leaks and crashes: the analysis tools for each OS

AddressSanitizer, Valgrind, Instruments, Application Verifier, WinDbg and heaptrack: find leaks, memory corruption and crashes on Windows, macOS and Linux.

At a glance

ToolPlatformsLicenceBest for
AddressSanitizer
  • Windows
  • macOS
  • Linux
Apache-2.0 with LLVM exception Compiler instrumentation in Clang, GCC and MSVC that stops at the first out-of-bounds or use-after-free access.
Valgrind (Memcheck)
  • Linux
GPL-2.0 Runs unmodified binaries on a synthetic CPU and reports leaks and invalid memory use — slow but thorough.
heaptrack
  • Linux
LGPL-2.1 Heap profiler that records every allocation with its call stack, far faster than Valgrind's Massif.
Instruments and leaks
  • macOS
Free with Xcode Apple's Leaks and Allocations instruments, plus the leaks command for scripted checks.
Application Verifier
  • Windows
Free (Windows SDK) Turns on heap, handle and lock checks inside a running process so that latent bugs crash early and loudly.
WinDbg and ProcDump
  • Windows
Free Capture crash dumps in the field with ProcDump, analyse them with WinDbg's !analyze.

Functional tests tell you that the application gives the right answer. They say nothing about the buffer it overran on the way, the handle it never closed or the megabytes it leaks every hour. Those defects surface as crashes on a customer's machine, weeks later, on one operating system only. Each system has excellent tools to catch them earlier — here is the map.

Start with sanitizers, everywhere

AddressSanitizer (ASan) is the best value in native-code testing. The compiler instruments every memory access; at run time the first invalid access stops the program with a precise report — what was accessed, where it was allocated and where it was freed. The slowdown is typically around 2×, small enough to run the whole test suite under it.

ToolchainHow to enable ASan
Clang or GCC (Linux, macOS)-fsanitize=address -fno-omit-frame-pointer -g
XcodeScheme → Test → Diagnostics → Address Sanitizer
MSVC (Visual Studio 2019 16.9 and later)/fsanitize=address with /Zi
CMake, any platformAdd the flags to a dedicated asan build preset
clang++ -fsanitize=address,undefined -fno-omit-frame-pointer -g -O1 tests.cpp -o tests
ASAN_OPTIONS=detect_leaks=1 ./tests

The companion sanitizers are worth a build each: UndefinedBehaviorSanitizer (signed overflow, misaligned access), and on Linux and macOS ThreadSanitizer for data races. ThreadSanitizer cannot be combined with ASan in the same binary, hence one CI job per sanitizer.

Linux: Valgrind and heaptrack

Valgrind's Memcheck needs no recompilation: it runs the unmodified binary on a synthetic CPU and checks every byte. It is slow — often 20 to 50 times — but it finds uninitialised reads that ASan does not, and it works on third-party binaries you cannot rebuild.

valgrind --leak-check=full --show-leak-kinds=definite --error-exitcode=1 ./my-app --self-test

--error-exitcode=1 is what turns Valgrind into a CI gate: the job fails as soon as an error is reported. For memory that is not leaked but still grows — caches without bounds, lists that never shrink — heaptrack records every allocation with its stack and shows where the peak comes from, at a fraction of Valgrind's cost.

heaptrack ./my-app --replay session.log
heaptrack_gui heaptrack.my-app.*.zst

macOS: Instruments and the command line

Xcode's Instruments ships the Leaks, Allocations and Zombies templates. Allocations' generation marking is particularly effective: mark a generation, repeat a user action ten times, mark again — anything that survives every cycle is a leak or an unbounded cache.

For automated checks, the leaks command inspects a live process or launches one, and returns a non-zero exit status when it finds leaks:

MallocStackLogging=1 leaks --atExit -- ./my-app --self-test
xcrun xctrace record --template 'Leaks' --launch -- ./my-app

Windows: Application Verifier, WinDbg and dumps

Application Verifier changes the rules inside a running process: heap blocks get guard pages, handles are checked on every use, critical sections are validated. A latent bug that would corrupt memory silently now breaks immediately, inside the debugger, at the guilty line.

appverif /verify MyApp.exe /faults
# run the test suite under a debugger, then reset:
appverif /n MyApp.exe

For crashes on machines you do not control, collect a dump. ProcDump from Sysinternals writes one when the process crashes or hangs, and WinDbg opens it with symbols resolved from your symbol server:

procdump -ma -e -x C:\dumps MyApp.exe
# then, in WinDbg:
# !analyze -v

Keep your PDB files for every released build. A dump without matching symbols tells you that the program crashed, not where.

Managed runtimes have their own tools

In .NET, Java or Node.js, leaks are references kept alive rather than memory never freed. Use the runtime's tools on all three systems: dotnet-counters and dotnet-gcdump for .NET, Java Flight Recorder and heap dumps for the JVM, and heap snapshots in the Chrome DevTools for Node.js and Electron.

Putting it into CI

  1. Add an ASan + UBSan build of the test suite on every pull request, on each OS where your compiler supports it.
  2. Add a ThreadSanitizer job on Linux if your code is multi-threaded.
  3. Run Valgrind nightly on Linux over the slowest, most thorough tests.
  4. Configure crash dump collection in your test environments so that a crash during a UI test leaves something to analyse.
  5. Treat a sanitizer report as a failing test — never as a warning to read later.

Run these jobs on every system in your CI matrix: memory bugs love platform-specific code paths, and the allocator on each system hides different mistakes.

  • memory leaks
  • sanitizers
  • valgrind
  • instruments
  • windbg
  • crash dumps