A firmware instruction trace is not code coverage

Renode will dump every instruction your firmware executed. People see that file and conclude they have code coverage. They do not.
The gap is not a formatting problem. A firmware instruction trace is a list of addresses the core visited. Coverage is a statement about source structure. Getting from the first to the second needs a debug build, a separate post-processing tool, and a DWARF join. Even then, you get line coverage. That is what the tool actually ships. Branch coverage needs a control-flow graph this tool does not build; condition coverage and MC/DC need information a program counter stream never contains at all.
I already split the three unrelated things called trace in RTOS trace in a simulator. This post stays inside the middle one, CPU instruction trace, and asks what that artifact is evidence of.
What is in the file
Renode's execution tracing has four modes, selected on the command that turns it
on: cpu CreateExecutionTracing "tracer_name" @path-to-file <mode>, where mode
is PC, Opcode, PCAndOpcode or Disassembly.1 Output can go out binary
or gzipped. cpu DisableExecutionTracing turns it off.
PC mode gives you addresses and nothing else:
0x20400000
0x20400004
0x20400008
Opcode mode gives you the opposite half, raw opcodes with no addresses:
0x0297
0x1028293
0x30529073
PCAndOpcode pairs them:
0x20400000: 0x0297
Disassembly is the only mode that adds anything a human can read, and it comes
from Renode's own built-in LLVM-based disassembler:
0x20400000: 00000297 auipc t0, 0 [vinit (entry)]
0x20400004: 01028293 addi t0, t0, 16 [vinit+0x4 (guessed)]
The brackets are the tell. [vinit (entry)] and [vinit+0x4 (guessed)] are symbol-name guesses pulled from the ELF symbol table. Function
granularity, with the word "guessed" printed in the output where the tool is
interpolating. No file. No line. Not in the richest of the four modes.
That is the whole payload. Renode does not do source-line mapping, and the docs do not pretend otherwise: they hand that job to a separate downstream tool.

The conflation, dated and quoted
This is not a hypothetical mistake. Renode issue 266 was opened on 2021-11-18 with a reasonable question: "i just curious about when testing with renode using Robot Framework, renode can measure code coverage or not... so, i think renode can do this, it will be really great!!!"2
A maintainer answered honestly: "Tracking execution is something that can be easily done with Renode, but with the abundance of coverage tools out there it's difficult to select just one." Then a contributor pointed at the thing that had just landed: "we added the execution tracer module that can dump executed PCs / opcodes to a separate file."
That exchange is the entire problem in four messages. Someone asks for coverage. The available answer is a PC dump. The reporter's follow-up, "that's a good update. how can i use this from robot test framework?", is the sound of a gap being papered over. The issue got closed, then reopened later by a different person asking the obvious next thing: "how can we get an lcov coverage report from the trace?"
In 2021 that question had no documented answer. The tooling that answers it did not exist until February 2024, three years later.3 Nobody in that thread was confused about their job. The tooling was missing, and a trace file looks close enough to coverage that it kept getting handed over as if it were.
What the real pipeline costs
Antmicro built the missing piece and documented it, and the documentation is refreshingly blunt about the preconditions. "With an execution trace of a properly built binary and a dedicated script, you can create a code coverage report."4 Both qualifiers are load-bearing.
The script "combines data in [the DWARF format] (debug information available in ELF files...) with the number of executions of each instruction counted by Renode." DWARF supplies the address-to-source-line mapping. Renode supplies execution counts per address. Neither half is coverage on its own.
Properly built means what you would expect: "it's recommended to use the -g and
-O0 (or -Og) flags... The -g switch adds debug information to the binary,
specifically code line numbers for each machine instruction. The coverage
functionality requires that information, and you will see an error if it's
missing from the ELF file."
And optimization breaks it, stated plainly in the same doc: "Any optimization done by a compiler may prevent redundant lines of code from being executed. The compiler might also replicate the same code line across several addresses in the program or reorder the execution flow, which will make obtaining legitimate results impossible."
The tool is a separate CLI utility, renode-retracer, installed with pipx from
the Renode tree:
renode-retracer coverage trace.bin.gz \
--binary coverage-sample.elf \
--sources main.c additional.c \
--output coverage.zip \
--export-for-coverview
There is also a fallback for when DWARF is not available: hand the tool a manual
mapping file with --pc2line, lines like 0x800003d8 k210_ops.cpp:252 and
0x80000bb6 main.c:6. That flag is the cleanest proof of the argument. The
address-to-line correspondence is external data fed into the trace. The trace
never carried it.
Antmicro's own writeup says why they took this route rather than the usual one: "The most common method for code coverage analysis has the compiler add additional calls to the coverage infrastructure, meaning the code you're running to get the statistics is not the same as the code you'll be running in production... we leveraged the framework's capabilities to run tests on uninstrumented binaries."3 Their Coverview post repeats the mechanism and names the tradeoff: post-mortem PC-to-line translation "allows you to track line coverage of an executed application without additional instrumentation of the code, which is the typical approach used by tools like gcov."5
Line coverage. That word keeps appearing, and it is not an accident.
I went looking for the branch tag
The docs say "renode-retracer supports generation of line coverage info (DA)".4 That is Antmicro's own sentence and it is the safe thing to quote.
I wanted to know whether the implementation was narrower than the docs, so I read the tool's source. The entire LCOV emission path is one function:
def to_lcov_format(self) -> str:
return f"DA:{self.number},{self.most_executions()}"
That is the whole thing, at commit 091eb1a of the Renode tree.6 DA is
LCOV's line-hit-count tag. renode-retracer emits no BRDA records, so its
LCOV output contains no branch data. No FNDA, so not even function-hit
records. No condition or MC/DC tag, because LCOV's format has nothing for it and
the tool has nothing to put
there. This is my reading of their source rather than a claim they made, but the
ceiling is not a roadmap item. It is a line number and a count.

