Embedded HandbookApps
All sections › XV. C in depth
STM32F103, STM32F407 · level intermediate · 27 min read
Open in the interactive handbook

129б. Static analysis of firmware: the clang analyzer, gcc -fanalyzer and cppcheck in the pipeline

Section 129 compared gcc warnings, cppcheck with MISRA rules and -fanalyzer on a separate sloppy file: each tool was run by hand on one source file. Section 129а took the same approach of checking without a board as far as unit tests. Here we analyze a real build for Cortex-M: a CMake project is built by arm-none-eabi-gcc, its command database compile_commands.json drives three analyzers — gcc -fanalyzer, the clang analyzer (clang --analyze) and cppcheck — and their exit codes decide whether the build goes any further.

The test subject is a small register-level firmware for the F103 and F407 with seven planted bugs of different classes and two places where there is no bug, but an analyzer may decide otherwise. The result is an "error × tool" table from the run, a list of what this kind of analysis does not find, and ways to remove false positives without switching the checks off.

References: CMSIS 5.9.0 headers (core_cm3.h, core_cm4.h), stm32f103xb.h from STM32CubeF1 1.8.7, stm32f407xx.h from the Keil STM32F4xx_DFP 2.17.1 pack; the built-in help of clang 14 itself (clang -cc1 -help, clang -cc1 -analyzer-config-help). The idea of connecting the clang analyzer to an arm-none-eabi-gcc build through the command database comes from the Memfault article about CodeChecker (interrupt.memfault.com, "Static Analysis with CodeChecker"); the text and code here are our own. License: the article — idea only, retold.

Terms used in the documentation

Term Meaning
static analysis finding bugs without running the code
path-sensitive analysis walking the branches while tracking the conditions
compilation database (compile_commands.json) the list of compile commands of a build
checker one analyzer check (core.NullDereference, unix.Malloc)
translation unit one .c file with all its headers
cross translation unit analysis (CTU) analysis across several source files
false positive a warning where there is no bug
inline suppression silencing a warning with a comment on the line
noreturn function a function that never returns
quality gate a pipeline step that stops the build

129б.1 What we look for and with what

A compiler looks at each line almost in isolation. A path analyzer walks a function branch by branch: "if id is not found, c is NULL, and then line 45 writes to address zero". This finds bugs that are visible only on one path out of many: a NULL dereference, a leak on an early return, a double free, an uninitialized variable, division by zero.

The tools in the run:

Tool Image How it is run
arm-none-eabi-gcc 12.2.1, -Wall -Wextra -O2 stm-arm:1 a normal CMake + Ninja build
gcc -fanalyzer (the same arm-none-eabi-gcc) stm-arm:1 the commands from compile_commands.json plus -fanalyzer
clang 14.0.6, --analyze stm-sys:1 the same commands, rewritten for clang --target=arm-none-eabi
cppcheck 2.10 stm-lint:1 cppcheck --project=compile_commands.json

clang-tidy, the scan-build wrapper and CodeChecker were not run in this section. clang-tidy reads the same compile_commands.json, and its clang-analyzer-* checks are the same path analyzer as in clang --analyze; the command-database recipe fits it, but its findings were not cross-checked here.

The arm-none-eabi-gcc build writes the command database; gcc -fanalyzer, clang --analyze and cppcheck run over it in three images, and their exit codes form the gate; below is a table of which planted bugs and which of the two bug-free places each tool flagged


129б.2 The test firmware

Six files: the vector table and startup without assembler (vectors.c), the main loop (main.c), register-level USART1 with interrupt-driven receive (uart.c), ADC reading (sensor.c), a halt that records the line number (fault.c) and the application part app.c in two variants: bad — with bugs, good — fixed. The board and the variant are selected with CMake variables:

cmake_minimum_required(VERSION 3.22)
project(sa C)
set(CMAKE_C_STANDARD 11)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)            # база команд для анализаторов

set(BOARD f103 CACHE STRING "плата: f103 или f407")
set(VARIANT bad CACHE STRING "bad — с подложенными ошибками, good — исправленный")
set(CMSIS_CORE   /cmsis/core   CACHE PATH "CMSIS/Core/Include")
set(CMSIS_DEVICE /cmsis/f1     CACHE PATH "заголовки устройства")

if(BOARD STREQUAL "f407")
    set(MCU_FLAGS -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard)
    set(MCU_DEF STM32F407xx)
