The "Holy Bible" for embedded engineers
Leveraging static analysis tools and techniques to detect defects, ensure code quality, and prevent runtime failures in embedded software
Static analysis examines source code without executing it, identifying potential defects, security vulnerabilities, and code quality issues. In embedded systems, static analysis is crucial for catching problems early in the development cycle, especially in safety-critical applications where runtime failures can have severe consequences.
┌─────────────────────────────────────────────────────────────┐
│ Static Analysis Categories │
├─────────────────────────────────────────────────────────────┤
│ Syntax Analysis │ Grammar and language rule checking │
│ Semantic Analysis │ Meaning and logic verification │
│ Data Flow Analysis │ Variable usage and data tracking │
│ Control Flow Analysis│ Execution path and logic flow │
│ Type Checking │ Data type safety and compatibility │
│ Security Analysis │ Vulnerability and threat detection │
└─────────────────────────────────────────────────────────────┘
Data flow analysis tracks how data moves through your program:
// Example: Potential null pointer dereference
void process_data(uint8_t *data, uint32_t length) {
if (data == NULL) {
return; // Early return
}
// Data flow analysis should recognize 'data' is non-null here
for (uint32_t i = 0; i < length; i++) {
data[i] = process_byte(data[i]); // Safe access
}
}
Control flow analysis examines the execution paths through your code:
// Example: Unreachable code detection
void hardware_control(uint8_t command) {
if (command == CMD_START) {
start_hardware();
} else if (command == CMD_STOP) {
stop_hardware();
} else if (command == CMD_RESET) {
reset_hardware();
} else {
// This path should be unreachable if command validation is proper
handle_invalid_command(command);
}
}
Type safety analysis ensures data types are used correctly:
// Example: Type mismatch detection
typedef struct {
uint32_t id;
uint16_t value;
} sensor_data_t;
void process_sensor_data(sensor_data_t *sensor) {
// Static analysis should flag this type mismatch
uint8_t temp = sensor->value; // uint16_t to uint8_t conversion
}
// Simple static analysis rule structure
typedef struct {
uint32_t rule_id;
const char *rule_name;
const char *description;
uint32_t severity; // 1=Low, 2=Medium, 3=High, 4=Critical
bool (*check_function)(const char *code, uint32_t line);
} static_analysis_rule_t;
// Rule checking function type
typedef bool (*rule_checker_t)(const char *code, uint32_t line);
// Analysis result structure
typedef struct {
uint32_t line_number;
uint32_t rule_id;
uint32_t severity;
const char *message;
const char *suggestion;
} analysis_result_t;
#define MAX_RESULTS 100
#define MAX_RULES 50
static_analysis_rule_t rules[MAX_RULES];
analysis_result_t results[MAX_RESULTS];
uint32_t rule_count = 0;
uint32_t result_count = 0;
// Check for potential null pointer dereference
bool check_null_pointer_deref(const char *code, uint32_t line) {
// Simple pattern matching for demonstration
if (strstr(code, "->") && strstr(code, "NULL")) {
return true; // Potential issue found
}
return false;
}
// Check for uninitialized variables
bool check_uninitialized_vars(const char *code, uint32_t line) {
// Look for variable declarations without initialization
if (strstr(code, "uint32_t") || strstr(code, "int") || strstr(code, "char")) {
if (!strstr(code, "=") && strstr(code, ";")) {
return true; // Potential uninitialized variable
}
}
return false;
}
// Check for magic numbers
bool check_magic_numbers(const char *code, uint32_t line) {
// Look for hardcoded numbers that might be magic numbers
if (strstr(code, " 0x") || strstr(code, " 0b")) {
return true; // Potential magic number
}
return false;
}
// Register a new analysis rule
uint32_t register_analysis_rule(const char *name, const char *desc,
uint32_t sev, rule_checker_t checker) {
if (rule_count >= MAX_RULES) {
return UINT32_MAX; // Error
}
rules[rule_count].rule_id = rule_count;
rules[rule_count].rule_name = name;
rules[rule_count].description = desc;
rules[rule_count].severity = sev;
rules[rule_count].check_function = checker;
return rule_count++;
}
// Analyze a single line of code
void analyze_code_line(const char *code, uint32_t line_number) {
for (uint32_t i = 0; i < rule_count; i++) {
if (rules[i].check_function(code, line_number)) {
// Issue found, add to results
if (result_count < MAX_RESULTS) {
results[result_count].line_number = line_number;
results[result_count].rule_id = i;
results[result_count].severity = rules[i].severity;
results[result_count].message = rules[i].description;
results[result_count].suggestion = "Review and fix the issue";
result_count++;
}
}
}
}
// Generate analysis report
void generate_analysis_report(void) {
printf("=== Static Analysis Report ===\n");
printf("Total Issues Found: %u\n", result_count);
uint32_t critical_count = 0;
uint32_t high_count = 0;
uint32_t medium_count = 0;
uint32_t low_count = 0;
for (uint32_t i = 0; i < result_count; i++) {
switch (results[i].severity) {
case 4: critical_count++; break;
case 3: high_count++; break;
case 2: medium_count++; break;
case 1: low_count++; break;
}
}
printf("Critical: %u, High: %u, Medium: %u, Low: %u\n",
critical_count, high_count, medium_count, low_count);
// Print detailed results
for (uint32_t i = 0; i < result_count; i++) {
printf("Line %u [%s]: %s\n",
results[i].line_number,
get_severity_string(results[i].severity),
results[i].message);
}
}
// Advanced rule for checking interrupt safety
bool check_interrupt_safety(const char *code, uint32_t line) {
// Check for potential issues in interrupt context
if (strstr(code, "malloc") || strstr(code, "free")) {
return true; // Dynamic allocation in interrupt context
}
if (strstr(code, "printf") || strstr(code, "sprintf")) {
return true; // I/O operations in interrupt context
}
return false;
}
// Rule for checking hardware register access patterns
bool check_register_access_patterns(const char *code, uint32_t line) {
// Look for proper register access patterns
if (strstr(code, "0x") && strstr(code, "=")) {
// Check if it's a volatile pointer access
if (!strstr(code, "volatile")) {
return true; // Missing volatile qualifier
}
}
return false;
}
// CMake integration example
void integrate_with_cmake(void) {
printf("Static analysis integration with CMake:\n");
printf("1. Add cppcheck target\n");
printf("2. Configure analysis rules\n");
printf("3. Set up pre-commit hooks\n");
printf("4. Integrate with CI/CD pipeline\n");
}
// Pre-commit hook integration
void setup_pre_commit_hooks(void) {
printf("Setting up pre-commit hooks:\n");
printf("1. Install pre-commit framework\n");
printf("2. Configure static analysis tools\n");
printf("3. Set up rule violations as warnings/errors\n");
printf("4. Configure automatic fixes where possible\n");
}
// Check for resource leak patterns
bool check_resource_leaks(const char *code, uint32_t line) {
// Look for file handles, memory allocations, etc.
if (strstr(code, "fopen") && !strstr(code, "fclose")) {
return true; // Potential file handle leak
}
if (strstr(code, "malloc") && !strstr(code, "free")) {
return true; // Potential memory leak
}
return false;
}
// Check for race condition patterns
bool check_race_conditions(const char *code, uint32_t line) {
// Look for shared variable access without protection
if (strstr(code, "global_") && strstr(code, "=")) {
if (!strstr(code, "mutex") && !strstr(code, "lock")) {
return true; // Potential race condition
}
}
return false;
}
Q: What is the difference between static and dynamic analysis? A: Static analysis examines code without execution, identifying potential issues through code inspection. Dynamic analysis runs the code and monitors its behavior at runtime. Static analysis catches issues early but may have false positives, while dynamic analysis finds actual runtime issues but requires execution.
Q: What are the main benefits of static analysis in embedded systems? A: Early defect detection, improved code quality, compliance with safety standards, reduced testing costs, identification of security vulnerabilities, and prevention of runtime failures that could be catastrophic in safety-critical systems.
Q: How would you handle false positives in static analysis? A: Document legitimate false positives, configure rules appropriately, use suppressions for known cases, continuously improve rule quality, and involve the team in rule refinement to reduce false positive rates over time.
Q: What types of issues can static analysis detect in embedded C code? A: Null pointer dereferences, uninitialized variables, memory leaks, buffer overflows, type mismatches, unreachable code, resource leaks, race conditions, and violations of coding standards and best practices.
Q: How would you integrate static analysis into a continuous integration pipeline for embedded systems? A: Configure analysis tools to run automatically on code commits, set up quality gates based on analysis results, integrate with build systems like CMake, use pre-commit hooks for immediate feedback, and configure reporting and notification systems for the team.
Q: How do you balance static analysis thoroughness with build performance? A: Use incremental analysis where possible, configure tools to analyze only changed files, run comprehensive analysis during nightly builds, use parallel analysis on multiple cores, and configure analysis depth based on build type (debug vs. release).
Next Steps: Explore Dynamic Analysis for runtime behavior analysis or Code Coverage for testing completeness assessment.