MISRA C and Static Code Analysis

MISRA C and Static Code Analysis

An engineering treatment of static analysis for embedded and critical C through MISRA C, Cppcheck, Splint, Astrée and TI Code Composer Studio, from lexical checks to abstract interpretation and property-specific guarantees.

Static code analysis is not merely the act of inspecting source without running it; it is the engineering problem of deciding which program properties can be inferred from which abstract model, and with what confidence. A lightweight rule scanner and an abstract-interpretation analyzer may both be called static-analysis tools, yet their guarantees, computational cost and failure modes are fundamentally different. In embedded and critical C software, the useful question is therefore not "how many warnings did the tool find?" but "which properties can it establish, which findings are heuristic, and which assumptions does the result depend on?"

During my 2015 internship in Baykar's UAV software unit, I worked on programming standards and static code analysis. I studied MISRA C, developed C-based checks of my own, and also worked with output from Cppcheck and Splint. I examined Astrée from the abstract-interpretation side and Texas Instruments Code Composer Studio and its compiler tooling from the embedded build, diagnostics and debugging side. I later carried the same checking approach into the Mini UAV software I developed for my undergraduate project. Looking back, the durable lesson was not a list of individual rules. It was seeing early that different analyzers can produce different answers for the same source because they model different properties.

MISRA C Is Not a Static Analyzer

MISRA C is often reduced to a green or red compliance counter in a tool window. That loses the engineering intent. MISRA C is a family of guidelines for using C more predictably in critical software. It constrains language use, coding practices and deviation handling; it is not itself an analysis engine.

For work performed in 2015, MISRA C:2012 is the historically appropriate reference point. MISRA C:2023 exists today, but it would be inaccurate to describe an older workflow as though the later edition had been used. More importantly, a claim of "MISRA compliant" cannot be established merely because one analyzer produces zero diagnostics. MISRA Compliance:2020 requires the project to define which guidelines apply, how they are enforced, how deviations are controlled and how externally developed components are treated. A static-analysis report is evidence within that process, not the process itself.

The distinction matters in practice. Some guidelines can be enforced automatically. Others require manual review, design evidence or project-specific reasoning. A deviation can be legitimate when it is controlled and justified. The objective is not to silence the checker; it is to make the reason, scope and safety argument auditable.

From Text Scanning to a Program Model

Some of the first checks I implemented during the internship used explicit state tracking while scanning C source character by character. A plain text search becomes unreliable as soon as comments, string literals, character literals, preprocessor lines and normal code are not distinguished. The characters // inside a string do not begin a comment. An = may mean something very different depending on expression context. A switch rule cannot be interpreted reliably by finding the word break somewhere nearby.

A scanner of this kind still has value for narrowly defined local rules. If the source length is N, a single lexical pass is commonly O(N). Semantic claims require a richer model:

source -> tokens -> syntax tree -> symbols and types -> control-flow graph -> data flow -> findings

A local rule over an AST can often be evaluated roughly linearly in the number of nodes. Basic control-flow traversal is O(V + E), where V is the number of graph nodes and E the control-flow edges. Data-flow analysis may revisit nodes until a fixed point is reached. A simplified form is

OUT(n) = F_n(merge(OUT(p))), p ∈ pred(n)

and the actual cost depends on the abstract domain, convergence strategy, call graph, path sensitivity and context sensitivity. There is no useful universal statement such as "static analysis is O(n)" for the whole problem.

Cppcheck: Practical Bug Finding with Low Friction

Cppcheck is valuable because it targets categories of undefined behavior and dangerous C/C++ constructs that ordinary compilation may not diagnose. Its official documentation lists classes such as null-pointer dereference, out-of-bounds access, division by zero, invalid shift operands, uninitialized values and memory-management problems. Its willingness to analyze compiler extensions and non-standard syntax is also relevant to embedded codebases.

A low-false-positive objective must not be confused with soundness. Cppcheck's own manual explicitly states that its checks are not perfect and that it can fail to detect bugs that should be reported. It can be an effective engineering bug finder without being a proof that every execution is safe.

Current open-source Cppcheck releases include a misra.py add-on for MISRA C:2012, while the official manual also states that open-source MISRA coverage is partial. I do not project this current feature set backward onto the exact version I used in 2015. The longer-lived lesson is that the statement "this tool supports MISRA" is incomplete unless the edition, rule coverage and enforcement mechanism are known.

In my internship tooling, I normalized Cppcheck output into the same result model as my own checks. That pattern still makes sense: centralizing findings is useful, but the normalization layer must preserve provenance. Two diagnostics labeled "warning" do not necessarily represent the same confidence or guarantee.

Splint: Adding Contracts to C Source

Splint represents a different branch of static checking. Its documentation describes traditional lint checks such as use-before-definition, unreachable code, ignored return values, likely infinite loops and fall-through, then extends the analysis through source annotations.

The idea was particularly useful to me because it exposes a fundamental limitation of purely syntactic inference. A tool cannot always infer from C syntax whether a pointer may be null, which caller-visible state a function is allowed to modify, or what ownership assumptions apply to a buffer. An annotation converts some programmer intent into a machine-checkable contract.

That extra information has a maintenance cost. An incorrect annotation can encode an incorrect assumption. A strict annotation regime can also impose substantial adoption cost on legacy code. Splint is historically important precisely because it illustrates the trade: stronger analysis may come not only from a more sophisticated engine, but also from making program intent more explicit.

Astrée and Abstract Interpretation

Astrée differs from many bug-finding tools because its objective is phrased as a proof problem for a defined class of properties. The official project description presents Astrée as an abstract-interpretation-based analyzer intended to prove the absence of specified run-time errors in C programs, with particular emphasis on real-time embedded control software.