else()
    set(MCU_FLAGS -mcpu=cortex-m3 -mthumb)
    set(MCU_DEF STM32F103xB)
endif()

add_executable(fw src/vectors.c src/main.c src/uart.c src/sensor.c src/fault.c
                  src/${VARIANT}/app.c)
target_compile_definitions(fw PRIVATE ${MCU_DEF})
target_include_directories(fw PRIVATE src src/${VARIANT})
target_include_directories(fw SYSTEM PRIVATE ${CMSIS_CORE} ${CMSIS_DEVICE})
target_compile_options(fw PRIVATE ${MCU_FLAGS} -O2 -g -Wall -Wextra
    -ffunction-sections -fdata-sections)
target_link_options(fw PRIVATE ${MCU_FLAGS} -T${CMAKE_SOURCE_DIR}/${BOARD}.ld
    -L${CMAKE_SOURCE_DIR} --specs=nano.specs --specs=nosys.specs -Wl,--gc-sections)

CMAKE_EXPORT_COMPILE_COMMANDS is the same as in 115а.6, except that here the command database is needed not by an editor but by the analyzers. The CMSIS headers are included as SYSTEM — in the command they come through -isystem, and warnings about someone else's code do not get in the way. The firmware links on both boards: on the F103 the bad variant is 2028 bytes of code, good is 2048; on the F407 — 2124 and 2136.

The application part with the bugs, in full. Each bug is in its own function, its number is in a comment:

/* app.c (bad) — прикладная часть с подложенными ошибками B1…B7.
   F1, F2 — не ошибки: анализатор не знает, что fault_halt не возвращается. */
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include "board.h"
#include "app.h"
#include "fault.h"

#define N_CHANNELS 4u
#define GAIN_MAX   64u
#define LOG_MAX    32u
#define HIST_LEN   16u

typedef struct {
    uint8_t  id;
    uint16_t gain;
    uint16_t errors;
} channel_t;

static channel_t channels[N_CHANNELS] = { { 1u, 1u, 0u }, { 2u, 1u, 0u }, { 5u, 1u, 0u }, { 7u, 1u, 0u } };
static uint16_t hist[HIST_LEN];
static uint32_t app_errors;

static channel_t *find_channel(uint8_t id)
{
    for (uint32_t i = 0u; i < N_CHANNELS; i++) {
        if (channels[i].id == id) {
            return &channels[i];
        }
    }
    return NULL;
}

/* B1: NULL по одной ветке — канал с таким id не найден */
void channel_set_gain(uint8_t id, uint16_t gain)
{
    channel_t *c = find_channel(id);
    if (gain > GAIN_MAX) {
        if (c != NULL) {
            c->errors++;
        }
        gain = GAIN_MAX;
    }
    c->gain = gain;
}

/* B2: утечка — ранний выход после malloc */
int log_store(const uint8_t *data, uint32_t n)
{
    uint8_t *rec = malloc(n + 4u);
    if (rec == NULL) {
        return -1;
    }
    if (n > LOG_MAX) {
        return -2;
    }
    memcpy(rec, &n, 4u);
    memcpy(&rec[4], data, n);
    int rc = uart_write(rec, n + 4u);
    free(rec);
    return rc;
}

/* B3: двойное освобождение на ветке ошибки */
int log_send(uint8_t *rec, uint32_t n)
{
    int rc = uart_write(rec, n);
    if (rc != 0) {
        free(rec);
        app_errors++;
    }
    free(rec);
    return rc;
}

/* B4: выход за массив — в условии цикла <= вместо < */
void hist_clear(void)
{
    for (uint32_t i = 0u; i <= HIST_LEN; i++) {
        hist[i] = 0u;
    }
}

/* B5: неинициализированная переменная — код возврата прочитан, но raw всё равно берётся */
static int adc_read(uint32_t ch, uint16_t *out)
{
    if (ch > 17u) {
        return -1;
    }
    *out = (uint16_t)(ADC1->DR & 0x0FFFu);
    return 0;
}

uint32_t battery_mv(uint32_t ch)
{
    uint16_t raw;
    if (adc_read(ch, &raw) != 0) {
        app_errors++;
    }
    return (uint32_t)raw * 3300u / 4095u;
}

/* B5x: то же, но функция чтения — в другом файле (sensor.c) */
uint32_t temp_cdeg(uint32_t ch)
{
    uint16_t raw;
    if (sensor_read(ch, &raw) != 0) {
        app_errors++;
    }
    return (uint32_t)raw * 100u / 41u;
}

