There is a shortcut almost every embedded team reaches for sooner or later: run your cross-compiler coverage measurement on the host build instead of the target. Compile the same C and C++ sources with the laptop's native compiler, run the unit tests in seconds, and read off statement, decision and MC/DC percentages from a comfortable desktop environment. No flashing, no debug probe, no slow serial link. It feels like the same code, so it ought to be the same coverage.
It is not. The source you wrote is not the program that executes. Between your .c files and the bits that run on the microcontroller sits a cross-compiler whose optimizer rewrites control flow, deletes branches, fuses conditions and inlines functions in ways your host compiler never will. Coverage is measured against that transformed program — so a number gathered on the host can be confidently, quietly wrong about the firmware you are about to certify and ship.
Host coverage tells you how well your tests exercise the program your laptop compiler produced — not the program your cross-compiler will load onto the target. Those are two different binaries, and only one of them flies.
The host-build shortcut
Nobody adopts the host shortcut out of laziness. They adopt it because the target is inconvenient and the host is right there. The reasons are real:
- No hardware required. A developer can build and measure coverage on a CI runner without a board, a programmer, or a JTAG probe attached to it.
- Faster feedback. Native test binaries run far quicker than firmware stepped through a debugger or streamed over a 115200-baud link, so the inner loop stays tight.
- Mature host tooling. Desktop coverage tooling is familiar, scriptable, and easy to wire into a pull-request gate.
- Scarce targets. Hardware-in-the-loop rigs are shared, booked, and sometimes the silicon is still a few weeks from arriving.
All of that is legitimate. The host build is a fine place to develop tests and catch logic bugs early. The mistake is treating the host coverage report as the coverage report — the artifact that goes into the verification record or the certification package. For that, the number has to come from the binary that ships.
What actually changes on the target
The gap between host and target is not a rounding error; it comes from concrete differences in how the two builds are produced. Any one of these can move your numbers:
- A different compiler and optimization level. Your host might use GCC or Clang at
-O0for debuggability, while the firmware ships from IAR, Keil, Green Hills or a TI compiler at-O2or higher. Different optimizers make different transformations. - Intrinsics and inline assembly. Target builds reach for CPU intrinsics, DSP instructions and inline
asmblocks that simply do not exist — or are stubbed out — on the host. - Target-gated
#ifdefbranches. Whole regions of code are guarded by macros like#ifdef STM32or__ARM_FEATURE_DSP. On the host those branches are compiled out; the target compiles a different set in. - Word size and endianness. A 64-bit little-endian laptop and a 32-bit (or 16-bit) target disagree on integer widths, struct padding and byte order, changing which comparisons and casts behave how.
- Floating-point behavior. Hardware FPU versus software emulation, and differing rounding, can flip the result of a comparison and therefore which branch is taken.
- Undefined-behavior differences. Signed overflow, shifts, and aliasing that the host compiler leaves benign may be exploited by the target optimizer to delete code it considers unreachable.
- A different standard library. The target links a small embedded C library — newlib-nano, a vendor runtime — whose
memcpy,printfand math routines have different internal structure than glibc.
Why that changes your coverage numbers
Those differences matter because structural coverage is measured against the program's actual control flow, and optimization reshapes that control flow before it ever reaches the target. The instrumentation counts branches and conditions that exist after the compiler has had its way — not the tidy structure you typed.
- Branches compile away. When the optimizer proves a condition is always true (a configuration constant, a
#define, a value it can fold), it deletes the branch entirely. On the host that branch may survive and demand a test; on the target it does not exist, so there is nothing to cover. - New branches appear. Conversely, loop unrolling, vectorization and peephole transformations can introduce conditional structure the source never had.
- Conditional moves replace branches. The target compiler often turns
if (a) x = b;into a branchless conditional-move instruction. That removes a decision point your host build still reports as a branch to cover. - Short-circuit and MC/DC structure shifts. Reassociation and Boolean simplification of
&&/||chains change how many independent conditions remain, altering the branch and MC/DC obligations the analyzer derives.
The net effect is the dangerous part: host coverage can read a comforting green on paths that do not exist on the target, and can miss paths that do. The percentage looks the same; the meaning has drifted.
| Situation | Host build (-O0) | Target build (-O2) |
|---|---|---|
Config-constant if | Branch present — needs a test | Folded away — no branch |
if (a) x = b; | Reported as a decision | Conditional move — no decision |
| What "100%" means | Tests cover the laptop binary | Tests cover the shipped binary |
Intrinsic / inline asm | Stubbed or absent | Real instructions, real paths |
A green coverage report from the host proves your tests exercise a program that never leaves your desk.
Source coverage vs. object-code coverage
This is also why high-assurance standards care about the distinction. Source-level coverage measures the structure visible in your code; object-code coverage measures the structure in the generated machine code. They diverge exactly because the compiler adds and removes structure during optimization.
Under DO-178C, at Design Assurance Level A, when the compiler introduces control structure that is not traceable to the source, you owe additional object-code verification to show that the generated code's structure has been exercised too. (The same independence reasoning behind MC/DC applies here — see our MC/DC explained for DO-178C guide.) A host build, with a different compiler and a different optimization level, produces different object code — so object-code evidence gathered on the host says nothing certifiable about the firmware that ships.
Use the host to iterate quickly and shake out logic errors. But the coverage evidence in your verification record must come from the same cross-compiler and options that build the airborne software — anything else is measuring a binary that will never fly.
Measuring on the real cross-compiled build
The fix is conceptually simple: measure coverage on the program you actually ship. In practice that means three things.
- Instrument with the real toolchain. Build the instrumented version using the same cross-compiler and the same optimization flags that produce shipped firmware — not a host stand-in, not a debug-only configuration.
- Run where the code runs. Execute your requirements-based tests on the actual target, or on a faithful instruction-set simulator or emulator when hardware is scarce. The coverage data is then collected from real execution of the real binary.
- Get the data back out. On constrained devices that has its own challenges — there may be no file system to write results to. Our companion piece on measuring coverage on targets without a file system covers streaming coverage off bare-metal devices.
None of this means abandoning the host. Develop tests on the host for speed; produce the coverage of record on the target. The two stop disagreeing the moment the report comes from the binary that ships.
How RKTracer keeps host and target honest
This is exactly the problem RKTracer is built to remove. RKTracer instruments your code during compilation, using the actual cross-compiler that builds your firmware — and it auto-detects which one that is. GCC, IAR, Keil, Green Hills, TI and others are recognized automatically, so the instrumented build follows the same toolchain and the same optimization settings as the shipped build. There are no source changes and no manual wrappers: you prefix your existing build command, and the coverage you measure is the coverage that flies.
# Prefix your normal cross-build — no source edits, no wrappers $ rktracer make firmware detected: iar-arm 9.40 (target config, -Oh) ✓ instrumented 168 files — source unmodified # run requirements-based tests on the target / simulator $ ctest --target-runner $ rkresults --report html ✓ Statement 100% ✓ Decision 98.4% (host build read 100% — 6 branches folded out on target) ✓ MC/DC 97.1%
Because RKTracer rides the real cross-compiler, the optimizer's transformations are reflected in the numbers — host and target finally tell the same story.
RKTracer measures statement, decision, condition, MC/DC and multi-condition coverage, and runs the same way on the host, on embedded targets with or without a file system, on GPUs, and on simulators and emulators. For the wider picture of bringing this to constrained hardware, see embedded system testing. RKTracer is developed under an ISO 9001 quality management system, which matters when its output becomes verification evidence.
What to remember
- Coverage is measured against the compiled program — and the cross-compiler optimizes your firmware differently from your host compiler.
- Optimization, target-only
#ifdefbranches, intrinsics and word size all reshape control flow, so host percentages don't equal target percentages. - Use the host for fast iteration, but take the coverage of record from the same cross-compiled build that ships.
The bottom line
Host coverage is a development convenience, not a substitute for the real thing. The cross-compiler that builds your firmware optimizes differently from your laptop's compiler — folding branches, swapping decisions for conditional moves, compiling target-only code in and host-only code out — and structural coverage is measured against that transformed program. A green report on the host can be perfectly true about a binary that never ships.
Develop fast on the host, then measure the coverage of record on the same cross-compiled build that flies. Get the number from the binary that ships, and host and target stop telling you two different stories about the same code.