MC/DC is not branch coverage with extra steps

MC/DC — modified condition/decision coverage — asks one question that branch
coverage does not. Branch coverage asks whether if (a && b) went both ways.
MC/DC asks whether you showed that a changes the outcome on its own, and that
b does too. In practice that is one or two extra test cases per decision, and
a pile of confusion produced by arguing about the standard instead of looking at
the expression.
So look at the expression.
The two words you have to get right
A condition is a boolean expression containing no boolean operators. A decision is a boolean expression composed of conditions and zero or more boolean operators, and a decision without an operator is a condition. Those are the definitions from the DO-178B glossary, reproduced in the NASA tutorial that is still the best free explanation of the criterion.1
Two consequences that people miss.
First, a decision is not "an if". bool ok = a && b; is a decision even
though nothing branches. The certification authorities wrote a whole position
paper to close that loophole, CAST-10, and the FAA's own tool study restates its
conclusion: all boolean expressions in a program are subject to decision
coverage or MC/DC, not just the ones that steer control flow.23
Second: if a condition appears more than once in a decision, each occurrence is a distinct condition.1
a && b needs three tests, not four
| a | b | a && b |
|---|---|---|
| F | F | F |
| F | T | F |
| T | F | F |
| T | T | T |
To show a has an independent effect, pair row TT with row FT: only a moved,
and the outcome moved. To show b, pair TT with TF. Three rows do the job. Row
FF is never needed.
That is the whole trick, and it often scales to n+1 tests for a decision with n independent conditions.1 The alternative that guarantees the same thing by brute force is multiple condition coverage: every combination, 2^n tests. At four conditions that is 5 versus 16. At ten it is 11 versus 1024. MC/DC exists because someone did that arithmetic and wanted the strength without the bill.
Branch coverage on the same expression gets you there with two tests and tells
you nothing about b. GCC's manual shows the gap in its own example. Compile
if (a && b) with a = 1, b = 0, run it once, and gcov --conditions reports:
1: 8: if (a && b)
condition outcomes covered 1/4
condition 0 not covered (true false)
condition 1 not covered (true)
Both conditions were evaluated. One outcome out of four counted.4

Unique cause, masking, and the same variable twice
There are two accepted ways to demonstrate that independent effect.
Unique cause: between the two tests in a pair, only the condition of interest and the outcome may change. Everything else is held fixed. You do not need to look at the logic of the decision to believe the result.
Masking: more than one input may change between the two tests, as long as
analysis of the decision's internal logic shows the others could not have
affected the outcome. Short-circuit evaluation is the everyday case. In
a || (b && c), once a is true, nothing after it can move the result. GCC's
docs put it plainly: an outcome counts as covered only if it had an independent
effect, and in a masked position it does not, even though the condition was
evaluated.4
Now the repeated condition. Take Z = (a && b) || (a && c).
| a | b | c | Z |
|---|---|---|---|
| T | T | F | T |
| F | T | F | F |
Three inputs, four conditions, because each occurrence of a counts
separately.1 Those two rows show the independent effect of the input a.
They cannot show the independent effect of either occurrence, because you
cannot vary one occurrence of a variable while holding the other fixed. The FAA
tool study calls this a primary source of confusion about MC/DC, and it is the
reason masking exists at all: unique cause simply cannot be applied to coupled
conditions.3 A 2001 FAA study compared the forms and concluded masking
should be the preferred one.5
Practical version: if your tool reports coverage you cannot explain, look for a variable used twice in one decision. Then split the decision.

Who actually requires this
DO-178B/C, for Level A airborne software: MC/DC is the structural coverage objective at the top design assurance level. The FAA research states it directly, and the tool market exists because of it.5
ISO 26262 is a different animal, and summaries flatten it. The GCC paper says MC/DC is "mandated by standards such as DO-178 and ISO26262".6 That compression is where arguments start. ISO 26262 is paywalled and I have not read it, so I will not quote it or tell you what your assessor accepts. Its software-unit-testing methods come as tables with a recommendation scale, and secondary summaries place MC/DC as highly recommended at ASIL D rather than flatly required. If someone says it is mandatory, ask which table, which ASIL, and which recommendation level. Nothing here is compliance advice.
Which program did you measure?
MC/DC is defined on source constructs. Your target runs object code, and the two come apart the moment the optimizer does its job. The tutorial has a section on exactly that: nothing stops you analyzing object code, but showing that object-level coverage is equivalent to source-level coverage is not trivial in the general case. Short-circuit control forms simplify it. Leaning on compiler behavior for that argument may mean qualifying the compiler feature as a verification tool.1
The two open toolchains sit on opposite sides of that line.
| Clang | GCC | |
|---|---|---|
| Flag | -fcoverage-mcdc (with -fprofile-instr-generate -fcoverage-mapping) | -fcondition-coverage, then gcov --conditions |
| Operates on | AST and preprocessor information — source-based coverage mapping | Compiler control flow graph — CFG-based condition coverage |
| Form | test-vector based, capped at 32767 terms per expression | masking MC/DC |
| Languages | C, C++ | any front-end whose CFG looks like a BDD: C, C++, D, Rust (experimental) |
Clang's docs call MC/DC "most applicable to the embedded space" and document the caps, including the escape hatch when an expression explodes into too many test vectors.7 GCC 14 got it in 2024 by analyzing the compiler's CFG as a binary decision diagram rather than the syntax tree, which is why one implementation covers several languages.68 That does not by itself make the report equivalent to coverage measured on the final object code; source-to-object equivalence remains a separate assurance question. Clang's landed a year earlier with a six-condition limit, lifted the year after.6 Separate work covers MC/DC for Rust.9
Free MC/DC in a mainstream compiler is new. It was a paid-tool feature for two decades. If the last time you looked it cost five figures, look again.