/* B6: деление на ноль по пути — пустой массив */
uint16_t average(const uint16_t *v, uint32_t n)
{
    uint32_t sum = 0u;
    uint32_t cnt = 0u;
    for (uint32_t i = 0u; i < n; i++) {
        sum += v[i];
        cnt++;
    }
    return (uint16_t)(sum / cnt);
}

/* B7: гонка с прерыванием — чтение-изменение-запись счётчика, который растит USART1_IRQHandler */
void rx_consume(uint32_t n)
{
    rx_pending -= n;
}

/* F1: не ошибка — fault_halt не возвращается, но в fault.h это не сказано */
uint32_t cfg_checksum(const cfg_t *cfg)
{
    if (cfg == NULL) {
        fault_halt(__LINE__);
    }
    uint32_t s = 0u;
    for (uint32_t i = 0u; i < CFG_WORDS; i++) {
        s += cfg->w[i];
    }
    return s;
}

/* F2: то же через макрос CHECK */
uint32_t cfg_first(const cfg_t *cfg)
{
    CHECK(cfg != NULL);
    return cfg->w[0];
}

What is planted:

/* fault.h — останов с записью номера строки; объявлен без noreturn */
#ifndef FAULT_H
#define FAULT_H
#include <stdint.h>

void fault_halt(uint32_t line);

#define CHECK(c) do { if (!(c)) { fault_halt(__LINE__); } } while (0)

#endif

129б.3 The command database and clang for arm-none-eabi

In compile_commands.json the F103 build has six entries — one per .c file. For app.c the options the analyzer needs are:

-DSTM32F103xB -I/w/fw/src -I/w/fw/src/bad -isystem /cmsis/core -isystem /cmsis/f1 -mcpu=cortex-m3 -mthumb -std=gnu11

On the F407 they are -DSTM32F407xx, -isystem /cmsis/f4 and -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard. For clang to parse the same command, three things are missing.

  1. The target. There is one clang for all architectures: --target=arm-none-eabi, and it takes the core and FPU from the same -mcpu, -mfpu, -mfloat-abi.
  2. The newlib headers. arm-none-eabi-gcc supplies stdlib.h and string.h on its own; they are not in the command.
  3. A clean command. gcc options the analysis does not need (-O2, -ffunction-sections, -o) are dropped: what remains is macros, paths, the standard, the core and the ABI.

arm-none-eabi-gcc -xc -E -v - shows where newlib is:

/usr/lib/gcc/arm-none-eabi/12.2.1/include
/usr/lib/gcc/arm-none-eabi/12.2.1/include-fixed
/usr/lib/gcc/arm-none-eabi/12.2.1/../../../arm-none-eabi/include

The first two directories are gcc's own headers (stdint.h, stddef.h): clang has its own versions of those. The third one is newlib. A copy of it with the link resolved (cp -rL) becomes <sysroot>/include, and --sysroot tells clang where to look.

All of this is done by an 87-line script; it is also the gate: exit code 1 if there are more findings than the threshold.

#!/usr/bin/env python3
"""analyze.py — прогон анализатора по compile_commands.json сборки arm-none-eabi-gcc.

  analyze.py clang <каталог сборки> --sysroot <newlib> [--enable проверка] [--loop K] [--config ключ=значение]
  analyze.py gcc   <каталог сборки>
  общие ключи: --root <исходники> (пути в отчёте — от него), --max N (порог)

Печатает замечания строками «FIND файл:строка проверка | текст», в конце — «TOTAL n».
Код возврата: 0, если замечаний не больше N (по умолчанию 0), иначе 1; 2 — файл не разобран."""
import argparse, json, os, re, shlex, subprocess, sys

# ключи, которые нужны анализатору: макросы, пути, стандарт, ядро и ABI
KEEP = re.compile(r"^(-D|-U|-I|-isystem|-std=|-mcpu=|-mthumb$|-mfloat-abi=|-mfpu=)")
WITH_ARG = {"-D", "-U", "-I", "-isystem", "-include"}
DIAG = re.compile(r"^(?P<file>[^:\s]+):(?P<line>\d+):\d+: warning: (?P<msg>.*?) \[(?P<chk>[^\]]+)\]$")