Three different ceilings
The reason is not that nobody wrote the feature. It is that the information does not exist in the artifact, not entirely, and not in the same way for every coverage kind.
Branch coverage is not hopeless from a PC stream. A tool with the binary's full
control-flow graph can compare consecutive addresses and infer some
taken/not-taken outcomes, which is close to what Ozone does inside its own
engine. renode-retracer does not do that: to_lcov_format emits DA and
nothing else, so branch is a tooling gap here, not a law of physics. Condition
coverage and MC/DC are the harder wall, because no amount of PC-stream analysis
recovers a boolean expression's term structure. That has to come from the
compiler.
Look at how gcov gets branch data. --coverage is a synonym for -fprofile-arcs -ftest-coverage at compile time plus -lgcov at link.7 -ftest-coverage
emits a .gcno notes file holding the static flow-graph structure, generated
while compiling. -fprofile-arcs adds code: "for each function of your program
GCC creates a program flow graph, then finds a spanning tree for the graph. Only
arcs that are not on the spanning tree have to be instrumented: the compiler adds
code to count the number of times that these arcs are executed." Those counters
write a .gcda at program exit.
Two files, two different times, both produced by the compiler. Neither is a byproduct of running an uninstrumented binary under any external tracer. The graph structure that makes an arc count mean something is a compile-time artifact.
Condition coverage needs a third flag on top of that: -fcondition-coverage,
which adds "code so that program conditions are instrumented. During execution
the program records what terms in a conditional contribute to a decision," read
back with gcov --conditions.8 GCC documents this as masking MC/DC directly,
with the independence rule stated as "an outcome is considered covered if it has
an independent effect on the decision." Output looks like condition outcomes covered 1/4 and condition 0 not covered (true) for a line if (total != 45 && v == 1). Path coverage is yet another flag, -fpath-coverage. The pattern holds
at every rung: line, arc, condition, path, each one its own compile-time switch.
LLVM makes the point even harder to argue with. Clang's source-based coverage
"operates on AST and preprocessor information directly," and MC/DC is
-fcoverage-mcdc layered on -fprofile-instr-generate -fcoverage-mapping.9
Clang's definition ties the criterion to decision structure explicitly: MC/DC is
"the percentage of individual branch conditions that have been shown to
independently affect the decision outcome of the boolean expression they
comprise." The implementation limits give it away completely. Maximum 32767
terms in an expression. Test vectors capped at 2,147,483,646. Those are limits
on boolean-expression syntax, a concept with no representation in an address
stream at all. Clang even ships a second, separate gcov-compatible mode that
operates on DebugInfo instead. One compiler, two structurally different coverage
implementations, and neither of them is "read the PC trace."
If you want the criterion itself rather than the plumbing, that is MC/DC for firmware people. One sentence of lineage: MC/DC is the structural coverage objective at DO-178C Level A, which is why it shows up in embedded conversations at all.