What MC/DC does not prove
This is the part that gets skipped, and it is why the metric has a bad name.
Structural coverage analysis exists to determine which code structure was not exercised by the requirements-based tests.1 It is a check on your test suite, run after the tests are written from requirements. Read backwards — write tests until the number turns green — it inverts into something close to useless. Rushby's summary of the Heimdahl, George and Weber experiment is worth the discomfort: tests generated automatically to hit MC/DC on a flight guidance model detected relatively few seeded bugs, and generally did worse than random testing.10
Three things the number cannot see:
- Code that is not there. A missing requirement produces no uncovered branch. 100% MC/DC on the wrong logic is 100%.
- Unintended function. In the tutorial's words, structural testing fails to assure that there are no unintended functions. At best it confirms the object code and processor implement the source.1
- Everything physical. Interrupt timing, a peripheral that needs a settling delay, a DMA race, a register written in the wrong order, current draw. Full MC/DC on host builds says nothing about any of it. That split is MIL, SIL, PIL, HIL, and simulation versus HIL is where it lands in CI.
A gap in coverage is still information. When requirements-based tests fail to reach MC/DC, the tutorial's list of causes is short and every entry is actionable: inadequate test cases, inadequate requirements, dead code, or deactivated code.1 Dead code gets removed. The rest gets explained. That loop lives with the rest of the evidence in a firmware validation cycle.
Where I would start
Build your logic-heavy modules for the host, turn on -fcondition-coverage or
-fcoverage-mcdc, and read the uncovered outcomes on the decisions you already
care about. Keep decisions small enough that n+1 is a number you can look at.
If repeated conditions make the reported independence pairs impossible to
explain, simplify or decompose the decision deliberately. Then go argue about
the standard, if you still have to.
The criterion is not hard. It is three test cases for a && b and a rule about
what counts as one condition. Everything else is procedure around it.
Sources
Footnotes
-
Hayhurst, Veerhusen, Chilenski, Rierson, "A Practical Tutorial on Modified Condition/Decision Coverage," NASA/TM-2001-210876, 2001. Definitions, the n+1 minimum, source-versus-object coverage (2.5.1), and the DO-178B guidance on unachieved coverage. https://ntrs.nasa.gov/citations/20010057789 ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8
-
Certification Authorities Software Team, "What is a 'Decision' in Application of Modified Condition/Decision Coverage (MC/DC) and Decision Coverage (DC)?," Position Paper CAST-10, June 2002. The FAA has since removed the CAST papers from its site; this is the archived copy, which I could not fetch directly at the time of writing. https://web.archive.org/web/20170702144954/https://www.faa.gov/aircraft/air_cert/design_approvals/air_software/cast/cast_papers/media/cast-10.pdf ↩
-
FAA, "Software Verification Tools Assessment Study," DOT/FAA/AR-06/54, 2007. Summarizes CAST-10 and CAST-6, and section 4.3.3 covers multiple occurrences of a condition. https://www.faa.gov/sites/faa.gov/files/aircraft/air_cert/design_approvals/air_software/AR-06-54_VerificationTools.pdf ↩ ↩2
-
GCC manual, "Invoking Gcov" (
--conditions) and "Instrumentation Options" (-fcondition-coverage). https://gcc.gnu.org/onlinedocs/gcc/Invoking-Gcov.html ↩ ↩2 -
Chilenski, "An Investigation of Three Forms of the Modified Condition Decision Coverage (MCDC) Criterion," DOT/FAA/AR-01/18, April 2001. https://rosap.ntl.bts.gov/view/dot/42764 ↩ ↩2
-
Kvalsvik, "Modified Condition/Decision Coverage in the GNU Compiler Collection," arXiv:2501.02133, January 2025. https://arxiv.org/abs/2501.02133 ↩ ↩2 ↩3
-
Clang documentation, "Source-based Code Coverage," MC/DC Instrumentation. https://clang.llvm.org/docs/SourceBasedCodeCoverage.html ↩
-
Kvalsvik, "MC/DC merged into gcc," patch.no, 18 April 2024. https://patch.no/blog/2024-04-18-mcdc-merged-into-gcc/ ↩
-
Zaeske, Albini, Gilcher, Durak, "Towards Modified Condition/Decision Coverage of Rust," arXiv:2409.08708, 2024. https://arxiv.org/abs/2409.08708 ↩
-
Rushby, "Automated Test Generation and Verified Software," VSTTE position paper, summarizing Heimdahl, George and Weber on MC/DC-targeted test generation for a flight guidance model. https://www.vstte.ethz.ch/pdfs/vstte-rushby-position.pdf ↩