Measure Sanitizer Coverage with sancov-20
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 that you can rebuild with Clang, debug information in that executable, and a test command that exercises it. You do not need root privileges. Keep the instrumented build separate from a release build: coverage instrumentation changes the binary and can affect its runtime cost.
This guide uses the AddressSanitizer runtime to write coverage data. That is a convenient local test setup, not a claim that coverage results are a substitute for memory-safety testing. Do not run unknown binaries or coverage inputs on a machine where they can access sensitive data. A coverage file is data, but sancov-20 also reads the corresponding executable, so use an isolated working directory for untrusted material.
1. Check the installed tool
Start by confirming that the versioned executable is the one on your path. The package on the machine used for this guide reports LLVM 20.1.8. 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 useful 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 useful checkpoint 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 in the same way. Each process can create its own file, so do not assume that one test run produces one report. Preserve all files 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 and 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. That makes this action useful in a script, but it is 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 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 cause processing to fail.
The -demangle option makes C++ names readable and is the default form to try. Use -no-demangle when you specifically need linker-level names. -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. The older HTML action is explicitly marked as removed by sancov 20. Do not use -html-report; the documented replacement is to run -symbolize and use the LLVM coverage report server separately. That server 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 report deliberately or use the installed
-mergeaction after checking its inputs. Keep the originals until the merged result has been verified.
Done means
sancov-20 --versionreports the expected LLVM 20 installation.- Your instrumented test run creates a
.sancovfile. -print-coverage-statsreads the report with its matching binary.- You can identify covered functions and understand why HTML output is not a sancov 20 action.