def to_clang(argv, src, a):
    """Команда gcc → команда clang --analyze: оставить нужные ключи, добавить цель и newlib."""
    out, i = [], 1
    while i < len(argv):
        if argv[i] in WITH_ARG:
            out += argv[i:i + 2]
            i += 2
            continue
        if KEEP.match(argv[i]):
            out.append(argv[i])
        i += 1
    extra = []
    if a.enable:
        extra += ["-Xanalyzer", "-analyzer-checker=" + a.enable]
    if a.loop:                                   # сколько раз анализатор проходит цикл
        extra += ["-Xclang", "-analyzer-max-loop", "-Xclang", str(a.loop)]
    if a.config:
        extra += ["-Xclang", "-analyzer-config", "-Xclang", a.config]
    return (["clang", "--target=arm-none-eabi", "--sysroot=" + a.sysroot, "--analyze",
             "-Xanalyzer", "-analyzer-output=text",
             "-Xclang", "-analyzer-config", "-Xclang", "display-checker-name=true"] + extra + out + [src])


def to_gcc_analyzer(argv, src):
    """Та же команда gcc с -fanalyzer; объектный файл не нужен — анализ идёт при компиляции."""
    out, i = [argv[0]], 1
    while i < len(argv):
        if argv[i] == "-o":
            i += 2
            continue
        if argv[i] != src:
            out.append(argv[i])
        i += 1
    return out + ["-fanalyzer", "-o", "/dev/null", src]


def main():
    p = argparse.ArgumentParser()
    p.add_argument("tool", choices=["clang", "gcc"])
    p.add_argument("build")
    p.add_argument("--sysroot", default="")
    p.add_argument("--enable", default="")
    p.add_argument("--loop", type=int, default=0)
    p.add_argument("--config", default="")
    p.add_argument("--root", default=os.getcwd())
    p.add_argument("--max", type=int, default=0)
    a = p.parse_args()

    db = json.load(open(os.path.join(a.build, "compile_commands.json")))
    found = set()
    for e in db:
        argv = e["arguments"] if "arguments" in e else shlex.split(e["command"])
        cmd = to_clang(argv, e["file"], a) if a.tool == "clang" else to_gcc_analyzer(argv, e["file"])
        r = subprocess.run(cmd, cwd=e["directory"], capture_output=True, text=True)
        if r.returncode != 0 and "error:" in r.stderr:
            print("ERROR", e["file"], r.stderr.strip().splitlines()[0])
            return 2
        for ln in r.stderr.splitlines():
            m = DIAG.match(ln)
            if m and (a.tool == "clang" or m["chk"].startswith("-Wanalyzer")):
                where = os.path.relpath(m["file"], a.root)
                found.add("FIND %s:%s %s | %s" % (where, m["line"], m["chk"], m["msg"]))
    for f in sorted(found):
        print(f)
    print("TOTAL %d (порог %d)" % (len(found), a.max))
    return 0 if len(found) <= a.max else 1


if __name__ == "__main__":
    sys.exit(main())

The gcc mode is simpler: the command is the same, -fanalyzer is added to it, and the object file goes to /dev/null. The paths in the command database are absolute, so all three containers see the project and the build at the same paths /w/fw and /w/out.


129б.4 The run: who found what

F103 build, bad variant. Each cell shows the check that fired and the line it pointed to; "—" means the tool stayed silent.

Bug gcc -Wall -Wextra gcc -fanalyzer clang --analyze cppcheck
B1 NULL on one branch — null-dereference, 45 core.NullDereference, 45 nullPointerRedundantCheck, 45
B2 leak — malloc-leak, 55 unix.Malloc, 56 memleak, 56
B3 double free — double-free, 73 unix.Malloc, 73 doubleFree, 73
B4 out of bounds -Waggressive-loop-optimizations, 81 — — arrayIndexOutOfBounds, 81
B5 uninitialized -Wmaybe-uninitialized, 101 use-of-uninitialized-value, 101 core.UndefinedBinaryOperatorResult, 101 —
B5x the same, other file — — — —
B6 division by zero — — core.DivideZero, 123 —
B7 race with an interrupt — — — —
F1 (not a bug) — — false: core.NullDereference, 140 false: nullPointerRedundantCheck, 140
F2 (not a bug) — — — false: nullPointerRedundantCheck, 149
other — — — false ×2: comparePointers, vectors.c 21 and 25

