Pure-reference catalog of code-coverage strategy for embedded C/C++: the criteria hierarchy (statement / branch / decision / condition / MC/DC), the gcov toolchain (.gcno/.gcda, --coverage), the LLVM source-based toolchain (llvm-profdata / llvm-cov), host-build vs QEMU-build instrumentation, MISRA-C:2012 and DO-178C structural-coverage expectations by safety level (DAL A maps to MC/DC), and report-format choices. Use when choosing what structural-coverage level to require and wiring gcov / llvm-cov into the build; physical .gcda retrieval from hardware is in hardware-in-loop-reference, QEMU machine flags in qemu-system-test-runner, and to author the embedded tests themselves use googletest-embedded-arm or unity-test-framework-c.
74
93%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
This skill is a pure reference consumed by the per-tool
skills (googletest-embedded-arm,
unity-test-framework-c,
ceedling-build-runner,
qemu-system-test-runner)
and by the HIL reference
(hardware-in-loop-reference).
-fcoverage-mcdc).-O0 (--coverage for gcc; -fprofile-instr-generate -fcoverage-mapping for clang) - never optimise a coverage build.lcov.info as the durable artefact, render HTML for the developer, and gate the PR on changed-line coverage - see Coverage gates that work.The full gcov-to-HTML path for a single module is in Worked example below.
The standard hierarchy, from weakest to strongest. ISTQB glossary terms (cite by stable term ID; the glossary is a JS SPA and not WebFetchable):
| Criterion | What it requires | ISTQB term ID |
|---|---|---|
| Statement coverage | Every executable statement executed at least once | "statement coverage" |
| Branch coverage | Each branch of every decision point taken in both directions | "branch coverage" |
| Decision coverage | Each decision (e.g. if (x)) evaluated both true and false | "decision coverage" |
| Condition coverage | Each atomic boolean condition in a compound decision evaluated both true and false | "condition coverage" |
| MC/DC (Modified Condition/Decision Coverage) | Each condition independently affects the decision outcome | "modified condition decision coverage" |
| Multiple-condition coverage | All combinations of conditions in a decision exercised | "multiple condition coverage" |
The escalation matters because higher criteria find different
defect classes - branch coverage finds an unreached else;
MC/DC finds a short-circuit-evaluated condition whose change
never alters the decision.
Per gcc.gnu.org/onlinedocs/gcc/Invoking-Gcov.html:
Build the program (or test harness) with --coverage - a
convenience alias that "tells the compiler to generate additional
information needed by gcov (basically a flow graph of the
program) and also includes additional code in the object files
for generating the extra profiling information" (per the GCC
gcov invocation page).
arm-none-eabi-gcc --coverage -O0 -g \
main.c parser.c tests.c -o test_binary
# Equivalent to: -fprofile-arcs -ftest-coverage -lgcovCompilation produces a .gcno file (flow-graph) per source file
"at compile time". Running the binary produces a .gcda file
per source file with the accumulated execution counts (both per
the GCC docs).
./test_binary # writes parser.gcda etc.
gcov -b -c parser.c # produces parser.c.gcov text reportThe full gcov flag table and the .gcov annotation sentinels
(##### for unexecuted, - for non-executable) are in
references/gcov-flag-reference.md.
For HTML reports + CI integration, post-process with
lcov/genhtml:
lcov --capture --directory . --output-file coverage.info
genhtml coverage.info --output-directory coverage-html/LCOV's .info is the de-facto interchange format consumed by
Codecov, Coveralls, SonarQube. (LCOV is not GCC; it is a
separate Linux Test Project tool that wraps gcov.)
Per clang.llvm.org/docs/SourceBasedCodeCoverage.html:
clang --target=arm-none-eabi \
-fprofile-instr-generate -fcoverage-mapping \
-O0 -g main.c parser.c tests.c -o test_binaryThe two flags are independent: -fprofile-instr-generate
enables "instrumentation based profiling"; -fcoverage-mapping
emits the mapping that "describes the mapping between the source
ranges and the profiling instrumentation counters" (per the
LLVM Coverage Mapping Format docs at
llvm.org/docs/CoverageMappingFormat.html).
For MC/DC, add -fcoverage-mcdc per the Clang docs.
Running the binary writes default.profraw in the current
directory (or to the path in LLVM_PROFILE_FILE, with pattern
strings %p for PID, %h for hostname, %Nm for merge-pool -
per the Clang docs).
LLVM_PROFILE_FILE="raw/%p.profraw" ./test_binary
llvm-profdata merge -sparse raw/*.profraw -o test.profdata-sparse "produces smaller indexed profiles" (per Clang docs).
llvm-cov show ./test_binary -instr-profile=test.profdata \
-format=html -output-dir=cov-html/
llvm-cov report ./test_binary -instr-profile=test.profdata
llvm-cov export ./test_binary -instr-profile=test.profdata \
-format=lcov > coverage.infoshow emits per-line annotations, report emits the
file-level summary table, export -format=lcov produces a file
compatible with the gcov-flavoured LCOV .info consumed by
Codecov / SonarQube. (Per the same Clang Source-Based Code
Coverage page.)
The LLVM coverage-mapping format (per llvm.org/docs/CoverageMappingFormat.html) distinguishes:
#ifdef branches not taken at compile time).-fcoverage-mapping).This region taxonomy is why llvm-cov can show separate counts inside macro expansions - gcov cannot.
A practical trade-off for embedded teams:
| Approach | Coverage accuracy | Cost | Notes |
|---|---|---|---|
| Host build (same source, x86_64 toolchain, no MCU) | Misses MCU-specific paths | Lowest | Default for Ceedling per throwtheswitch.org/ceedling; use when business logic dominates |
| QEMU system emulation | Catches arch-specific paths (endianness, alignment) | Medium | See qemu-system-test-runner; reports written to host filesystem via virtio / semihosting |
| On-target with semihosting | Highest fidelity | Highest (flash space, RAM for counters) | .gcda files written back via semihosting; needs ARM --specs=rdimon.specs (librdimon is the gcc-arm-none-eabi semihosting library - see developer.arm.com toolchain docs) |
| On-target with file-system shim | High | High | Counters streamed over UART / SWO; host re-assembles .gcda |
For most safety-critical projects, the standard recipe is host build for fast loop, QEMU build for arch sanity, on-target build for the certification artefact.
gcov branch coverage on one module, host build (the fast-loop default from the instrumentation table).
# 1. instrument + build on the host at -O0 (writes parser.gcno at compile time)
gcc --coverage -O0 -g parser.c parser_test.c -o parser_test
# 2. run - writes parser.gcda next to parser.gcno
./parser_test
# 3. branch counts as a text report
gcov -b -c parser.c # -> parser.c.gcov (scan for ##### lines)
# 4. interchange artefact + HTML for the team
lcov --capture --directory . --output-file coverage.info
genhtml coverage.info --output-directory coverage-html/
# 5. gate the PR on coverage.info branch totalsThe host loop is fastest, but it misses MCU-specific paths - pair it with at least one QEMU or on-target run before claiming the number for the MCU (see the instrumentation table and the "coverage measured on host then claimed for the MCU" anti-pattern below).
Coverage targets are set by the project's safety standard, not by taste. DAL A / ASIL D demand MC/DC; DAL B demands decision coverage; DAL C / ASIL A demand statement coverage; MISRA-C:2012 and IEC 62304 Class C prescribe no numeric target but expect a structural-coverage justification. The full per-standard table (DO-178C DAL A-D, ISO 26262 ASIL A-D, MISRA-C:2012 §8, IEC 62304) is in references/safety-standards-and-formats.md. Treat every listed level as the floor, not a turnkey recipe.
Produce lcov.info as the durable artefact, render HTML for the
developer, and gate on the .info totals. gcov also emits text
.gcov and .gcov.json.gz; llvm-cov emits .profdata, HTML,
and lcov / JSON exports; gcovr --xml produces Cobertura for
Jenkins. The full producer / consumer matrix is in
references/safety-standards-and-formats.md.
| Gate | Why |
|---|---|
Per-file branch coverage minimum (e.g. 80% per file, 90% per function with __attribute__((critical))) | Catches unreviewed new code without holding back legacy files |
| No regressions on changed lines | PR-scoped; lets the absolute number drift down only for code not touched |
MC/DC on annotated decisions (clang -fcoverage-mcdc + a _MCDC decorator) | Targets the cost where the standard demands it |
A flat "global ≥85% branch" gate is the failure mode - it penalises the team for unreviewed legacy code and rewards removing tests for hard-to-cover error paths.
| Anti-pattern | Why it fails | Fix |
|---|---|---|
Optimising the test build with -O2 | gcov / llvm-cov measure post-optimisation flow; branches collapse | Use -O0 for the coverage build per GCC gcov docs guidance |
| Mixing gcov and llvm-cov artefacts | .gcno and .profraw come from different toolchains; tools can't merge them | Pick one toolchain per build; document why |
| Coverage of test code counted as product coverage | Inflates numbers | Exclude tests/, mocks/, framework/ directories in lcov / llvm-cov filter |
| MC/DC reported from gcov | gcov doesn't measure MC/DC | Use clang -fcoverage-mcdc per the Clang Source-Based Coverage page; gcov gives at best condition coverage via -b |
| Coverage measured on host then claimed for the MCU | Endianness / alignment / weak-symbol paths uncovered | Pair host coverage with at least one QEMU or on-target run |
| Counters compiled but never written back from MCU | .gcda missing; gcov sees only the .gcno flow-graph and reports 0% | Implement _write / _exit semihosting hook; ARM --specs=rdimon.specs per developer.arm.com GNU toolchain |
else branch.&& / || - these compile to two branches;
the count attributes to the source line, not the individual
operand. For per-operand visibility, use clang
-fcoverage-mcdc per the Clang docs..gcda files accumulate across runs. Re-run without
deleting them and counters keep climbing. Use
__gcov_reset() (provided by libgcov) to zero between
scenarios - per the gcov source.gcov-tool overlap for saturated counts.Cited inline above. Foundational documents:
--specs=rdimon.specs semihosting) -
developer.arm.com/Tools and Software/GNU Toolchain.