The honest version of the claim
I am not saying trace-derived coverage is a scam. Commercial hardware-trace tooling sells it. SEGGER's page for Ozone with J-Trace says exactly that: "Ozone's trace capability can provide accurate information about instruction execution on the target system. This allows a full code coverage analysis of the firmware."10
That is a real product doing real work, and it does not contradict anything above. Ozone performs its own address-to-source mapping and its own analysis of decision structure inside the debugger. The work still happens. It just happens somewhere you do not have to invoke it yourself. Nobody is claiming the raw trace file is the coverage report.
So the precise claim is this. A trace file is not coverage. Every route from a
trace to coverage adds a mapping and analysis step, and that step lives in one of
three places: a separate script, a debugger's internal engine, or the compiler.
Where it lives sets your ceiling. Machine-level branch outcomes can be inferred
from a PC stream plus a reconstructed control-flow graph. renode-retracer
just does not do that. Source-level condition coverage and MC/DC are a harder
wall: they need the compiler to have known about the decision structure before
the binary was built, which no amount of post-hoc PC analysis recovers.
The practical version: if you are pointing at a trace file as coverage evidence,
check what produced it and what tag it emits. If your build has -O2 in it, the
line mapping is already suspect by the tool's own documentation. And if somebody
tells you an emulator gave them MC/DC, ask which flag was on the compile line.
There is exactly one right answer and a trace does not have it. Same reflex as
what cycle-accurate actually means:
the word is doing more work than the tool is.
Sources
Footnotes
-
Renode documentation, "Execution tracing in Renode." Mode list, per-mode output examples, and the
CreateExecutionTracing/DisableExecutionTracingsyntax. https://renode.readthedocs.io/en/latest/execution-tracing/execution-tracing.html ↩ -
Renode issue #266, "Test with renode and code coverage report," opened 2021-11-18, closed and later reopened with the unanswered LCOV question. https://github.com/renode/renode/issues/266 ↩
-
Antmicro, "Introducing code coverage reporting in Renode," 2024-02-27. Rationale for uninstrumented binaries and the custom trace-to-coverage tool. https://renode.io/news/introducing-code-coverage-reporting-in-renode/ ↩ ↩2
-
Renode documentation, "Generating a coverage report." DWARF join, the
-gand-O0/-Ogbuild requirement, the optimization warning,renode-retracerinvocation,--pc2linefallback, and "line coverage info (DA)". https://renode.readthedocs.io/en/latest/execution-tracing/coverage-report.html ↩ ↩2 -
Antmicro, "Renode integration with Coverview for improved code coverage analysis," 2025-06-17. Post-mortem PC-to-line translation, contrasted with gcov. https://antmicro.com/blog/2025/06/renode-integration-with-coverview ↩
-
renode-retracersource,tools/execution_tracer/execution_tracer/coverage.py, at commit091eb1aca70127a5508573b3f452fdf40eca2ce9. Theto_lcov_formatmethod emitsDAonly; the absence ofBRDA,FNDAand any condition tag is my own reading of the codebase, not an Antmicro statement. https://github.com/renode/renode/blob/091eb1aca70127a5508573b3f452fdf40eca2ce9/tools/execution_tracer/execution_tracer/coverage.py#L67 ↩ -
GCC Online Documentation, "Program Instrumentation Options."
--coverage,-fprofile-arcs,-ftest-coverage,-fcondition-coverage,-fpath-coverage, and the spanning-tree arc instrumentation mechanism. https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html ↩ -
GCC Online Documentation, "Invoking Gcov."
gcov --conditionsoutput format and the masking MC/DC independence rule. https://gcc.gnu.org/onlinedocs/gcc/Invoking-Gcov.html ↩ -
Clang documentation, "Source-based Code Coverage." AST-based operation, the separate DebugInfo-based gcov mode,
-fcoverage-mcdc, the MC/DC definition, and the term and test-vector limits. https://clang.llvm.org/docs/SourceBasedCodeCoverage.html ↩ -
SEGGER, "Code Coverage" for Ozone with J-Trace. https://www.segger.com/products/development-tools/ozone-j-link-debugger/technology/code-coverage/ ↩