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 подавление комментарием в строке noreturnfunctionфункция без возврата 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; рецепт с базой команд к нему подходит, но его
находки здесь не сверялись.
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];
}
Что подложено:
- B1 —
find_channelвозвращаетNULLдля неизвестногоid; проверка есть только на одной ветке, строка 45 пишет по нулевому адресу. - B2 — при
n > LOG_MAXвыход раньшеfree: утечка кучи. - B3 — при ошибке передачи
recосвобождается дважды. - B4 —
i <= HIST_LEN: запись вhist[16]за концом массива. - B5 — код возврата
adc_readпроверен, ноrawберётся и на ветке ошибки, где его никто не записал. - B5x — то же самое, но функция чтения
sensor_readлежит в другом файле,sensor.c. - B6 — при
n == 0деление наcnt, равный нулю. - B7 —
rx_pending -= nв главном цикле: чтение, вычитание и запись — три команды, и прерывание USART1 между ними теряет свойrx_pending++(та же гонка, чтоcounter++в 30.1). - F1, F2 — ошибки нет: при
cfg == NULLвызываетсяfault_halt, которая не возвращается. Но вfault.hэто не сказано:
/* 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 разобрал ту же команду,
не хватает трёх вещей.
- Цели. clang один на все архитектуры:
--target=arm-none-eabi, а ядро и FPU он берёт из тех же-mcpu,-mfpu,-mfloat-abi. - Заголовков newlib.
stdlib.hиstring.harm-none-eabi-gcc подставляет сам, в команде их нет. - Чистой команды. Ключи 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; выход за массив — только cppcheck и gcc, причём gcc —
не предупреждением о границах, а побочно: оптимизатор при
-O2заметил, что 17-й проход цикла — неопределённое поведение. - cppcheck не увидел неинициализированную
raw(B5): путь идёт через вызовadc_read, которая на ветке ошибки ничего не пишет по указателю. - B5x никто не нашёл. Каждый инструмент разбирает один
.cза раз: проsensor_readизsensor.cизвестно только объявление. Прогон-fanalyzer -fltoпо всей программе одной компоновкой нашёл те же 4 ошибки, а B5x тоже пропустил. Межмодульный режим clang (CTU) здесь не запускался. - B7 не найдёт ни один. Анализатор не знает, что
USART1_IRQHandlerможет прерватьrx_consumeмежду чтением и записью: для него это две независимые функции. Гонки с прерываниями остаются правилам кода (раздел 30) и обзору.
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 Итоги
compile_commands.jsonиз сборки CMake — 6 команд на прошивку; по ним идутgcc -fanalyzer,clang --analyzeс--target=arm-none-eabiи--sysrootк newlib иcppcheck --project.- Из восьми подложенных ошибок:
-Wall -Wextra— 2,-fanalyzer— 4, clang — 5, cppcheck — 4; вместе — 6 из 8. Деление на ноль нашёл только clang, выход за массив — только cppcheck и gcc. - Не нашёл никто: ошибку через функцию из другого файла (B5x, нужен межмодульный анализ) и гонку с прерыванием (B7).
- Ложных: clang — 1, cppcheck — 4, gcc — 0.
noreturnуfault_haltубрал F1 и F2 у обоих; сравнение адресов из скрипта компоновщика — комментарийcppcheck-suppressи--inline-suppr. - Настройки анализатора меняют ответ:
-analyzer-max-loop 17нашёл B4, но спрятал B1. - Ворота: на
badкод возврата 1 у трёх анализаторов, наgood— 0; порог--max— для старого кода, его только снижают.
Полные исходники
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 «Плата для микроконтроллера: принципы».