armhub.dev
Tools · 10 pages
Tools

Static analysis for firmware

Catching bugs before the firmware runs — clang-tidy, scan-build, Coverity, Polyspace, PC-lint Plus, Cppcheck, MISRA-C 2012 conformance, and how to make CI gating actually useful.

6 min read · 1,301 wordsupdated · May 13, 2026

Static analysis runs your firmware source through a checker that flags bugs without executing the code: null-pointer dereferences, buffer overruns, use-after-free, integer overflow, dead stores, missing default cases, type confusion, race conditions on shared state. On a desktop project it's a nice-to-have. On firmware — where a bug ships to a fleet of devices that you can't roll back, that may run for a decade, that you may not be allowed to update over the air — it is a baseline expectation, and on safety-certified projects it is mandatory.

This page covers the tooling landscape (open-source linters, commercial analysers, formal-grade tools), MISRA-C conformance, the practicalities of CI integration, and how to keep false-positive volume manageable so engineers actually act on the findings.

Static analysis tools at a glance
clang-tidy
Open-source, ~200 checks, integrates with cmake / compile_commands.json; cert-* and bugprone-* checks closely match MISRA
scan-build / Clang Static Analyzer
Open-source, path-sensitive symbolic execution; flags null deref, leaks, use-after-free at line precision
Cppcheck
Open-source, C/C++ only, fewer false positives than clang-tidy but shallower analysis; MISRA add-on available
PC-lint Plus
Commercial (Gimpel), authoritative MISRA-C 2012 conformance; ~$5k seat
Coverity
Commercial (Synopsys), inter-procedural, finds defects in millions of LOC; cloud-based reports
Polyspace Bug Finder + Code Prover
Commercial (MathWorks), formal abstract interpretation, mathematically proves absence of run-time errors
Sparse
Open-source, Linux-kernel-grown, annotations like __must_check / __user enforced statically
CodeChecker / SARIF
Common output format; lets multiple tools feed one CI dashboard
MISRA-C 2012
143 rules + 16 directives; mandatory in ISO 26262 ASIL-B and above, IEC 62304 Class B+

Two families of tools

Static analysers fall on a spectrum from pattern matching to formal abstract interpretation.