Score over the eight real bugs: -Wall -Wextra — 2, -fanalyzer — 4, clang — 5, cppcheck — 4. Together — 6 of 8: nobody found B5x and B7. False positives: gcc — 0, clang — 1, cppcheck — 4. The F407 build gave the same table line for line: the core and the FPU do not affect the findings.

What this shows:


129б.5 Why the analyzer stayed silent

A path analyzer has a budget, and the defaults change the result.

A loop is walked only a few times. clang has a bounds check alpha.security.ArrayBoundV2 (alpha means experimental, off by default). When enabled, it did not find B4: the analyzer never reaches the 17th iteration of the loop in hist_clear. It did produce 2 false positives in Reset_Handler instead — on copying .data and zeroing .bss, where the bounds are set by the linker script. With the -analyzer-max-loop 17 option B4 is found — but B1 disappears.

Why B1 disappeared. Spending longer on loops, clang was able to walk all of find_channel and saw that the NULL came as the return value of a called function. By default it hides such paths: the suppress-null-return-paths setting (default true) is, according to the clang help, a heuristic against reports where the path goes through functions that are "more careful than the caller". With suppress-null-return-paths=false B1 came back: 6 findings instead of 5. Conclusion: raising the budget for the sake of one bug means risking others; a second tool is more reliable.

A check inside a macro. F1 and F2 are the same code: a NULL check, a call to fault_halt and an access to cfg. In F1 the check is written as an if, and clang reported a false positive. In F2 the same check is inside the CHECK macro — and clang stayed silent. cppcheck flagged both places. So clang's silence on assert-like macros is no proof that noreturn is not needed.


129б.6 False positives and the gate in the pipeline

The best suppression is telling the truth in the code. In the fixed variant fault.h differs by one line:

/* fault.h — останов с записью номера строки; анализатор знает, что возврата нет */
#ifndef FAULT_H
#define FAULT_H
#include <stdint.h>

__attribute__((noreturn)) void fault_halt(uint32_t line);

#define CHECK(c) do { if (!(c)) { fault_halt(__LINE__); } } while (0)

#endif

With __attribute__((noreturn)) both clang and cppcheck understand that there is no path after fault_halt: F1 and F2 disappeared for both, while the code of cfg_checksum and cfg_first did not change by a single character.

The fixed functions:

void channel_set_gain(uint8_t id, uint16_t gain)
{
    channel_t *c = find_channel(id);
    if (c == NULL) {
        app_errors++;
        return;
    }
    if (gain > GAIN_MAX) {
        c->errors++;
        gain = GAIN_MAX;
    }
    c->gain = gain;
}
int log_store(const uint8_t *data, uint32_t n)
{
    if (n > LOG_MAX) {
        return -2;
    }
    uint8_t *rec = malloc(n + 4u);
    if (rec == NULL) {
        return -1;
    }
    memcpy(rec, &n, 4u);
    memcpy(&rec[4], data, n);
    int rc = uart_write(rec, n + 4u);
    free(rec);
    return rc;
}
uint16_t average(const uint16_t *v, uint32_t n)
{
    if ((v == NULL) || (n == 0u)) {
        return 0u;
    }
    uint32_t sum = 0u;
    for (uint32_t i = 0u; i < n; i++) {
        sum += v[i];
    }
    return (uint16_t)(sum / n);
}
void rx_consume(uint32_t n)
{
    uint32_t primask = __get_PRIMASK();
    __disable_irq();
    rx_pending -= n;
    __set_PRIMASK(primask);
}

The last one is the fix for B7, which the analysis did not ask for: interrupts are disabled for the duration of the read-modify-write, and then the previous PRIMASK is restored, rather than unconditionally enabling them.

When the code is honest but the warning remains. cppcheck believes that &_sdata and &_edata point to different objects and must not be compared. For the C language that is true, but the bounds are set by the linker script. Such a place is suppressed with a comment on the line — naming the check, not everything:

void Reset_Handler(void)
{
    /* _sdata.._edata и _sbss.._ebss — границы одной области из скрипта компоновщика */
    uint32_t *src = &_sidata;
    // cppcheck-suppress comparePointers
    for (uint32_t *dst = &_sdata; dst < &_edata; dst++) {
        *dst = *src++;
    }
    // cppcheck-suppress comparePointers
    for (uint32_t *dst = &_sbss; dst < &_ebss; dst++) {
        *dst = 0u;
    }
    (void)main();
    Default_Handler();
}

