Static and Dynamic Analysis
Code analysis represents a critical discipline in embedded systems development, where software defects can have severe consequences ranging from product failures to safety hazards. Static and dynamic analysis techniques provide complementary approaches to discovering bugs, security vulnerabilities, performance bottlenecks, and code quality issues before they manifest in deployed systems.
Static analysis examines source code without executing it, applying sophisticated algorithms to detect potential problems based on code structure and data flow. Dynamic analysis observes program behavior during execution, capturing runtime information about memory usage, performance characteristics, and actual code paths. Together, these techniques form an essential quality assurance framework for embedded software development.
Understanding Static Analysis
Static analysis tools examine source code, and sometimes compiled binaries, to identify potential defects without running the program. These tools apply formal methods, pattern matching, and data flow analysis to detect issues that might escape manual code review and testing.
How Static Analyzers Work
Static analyzers operate by building models of program behavior and checking those models against rules representing correct or problematic patterns. The analysis process typically involves several stages:
Parsing and semantic analysis: The analyzer parses source code into an abstract syntax tree and performs semantic analysis similar to a compiler. This stage catches syntax errors and basic type mismatches.
Control flow analysis: The tool constructs a control flow graph representing possible execution paths through the code. This graph enables analysis of reachability, dead code detection, and path-sensitive defect detection.
Data flow analysis: By tracking how data moves through the program, analyzers can detect uninitialized variables, null pointer dereferences, and information leaks. Data flow analysis follows values through assignments, function calls, and conditional branches.
Abstract interpretation: Advanced analyzers use abstract interpretation to reason about program behavior without executing every possible path. This technique approximates program states to prove properties about all possible executions.
Types of Static Analysis
Static analysis spans a spectrum from lightweight linting to heavy-weight formal verification:
Linting: Basic static analysis checking coding style, simple bug patterns, and suspicious constructs. Linters run quickly and integrate easily into development workflows but catch only surface-level issues.
Bug finding: More sophisticated analysis targeting specific defect categories such as null pointer dereferences, buffer overflows, resource leaks, and race conditions. These tools balance analysis depth against false positive rates.
Security analysis: Specialized tools focus on security vulnerabilities including injection flaws, cryptographic weaknesses, and information disclosure. Security analyzers often implement specific vulnerability taxonomies like CWE (Common Weakness Enumeration).
Formal verification: The most rigorous static analysis mathematically proves program properties. Formal methods can guarantee absence of certain defect classes but require significant expertise and may not scale to large codebases.
Static Analysis for Embedded Systems
Embedded systems present unique static analysis challenges and opportunities:
Hardware interaction: Analyzers must understand volatile qualifiers, memory-mapped I/O, and hardware-specific behavior. Generic tools may produce false positives or miss defects related to hardware access patterns.
Concurrency: Interrupt-driven code and RTOS-based systems require analysis of concurrent access patterns. Detecting race conditions in embedded software requires understanding of interrupt priorities and synchronization mechanisms.
Resource constraints: Static analysis can verify stack usage bounds, detect memory leaks in systems without garbage collection, and ensure code fits within memory constraints.
Safety standards: Standards like MISRA C, CERT C, and IEC 61508 define coding rules that static analyzers can automatically enforce. Compliance checking is a primary use case for embedded static analysis.
Static Analysis Tools
A wide ecosystem of static analysis tools serves embedded development, ranging from open-source utilities to commercial products with formal verification capabilities.
Compiler Warnings
Compilers themselves provide the first line of static analysis defense. Modern compilers include sophisticated warning systems that catch many common errors:
GCC warnings: The -Wall and -Wextra flags enable extensive warnings. Additional flags like -Wconversion, -Wshadow, and -Wformat catch specific issue categories. The -Werror flag treats warnings as errors, enforcing clean builds.
Clang warnings: Clang provides excellent diagnostics with clear explanations. The -Weverything flag enables all warnings, useful for exploring available checks even if not practical for regular builds.
Treating warnings seriously: Many embedded projects enforce zero-warning policies. Warnings often indicate subtle bugs or maintenance hazards that warrant attention.
Open-Source Static Analyzers
Several capable open-source tools provide static analysis for embedded C code:
Cppcheck: A widely-used static analyzer for C and C++ that detects undefined behavior, dangerous coding patterns, and style issues. Cppcheck has low false positive rates and integrates with many development environments.
Clang Static Analyzer: Built on the Clang compiler infrastructure, this path-sensitive analyzer detects memory management errors, API misuse, and logic errors. The scan-build wrapper simplifies integration with existing build systems.
Clang-Tidy: A linter and static analyzer that includes checks for modernization, readability, and performance. Clang-Tidy can automatically fix many issues it detects.
Infer: Facebook's static analyzer uses separation logic to find null pointer dereferences, memory leaks, and concurrency issues. Infer scales to large codebases and provides incremental analysis.
PVS-Studio: While commercial, PVS-Studio offers free licenses for open-source projects. It detects a wide range of bugs and provides MISRA compliance checking.
Commercial Static Analysis Tools
Commercial tools often provide deeper analysis, better support, and certification evidence for safety-critical applications:
Polyspace: MathWorks' Polyspace products use abstract interpretation to prove absence of runtime errors. Polyspace Bug Finder detects defects while Polyspace Code Prover provides formal verification of critical code.
Coverity: A comprehensive static analysis platform detecting security vulnerabilities, quality defects, and compliance violations. Coverity's analysis engine handles large codebases effectively.
Klocwork: Provides static analysis with emphasis on security vulnerabilities and coding standards compliance. Klocwork integrates with development environments and CI/CD pipelines.
PC-lint/FlexeLint: Long-established tools particularly strong at MISRA compliance checking. PC-lint provides detailed diagnostics and extensive configurability.
Parasoft C/C++test: Combines static analysis, unit testing, and code coverage in an integrated platform. Strong support for automotive and medical device standards.
LDRA: Provides static and dynamic analysis tools designed for safety-critical embedded systems. LDRA tools support DO-178C, ISO 26262, and IEC 62304 certification.
MISRA Compliance Checking
MISRA C guidelines define a subset of C designed to improve safety, security, and reliability in embedded systems. Static analyzers play a central role in MISRA compliance:
Rule categories: MISRA rules fall into mandatory, required, and advisory categories. Static analyzers typically check all categories, allowing teams to configure which violations trigger warnings or errors.
Decidable versus undecidable rules: Some MISRA rules can be checked automatically with high confidence while others require human judgment. Tools classify rules by decidability to help teams understand analysis limitations.
Deviation management: When MISRA rules must be violated for technical reasons, formal deviation procedures document the rationale. Some tools provide deviation tracking integrated with analysis results.
Common MISRA violations: Implicit type conversions, lack of explicit braces, complex expressions, and pointer arithmetic frequently trigger MISRA warnings. Understanding these patterns helps developers write MISRA-compliant code from the start.
Understanding Dynamic Analysis
Dynamic analysis examines program behavior during execution, providing information that static analysis cannot obtain. By observing actual runtime behavior, dynamic analysis detects issues dependent on specific inputs, timing, or environmental conditions.
How Dynamic Analysis Works
Dynamic analysis tools instrument programs to observe their behavior during execution. Instrumentation approaches include:
Source-level instrumentation: The tool modifies source code to insert monitoring code before compilation. This approach provides detailed information but may significantly affect performance.
Compiler instrumentation: The compiler adds monitoring code during compilation. Sanitizers like AddressSanitizer and MemorySanitizer use this approach for efficient runtime checking.
Binary instrumentation: Tools modify compiled code either statically or dynamically to add monitoring. This approach works with code lacking source access but may have higher overhead.
Sampling: Rather than instrumenting every operation, sampling-based tools periodically capture program state. This approach has lower overhead but may miss short-lived issues.
Advantages of Dynamic Analysis
Dynamic analysis provides capabilities beyond static analysis reach:
Actual behavior: Dynamic analysis shows what the program actually does rather than what it might do. This eliminates false positives from paths that never execute in practice.
Input-dependent issues: Problems triggered by specific inputs or input sequences are visible during dynamic analysis when those inputs occur.
Timing and performance: Dynamic analysis captures actual timing behavior, enabling performance optimization and detection of timing-dependent bugs.
Memory behavior: Runtime memory analysis reveals actual allocation patterns, fragmentation, and leaks that static analysis can only approximate.
Limitations of Dynamic Analysis
Dynamic analysis has inherent limitations that complement static analysis:
Coverage dependency: Dynamic analysis only observes executed paths. Code paths not exercised during testing remain unanalyzed.
Runtime overhead: Instrumentation adds execution time and memory overhead. Heavy instrumentation may make real-time systems miss deadlines.
Environment dependency: Results depend on the execution environment. Issues present only in the target environment may not appear when analyzing on development systems.
Reproducibility: Non-deterministic behavior, especially in concurrent systems, may cause issues to appear intermittently.
Runtime Error Detection
Runtime checkers detect errors during program execution, catching issues that cause undefined behavior before they corrupt program state or crash the system.
Address Sanitizer
AddressSanitizer (ASan) detects memory access errors including buffer overflows, use-after-free, and use-after-return. It has become an essential tool for C and C++ development:
Detection capabilities: ASan catches out-of-bounds accesses to heap, stack, and global objects. It detects use-after-free by quarantining freed memory and checking accesses against the quarantine list.
Shadow memory: ASan uses shadow memory to track the validity of each memory location. The shadow memory maps each 8 bytes of application memory to 1 byte of shadow memory encoding accessibility.
Performance impact: ASan typically adds 2x slowdown and 3x memory overhead. While significant, this overhead is acceptable for testing and often for development builds.
Embedded considerations: Memory overhead may preclude ASan use on resource-constrained targets. Development systems and simulators with more memory enable ASan testing of embedded code.
Memory Sanitizer
MemorySanitizer (MSan) detects use of uninitialized memory, a common source of undefined behavior:
Tracking initialization: MSan tracks whether each byte of memory has been initialized. Reading uninitialized memory triggers an error report.
Origin tracking: MSan can track where uninitialized values originate, helping developers understand how uninitialized data propagates through the program.
Usage requirements: MSan requires instrumenting all code, including libraries. Using MSan with uninstrumented libraries produces false positives.
Undefined Behavior Sanitizer
UndefinedBehaviorSanitizer (UBSan) detects various forms of undefined behavior in C and C++:
Integer overflow: Signed integer overflow is undefined behavior in C. UBSan can detect overflows that would otherwise silently wrap or cause unpredictable results.
Shift errors: Shifting by negative amounts or amounts exceeding type width is undefined. UBSan catches these errors.
Null pointer dereference: Dereferencing null pointers causes undefined behavior. UBSan detects such dereferences before they crash the program.
Type violations: Accessing objects through pointers of wrong type violates strict aliasing rules. UBSan can detect some aliasing violations.
Low overhead: Many UBSan checks have minimal overhead, making UBSan practical for production builds in some cases.
Thread Sanitizer
ThreadSanitizer (TSan) detects data races in concurrent programs:
Race detection: TSan identifies when multiple threads access shared data without proper synchronization, with at least one access being a write.
Happens-before tracking: TSan tracks synchronization operations to understand which accesses are properly ordered and which represent true races.
Embedded relevance: While primarily designed for threaded programs, TSan concepts apply to interrupt-driven embedded code where main code and interrupts share data.
Memory Analysis
Memory analysis tools track allocation patterns, detect leaks, and identify inefficient memory usage. These tools are particularly valuable for embedded systems with limited memory resources.
Valgrind Memcheck
Valgrind's Memcheck tool provides comprehensive memory error detection:
Leak detection: Memcheck tracks all allocations and reports memory that was never freed. Reports classify leaks as definitely lost, indirectly lost, or still reachable.
Invalid access detection: Memcheck detects reads and writes to invalid addresses, including buffer overruns and use-after-free.
Uninitialized value tracking: Similar to MSan, Memcheck tracks uninitialized values and reports when they affect program behavior.
Overhead: Valgrind's dynamic binary instrumentation causes 10-50x slowdown. This limits Valgrind's applicability to testing rather than development workflows.
Host-only limitation: Valgrind runs on development hosts, not embedded targets. It can analyze embedded code cross-compiled to run on the host or running in simulators.
Heap Profiling
Heap profilers track dynamic memory allocation patterns over program execution:
Allocation tracking: Profilers record each allocation and deallocation, capturing size, timing, and call stack information.
Peak usage: Understanding peak memory usage helps size memory pools and ensure systems stay within RAM limits.
Fragmentation analysis: Long-running embedded systems can suffer from heap fragmentation. Profilers visualize allocation patterns that cause fragmentation.
Allocation hot spots: Identifying code that allocates frequently guides optimization toward reducing allocation overhead or moving to static allocation.
Stack Analysis
Stack overflow is a common embedded system failure mode. Stack analysis tools help prevent overflow:
Static stack analysis: Compiler options and dedicated tools analyze call graphs to compute maximum stack usage without running the program.
Stack painting: Filling the stack with known patterns before execution reveals high-water marks by examining how much of the pattern was overwritten.
Runtime stack checking: Some RTOSes and runtime systems provide stack overflow detection during execution, catching overflow before it corrupts other memory.
Worst-case analysis: Computing maximum stack usage requires considering all possible call paths, including interrupt nesting. Static analysis tools help bound worst-case usage.
Memory Leak Detection in Embedded Systems
Memory leaks in embedded systems can cause gradual resource exhaustion leading to eventual system failure:
Wrapper functions: Wrapping malloc and free with tracking versions enables leak detection even on targets without dynamic analysis tool support.
Pool-based allocation: Using fixed-size memory pools instead of general-purpose malloc simplifies leak tracking and eliminates fragmentation concerns.
Allocation logging: Logging allocations to flash or sending them over debug interfaces enables post-mortem analysis of memory usage in deployed systems.
Periodic consistency checks: Runtime checks can verify memory pool integrity and detect leaks before they cause failures.
Performance Profiling
Profilers measure program performance, identifying bottlenecks and guiding optimization efforts. For embedded systems with real-time requirements, profiling is essential for meeting timing constraints.
Types of Profiling
Different profiling approaches trade accuracy against overhead:
Instrumentation profiling: Inserting measurement code at function entries and exits provides exact call counts and timing. High overhead limits applicability to development testing.
Sampling profiling: Periodically sampling the program counter reveals where time is spent with lower overhead. Statistical sampling may miss short functions.
Hardware-assisted profiling: Modern processors include performance monitoring units that count events like cache misses, branch mispredictions, and instruction counts with minimal overhead.
Tracing: Recording detailed execution traces enables post-mortem analysis of program flow and timing. Tracing can capture more detail than sampling but generates large data volumes.
Profiling Tools
Various tools address different profiling needs:
gprof: The traditional GNU profiler combines instrumentation for call counts with sampling for time distribution. Widely available but limited in capabilities.
perf: Linux's perf tool provides access to hardware performance counters, sampling, and tracing. While Linux-specific, perf concepts apply to embedded profiling.
Valgrind Callgrind: Provides detailed call graph profiling through simulation. High overhead but exact results.
IDE-integrated profilers: Many embedded IDEs include profilers integrated with debuggers and trace tools. These often leverage hardware debug interfaces for low-overhead profiling.
Trace analyzers: Tools like Percepio Tracealyzer and SEGGER SystemView visualize RTOS behavior including task execution, interrupts, and system calls.
Embedded-Specific Profiling Considerations
Profiling embedded systems presents unique challenges:
Observer effect: Profiling overhead can alter timing enough to mask or create timing problems. Minimal-overhead techniques are essential for real-time systems.
Limited resources: Targets may lack resources to run profiling infrastructure. Off-loading analysis to host systems through debug interfaces helps.
Interrupt and DMA timing: Profiling must account for time spent in interrupt handlers and time when the CPU is stalled waiting for DMA.
Power profiling: For battery-powered devices, power consumption profiling is as important as timing. Specialized tools correlate code execution with power measurements.
Using Profiling Results
Effective use of profiling data requires systematic analysis:
Identify hot spots: Focus optimization on code consuming the most time. Optimizing code that executes rarely provides little benefit.
Understand call patterns: Excessive function calls or deep call chains may indicate opportunities for inlining or restructuring.
Cache behavior: Cache miss profiling reveals memory access patterns that reduce performance. Data structure reorganization or algorithmic changes can improve cache utilization.
Measure improvement: Profile before and after optimization to verify changes actually improve performance. Intuition about optimization effects is often wrong.
Code Coverage Analysis
Code coverage measures which parts of a program execute during testing. Coverage metrics help assess test suite completeness and identify untested code.
Coverage Metrics
Different coverage metrics measure different aspects of test completeness:
Statement coverage: Measures whether each statement executes at least once. The most basic metric, easily achieved but insufficient alone.
Branch coverage: Measures whether each branch direction (true and false) executes. Branch coverage subsumes statement coverage.
Condition coverage: For compound conditions, measures whether each atomic condition evaluates to both true and false.
Modified condition/decision coverage (MC/DC): A rigorous metric required by aerospace standards showing that each condition independently affects the decision outcome.
Path coverage: Measures coverage of distinct execution paths. Theoretically ideal but often impractical due to path explosion from loops and conditionals.
Coverage Tools
Various tools provide coverage measurement for embedded C code:
gcov: GCC's built-in coverage tool provides statement and branch coverage. Lightweight and widely available.
lcov: A front-end to gcov providing HTML reports and coverage data aggregation across multiple test runs.
BullseyeCoverage: A commercial tool providing function, decision, and condition coverage with lower overhead than gcov.
Testwell CTC++: Provides MC/DC coverage and supports safety-critical development workflows.
IDE-integrated coverage: Many embedded development environments include coverage tools integrated with testing frameworks.
Coverage in Embedded Development
Applying coverage analysis to embedded systems requires addressing several challenges:
Target constraints: Full coverage instrumentation may exceed target memory. Host-based testing with simulators or stubbed hardware enables coverage analysis.
Hardware-dependent code: Code paths triggered by hardware events may be difficult to exercise in automated testing. Hardware-in-the-loop testing helps achieve coverage of hardware-dependent code.
Safety standards requirements: Standards like DO-178C and ISO 26262 specify coverage requirements. MC/DC coverage is required for the highest criticality levels.
Coverage targets: While 100% coverage is a common goal, achieving it may not be practical. Understanding which code remains uncovered and why is more important than the percentage.
Integrating Analysis into Development Workflows
Analysis tools provide maximum benefit when integrated into regular development practices rather than applied only before release.
Continuous Integration
Automated analysis in CI pipelines catches issues early:
Build-time analysis: Run static analyzers as part of every build. Configure analyzers to fail builds on new warnings.
Automated testing with sanitizers: Run test suites with sanitizers enabled to catch memory errors and undefined behavior automatically.
Coverage tracking: Track coverage over time and alert on decreases. Require coverage thresholds for merge approval.
Incremental analysis: Some tools support incremental analysis, checking only changed code for faster feedback.
Developer Workflow Integration
Making analysis easy for developers increases adoption:
IDE integration: Analyzers integrated into development environments show issues inline with code, enabling immediate correction.
Pre-commit hooks: Running quick analysis before commits prevents introduction of easily-detected issues.
Editor plugins: Real-time linting as code is written provides immediate feedback.
Easy local execution: Developers should be able to run the same analysis locally that CI runs.
Managing Analysis Results
Effective analysis programs require managing findings systematically:
Baseline establishment: When first applying analysis to existing code, establish a baseline and focus on preventing new issues while gradually addressing existing ones.
False positive management: Document and suppress false positives to maintain signal-to-noise ratio. Review suppressions periodically.
Prioritization: Focus on high-severity findings and issues in critical code paths. Not all warnings require immediate attention.
Trend tracking: Monitor analysis metrics over time to ensure code quality improves or at least does not degrade.
Analysis in Safety-Critical Development
Safety-critical projects have specific analysis requirements:
Tool qualification: Safety standards may require qualification of analysis tools to ensure their results are trustworthy.
Traceability: Analysis results must be traceable to requirements and test cases for certification evidence.
Documentation: Analysis methodology, configuration, and results require formal documentation.
Deviation justification: Suppressed warnings or deviations from coding standards require documented technical justification.
Selecting Analysis Tools
Choosing appropriate analysis tools depends on project requirements, target constraints, and development context.
Evaluation Criteria
Consider multiple factors when selecting tools:
Detection capabilities: What types of issues does the tool detect? Does it cover the defect categories most relevant to your application?
False positive rate: Too many false positives waste developer time and lead to ignored warnings. Evaluate tools on representative code.
Integration: How well does the tool integrate with your build system, IDE, and CI infrastructure?
Performance: How long does analysis take? Can it keep up with development iteration speed?
Target support: For dynamic analysis, does the tool support your target platform or require host-based testing?
Standards support: If compliance with MISRA, CERT, or other standards is required, verify the tool covers necessary rules.
Cost: Consider license costs, training costs, and ongoing maintenance effort.
Building a Tool Chain
Most projects benefit from multiple complementary tools:
Multiple static analyzers: Different analyzers find different issues. Running multiple tools catches more bugs than any single tool.
Static and dynamic combination: Static analysis catches issues in code paths not reached during testing. Dynamic analysis catches issues dependent on runtime conditions. Both are necessary.
Layered approach: Use fast, lightweight tools continuously during development. Apply deeper analysis periodically or before releases.
Commercial and open-source: Open-source tools provide baseline capabilities. Commercial tools may add value for specific needs like safety certification support.
Best Practices
Effective use of analysis tools requires more than simply running them:
Configuration and Customization
Tools require configuration to match project needs:
Rule selection: Enable rules relevant to your project and disable those generating noise without value.
Severity calibration: Adjust severity levels to match your project's risk profile. Security-critical code may treat warnings as errors.
Custom rules: Many tools support custom rules for project-specific patterns or architectural constraints.
Exclusions: Configure exclusions for generated code, third-party libraries, or code with different quality requirements.
Training and Documentation
Tools provide value only when developers understand and use them effectively:
Developer training: Train developers on tool capabilities, result interpretation, and appropriate responses to findings.
Process documentation: Document which tools run when, how to interpret results, and how to address findings.
Knowledge sharing: Share interesting findings and lessons learned to improve team understanding of common issues.
Continuous Improvement
Analysis programs should evolve with the project:
Regular review: Periodically review tool configuration and effectiveness. Adjust as the project evolves.
New tool evaluation: The analysis tool landscape changes. Periodically evaluate new tools that might provide additional value.
Feedback incorporation: Gather developer feedback on tool effectiveness and pain points. Adjust processes based on practical experience.
Summary
Static and dynamic analysis tools are essential components of embedded systems quality assurance. Static analyzers detect defects by examining source code, enforcing coding standards, and mathematically proving properties. Dynamic analysis through runtime checkers, memory analyzers, and profilers reveals actual program behavior, catching issues dependent on specific execution conditions.
Effective analysis programs combine multiple tools, integrate analysis into development workflows, and systematically manage findings. For safety-critical embedded systems, analysis tools provide evidence necessary for certification while helping developers create more reliable code.
The investment in establishing robust analysis infrastructure pays dividends throughout project lifecycles. Defects caught early through automated analysis cost far less to fix than those found during integration testing or, worse, in deployed products. By making analysis an integral part of development rather than a late-stage gate, teams build quality into embedded software from the beginning.