Pattern-based tools (Cppcheck, basic clang-tidy checks, PC-lint's mainline rules) walk the AST or the LLVM IR, match templates like "function with no return on this path", and flag them. Fast, cheap, occasionally noisy. Run them on every commit.

Path-sensitive symbolic execution (Clang Static Analyzer / scan-build, Coverity, deeper PC-lint checks) explores feasible execution paths, tracks symbolic values, and proves properties along each path: "on this path, ptr is null when dereferenced". Slower (minutes to hours for a million-line codebase), much more precise.

Formal abstract interpretation (Polyspace Code Prover, Astrée) computes over-approximations of every reachable state using interval arithmetic and lattice theory. The output is mathematically sound: "this division can never be by zero" or "this array index can never exceed bound" — provable, not heuristic. Slower still (hours to days per build), and the cost is reflected in the licence price. Used on aerospace, automotive ASIL-D, medical Class III firmware.

ToolApproachSpeedFalse positive rateNotes
clang-tidyAST patternsSecondsModerateUse cert-, bugprone-, misc-* groups
scan-buildSymbolic execMinutesModeratescan-build make wrapper
CppcheckAST + light dataflowSecondsLowMISRA add-on (commercial)
PC-lint PlusHybridMinutesLow (with config)MISRA 2012 reference
CoveritySymbolic execHoursLowCloud or on-prem
Polyspace Bug FinderSymbolicHoursLowMISRA, defects
Polyspace Code ProverAbstract interpretationHours-daysVery low (sound)Mathematical proof
AstréeAbstract interpretationHoursVery lowSpecialised on embedded C

MISRA-C 2012 conformance

MISRA-C is the dominant coding standard for safety-critical C. The 2012 edition has 143 rules grouped into 21 categories, classified as Mandatory, Required, or Advisory. Mandatory rules must not be deviated from. Required can be deviated with a documented justification. Advisory are recommendations.

A few of the higher-impact rules:

RuleCategoryWhat it forbids
11.3RequiredCasting between pointers to objects of different essential types
13.5RequiredShort-circuit operators with side-effects in the right-hand operand
14.4RequiredThe controlling expression of an if/while/for must be a boolean type
15.5AdvisoryMultiple return from a function
17.7RequiredDiscarding the return value of a non-void function
18.4AdvisoryPointer arithmetic (use indexing instead)
21.3RequiredNo malloc/free family in safety-critical code
22.7RequiredThe macro EOF shall only be compared with the unmodified return value of getchar/getc/fgetc

A full MISRA-C 2012 checker is one of the differentiators of commercial tools — PC-lint Plus, Polyspace Bug Finder, Coverity, and LDRA all have certified MISRA rule sets that map each finding to the rule number and category. Open-source tools (Cppcheck with the MISRA add-on, clang-tidy's cert-* and misc-* checks) cover about 60–70% of MISRA — enough for "MISRA-aware" but not enough for formal "MISRA-compliant".

Compliance is rarely 100%. A typical claim is "MISRA-C 2012 conformant with 12 documented deviations" — each deviation has a justification (e.g. "Rule 21.3 deviated for module X because the FreeRTOS heap_4 implementation uses pvPortMalloc; reviewed and accepted by safety case section 4.2.1").

clang-tidy as the daily driver

For a project that hasn't bought into a commercial tool, clang-tidy gets you 80% of the value. It plugs into compile_commands.json (cmake emits this automatically with CMAKE_EXPORT_COMPILE_COMMANDS=ON), runs on the same translation units the compiler sees, and reports findings in standard compiler-message format that IDEs and CI dashboards already understand.

A starter .clang-tidy for embedded C:

Checks: >
  -*,
  bugprone-*,
  cert-*,
  clang-analyzer-*,
  cppcoreguidelines-init-variables,
  cppcoreguidelines-no-malloc,
  misc-misplaced-const,
  misc-redundant-expression,
  misc-static-assert,
  misc-unused-parameters,
  modernize-use-bool-literals,
  performance-*,
  portability-*,
  readability-non-const-parameter
WarningsAsErrors: 'bugprone-*,cert-*,clang-analyzer-*'

Running:

$ run-clang-tidy -p build -checks='-*,bugprone-*,cert-*' src/

A common pattern is to enable WarningsAsErrors only for the bugprone and cert groups (high signal, low noise) and run the others as warnings — visible in the CI report but not blocking.

CI gating that engineers don't hate

The fastest way to get static analysis ignored is to drop a thousand findings on the team on day one. The path that works:

  1. Baseline the existing findings. Run the tool, snapshot the warnings to a file, commit it.
  2. Block only new warnings. CI runs the analyser on the diff or the changed translation units; new warnings fail the build, baselined warnings don't.
  3. Burn down the baseline. Allocate 5% of each sprint to fixing baseline warnings, oldest first. The baseline shrinks; eventually it's empty and the gate becomes "no new warnings ever".
  4. Categorise by severity. A bugprone-suspicious-string-compare is not a clang-analyzer-core-NullDereference. Block the latter, warn on the former.
  5. Suppress with justification, in code. Each // NOLINT(check-name): reason lives next to the suppression, gets reviewed in PR, and shows up in git log -p if reverted. Centralised suppression files become uncurated landfills.

Output should land in a single format — SARIF (Static Analysis Results Interchange Format) is the OASIS standard. CodeChecker, GitLab, GitHub, SonarQube, and Polyspace all consume and emit SARIF, so multi-tool aggregation works without custom adapters.

CI gate levelToolWhen
Pre-commit hookclang-tidy on changed files<30 s feedback
Per-PRclang-tidy full + Cppcheck<5 min
NightlyCoverity / Polyspace Bug Finder<2 h
Per-releasePolyspace Code ProverHours-days

False-positive triage

Every analyser produces false positives. The trick is to keep the rate below the threshold at which engineers stop reading findings — empirically about 10%. Above that, the tool gets ignored within a release cycle.

Sources of false positives:

  • Path infeasibility the tool can't prove. "On this path, len > 0 so the loop body executes" — but the tool doesn't know that. Add an assertion that documents the precondition; the analyser narrows its abstract value and the warning goes away.
  • Hardware semantics the tool doesn't know. A read from a memory-mapped peripheral returns different values across reads; the tool sees a redundant load. Mark the pointer volatile (which you should be doing anyway).
  • Intentional UB. Embedded code occasionally relies on implementation-defined or unspecified behaviour (signed overflow, bit manipulation of negative values, type punning via union). Wrap with /*lint -e... */ or // NOLINTNEXTLINE with a justification.
  • Compiler-built-in patterns. Some compilers expand memset inline using patterns the analyser flags as "dead store"; configure the tool to recognise the built-in.

A weekly triage rota — one engineer for an hour — keeps the queue clean. Without it, even good tools accumulate years of unactioned warnings and become CI noise.

Where the line is

Static analysis catches: null deref, buffer overflow, integer overflow, type confusion, missing case in switch, dead code, locking errors, leaked resources, incorrect format strings, suspicious arithmetic.

Static analysis does not catch: wrong algorithm, wrong constants, wrong sign on a coefficient, race conditions on data the tool doesn't model as shared, performance regressions, hardware fault sensitivity, EMC issues, supply-chain vulnerabilities in linked libraries.

The mature firmware development process layers static analysis with unit tests, integration tests, hardware-in-the-loop simulation, fuzzing, and formal verification on the most-critical modules. Each catches what the others miss.

Cross-references

  • Formal methods — when you need stronger proof than static analysis can give.
  • Toolchains — generating compile_commands.json from your build system.
  • Build systems — CMake / Bazel hooks for analyser integration.
  • GDB — runtime confirmation of static findings.
  • MISRA-C 2012 (with Amendments 1 and 2) — purchase from misra.org.uk.
  • CERT C Coding Standard — open and complementary to MISRA.