Abstract interpretation avoids enumerating every concrete execution state. It computes a safe over-approximation in a smaller abstract domain. If

x ∈ [a, b]
y ∈ [c, d]

then interval abstraction can safely derive

x + y ∈ [a + c, b + d]

Every concrete sum is covered, although the interval may also contain values that no real execution can reach. That is the price of sound over-approximation: avoiding missed behaviors can introduce spurious states and therefore false alarms.

The mathematical foundation comes from Patrick and Radhia Cousot's 1977 work on abstract interpretation and fixed-point approximation. Astrée applies this framework to safety-critical C using a combination of abstract domains, convergence techniques and domain-specific precision. The 2003 PLDI paper by Blanchet and colleagues demonstrated that this approach could be made practical for large classes of safety-critical embedded software while keeping false alarms low for the targeted programs.

Two boundaries are essential. General program verification is undecidable; there is no automatic analyzer that can prove every property of every C program. Astrée's strong guarantee is meaningful because its target property and program assumptions are constrained. The official Astrée description, for example, describes a structured-C class without dynamic memory allocation and recursion for the targeted analyses. A guarantee without its scope is not an engineering guarantee.

Code Composer Studio and Target-Specific Reality

Code Composer Studio should not be classified as a static analyzer. CCStudio is an integrated environment for TI microcontrollers and processors, combining compiler, debugger, profiler and target-development tooling. Some static diagnostics come from the compiler toolchain rather than the IDE itself. TI compiler families have historically exposed MISRA-related checking and configurable diagnostics, but exact support is dependent on compiler family and version.

That distinction is not academic in embedded C. A desktop analyzer may flag a construct based on language portability while the target compiler defines a particular implementation-dependent behavior. The reverse can also occur: the compiler accepts code that a critical-software coding standard intentionally forbids. volatile, integer widths, bit-field layout, inline assembly, memory-mapped registers and compiler intrinsics live close to this boundary.

While working with TMS570 and embedded networking during the internship, I learned not to equate compiler diagnostics with independent static-analysis results. Source-level warnings are only one layer of evidence. An embedded defect may originate in source, build options, peripheral configuration or the physical signal itself. CCStudio debugging and target observation therefore complement static analysis rather than replacing it.

False Positives, False Negatives and the Language of Guarantees

The first thing to read in a static-analysis report is the tool's guarantee model.

An unsound bug finder can be extremely useful while intentionally accepting that some true defects will be missed in exchange for lower noise. A sound analyzer aims not to miss possible failures for its defined property and assumptions, but safe over-approximation may produce false positives. Path sensitivity, context sensitivity and relational domains can improve precision while increasing CPU and memory cost. Naive path enumeration can grow exponentially with branching.

This is why "tool A found more issues than tool B" is a weak engineering comparison. The property set, configuration, suppressions, library models and target-platform knowledge may be different. Warning counts are comparable only after those semantics are aligned.

I find it more useful to separate findings into three groups: direct language or memory-safety problems, project-standard violations, and suspicious behavior requiring additional evidence. That prevents a high-volume style rule from obscuring a potentially serious undefined behavior report and reduces the warning-fatigue effect.

Layered Verification Instead of One Tool

The analyzer I developed in 2015 was not a complete C front end or a formal verifier by today's standards. It combined lexical state tracking for selected programming-standard checks with a desktop interface and results from external analyzers. Its value came from being narrow, repeatable and auditable: checks that were easy to forget in manual review could be executed consistently.

A mature critical-C verification chain asks different tools different questions. Compiler diagnostics expose issues visible to the target toolchain. MISRA enforcement constrains language and coding practice. Cppcheck-style analysis searches practical defect classes. Annotation-assisted analysis makes parts of programmer intent explicit. Abstract interpretation can establish stronger property-specific guarantees. Unit, integration, HIL and system tests measure behaviors that a static model cannot fully represent.

I do not treat any one of these layers as making the others obsolete. The engineering decision is to know which risk each layer reduces and which blind spot remains after it runs.

The most mature static-analysis process is not the one with the fewest warnings on a dashboard. It is the one that can state which rules were automatically enforced, which properties were actually proved, which deviations were consciously accepted, and which behaviors still require execution, measurement or observation on the target.

The principle I first applied while automating programming-standard checks in 2015 has remained stable: a repeatable rule should not depend on human memory alone. What changed with experience is how I judge the output. I now read confidence from the analysis model, assumptions and proof scope rather than from the name of the tool.

References

  • MISRA. MISRA Compliance:2020 - Achieving compliance with MISRA Coding Guidelines. The MISRA Consortium, 2020. source
  • MISRA. MISRA C:2023 Addendum 2 - Coverage of ISO/IEC 17961. The MISRA Consortium, 2024. source
  • Cousot, Patrick; Cousot, Radhia. Abstract Interpretation: A Unified Lattice Model for Static Analysis of Programs by Construction or Approximation of Fixpoints. POPL, 1977. DOI: 10.1145/512950.512973.
  • Blanchet, Bruno; Cousot, Patrick; Cousot, Radhia; Feret, Jérôme; Mauborgne, Laurent; Miné, Antoine; Monniaux, David; Rival, Xavier. A Static Analyzer for Large Safety-Critical Software. PLDI, 2003. DOI: 10.1145/781131.781153.
  • École Normale Supérieure / CNRS / INRIA. The Astrée Static Analyzer. source
  • Cppcheck Project. Cppcheck - A Tool for Static C/C++ Code Analysis. source
  • Cppcheck Project. Cppcheck Manual. source
  • Splint Project. Splint Home Page. source
  • Evans, David. Splint User's Manual. Version 3.1.1, 2003. source
  • Texas Instruments. CCStudio Integrated Development Environment. source
QR code for this page