Measure Sanitizer Coverage with sancov-20
sancov-20 turns raw AddressSanitizer coverage data into a summary you can actually read: which functions your test run reached, and which it did not. You will finish with a real .sancov file, a coverage summary, and a list of functions reached by your test run. The examples use the LLVM 20 tool shipped in Debian or Ubuntu as clang-tools-20. Allow roughly 15 minutes if Clang is already installed.
The route
Jump straight to the step you need, or tick off Done means at the end.
Before you start
- You need: an ELF executable you can rebuild with Clang, debug information in that executable, and a test command that exercises it.
- No root needed.
- Keep builds separate. Coverage instrumentation changes the binary and can affect its runtime cost, so keep the instrumented build away from a release build.
This guide uses the AddressSanitizer runtime to write coverage data. That is a convenient local test setup, not a claim that coverage results substitute for memory-safety testing. Do not run unknown binaries or coverage inputs on a machine where they can reach sensitive data. A coverage file is just data, but sancov-20 also reads the matching executable, so use an isolated working directory for anything untrusted.
1. Check the installed tool
Start by confirming the versioned executable is the one on your path. The package on the machine used for this guide reports LLVM 20.1.8. Here is the naming quirk worth knowing: the manpage calls the program sancov, but the installed command is named sancov-20.
$ command -v sancov-20
/usr/bin/sancov-20
$ sancov-20 --version
Ubuntu LLVM version 20.1.8
Optimized build.
$ dpkg-query -W -f='${Package} ${Version}\n' clang-tools-20
clang-tools-20 1:20.1.8~...
The package revision after the LLVM version is distribution-specific, so your final line may differ. Check the action list too:
$ sancov-20 --help
Checkpoint
The local version must list -print-coverage-stats, -covered-functions, -not-covered-functions, -print, -print-coverage-pcs, -merge and -symbolize. It also says that an action is required.
2. Build an instrumented test binary
For a quick smoke test, save this small C++ program as cov.cc. It has one branch, which makes it easy to see the difference between running the program with and without an argument.
#include <cstdio>
void branch(bool take) {
if (take) {
std::puts("taken");
}
}
int main(int argc, char **argv) {
branch(argc > 1);
return 0;
}
Compile with debug information, AddressSanitizer, and the trace-pc-guard SanitizerCoverage mode:
$ clang++-20 -g cov.cc \
-fsanitize=address \
-fsanitize-coverage=trace-pc-guard \
-o cov
The debug information matters later: raw program counters are fine for a quick count, but symbolization needs the matching binary and its debug data. Do not strip or replace cov before processing its coverage files.
3. Generate a coverage file
Set ASAN_OPTIONS=coverage=1 for the process that runs the instrumented executable. The runtime writes one file with a name like cov.12345.sancov in the current directory by default; the digits are a process ID and will change:
$ ASAN_OPTIONS=coverage=1 ./cov example
taken
SanitizerCoverage: ./cov.12345.sancov: 3 PCs written
$ ls -l ./*.sancov
-rw-r----- 1 you you ... ./cov.12345.sancov
Your PC count and file size can differ with the compiler, optimisation settings, target architecture and source. The checkpoint that matters is that the command exits successfully and a new .sancov file appears beside the binary. If you prefer a separate directory, the Sanitizer runtime accepts ASAN_OPTIONS=coverage=1:coverage_dir=/path/to/coverage.
Run your real test suite the same way. Each process can create its own file, so do not assume one test run produces one report; preserve every file until you have merged or archived the results you need.
4. Read the raw coverage
The action comes immediately after the options and before the input files. For coverage statistics, pass the instrumented binary first, then the matching raw report:
$ sancov-20 -print-coverage-stats ./cov ./cov.12345.sancov
all-edges: 4
cov-edges: 3
all-functions: 2
cov-functions: 2
The exact counts depend on the build and the paths taken. all-edges and all-functions describe what the binary contains; the cov- values describe what this report reached, which makes it useful in a script but not a source-level percentage.
To see the recorded addresses instead, use -print. To inspect the instrumentation points present in the binary, use -print-coverage-pcs. The manpage assigns that latter action to coverage-instrumented binaries, while -print-coverage is for raw .sancov files.
5. Inspect covered and missed functions
Use the function actions when a count is not enough. They take the binary and report together:
$ sancov-20 -covered-functions ./cov ./cov.12345.sancov
/work/example/cov.cc:3 branch(bool)
/work/example/cov.cc:9 main
$ sancov-20 -not-covered-functions ./cov ./cov.12345.sancov
Paths, line numbers and the set of missed functions are all build-dependent. Treat the output as a diagnostic for this exact executable and report pair: rebuilding cov, changing optimisation, or passing a report from another binary can make the addresses meaningless, or make processing fail outright.
-demanglemakes C++ names readable, and is the default form to try.-no-demanglegives linker-level names when you specifically need them.-strip_path_prefix=/work/example/can shorten paths in reports; verify the prefix matches the paths embedded in your build.
6. Symbolize when you need source locations
Raw reports contain executed offsets, not a complete source coverage report. The installed manpage documents -symbolize as producing a symbolized JSON report from a binary report. Give it the raw report and the matching executable:
$ sancov-20 -symbolize ./cov.12345.sancov ./cov > ./cov.12345.symcov
$ head -n 5 ./cov.12345.symcov
The output format is machine-readable. Keep it with the exact binary used for symbolization. Here is the fact that trips people up: the older HTML action is explicitly marked as removed by sancov 20. Do not use -html-report; the documented replacement is running -symbolize and using the LLVM coverage report server separately, which is not part of the action list printed by this installed command.
Common traps
- No
.sancovfile: check that the executed program was built with SanitizerCoverage and thatASAN_OPTIONS=coverage=1was applied to the child process, not just to an unrelated shell command. - Symbolization looks wrong: use the same unstripped executable that produced the report. A report from a different build is not interchangeable.
- The HTML action fails: that is expected in this version. Use
-symbolizeinstead. - Paths are noisy: add
-strip_path_prefix=<string>with a real prefix, or configure a stable build directory before collecting data. - Several reports exist: process each one deliberately, or use the installed
-mergeaction after checking its inputs. Keep the originals until the merged result has been verified.
Done means
- Version confirmed:
sancov-20 --versionreports the expected LLVM 20 installation. - File produced: your instrumented test run creates a
.sancovfile. - Stats read:
-print-coverage-statsreads the report with its matching binary. - Functions identified: you can identify covered functions and know why HTML output is not a sancov 20 action.