The comment only works with the --inline-suppr option: without it cppcheck reports these 2 warnings on the good variant, with it — 0. On the bad variant 6 remain with --inline-suppr.

The gate. Each pipeline job runs in its own image, and its exit code stops the build:

# Этап анализа для .gitlab-ci.yml (раздел 110). Все задания видят проект по одному пути —
# $CI_PROJECT_DIR, поэтому абсолютные пути из compile_commands.json годятся во всех образах.
stages: [build, analyze]

variables:
  BOARD: f103
  VARIANT: good
  B: $CI_PROJECT_DIR/build/$BOARD

build:
  stage: build
  image: stm-arm:1
  script:
    - cmake -S fw -B $B -G Ninja -DCMAKE_TOOLCHAIN_FILE=$CI_PROJECT_DIR/fw/arm-gcc.cmake -DBOARD=$BOARD -DVARIANT=$VARIANT
      -DCMSIS_CORE=$CI_PROJECT_DIR/cmsis/core -DCMSIS_DEVICE=$CI_PROJECT_DIR/cmsis/device
    - cmake --build $B
    - mkdir -p $B/sysroot && cp -rL /usr/lib/arm-none-eabi/include $B/sysroot/
  artifacts:
    paths: [build/]

gcc-analyzer:
  stage: analyze
  image: stm-arm:1
  needs: [build]
  script:
    - python3 tools/analyze.py gcc $B --root fw --max 0

clang-analyzer:
  stage: analyze
  image: stm-sys:1
  needs: [build]
  script:
    - python3 tools/analyze.py clang $B --sysroot $B/sysroot --root fw --max 0

cppcheck:
  stage: analyze
  image: stm-lint:1
  needs: [build]
  script:
    - cppcheck --project=$B/compile_commands.json --enable=warning,style,performance,portability
      --platform=unix32 --inline-suppr --error-exitcode=1 --quiet

The jobs were run one after another the way a runner would run them: each in its own image, the project mounted at /builds/sa, the build/ directory shared.

Job bad good
build exit 0, 2 findings exit 0, 0 findings
gcc-analyzer exit 1, 4 findings exit 0, 0 findings
clang-analyzer exit 1, 6 findings exit 0, 0 findings
cppcheck exit 1, 6 findings exit 0, 0 findings

On bad all three analyzers return exit code 1, on good — 0; the build with two -Wall warnings passes — -Werror (129.4) is not enabled here. The --max threshold is for legacy code: the findings that already exist are accepted as a baseline, and new ones are not let through. On bad with --max 7 clang reports its 6 findings and exit code 0. The threshold is only ever lowered.


129б.7 Pitfalls

clang: 'stdlib.h' file not found. The command from the database is gcc's, and gcc supplies the newlib headers on its own. You need --sysroot with the include directory of arm-none-eabi-gcc. Copy it with cp -rL: in Debian this directory is a symbolic link, and a plain cp -r copies the link itself, which leads nowhere in another container.

The analyzer cannot find the files from the command database. The paths in compile_commands.json are absolute. If the build runs in one container and the analysis in another, mount the project and the build directory at the same paths. In GitLab all jobs see the project in $CI_PROJECT_DIR.

CMake was unable to find a build program corresponding to "Ninja". This is what a relative toolchain path looks like: -DCMAKE_TOOLCHAIN_FILE=fw/arm-gcc.cmake is resolved by CMake from the build directory, the file is not found, no compiler is set — and the message talks about Ninja. Make the path absolute, through $CI_PROJECT_DIR.

gcc -fanalyzer with -fsyntax-only is silent. The analysis runs together with compilation; without it there are no findings. Compile to /dev/null.

You enabled an experimental check and got noise. alpha.security.ArrayBoundV2 gave 2 false positives on this firmware and did not find B4. Experimental checks are for one-off runs, not for the gate.

You raised the budget and lost a finding. -analyzer-max-loop 17 found B4 and hid B1 (129б.5). Pin the analyzer settings in the script, otherwise runs cannot be compared.

The analysis is clean, so there are no races. No: none of the tools sees B7. An empty report means "we did not find what we know how to look for".


129б.8 Summary


Full sources

main.c — the main loop: everything that calls the application functions.

/* main.c — главный цикл: всё, что вызывает прикладные функции */
#include <stdlib.h>
#include <string.h>
#include "board.h"
#include "app.h"

static const cfg_t cfg = { { 1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u } };

