GCC Products¶
The default coverage path: a product compiled with GNU GCC on a Unix-like target, counters fetched over the network. Everything on the main Coverage Collection page applies as-is; this page collects the GCC-specific setup.
Build flags¶
Compile and link the product with --coverage (shorthand for
-fprofile-arcs -ftest-coverage plus the gcov link library). Keep the build
tree around: the compiler writes one .gcno (notes) file per object at
build time, and the report step needs them to decode the .gcda counters
the product writes at run time.
CFLAGS += --coverage -O0
LDFLAGS += --coverage
-O0 is not required, but optimized builds fold lines and branches
together and make the line table noticeably harder to read.
Where counters land¶
Each instrumented process writes its .gcda files on exit, by default next
to the build’s object files (the absolute path is baked in at compile time).
Point [coverage].gcda_remote_dir in .otto/settings.toml at that
directory so otto cov get knows where to fetch from — see
the configuration section.
Version matching¶
The gcov that processes the counters must match the GCC major version
that compiled the product — the on-disk gcov record format changes between
releases (GCC 12 notably changed record length encoding, and GCC 14 added a
counter kind). A mismatched gcov fails with “record size mismatch” or
silently reports 0%. otto auto-discovers the right tool from the .gcno
version stamp where it can; see the toolchain resolution order on the main
page.
Cross-compiled products¶
A cross-GCC’s gcov cannot be discovered from the .gcno alone —
configure it per host in lab.json:
{
"element": "target1",
"toolchain": {
"sysroot": "/opt/toolchains/arm-none-eabi",
"gcov": "bin/arm-none-eabi-gcov",
"lcov": "/usr/bin/lcov"
}
}
lcov is a host-side orchestrator, not part of the cross toolchain — point
it at the otto host’s own lcov.
Stale deploys: the .gcno stamp guard¶
Every GCC compilation stamps a fresh 32-bit value into both the .gcno
notes file and the gcov_info struct linked into the binary —
recompiling even an unchanged source produces a new stamp. Counters
are only decodable against the exact compilation that produced them; a
.gcda whose stamp disagrees with the .gcno is refused at collection
(“stamp mismatch with notes file”), which otto surfaces as the
CoverageDataMismatchError described in
Stale Builds: “stamp mismatch” and the e2e base_commit guard.
When the product runs in place, straight out of the build tree, that error and its remedy (re-collect) are all you need. A build-time guard earns its keep when the flow has a ship step — the product is built on one machine and deployed to the target Unix hosts. Rebuild locally after deploying, and every later run of the stale deploy produces undecodable counters; without a guard, the mistake surfaces minutes later at collection instead of at the moment it was made.
The check: every .gcno’s stamp must appear in the binary about to be
shipped. The stamp is the 32-bit word at byte offset 8 of the .gcno;
in the binary it sits inside each gcov_info, following the 4-byte gcov
format-version marker (the .gcno’s bytes 4–8) — 8 bytes after it on
32-bit targets, 16 on 64-bit (the struct’s next pointer plus alignment
padding sit between):
import glob, struct, sys, pathlib
build, binary = pathlib.Path(sys.argv[1]), pathlib.Path(sys.argv[2])
notes = {f: (b[4:8], struct.unpack_from("<I", b, 8)[0])
for f in glob.glob(str(build / "**" / "*.gcno"), recursive=True)
for b in [pathlib.Path(f).read_bytes()]}
blob = binary.read_bytes()
embedded = {struct.unpack_from("<I", blob, i + off)[0]
for ver in {v for v, _ in notes.values()}
for off in (8, 16) # 32-bit / 64-bit gcov_info layouts
for i in range(len(blob) - 24) if blob[i:i + 4] == ver}
stale = {f: s for f, (_, s) in notes.items() if s not in embedded}
if stale:
sys.exit("stamp guard: %s does not embed the stamp of %s "
"— it is stale relative to the notes; re-link before deploying"
% (binary, ", ".join("%s (%#x)" % kv for kv in stale.items())))
print("stamp guard OK: %d notes file(s) match" % len(notes))
Run it as the last step of the build (or the first step of the deploy),
pointed at a build tree containing only the objects that actually link
into the shipped binary — .gcno files from other targets sharing the
tree (unit tests, tools) would false-positive. The embedded flavor of
the same guard — where the ship step is llext load_hex — is on the
embedded page; clang products
need a different approach entirely — see
the clang page.