Measure C Test Coverage with GCC 13's gcov
You will compile a small C program with GCC coverage instrumentation, run it, and produce a readable report showing which lines and branches your test exercised. The workflow keeps generated files in a disposable directory and uses the installed GCC 13.3.0 tools on this machine.
The route
Jump straight to the step you need, or tick off Done means at the end.
Allow about fifteen minutes. You need a shell, GCC, a C source file, and a test command that can run the instrumented program. No elevated privileges are needed. Coverage data can contain source paths and test behaviour, so treat the generated files as project data rather than sending them to an untrusted service.
Checkpoint
The finished result is a .gcov listing plus optional branch and JSON reports. The report is evidence about the test run, not proof that the program is correct.
1. Confirm the installed toolchain
Check the version and the target-prefixed executable before starting. This avoids mixing data generated by one GCC installation with a different gcov implementation:
$ gcc --version | head -n 1
gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0
$ x86_64-linux-gnu-gcov-13 --version | head -n 1
gcov (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0
The unqualified gcov, gcov-13, x86_64-linux-gnu-gcov and x86_64-linux-gnu-gcov-13 names refer to the same GCC 13 coverage tool in this installation. Use the explicit target-prefixed name in scripts when the compiler target matters.
2. Create an isolated coverage build
Make a new directory and enter it. The directory will receive object files, runtime counters and reports:
$ mkdir -p /tmp/gcov-example
$ cd /tmp/gcov-example
$ printf '%s\n' 'temporary coverage files live here'
For a real project, use a build directory ignored by version control. Do not run gcov from a random directory and then search your source tree for unfamiliar generated files.
Save this minimal program as decision.c. It has one branch that a single test input will leave partly uncovered:
#include <stdio.h>
static int classify(int value)
{
if (value > 0)
return 1;
return 0;
}
int main(void)
{
printf("%d\n", classify(4));
return 0;
}
Keep one principal statement per line while investigating coverage. gcov reports at line level, so several statements on one line make the result harder to interpret. The tool also reports less usefully when optimisation folds or combines code. For a coverage run, prefer a debug build without optimisation.
3. Compile with coverage instrumentation
Compile and link with GCC's --coverage option. It enables the extra notes and runtime counters required by gcov:
$ gcc --coverage -O0 -g decision.c -o decision
$ ls decision decision.gcno
decision decision.gcno
The compiler creates a .gcno file containing the coverage structure. The running program will later create .gcda data beside the object or executable. These are generated artefacts, not source files to edit.
Checkpoint
If decision.gcno is missing, stop here. Check that GCC accepted --coverage and that you are looking in the directory containing the object file. Running gcov before instrumentation exists cannot produce a useful report.
4. Run the tests that should count
Run the instrumented executable with the same test command you use for the build. This example prints one value and writes the counter data:
$ ./decision
1
$ test -s decision.gcda && echo 'coverage data written'
coverage data written
The .gcda file is updated when the program exits normally. A crash, forced termination or unwritable output directory can leave no data or incomplete data. Remove old counters before a clean comparison only when you deliberately want to discard the previous run:
$ rm -f decision.gcda decision.c.gcov decision.gcov.json.gz
Warning
That command permanently removes the selected generated reports and counters. It does not change the source or executable, but do not use it if the counters are the only record of an earlier test run. Re-run the instrumented tests to recreate them.
5. Generate and read the line report
Run gcov with the source file name from the same working directory used for compilation:
$ x86_64-linux-gnu-gcov-13 decision.c
File 'decision.c'
Lines executed:85.71% of 7
Creating 'decision.c.gcov'
The exact percentage depends on the source and compiler details. Open the report and look at its colon-separated fields:
$ sed -n '1,12p' decision.c.gcov
-: 0:Source:decision.c
1: 1:#include <stdio.h>
1: 3:static int classify(int value)
-: 4:{
1: 5: if (value > 0)
1: 6: return 1;
#####: 7: return 0;
Each normal line is shown as execution count, source line number and source text. A hyphen means the line has no executable code. A row marked ##### is reachable but was not executed. With this test, the negative return path is the useful gap: add a test that calls classify with zero or a negative value, then rerun the program and gcov.
gcov sums contributions when you pass several input files. Usually pass the same source list represented by the final link. Do not assume that a report from one executable describes another executable built from stale object files.
6. Add branch and function detail
Use --branch-probabilities for branch frequencies and --branch-counts when counts are more useful than percentages. --function-summaries adds a summary for each function:
$ x86_64-linux-gnu-gcov-13 --branch-probabilities --branch-counts --function-summaries decision.c
File 'decision.c'
Lines executed:85.71% of 7
Branches executed:100.00% of 2
Taken at least once:50.00% of 2
Creating 'decision.c.gcov'
Exact summary wording varies with the input and installed build. The important distinction is that line coverage can look healthy while one side of a conditional remains untested. Use the branch section in the .gcov file to choose the next test, rather than treating a single percentage as a release gate.
7. Produce machine-readable output when needed
For tooling, use --json-format. GCC 13 writes a gzip-compressed file ending in .gcov.json.gz and includes compiler, working-directory, function, line and optional branch data:
$ x86_64-linux-gnu-gcov-13 --json-format decision.c
$ gzip -cd decision.gcov.json.gz | head -c 160
{"format_version": "1", "gcc_version": "13.3.0", "current_working_directory": "/tmp/gcov-example", "data_file": "decision.c"}
Do not build a parser around the order or number of preamble fields. The GCC 13 manual says that this preamble can grow; identify fields by their names. Store the JSON beside the matching executable and source revision.
Common traps
- No data file: check that the instrumented executable ran and could write beside its object files.
- Source not found: run gcov from the compiler's working directory, or use
--object-directoryfor the directory containing the data files. - Unexpected filenames:
--preserve-pathskeeps directory information, while--hash-filenamesavoids filesystem-length problems when that mode creates long names. - Misleading totals: confirm that old
.gcdafiles are not being accumulated from unrelated test runs. - Missing headers in the report: gcov can report source or header files containing instrumented code; use
--long-file-nameswhen included files need distinct output names.
Done means
- The compiler and gcov versions are known and match the intended build.
- The executable was built with
--coverageand produced a.gcdafile after the test run. - The line report was generated from the correct working directory and inspected for unexecuted lines.
- Branch detail was checked where condition outcomes matter.
- Generated files are isolated, reproducible and either ignored or removed deliberately after review.