int main(void)
{
    static const uint8_t hello[] = "sa129b\r\n";
    uint16_t samples[4] = { 100u, 200u, 300u, 400u };

    uart_init(115200u);
    hist_clear();
    channel_set_gain(5u, 80u);
    (void)log_store(hello, sizeof hello - 1u);
    uint8_t *rec = malloc(8u);
    if (rec != NULL) {
        memcpy(rec, hello, 8u);
        (void)log_send(rec, 8u);
    }
    for (;;) {
        uint32_t s = battery_mv(3u) + temp_cdeg(16u) + average(samples, 4u) + cfg_checksum(&cfg) + cfg_first(&cfg);
        (void)uart_write((const uint8_t *)&s, sizeof s);
        if (rx_pending > 0u) {
            rx_consume(rx_pending);
        }
    }
}

vectors.c — the vector table and startup without assembler.

/* vectors.c — таблица векторов и старт без ассемблера */
#include <stdint.h>
#include "board.h"

extern uint32_t _estack, _sidata, _sdata, _edata, _sbss, _ebss;
int main(void);
void Reset_Handler(void);
void USART1_IRQHandler(void);

static void Default_Handler(void)
{
    for (;;) {
    }
}

void Reset_Handler(void)
{
    /* _sdata.._edata и _sbss.._ebss — границы одной области из скрипта компоновщика */
    uint32_t *src = &_sidata;
    // cppcheck-suppress comparePointers
    for (uint32_t *dst = &_sdata; dst < &_edata; dst++) {
        *dst = *src++;
    }
    // cppcheck-suppress comparePointers
    for (uint32_t *dst = &_sbss; dst < &_ebss; dst++) {
        *dst = 0u;
    }
    (void)main();
    Default_Handler();
}

__attribute__((section(".isr_vector"), used))
static void (*const vectors[16 + USART1_IRQn + 1])(void) = {
    [0] = (void (*)(void))&_estack,
    [1] = Reset_Handler,
    [2 ... 15] = Default_Handler,
    [16 ... 16 + USART1_IRQn - 1] = Default_Handler,
    [16 + USART1_IRQn] = USART1_IRQHandler,
};

uart.c — register-level USART1: polled transmit, interrupt-driven receive.

/* uart.c — USART1 на регистрах: передача ожиданием, приём по прерыванию */
#include <stddef.h>
#include "board.h"
#include "app.h"

#define RX_LEN 64u

volatile uint32_t rx_pending;
static volatile uint8_t rx_buf[RX_LEN];
static volatile uint32_t rx_head;

void uart_init(uint32_t baud)
{
#if defined(STM32F407xx)
    RCC->AHB1ENR |= RCC_AHB1ENR_GPIOAEN;
    RCC->APB2ENR |= RCC_APB2ENR_USART1EN;
    GPIOA->MODER = (GPIOA->MODER & ~(GPIO_MODER_MODER9 | GPIO_MODER_MODER10))
                 | GPIO_MODER_MODER9_1 | GPIO_MODER_MODER10_1;      /* PA9, PA10 — AF */
    GPIOA->AFR[1] = (GPIOA->AFR[1] & ~0x0FF0u) | 0x0770u;          /* AF7 = USART1 */
#else
    RCC->APB2ENR |= RCC_APB2ENR_IOPAEN | RCC_APB2ENR_USART1EN;
    GPIOA->CRH = (GPIOA->CRH & ~0x0FF0u) | 0x04B0u;                 /* PA9 AF push-pull, PA10 вход */
#endif
    USART1->BRR = (PCLK2_HZ + baud / 2u) / baud;
    USART1->CR1 = USART_CR1_UE | USART_CR1_TE | USART_CR1_RE | USART_CR1_RXNEIE;
    NVIC_EnableIRQ(USART1_IRQn);
}

int uart_write(const uint8_t *p, uint32_t n)
{
    if (p == NULL) {
        return -1;
    }
    for (uint32_t i = 0u; i < n; i++) {
        while ((USART1->SR & USART_SR_TXE) == 0u) {
        }
        USART1->DR = p[i];
    }
    return 0;
}

void USART1_IRQHandler(void)
{
    if ((USART1->SR & USART_SR_RXNE) != 0u) {
        rx_buf[rx_head] = (uint8_t)USART1->DR;
        rx_head = (rx_head + 1u) % RX_LEN;
        rx_pending++;
    }
}

sensor.c — reading an ADC channel.

