Embedded HandbookПриложения
Все разделы › XV. Язык C глубже
STM32F103, STM32F407 · уровень средний · 27 мин чтения
Открыть в интерактивном справочнике

129б. Статический анализ прошивки: clang-анализатор, gcc -fanalyzer и cppcheck в конвейере

Раздел 129 сравнил предупреждения gcc, cppcheck с правилами MISRA и -fanalyzer на отдельном небрежном файле: каждый инструмент запускали руками на одном исходнике. В 129а тот же подход к проверке без платы довели до модульных тестов. Здесь — анализ настоящей сборки под Cortex-M: проект на CMake собирает arm-none-eabi-gcc, по его базе команд compile_commands.json идут три анализатора — gcc -fanalyzer, анализатор clang (clang --analyze) и cppcheck, — а их коды возврата решают, пройдёт ли сборка дальше.

Подопытный — маленькая прошивка на регистрах для F103 и F407 с семью подложенными ошибками разных классов и двумя местами, где ошибки нет, но анализатор может решить иначе. Итог — таблица «ошибка × инструмент» из прогона, список того, чего такой анализ не находит, и способы убрать ложные срабатывания, не выключая проверки.

Опоры: заголовки CMSIS 5.9.0 (core_cm3.h, core_cm4.h), stm32f103xb.h из STM32CubeF1 1.8.7, stm32f407xx.h из пака Keil STM32F4xx_DFP 2.17.1; справка самого clang 14 (clang -cc1 -help, clang -cc1 -analyzer-config-help). Идея связать анализатор clang со сборкой arm-none-eabi-gcc через базу команд — из статьи Memfault о CodeChecker (interrupt.memfault.com, «Static Analysis with CodeChecker»); текст и код здесь свои. Лицензия: статья — только идея, пересказ.

Как это называется в документации

Оригинал Перевод
static analysis статический анализ: поиск ошибок без запуска
path-sensitive analysis анализ путей: обход ветвей с учётом условий
compilation database (compile_commands.json) база команд компиляции
checker проверка анализатора (core.NullDereference, unix.Malloc)
translation unit единица трансляции: один .c со всеми заголовками
cross translation unit analysis (CTU) межмодульный анализ
false positive ложное срабатывание
inline suppression подавление комментарием в строке
noreturn function функция без возврата
quality gate ворота качества: шаг конвейера, который останавливает сборку

129б.1 Что ищем и чем

Компилятор смотрит на каждую строку почти отдельно. Анализатор путей проходит функцию по веткам: «если id не найден, c равен NULL, и тогда строка 45 пишет по нулевому адресу». Так находятся ошибки, которые видны только на одном пути из многих: разыменование NULL, утечка на раннем выходе, двойное освобождение, неинициализированная переменная, деление на ноль.

Инструменты в прогоне:

Инструмент Образ Как запускается
arm-none-eabi-gcc 12.2.1, -Wall -Wextra -O2 stm-arm:1 обычная сборка CMake + Ninja
gcc -fanalyzer (тот же arm-none-eabi-gcc) stm-arm:1 команды из compile_commands.json плюс -fanalyzer
clang 14.0.6, --analyze stm-sys:1 те же команды, переписанные под clang --target=arm-none-eabi
cppcheck 2.10 stm-lint:1 cppcheck --project=compile_commands.json

clang-tidy, обёртка scan-build и CodeChecker в этом разделе не запускались. clang-tidy читает ту же compile_commands.json, а его проверки clang-analyzer-* — это тот же анализатор путей, что в clang --analyze; рецепт с базой команд к нему подходит, но его находки здесь не сверялись.

Сборка arm-none-eabi-gcc пишет базу команд, по ней идут gcc -fanalyzer, clang --analyze и cppcheck в трёх образах, коды возврата образуют ворота; ниже — таблица, какие из подложенных ошибок и двух мест без ошибки отметил каждый инструмент


129б.2 Подопытная прошивка

Шесть файлов: таблица векторов и старт без ассемблера (vectors.c), главный цикл (main.c), USART1 на регистрах с приёмом по прерыванию (uart.c), чтение АЦП (sensor.c), останов с записью строки (fault.c) и прикладная часть app.c в двух вариантах: bad — с ошибками, good — исправленный. Плата и вариант выбираются переменными CMake:

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 — тот же, что в 115а.6, только база команд здесь нужна не редактору, а анализаторам. Заголовки CMSIS подключены как SYSTEM — в команде они идут через -isystem, и замечания к чужому коду не мешают. Прошивка компонуется на обеих платах: у F103 вариант bad — 2028 байт кода, good — 2048; у F407 — 2124 и 2136.

