
Static analysis catches undefined behavior, resource leaks, and coding-standard violations before they reach hardware. For embedded C, three tools dominate: Cppcheck for fast pattern-based checks, Clang Static Analyzer for deep path-sensitive bug hunting, and MISRA checkers for compliance-critical codebases. This article walks through integrating all three into a CI pipeline, interpreting their output, and suppressing false positives without weakening the safety net.
Embedded C runs close to hardware with no memory protection, no exceptions, and no garbage collector. A single buffer overrun, use-after-free, or signed-integer overflow can brick a device or open a safety hazard. Compilers catch syntax errors and some UB at -Wall -Wextra -Wpedantic, but they do not model data flow across function boundaries, track pointer lifetimes, or enforce coding standards like MISRA C:2012.
Static analysis fills this gap. It operates on the AST or IR without executing code, scaling to millions of lines and catching classes of bugs that testing misses: dead code, unreachable paths, violated preconditions, and rule violations that correlate with field failures.
| Tool | Analysis Type | Speed | False Positive Rate | MISRA Coverage | Best For |
|---|---|---|---|---|---|
| Cppcheck | Pattern/AST | Fast | Low (warning/style) | Partial (addon) | CI gate, style, quick wins |
| Clang Static Analyzer | Path-sensitive symbolic | Slow | Medium | None | Deep bugs, memory safety |
| Commercial MISRA (LDRA, Parasoft) | Full compliance | Slow | Low (qualified) | Complete (decidable + undecidable) | Certification evidence |
Use all three in a tiered pipeline: Cppcheck on every PR, Clang SA nightly, MISRA tool on release branches.
Cppcheck excels at finding null-pointer derefs, buffer overflows, uninitialized variables, and style violations with minimal setup. It parses C/C++ directly (no compile commands needed) and supports --enable=all plus addons.
cppcheck --enable=warning,performance,portability,style \--std=c11 \--platform=stm32f4 \--inline-suppr \--suppress=missingIncludeSystem \--suppress=unusedFunction \--force \--xml \--output-file=cppcheck-report.xml \src/
--platform=stm32f4 (or your MCU) defines platform-specific macros and sizes.--inline-suppr allows // cppcheck-suppress comments in source.--suppress=missingIncludeSystem silences noise from vendor HAL headers you cannot fix.--xml produces machine-readable output for CI parsers (GitLab SAST, GitHub Code Scanning).Cppcheck’s MISRA addon checks a subset of decidable rules:
cppcheck --addon=misra.py --enable=warning --xml src/ > misra-cppcheck.xml
Output includes rule IDs (e.g., MISRA2012-Rule-10.1). Rules requiring manual review (undecidable) are reported as information severity — they do not fail the build but flag for human audit.
uint32_t crc32_calculate(const uint8_t *data, size_t len){uint32_t crc = 0xFFFFFFFFu;// cppcheck-suppress knownConditionTrueFalse ; len validated by callerif (len == 0) {return ~crc;}// ...}
Document the justification in the comment. Audit suppressions quarterly.
Clang SA models execution paths, tracking symbolic values through branches, loops, and function calls. It finds bugs Cppcheck misses: complex null-deref chains, use-after-free across modules, division-by-zero on computed divisors, and lock-order inversions.
scan-build -o clang-sa-report \-enable-checker core,unix,cplusplus,security \-analyzer-config display-progress=true \make clean all
-enable-checker selects checker groups. For embedded, prioritize core (null, div-zero, deadcode), unix (POSIX API misuse), and security (taint, format-string).clang-sa-report/) shows exploded path graphs — click through to see the exact path to the bug.A typical report traces:
ptr != NULL).*ptr is accessed on a path where the constraint was not met.If the path is infeasible (dead code), mark it with __attribute__((analyzer_noreturn)) on the aborting function or add an assert() that Clang SA respects.
find_program(CLANG_SA scan-build)if (CLANG_SA)add_custom_target(clang-static-analysisCOMMAND ${CLANG_SA} -o ${CMAKE_BINARY_DIR}/clang-sa--use-cc=${CMAKE_C_COMPILER}--use-c++=${CMAKE_CXX_COMPILER}${CMAKE_COMMAND} --build ${CMAKE_BINARY_DIR}COMMENT "Running Clang Static Analyzer"VERBATIM)endif()
Run via make clang-static-analysis or cmake --build build --target clang-static-analysis in nightly CI.
MISRA C:2012 defines 159 rules (16 directives, 143 rules) across categories: required, mandatory, advisory. Required rules must be met (no violations). Mandatory violations need formal deviation. Advisory violations need justification.
For ISO 26262 ASIL-D or IEC 61508 SIL-3, the static analysis tool itself must be qualified (TQL-1 or equivalent). LDRA, Parasoft C/C++test, and QA-C have qualification kits. Cppcheck and Clang SA are not qualified — they supplement, not replace, the qualified tool.
1. Run qualified MISRA tool on full codebase (release branch)2. Categorize violations:- Required: zero tolerance → fix or redesign- Mandatory: file deviation with risk assessment- Advisory: fix or justify in coding standard appendix3. Generate compliance matrix (rule → file:line → status)4. Archive reports for audit
| Rule | Title | Typical Violation | Fix Pattern |
|---|---|---|---|
| Dir 4.6 | typedef for integer types | uint32_t used directly | typedef uint32_t u32_t; in std_types.h |
| Rule 10.1 | No implicit integer conversion | uint8_t a = 255 + 1; | uint8_t a = (uint8_t)(255u + 1u); |
| Rule 11.6 | No cast to/from pointer to integer | (uintptr_t)ptr | Use uintptr_t from <stdint.h>, document necessity |
| Rule 17.2 | No recursion | Recursive DFS | Rewrite iterative with explicit stack |
| Rule 21.3 | No dynamic memory | malloc/free | Static pools, memory pools, stack allocation |
┌─────────────────────────────────────────────────────────────┐│ CI Pipeline Stages │├─────────────────┬─────────────────────┬─────────────────────┤│ Pull Request │ Nightly Build │ Release Branch │├─────────────────┼─────────────────────┼─────────────────────┤│ cppcheck │ clang-static-analyzer│ qualified MISRA ││ (fast, warn) │ (deep, path-sens) │ (full compliance) ││ │ │ ││ fail on: │ fail on: │ fail on: ││ - warning │ - high-confidence │ - any required ││ - style │ bug reports │ - any mandatory ││ │ │ without deviation│└─────────────────┴─────────────────────┴─────────────────────┘
name: Static Analysison:pull_request:schedule: [cron: '0 2 * * *'] # nightlyworkflow_dispatch:jobs:cppcheck:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v4- name: Install cppcheckrun: sudo apt-get update && sudo apt-get install -y cppcheck- name: Run cppcheckrun: |cppcheck --enable=warning,performance,portability,style \--std=c11 --platform=stm32f4 --inline-suppr \--suppress=missingIncludeSystem --force \--xml --output-file=cppcheck.xml src/- name: Upload SARIFuses: github/codeql-action/upload-sarif@v3with:sarif_file: cppcheck.xmlcategory: cppcheckclang-sa:if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'runs-on: ubuntu-lateststeps:- uses: actions/checkout@v4- name: Install clangrun: sudo apt-get update && sudo apt-get install -y clang- name: Run scan-buildrun: |scan-build -o clang-sa-report \-enable-checker core,unix,security \make clean all- name: Upload reportuses: actions/upload-artifact@v4with:name: clang-static-analysispath: clang-sa-report/misra:if: github.ref == 'refs/heads/release/*' || github.event_name == 'workflow_dispatch'runs-on: ubuntu-lateststeps:- uses: actions/checkout@v4- name: Run qualified MISRA tool (example: LDRA)run: |# Requires licensed tool in runner imageldra_tbcl -config misra2012.tbcl -run- name: Upload compliance matrixuses: actions/upload-artifact@v4with:name: misra-compliancepath: misra-report/
| Cppcheck Severity | CI Action |
|---|---|
error | Fail build — memory safety, UB |
warning | Fail build — likely bug |
style | Warn only — maintainability |
performance | Warn only — optimization hint |
portability | Warn only — cross-platform risk |
information (MISRA) | Manual review queue |
core.NullDereferencecore.DivideZerounix.Malloc, core.StackAddressEscapesecurity.FloatLoopCounter, security.insecureAPI.strcpyLower priority: dead stores (core.DeadStores), unused return values (core.unusedReturnValue) — often intentional in embedded.
+-------------------------------------------------------------------+| STATIC ANALYSIS PIPELINE |+-------------------------------------------------------------------+| || PULL REQUEST || | || v || +----------------+ || | CPPCHECK | --fast, pattern-based--> SARIF (GitHub UI) || | (warning, | || | style, MISRA)| || +----------------+ || | || | FAIL on error/warning || v || MERGE || || NIGHTLY (schedule) || | || v || +----------------+ || | CLANG SA | --path-sensitive, deep--> HTML Report || | (core, unix, | symbolic execution (artifact) || | security) | || +----------------+ || | || | FAIL on high-confidence bugs || v || || RELEASE BRANCH || | || v || +----------------+ || | QUALIFIED MISRA| --full compliance--> Compliance Matrix || | TOOL (LDRA/ | 159 rules (audit artifact) || | Parasoft) | || +----------------+ || | || | FAIL on required/mandatory w/o deviation || v || DEPLOY || |+-------------------------------------------------------------------+
Static analysis is not a silver bullet — it produces false positives, misses concurrency bugs, and cannot verify runtime behavior. But a tiered pipeline (Cppcheck on PRs, Clang SA nightly, qualified MISRA on releases) catches entire classes of defects before they hit hardware. The key is tuning suppressions, triaging reports daily, and treating the compliance matrix as a living artifact, not a one-time audit checkbox.
Quick Links
Legal Stuff