/* sensor.c — чтение канала АЦП; на неверном канале *out не трогает */
#include "board.h"
#include "app.h"

int sensor_read(uint32_t ch, uint16_t *out)
{
    if (ch > 17u) {
        return -1;
    }
    *out = (uint16_t)(ADC1->DR & 0x0FFFu);
    return 0;
}

fault.c — record the line and wait for a reset by the watchdog.

/* fault.c — запомнить строку и ждать сброса сторожевым таймером */
#include "board.h"
#include "fault.h"

volatile uint32_t fault_line;

void fault_halt(uint32_t line)
{
    fault_line = line;
    __disable_irq();
    for (;;) {
    }
}

app.c — the fixed variant (the good directory): the same application functions without bugs B1…B7.

/* app.c (good) — те же функции, ошибки B1…B7 исправлены */
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include "board.h"
#include "app.h"
#include "fault.h"

#define N_CHANNELS 4u
#define GAIN_MAX   64u
#define LOG_MAX    32u
#define HIST_LEN   16u

typedef struct {
    uint8_t  id;
    uint16_t gain;
    uint16_t errors;
} channel_t;

static channel_t channels[N_CHANNELS] = { { 1u, 1u, 0u }, { 2u, 1u, 0u }, { 5u, 1u, 0u }, { 7u, 1u, 0u } };
static uint16_t hist[HIST_LEN];
static uint32_t app_errors;

static channel_t *find_channel(uint8_t id)
{
    for (uint32_t i = 0u; i < N_CHANNELS; i++) {
        if (channels[i].id == id) {
            return &channels[i];
        }
    }
    return NULL;
}

void channel_set_gain(uint8_t id, uint16_t gain)
{
    channel_t *c = find_channel(id);
    if (c == NULL) {
        app_errors++;
        return;
    }
    if (gain > GAIN_MAX) {
        c->errors++;
        gain = GAIN_MAX;
    }
    c->gain = gain;
}

int log_store(const uint8_t *data, uint32_t n)
{
    if (n > LOG_MAX) {
        return -2;
    }
    uint8_t *rec = malloc(n + 4u);
    if (rec == NULL) {
        return -1;
    }
    memcpy(rec, &n, 4u);
    memcpy(&rec[4], data, n);
    int rc = uart_write(rec, n + 4u);
    free(rec);
    return rc;
}

int log_send(uint8_t *rec, uint32_t n)
{
    int rc = uart_write(rec, n);
    if (rc != 0) {
        app_errors++;
    }
    free(rec);
    return rc;
}

void hist_clear(void)
{
    for (uint32_t i = 0u; i < HIST_LEN; i++) {
        hist[i] = 0u;
    }
}

static int adc_read(uint32_t ch, uint16_t *out)
{
    if (ch > 17u) {
        return -1;
    }
    *out = (uint16_t)(ADC1->DR & 0x0FFFu);
    return 0;
}

uint32_t battery_mv(uint32_t ch)
{
    uint16_t raw;
    if (adc_read(ch, &raw) != 0) {
        app_errors++;
        return 0u;
    }
    return (uint32_t)raw * 3300u / 4095u;
}

uint32_t temp_cdeg(uint32_t ch)
{
    uint16_t raw = 0u;
    if (sensor_read(ch, &raw) != 0) {
        app_errors++;
        return 0u;
    }
    return (uint32_t)raw * 100u / 41u;
}

uint16_t average(const uint16_t *v, uint32_t n)
{
    if ((v == NULL) || (n == 0u)) {
        return 0u;
    }
    uint32_t sum = 0u;
    for (uint32_t i = 0u; i < n; i++) {
        sum += v[i];
    }
    return (uint16_t)(sum / n);
}

void rx_consume(uint32_t n)
{
    uint32_t primask = __get_PRIMASK();
    __disable_irq();
    rx_pending -= n;
    __set_PRIMASK(primask);
}

uint32_t cfg_checksum(const cfg_t *cfg)
{
    if (cfg == NULL) {
        fault_halt(__LINE__);
    }
    uint32_t s = 0u;
    for (uint32_t i = 0u; i < CFG_WORDS; i++) {
        s += cfg->w[i];
    }
    return s;
}

uint32_t cfg_first(const cfg_t *cfg)
{
    CHECK(cfg != NULL);
    return cfg->w[0];
}

Next: section 130 "A board for a microcontroller: principles".

Open in the interactive handbook