Прикладная часть с ошибками — целиком. Каждая ошибка в своей функции, номер — в комментарии:

/* 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];
}

Что подложено:

/* 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 База команд и clang для arm-none-eabi

В compile_commands.json у сборки F103 шесть записей — по одной на .c. Для app.c ключи, которые нужны анализатору:

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

У F407 — -DSTM32F407xx, -isystem /cmsis/f4 и -mcpu=cortex-m4 -mthumb -mfpu=fpv4-sp-d16 -mfloat-abi=hard. Чтобы clang разобрал ту же команду, не хватает трёх вещей.

  1. Цели. clang один на все архитектуры: --target=arm-none-eabi, а ядро и FPU он берёт из тех же -mcpu, -mfpu, -mfloat-abi.
  2. Заголовков newlib. stdlib.h и string.h arm-none-eabi-gcc подставляет сам, в команде их нет.
  3. Чистой команды. Ключи gcc, которые анализу не нужны (-O2, -ffunction-sections, -o), выбрасываются: остаются макросы, пути, стандарт, ядро и ABI.

Где newlib, показывает arm-none-eabi-gcc -xc -E -v -:

/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

Первые два каталога — заголовки самого gcc (stdint.h, stddef.h): у clang такие свои. Третий — newlib. Его копия с раскрытой ссылкой (cp -rL) становится <sysroot>/include, и --sysroot говорит clang, где искать.

Всё это делает скрипт на 87 строк; он же — ворота: код возврата 1, если замечаний больше порога.

#!/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())

Режим gcc проще: команда та же, к ней добавлен -fanalyzer, объектный файл уходит в /dev/null. Пути в базе команд абсолютные, поэтому все три контейнера видят проект и сборку по одним и тем же путям /w/fw и /w/out.


129б.4 Прогон: кто что нашёл

Сборка F103, вариант bad. В клетке — проверка, которая сработала, и строка, на которую она указала; «—» — инструмент промолчал.

Ошибка gcc -Wall -Wextra gcc -fanalyzer clang --analyze cppcheck
B1 NULL по одной ветке — null-dereference, 45 core.NullDereference, 45 nullPointerRedundantCheck, 45
B2 утечка — malloc-leak, 55 unix.Malloc, 56 memleak, 56
B3 двойной free — double-free, 73 unix.Malloc, 73 doubleFree, 73
B4 выход за массив -Waggressive-loop-optimizations, 81 — — arrayIndexOutOfBounds, 81
B5 неинициализированная -Wmaybe-uninitialized, 101 use-of-uninitialized-value, 101 core.UndefinedBinaryOperatorResult, 101 —
B5x то же, другой файл — — — —
B6 деление на ноль — — core.DivideZero, 123 —
B7 гонка с прерыванием — — — —
F1 (не ошибка) — — ложное: core.NullDereference, 140 ложное: nullPointerRedundantCheck, 140
F2 (не ошибка) — — — ложное: nullPointerRedundantCheck, 149
прочее — — — ложное ×2: comparePointers, vectors.c 21 и 25

Счёт по восьми настоящим ошибкам: -Wall -Wextra — 2, -fanalyzer — 4, clang — 5, cppcheck — 4. Вместе — 6 из 8: B5x и B7 не нашёл никто. Ложных: у gcc — 0, у clang — 1, у cppcheck — 4. Сборка F407 дала ту же таблицу строка в строку: ядро и FPU на находки не влияют.

Что видно:


129б.5 Почему анализатор промолчал

У анализатора путей есть бюджет, и умолчания меняют результат.

Цикл проходится лишь несколько раз. У clang есть проверка границ alpha.security.ArrayBoundV2 (alpha — экспериментальная, по умолчанию выключена). Включённая, она B4 не нашла: до 17-го прохода цикла в hist_clear анализатор не доходит. Зато дала 2 ложных срабатывания в Reset_Handler — на копировании .data и обнулении .bss, где границы задаёт скрипт компоновщика. С ключом -analyzer-max-loop 17 B4 найдена — но пропала B1.

Почему пропала B1. Дольше разбирая циклы, clang смог целиком пройти find_channel и увидел, что NULL пришёл возвратом из вызванной функции. Такие пути он по умолчанию прячет: настройка suppress-null-return-paths (по умолчанию true) — по справке clang, эвристика против отчётов, где путь идёт через функции «осторожнее, чем вызывающий». С suppress-null-return-paths=false B1 вернулась: 6 находок вместо 5. Вывод: поднимать бюджет ради одной ошибки — значит рисковать другими; надёжнее второй инструмент.

Проверка в макросе. F1 и F2 — один и тот же код: проверка на NULL, вызов fault_halt и обращение к cfg. В F1 проверка написана if, и clang выдал ложное срабатывание. В F2 та же проверка внутри макроса CHECK — и clang промолчал. cppcheck отметил оба места. Значит, тишина clang на assert-подобных макросах — не доказательство, что noreturn не нужен.


129б.6 Ложные срабатывания и ворота в конвейере

Лучшее подавление — сказать правду в коде. В исправленном варианте fault.h отличается одной строкой:

/* 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

С __attribute__((noreturn)) и clang, и cppcheck понимают, что после fault_halt пути нет: F1 и F2 исчезли у обоих, а код cfg_checksum и cfg_first не изменился ни на букву.

Исправленные функции:

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);
}

Последняя — лечение B7, которого анализ не требовал: прерывания запрещены на время чтения-изменения-записи, а потом возвращается прежний PRIMASK, а не безусловное разрешение.

Когда код честный, а замечание остаётся. cppcheck считает, что &_sdata и &_edata указывают на разные объекты, и сравнивать их нельзя. Для языка C это правда, но границы задаёт скрипт компоновщика. Такое место подавляется комментарием в строке — с именем проверки, а не целиком:

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();
}

Комментарий работает только с ключом --inline-suppr: без него cppcheck на варианте good выдаёт эти 2 замечания, с ним — 0. На варианте bad с --inline-suppr остаются 6.

Ворота. Каждое задание конвейера — в своём образе, код возврата останавливает сборку:

# Этап анализа для .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

Задания выполнены по очереди так, как их выполнил бы раннер: каждое в своём образе, проект смонтирован в /builds/sa, каталог build/ общий.

Задание bad good
build код 0, замечаний 2 код 0, замечаний 0
gcc-analyzer код 1, замечаний 4 код 0, замечаний 0
clang-analyzer код 1, замечаний 6 код 0, замечаний 0
cppcheck код 1, замечаний 6 код 0, замечаний 0

На bad код возврата 1 у всех трёх анализаторов, на good — 0; сборка с двумя предупреждениями -Wall проходит — -Werror (129.4) здесь не включён. Порог --max нужен старому коду: замечания, которые уже есть, принимают как исходный уровень, а новые не пропускают. На bad с --max 7 clang выдаёт свои 6 находок и код возврата 0. Порог только снижают.


129б.7 Грабли

clang: 'stdlib.h' file not found. Команда из базы — от gcc, а заголовки newlib gcc подставляет сам. Нужен --sysroot с каталогом include от arm-none-eabi-gcc. Копировать с cp -rL: в Debian этот каталог — символьная ссылка, и простой cp -r переносит саму ссылку, которая в другом контейнере никуда не ведёт.

Анализатор не находит файлы из базы команд. Пути в compile_commands.json абсолютные. Сборка в одном контейнере, анализ в другом — проект и каталог сборки монтируют по тем же путям. В GitLab все задания видят проект в $CI_PROJECT_DIR.

CMake was unable to find a build program corresponding to "Ninja". Так выглядит относительный путь к тулчейну: -DCMAKE_TOOLCHAIN_FILE=fw/arm-gcc.cmake CMake ищет от каталога сборки, файл не находится, компилятор не задан — а сообщение говорит о Ninja. Путь — абсолютный, через $CI_PROJECT_DIR.

gcc -fanalyzer с -fsyntax-only молчит. Анализ идёт вместе с компиляцией; без неё замечаний нет. Компилировать в /dev/null.

Включили экспериментальную проверку — получили шум. alpha.security.ArrayBoundV2 на этой прошивке дала 2 ложных и не нашла B4. Экспериментальные проверки — для разовых прогонов, не для ворот.

Подняли бюджет — потеряли находку. -analyzer-max-loop 17 нашёл B4 и спрятал B1 (129б.5). Настройки анализатора закрепляют в скрипте, иначе прогоны нельзя сравнивать.

Анализ чист — значит, гонок нет. Нет: B7 не видит ни один инструмент. Пустой отчёт значит «не нашли то, что умеем искать».


129б.8 Итоги


Полные исходники

main.c — главный цикл: всё, что вызывает прикладные функции.

/* 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 — таблица векторов и старт без ассемблера.

/* 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 — USART1 на регистрах: передача ожиданием, приём по прерыванию.

/* 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 — чтение канала АЦП.

/* 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 — запомнить строку и ждать сброса сторожевым таймером.

/* 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 — исправленный вариант (каталог good): те же прикладные функции без ошибок 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];
}

Дальше: раздел 130 «Плата для микроконтроллера: принципы».

Открыть в интерактивном справочнике