Embedded HandbookПриложения
Все разделы › XII. Сети и DevOps для инженера
STM32F103, STM32F407 · уровень средний · 28 мин чтения
Открыть в интерактивном справочнике

110а. Прошивка под присмотром конвейера: дельта размера на каждую правку и assert без строк в рабочей сборке

Конвейер из раздела 110 собирает образ, гоняет тесты и складывает артефакты. Но один вопрос он оставляет без ответа: насколько эта правка сделала прошивку больше. На F103 с 64 КБ флеш-памяти сборка обычно ломается не от одной большой правки, а от десятка маленьких, каждая из которых «всего на килобайт». Когда компоновщик наконец пишет region FLASH overflowed, найти виноватого уже трудно. Лекарство — мерить размер на каждом запросе слияния и писать разницу прямо в запрос.

Вторая половина раздела — про то, что часто и раздувает образ: утверждения. Библиотечный assert тащит в прошивку имя файла, текст условия и printf. Здесь он заменён макросом, который сохраняет два адреса — где сработало и откуда вызвали, — переживает сброс и потом превращается в файл и строку на компьютере.

Опоры: свой скрипт сравнения size_diff.py на Python (полностью ниже), вывод arm-none-eabi-size, arm-none-eabi-nm и map-файл компоновщика GNU ld; адреса DHCSR и AIRCR — заголовок core_cm3.h из CMSIS 5.9.0; .noinit и SYSRESETREQ — как в разделе 35в, BKPT без отладчика — 35а. Опыты — в образе stm-arm:1: arm-none-eabi-gcc 12.2.1, newlib-nano, QEMU 7.2.22 (машины netduinoplus2 для F4 и stm32vldiscovery для F1), addr2line 2.40. Идея обеих частей — заметки в блоге Interrupt (interrupt.memfault.com) о дельте размера и об утверждениях; текст, код и числа здесь свои.

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

Оригинал Перевод
code size delta дельта размера: на сколько байт выросла сборка
text, data, bss код и константы; начальные значения переменных; обнуляемые переменные
merge request, pull request запрос слияния
merge base общий предок ветки и основной ветки
map file map-файл: где что лежит после компоновки
size budget, threshold бюджет размера, порог
assertion, assert утверждение: проверка предположения во время работы
__FILE__, __FILE_NAME__ путь к файлу, как его передали компилятору; только имя файла (gcc 12+)
__builtin_return_address(0) адрес возврата из текущей функции, то есть LR на входе
DHCSR, C_DEBUGEN регистр отладки ядра; бит «отладчик включил отладку»
cross-jumping слияние одинаковых хвостов кода в одно место

110а.1 Что считать: text, data, bss и две памяти

Сверху запрос слияния: общий предок и ветка собираются, size_diff.py сравнивает их, пишет отчёт и код возврата, справа итоги четырёх правок; снизу сработавшее утверждение: два адреса в .noinit, сброс, после старта addr2line называет rb_push demo.c:39 и receive_frame demo.c:56

arm-none-eabi-size без ключей печатает три числа. text — код и константы, они лежат во флеш-памяти. data — переменные с начальным значением: в ОЗУ они живут, но их начальные значения стартовый код копирует из флеш-памяти, поэтому data считается дважды. bss — обнуляемые переменные, только ОЗУ; сюда же скрипт компоновщика этого раздела кладёт резерв стека и кучи. Отсюда две суммы, которые и надо сторожить:

Опытная прошивка — F103: USART1, АЦП, скользящее среднее и строка температуры через printf("%ld") раз в секунду. Исходная сборка (-Os, newlib-nano): text 7 224, data 100, bss 1 592 — Flash 7 324 байта, ОЗУ 1 692. Из этих 1 592 байт bss 1 536 — резерв стека и кучи.

Одного итога мало. Чтобы понять, что выросло, нужны три среза: секции (size -A), символы (nm --size-sort -S) и файлы — по map-файлу, где у каждой входной секции указан объектник или член библиотеки. Срез по файлам особенно полезен для библиотек: символов в них десятки, а файл один — libc_nano.a(lib_a-dtoa.o).


110а.2 Скрипт сравнения двух сборок

Скрипт берёт два ELF (и, если есть, два map-файла), сам вызывает arm-none-eabi-size и arm-none-eabi-nm, считает разницу и печатает отчёт в Markdown. Код возврата — 0, если пороги соблюдены, 1 — если хоть один превышен, 2 — если что-то не запустилось: конвейер отличит «сборка слишком большая» от «скрипт сломался». Целиком:

#!/usr/bin/env python3
"""Раздел 110а: сравнение размеров двух сборок прошивки для запроса слияния.

size_diff.py BASE.elf NEW.elf [--map-base B.map --map-new N.map] [пороги] [--report out.md]

Разбирает вывод arm-none-eabi-size (-B и -A), arm-none-eabi-nm --size-sort -S
и map-файл компоновщика; печатает отчёт в Markdown; код возврата 0 — пороги
соблюдены, 1 — превышен хотя бы один, 2 — ошибка запуска."""
import argparse
import os
import re
import subprocess
import sys


def tool(prefix, name, *args):
    return subprocess.run([prefix + name, *args], capture_output=True, text=True, check=True).stdout


def parse_berkeley(text):
    """`size -B`: text data bss dec hex имя — берётся последняя строка."""
    f = text.strip().splitlines()[-1].split()
    return {"text": int(f[0]), "data": int(f[1]), "bss": int(f[2])}


def parse_sections(text):
    """`size -A`: «имя размер адрес»; адрес 0 — секция не грузится (отладка, атрибуты)."""
    sec = {}
    for ln in text.splitlines():
        m = re.match(r"^(\.\S+)\s+(\d+)\s+(\d+)\s*$", ln)
        if m and int(m.group(3)) != 0:
            sec[m.group(1)] = int(m.group(2))
    return sec


def parse_symbols(text):
    """`nm --size-sort -S`: «адрес размер тип имя». Имена с одним адресом и размером
    (__aeabi_dadd и __adddf3) — один кусок кода: «имя = имя». Одноимённые static — имя#2."""
    by_addr = {}
    for ln in text.splitlines():
        p = ln.split()
        if len(p) == 4:
            by_addr.setdefault((p[0], p[1]), []).append(p[3])
    sym = {}
    for (addr, size), names in sorted(by_addr.items()):
        name = " = ".join(sorted(names))
        key, i = name, 2
        while key in sym:
            key, i = "%s#%d" % (name, i), i + 1
        sym[key] = int(size, 16)
    return sym


def parse_map(text):
    """Map-файл GNU ld: байты каждого входного файла (объектника или члена архива)
    в загружаемых выходных секциях; секции с адресом 0 — отладочные, не считаются."""
    body = text.split("Linker script and memory map", 1)[-1]
    per_file, out_ok, pend = {}, False, None
    for ln in body.splitlines():
        m = re.match(r"^(\.\S+)(?:\s+0x([0-9a-f]+)\s+0x[0-9a-f]+)?", ln)
        if m:                                      # выходная секция с нулевой колонки
            pend = None
            if m.group(2) is None:
                pend = "out"                       # длинное имя: адрес на следующей строке
            else:
                out_ok = int(m.group(2), 16) != 0
            continue
        m = re.match(r"^ (\.\S+|COMMON)(?:\s+0x([0-9a-f]+)\s+0x([0-9a-f]+)\s+(\S.*))?$", ln)
        if m:                                      # входная секция
            pend = "in" if m.group(2) is None else None
            if m.group(2) is not None and out_ok:
                f = os.path.basename(m.group(4).strip())
                per_file[f] = per_file.get(f, 0) + int(m.group(3), 16)
            continue
        m = re.match(r"^\s+0x([0-9a-f]+)\s+0x([0-9a-f]+)(?:\s+(\S.*))?$", ln)
        if m and pend == "out":
            out_ok = int(m.group(1), 16) != 0
        elif m and pend == "in" and m.group(3) and out_ok:
            f = os.path.basename(m.group(3).strip())
            per_file[f] = per_file.get(f, 0) + int(m.group(2), 16)
        pend = None
    return {k: v for k, v in per_file.items() if v}


def totals(b):
    """Flash = text + data (начальные значения .data лежат во флеш-памяти), ОЗУ = data + bss."""
    return dict(b, flash=b["text"] + b["data"], ram=b["data"] + b["bss"])


def changed(a, b):
    """Изменившиеся ключи: (имя, было, стало), по убыванию модуля разницы."""
    rows = [(k, a.get(k, 0), b.get(k, 0)) for k in set(a) | set(b) if a.get(k, 0) != b.get(k, 0)]
    return sorted(rows, key=lambda r: (-abs(r[2] - r[1]), r[0]))


def verdict(old, new, a):
    """Нарушенные пороги — список строк; пустой список означает зелёную сборку."""
    bad = []
    grow = new["flash"] - old["flash"]
    if a.max_grow_bytes is not None and grow > a.max_grow_bytes:
        bad.append("Flash выросла на %d байт, порог %d" % (grow, a.max_grow_bytes))
    if a.max_grow_pct is not None and grow * 100.0 > a.max_grow_pct * old["flash"]:
        bad.append("Flash выросла на %s %%, порог %s %%" % (pct(grow, old["flash"]), num(a.max_grow_pct)))
    ram_grow = new["ram"] - old["ram"]
    if a.max_ram_grow_bytes is not None and ram_grow > a.max_ram_grow_bytes:
        bad.append("ОЗУ выросло на %d байт, порог %d" % (ram_grow, a.max_ram_grow_bytes))
    if a.flash_size and a.flash_size - new["flash"] < a.min_free_flash:
        bad.append("свободной Flash %d байт, нужно не меньше %d" % (a.flash_size - new["flash"], a.min_free_flash))
    if a.ram_size and a.ram_size - new["ram"] < a.min_free_ram:
        bad.append("свободного ОЗУ %d байт, нужно не меньше %d" % (a.ram_size - new["ram"], a.min_free_ram))
    return bad


def num(x):
    return ("%g" % x).replace(".", ",")


def pct(d, base):
    return ("%+.2f" % (d * 100.0 / base)).replace(".", ",") if base else "—"


def report(name_old, name_new, old, new, sec, sym, files, bad, top):
    out = ["### Размер прошивки: %s → %s" % (name_old, name_new), "",
           "| Область | Было | Стало | Разница |", "|---|---:|---:|---:|"]
    for k, title in (("text", "text"), ("data", "data"), ("bss", "bss"),
                     ("flash", "**Flash** (text+data)"), ("ram", "**ОЗУ** (data+bss)")):
        d = new[k] - old[k]
        out.append("| %s | %d | %d | %+d (%s %%) |" % (title, old[k], new[k], d, pct(d, old[k])))
    for title, rows in (("Секции", sec), ("Символы", sym[:top]), ("Файлы (по map)", files[:top])):
        if rows:
            out += ["", "**%s:**" % title, "", "| Имя | Было | Стало | Разница |", "|---|---:|---:|---:|"]
            out += ["| `%s` | %d | %d | %+d |" % (n, a, b, b - a) for n, a, b in rows]
    out += ["", "**Итог:** " + ("порог превышен — " + "; ".join(bad) if bad else "в пределах порогов"), ""]
    return "\n".join(out)


def main():
    ap = argparse.ArgumentParser(description="дельта размера двух сборок")
    ap.add_argument("base")
    ap.add_argument("new")
    ap.add_argument("--map-base")
    ap.add_argument("--map-new")
    ap.add_argument("--prefix", default="arm-none-eabi-")
    ap.add_argument("--top", type=int, default=8)
    ap.add_argument("--max-grow-bytes", type=int)
    ap.add_argument("--max-grow-pct", type=float)
    ap.add_argument("--max-ram-grow-bytes", type=int)
    ap.add_argument("--flash-size", type=int, default=0)
    ap.add_argument("--ram-size", type=int, default=0)
    ap.add_argument("--min-free-flash", type=int, default=0)
    ap.add_argument("--min-free-ram", type=int, default=0)
    ap.add_argument("--report")
    a = ap.parse_args()
    try:
        old = totals(parse_berkeley(tool(a.prefix, "size", "-B", a.base)))
        new = totals(parse_berkeley(tool(a.prefix, "size", "-B", a.new)))
        sec = changed(parse_sections(tool(a.prefix, "size", "-A", a.base)),
                      parse_sections(tool(a.prefix, "size", "-A", a.new)))
        sym = changed(parse_symbols(tool(a.prefix, "nm", "--size-sort", "-S", a.base)),
                      parse_symbols(tool(a.prefix, "nm", "--size-sort", "-S", a.new)))
        files = []
        if a.map_base and a.map_new:
            with open(a.map_base, encoding="utf-8") as fb, open(a.map_new, encoding="utf-8") as fn:
                files = changed(parse_map(fb.read()), parse_map(fn.read()))
    except (OSError, subprocess.CalledProcessError, ValueError, IndexError) as e:
        print("size_diff: %s" % e, file=sys.stderr)
        return 2
    bad = verdict(old, new, a)
    text = report(a.base, a.new, old, new, sec, sym, files, bad, a.top)
    print(text)
    if a.report:
        with open(a.report, "w", encoding="utf-8") as f:
            f.write(text)
    return 1 if bad else 0


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

Три места, где легко ошибиться.


110а.3 Опыт: четыре правки на настоящей прошивке

От исходной сборки сделаны четыре правки — каждая как отдельный запрос слияния. Первая — вывод вещественным printf:

@@ -1,3 +1,3 @@
 # Настройки сборки: правки из запросов слияния меняют именно их.
 OPT = -Os
-LDFLAGS =
+LDFLAGS = -u _printf_float
@@ -28,8 +28,7 @@

 static void log_temp(int32_t t_dc)
 {
-    int32_t a = t_dc < 0 ? -t_dc : t_dc;
-    printf("t=%s%ld.%ld\n", t_dc < 0 ? "-" : "", (long)(a / 10), (long)(a % 10));
+    printf("t=%.1f\n", t_dc / 10.0f);
 }

 int main(void)

Вторая меняет одну строку config.mk: -O2 вместо -Os. Третья заменяет линейный датчик на терморезистор с таблицей из 33 точек — и забывает const. Четвёртая — та же таблица с const. Пороги для всех одни: Flash не больше чем на 1 024 байта и не больше чем на 2 %, ОЗУ не больше чем на 64 байта, свободными должны остаться 2 048 байт Flash и 1 024 байта ОЗУ (F103C8: 64 КБ и 20 КБ).

Правка text data Flash ОЗУ Сборка
printf("%.1f") +14 308 +364 +14 672 (+200,33 %) +364 красная
-O2 вместо -Os +40 0 +40 (+0,55 %) 0 зелёная
таблица без const +60 +68 +128 (+1,75 %) +68 красная
таблица с const +124 0 +124 (+1,69 %) 0 зелёная

Отчёт для третьей правки — так, как он попадает в запрос:

### Размер прошивки: base/build/fw.elf → lut/build/fw.elf

| Область | Было | Стало | Разница |
|---|---:|---:|---:|
| text | 7224 | 7284 | +60 (+0,83 %) |
| data | 100 | 168 | +68 (+68,00 %) |
| bss | 1592 | 1592 | +0 (+0,00 %) |
| **Flash** (text+data) | 7324 | 7452 | +128 (+1,75 %) |
| **ОЗУ** (data+bss) | 1692 | 1760 | +68 (+4,02 %) |

**Секции:**

| Имя | Было | Стало | Разница |
|---|---:|---:|---:|
| `.data` | 100 | 168 | +68 |
| `.text` | 6796 | 6856 | +60 |

**Символы:**

| Имя | Было | Стало | Разница |
|---|---:|---:|---:|
| `ntc_code` | 0 | 66 | +66 |
| `raw_to_dc` | 20 | 76 | +56 |

**Файлы (по map):**

| Имя | Было | Стало | Разница |
|---|---:|---:|---:|
| `filter.o` | 62 | 184 | +122 |

**Итог:** порог превышен — ОЗУ выросло на 68 байт, порог 64

Что показали срезы.


110а.4 В конвейере: общий предок, порог, отчёт

Сравнивать надо с общим предком ветки и основной ветки, а не с вершиной основной ветки. Опыт: пока запрос с printf("%.1f") висел на проверке, в основную ветку влили правку коллеги, который убрал printf совсем (образ 1 184 байта, −6 140). Сравнение ветки с новой вершиной показало рост +20 812 байт вместо +14 672 — правке приписана чужая экономия. В обратном случае (в основной ветке что-то выросло) правка выглядела бы меньше, чем есть.

Задание для запроса слияния (синтаксис GitLab CI; команды сборки и сравнения — те же, что в опыте):

# Раздел 110а: задание «размер» для запроса слияния (синтаксис GitLab CI).
# Образ сборки — с arm-none-eabi-gcc, make, python3 и git, закреплённый по хэшу (раздел 110.3).
size-delta:
  stage: test
  image: registry.example.com/fw-build@sha256:…
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
  variables:
    GIT_DEPTH: "0"                       # нужна история: иначе общего предка не найти
  script:
    - git fetch origin "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
    - BASE=$(git merge-base "origin/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" HEAD)
    - git worktree add /tmp/base "$BASE"
    - make -s -C /tmp/base
    - make -s
    - python3 tools/size_diff.py /tmp/base/build/fw.elf build/fw.elf
        --map-base /tmp/base/build/fw.map --map-new build/fw.map
        --max-grow-bytes 1024 --max-grow-pct 2 --max-ram-grow-bytes 64
        --flash-size 65536 --ram-size 20480 --min-free-flash 2048 --min-free-ram 1024
        --report size.md
  artifacts:
    when: always                         # отчёт нужен и тогда, когда задание упало
    paths: [size.md, build/fw.elf, build/fw.map]

Правила, которые стоит перенести в любой сервис:

Готовый инструмент для разбора двух ELF по секциям и символам — bloaty (Apache-2.0, bloaty -d symbols new.elf -- old.elf); в разделе он не запускался.


110а.5 Цена утверждения: строки, библиотека, место вызова

Тестовый файл — 100 функций, в каждой по утверждению с разными условиями (p->len <= 1, p->type != 0x08 …). Собран для F103 с -Os по абсолютному пути /builds/acme/sensor-node/firmware/src/drivers/sensors/checks.c (62 знака) — так обычно передаёт пути CMake, и именно этот путь подставит __FILE__. Шесть способов написать ASSERT плюс выключенный, как с NDEBUG:

Вариант Объектник, байт На одно утверждение Образ, text Прибавка к образу
выключено 998 — 1 596 —
assert из newlib 6 584 55,86 14 092 +12 496
__FILE__ и строка 3 291 22,93 3 908 +2 312
то же с -fmacro-prefix-map 3 257 22,59 3 876 +2 280
__FILE_NAME__ и строка 3 237 22,39 3 856 +2 260
PC и LR 2 622 16,24 3 368 +1 772
только вызов 2 222 12,24 2 840 +1 244

Откуда берутся байты.

00000000 <check_000>:
   0:   ldrh    r0, [r0, #0]
   2:   mov r1, lr
   4:   cmp r0, #1
   6:   push    {r3, lr}
   8:   bls.n   10 <check_000+0x10>
   a:   mov r0, pc
   c:   bl  0 <assert_failed>
  10:   pop {r3, pc}

110а.6 Свой assert: два адреса в .noinit, сброс, addr2line

Макрос кладёт в r0 текущий PC (команда mov r0, pc в Thumb читает адрес самой команды плюс 4), в r1 — LR на входе в функцию, то есть место, откуда её вызвали, и уходит в обработчик:

/* Раздел 110а: утверждение, которое оставляет в прошивке два адреса вместо строк. */
#ifndef ASSERT_PC_H
#define ASSERT_PC_H
#include <stdint.h>

#define ASSERT_MAGIC 0x54525341u                 /* байты A S R T в памяти */

typedef struct {
    uint32_t magic;
    uint32_t count;                              /* сработавших подряд с последней очистки */
    uint32_t pc;                                 /* где сработало утверждение */
    uint32_t lr;                                 /* куда вернулась бы функция с ним */
    uint32_t check;                              /* ~(magic ^ count ^ pc ^ lr) */
} assert_rec_t;

extern assert_rec_t assert_rec;
int assert_rec_valid(void);
__attribute__((noreturn)) void assert_failed(uint32_t pc, uint32_t lr);

#define ASSERT_STR2(x) #x
#define ASSERT_STR(x) ASSERT_STR2(x)

/* Номер строки в комментарии делает вставку у каждого утверждения своей:
 * иначе компилятор сливает утверждения одной функции в одно место вызова. */
#define ASSERT(c)                                                        \
    do {                                                                 \
        if (!(c)) {                                                      \
            uint32_t pc_;                                                \
            __asm volatile("mov %0, pc @ line " ASSERT_STR(__LINE__)     \
                           : "=r"(pc_));                                 \
            assert_failed(pc_, (uint32_t)__builtin_return_address(0));   \
        }                                                                \
    } while (0)

#endif

Обработчик пишет 20 байт в .noinit — область в конце ОЗУ, которую стартовый код не трогает (как устроить её в скрипте компоновщика — 35в.2). Если подключён отладчик (бит C_DEBUGEN в DHCSR), ядро останавливается на BKPT прямо на месте. Без отладчика BKPT превратился бы в HardFault (35а), поэтому бит проверяется до команды, а дальше — программный сброс:

/* Раздел 110а: обработчик утверждения — запись в .noinit, затем остановка или сброс. */
#include "assert_pc.h"
#include "regs.h"

assert_rec_t assert_rec __attribute__((section(".noinit")));

static uint32_t rec_check(void)
{
    return ~(assert_rec.magic ^ assert_rec.count ^ assert_rec.pc ^ assert_rec.lr);
}

int assert_rec_valid(void)
{
    return assert_rec.magic == ASSERT_MAGIC && assert_rec.check == rec_check();
}

__attribute__((noreturn, noinline, cold)) void assert_failed(uint32_t pc, uint32_t lr)
{
    __asm volatile("cpsid i" ::: "memory");      /* прерывания больше не мешают */
    uint32_t n = assert_rec_valid() ? assert_rec.count + 1u : 1u;
    assert_rec.magic = ASSERT_MAGIC;
    assert_rec.count = n;
    assert_rec.pc = pc;
    assert_rec.lr = lr;
    assert_rec.check = rec_check();              /* последним: оборванная запись не пройдёт */
    if (SCB_DHCSR & DHCSR_C_DEBUGEN)             /* отладчик подключён — остановиться тут */
        __asm volatile("bkpt #0");
    __asm volatile("dsb" ::: "memory");
    SCB_AIRCR = AIRCR_RESET;                     /* без отладчика — перезапуск, ОЗУ цело */
    __asm volatile("dsb" ::: "memory");
    for (;;) {
    }
}

Опыт — кольцевой буфер на 8 байт. Первый запуск кладёт в него 12 байт, второй читает из пустого:

__attribute__((noinline)) void rb_push(rb_t *b, uint8_t x)
{
    ASSERT(b->count < RB_SIZE);                              /* УТВ-1 */
    b->data[(b->head + b->count) % RB_SIZE] = x;
    b->count++;
}

__attribute__((noinline)) uint8_t rb_pop(rb_t *b)
{
    ASSERT(b->count > 0u);                                   /* УТВ-2 */
    uint8_t x = b->data[b->head];
    b->head = (uint8_t)((b->head + 1u) % RB_SIZE);
    b->count--;
    return x;
}

__attribute__((noinline)) static void receive_frame(unsigned n)
{
    for (unsigned i = 0; i < n; i++)
        rb_push(&rx, (uint8_t)i);                            /* ВЫЗОВ-1 */
}

__attribute__((noinline)) static void drain(unsigned n)
{
    for (unsigned i = 0; i < n; i++)
        sink += rb_pop(&rx);                                 /* ВЫЗОВ-2 */
}

После старта прошивка печатает запись, если она цела. В QEMU (F4, netduinoplus2):

boot
no assert record
dhcsr=0x00000000
boot
ASSERT #1 pc=0x08000174 lr=0x08000199
dhcsr=0x00000000
boot
ASSERT #2 pc=0x080001b2 lr=0x080001d3
dhcsr=0x00000000
halt

DHCSR в QEMU читается нулём — отладчика нет, путь со сбросом. На компьютере адреса превращаются в места по тому же ELF. Из PC вычитается 4 (поправка mov rX, pc), из LR — 3: сбросить бит Thumb и попасть внутрь команды вызова, а не на следующую за ней:

decode #1 pc 0x08000174 at 0x08000170 -> rb_push at demo.c:39 (discriminator 1)
decode #1 lr 0x08000199 call 0x08000196 -> receive_frame at demo.c:56
decode #2 pc 0x080001b2 at 0x080001ae -> rb_pop at demo.c:46 (discriminator 1)
decode #2 lr 0x080001d3 call 0x080001d0 -> drain at demo.c:62

Первая строка каждой пары — где сработало (demo.c:39 и demo.c:46 — строки с ASSERT), вторая — кто позвал (demo.c:56 и demo.c:62). На F1 (stm32vldiscovery) адреса другие — pc=0x08000178, lr=0x0800019d — а ответ addr2line тот же. ELF с отладочными данными для этого обязан храниться рядом с выпущенным образом (110.9); проверить, что запись подходит именно к этому ELF, помогает отпечаток сборки (35в).


110а.7 Грабли

Отчёт показывает чужие правки. Сравнение с вершиной основной ветки: в опыте +20 812 байт вместо настоящих +14 672. Сравнивать с git merge-base.

Таблица без const ест ОЗУ дважды. data лежит и во флеш-памяти, и в ОЗУ: +68 байт ОЗУ на таблицу в 66 байт. Если же таблица static и в неё никто не пишет, gcc сам уводит её в .rodata (в опыте символ стал типа r) — и ошибка не видна, пока кто-нибудь не уберёт static.

printf("%.1f") в newlib-nano молча не печатает. Без -u _printf_float функции вывода вещественных в образ не попадают (_printf_float в таблице символов нет), хотя образ всё равно вырос до 9 304 байт text. Ключ включает печать — и ещё 12 КБ.

С -flto пропадает _write. Библиотеку компилятор при оптимизации всей программы не видит и выбрасывает функцию, которую зовёт только она: undefined reference to _write. Помогает __attribute__((used)).

Утверждения одной функции сливаются в одно. Три ASSERT подряд в одной функции, вариант «только вызов»: и при -Os, и при -O2 остаётся одно место вызова, и любое из трёх утверждений покажет строку 6. С mov r0, pc то же самое, пока вставки одинаковые: одно место, строка 8. Номер строки в комментарии вставки (@ line 7) делает их разными — три места, строки 6, 7 и 8. Цена — те же 8 байт команд, но gcc перестаёт выносить ветку с утверждением в отдельную функцию: без номера строки у всех 100 функций тестового файла появились .part.0, а в них __builtin_return_address(0) указывает уже не на вызывающего, а обратно в саму функцию. С номером строки — ни одной .part.0, и объектник вырос с 2 232 до 2 622 байт.

BKPT без отладчика. Не остановка, а HardFault. Сначала проверить C_DEBUGEN.

assert с побочным действием. ASSERT(init() == 0) при выключенных утверждениях не вызовет init (74.9). Это правило не меняется от того, что утверждения оставлены в рабочей сборке: их могут выключить завтра.

Утверждение там, где сброс опаснее ошибки. Обработчик перезапускает прибор. Если на выходах в этот момент привод или нагреватель, сначала безопасное состояние (129.7), а проверку входных данных извне — обычной обработкой ошибки, не утверждением (129.6).


110а.8 Итоги

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

app.c — главный цикл прошивки, на которой меряются четыре правки.

/* Раздел 110а: главный цикл — раз в 10 мс отсчёт АЦП, раз в секунду строка в UART. */
#include <stdio.h>

#include "filter.h"
#include "regs.h"
#include "uart.h"

static filt_t filt;

static void adc_init(void)
{
    RCC_APB2ENR |= APB2_ADC1EN;
    ADC1_CR2 = ADC_CR2_ADON;
    ADC1_CR2 |= ADC_CR2_CAL;
    while (ADC1_CR2 & ADC_CR2_CAL) {
    }
    ADC1_SQR3 = 0;                          /* канал 0 — PA0 */
    ADC1_CR2 |= ADC_CR2_SWSTART_SEL | ADC_CR2_EXTTRIG;
}

static uint16_t adc_read(void)
{
    ADC1_CR2 |= ADC_CR2_SWSTART;
    while (!(ADC1_SR & ADC_SR_EOC)) {
    }
    return (uint16_t)ADC1_DR;
}

static void log_temp(int32_t t_dc)
{
    int32_t a = t_dc < 0 ? -t_dc : t_dc;
    printf("t=%s%ld.%ld\n", t_dc < 0 ? "-" : "", (long)(a / 10), (long)(a % 10));
}

int main(void)
{
    uart_init();
    adc_init();
    uart_puts("boot\n");
    SYST_RVR = 8000000u / 100u - 1u;        /* 10 мс от HSI 8 МГц */
    SYST_CSR = 5u;                          /* ENABLE, CLKSOURCE = ядро */
    unsigned n = 0;
    for (;;) {
        while (!(SYST_CSR & SYST_CSR_COUNTFLAG)) {
        }
        filt_push(&filt, adc_read());
        if (++n == 100u) {
            n = 0;
            log_temp(raw_to_dc(filt_avg(&filt)));
        }
    }
}

assert_cfg.h — пять способов написать ASSERT для замера цены.

/* Раздел 110а: пять способов написать ASSERT — для замера цены. VARIANT задаётся ключом -D. */
#ifndef ASSERT_CFG_H
#define ASSERT_CFG_H
#include <stdint.h>

typedef struct {
    uint16_t len;
    uint8_t type, flags;
    uint32_t id;
} pkt_t;

#if VARIANT == 0                                 /* выключено, как с NDEBUG */
#define ASSERT(c) ((void)0)
#elif VARIANT == 1                               /* assert из newlib */
#include <assert.h>
#define ASSERT(c) assert(c)
#elif VARIANT == 2 || VARIANT == 3               /* файл и строка */
__attribute__((noreturn)) void assert_file_line(const char *file, int line);
#if VARIANT == 2
#define ASSERT(c) do { if (!(c)) assert_file_line(__FILE__, __LINE__); } while (0)
#else
#define ASSERT(c) do { if (!(c)) assert_file_line(__FILE_NAME__, __LINE__); } while (0)
#endif
#elif VARIANT == 4                               /* PC и LR */
#include "assert_pc.h"
#elif VARIANT == 5                               /* только вызов: адрес берёт обработчик */
__attribute__((noreturn)) void assert_here(void);
#define ASSERT(c) do { if (!(c)) assert_here(); } while (0)
#endif

#endif

measure.c — прошивка для замера: сто проверок и обработчики вариантов.

/* Раздел 110а: прошивка для замера — вызывает все 100 проверок; обработчики вариантов 2, 3, 5. */
#include "assert_cfg.h"
#include "regs.h"

extern int (*const checks[100])(const pkt_t *);
volatile int acc;
const pkt_t probe = {12, 3, 1, 7};

#if VARIANT == 2 || VARIANT == 3 || VARIANT == 5
static struct {
    uintptr_t where;
    int line;
} last __attribute__((section(".noinit")));

static __attribute__((noreturn)) void reset(void)
{
    __asm volatile("dsb" ::: "memory");
    SCB_AIRCR = AIRCR_RESET;
    for (;;) {
    }
}
#endif

#if VARIANT == 2 || VARIANT == 3
void assert_file_line(const char *file, int line)
{
    last.where = (uintptr_t)file;
    last.line = line;
    reset();
}
#elif VARIANT == 5
__attribute__((noinline)) void assert_here(void)
{
    last.where = (uintptr_t)__builtin_return_address(0);
    reset();
}
#endif

int main(void)
{
    for (unsigned i = 0; i < 100u; i++)
        acc += checks[i](&probe);
    for (;;) {
    }
}

demo.c — опыт со своим assert: переполнение кольца, чтение из пустого, запись в .noinit.

/* Раздел 110а: опыт. Запуск 1 — переполнение кольцевого буфера, запуск 2 — чтение
 * из пустого, запуск 3 — только печать записи. Между запусками — сброс из assert_failed. */
#include "assert_pc.h"
#include "regs.h"

#define RB_SIZE 8u

typedef struct {
    uint8_t data[RB_SIZE];
    uint8_t head, count;
} rb_t;

static rb_t rx;
volatile uint32_t sink;

static void putc1(char c)
{
    while (!(USART1_SR & (1u << 7))) {
    }
    USART1_DR = (uint32_t)c;
}

static void puts1(const char *s)
{
    while (*s)
        putc1(*s++);
}

static void puthex(uint32_t v)
{
    static const char hex[] = "0123456789abcdef";
    puts1("0x");
    for (int i = 28; i >= 0; i -= 4)
        putc1(hex[(v >> i) & 0xFu]);
}

__attribute__((noinline)) void rb_push(rb_t *b, uint8_t x)
{
    ASSERT(b->count < RB_SIZE);                              /* УТВ-1 */
    b->data[(b->head + b->count) % RB_SIZE] = x;
    b->count++;
}

__attribute__((noinline)) uint8_t rb_pop(rb_t *b)
{
    ASSERT(b->count > 0u);                                   /* УТВ-2 */
    uint8_t x = b->data[b->head];
    b->head = (uint8_t)((b->head + 1u) % RB_SIZE);
    b->count--;
    return x;
}

__attribute__((noinline)) static void receive_frame(unsigned n)
{
    for (unsigned i = 0; i < n; i++)
        rb_push(&rx, (uint8_t)i);                            /* ВЫЗОВ-1 */
}

__attribute__((noinline)) static void drain(unsigned n)
{
    for (unsigned i = 0; i < n; i++)
        sink += rb_pop(&rx);                                 /* ВЫЗОВ-2 */
}

int main(void)
{
    RCC_APB2ENR |= USART1_EN;
    USART1_BRR = BRR_115200;
    USART1_CR1 = (1u << 13) | (1u << 3);                     /* UE, TE */

    puts1("boot\n");
    uint32_t n = 0;
    if (assert_rec_valid()) {
        n = assert_rec.count;
        puts1("ASSERT #");
        putc1((char)('0' + n));
        puts1(" pc=");
        puthex(assert_rec.pc);
        puts1(" lr=");
        puthex(assert_rec.lr);
        puts1("\n");
    } else {
        puts1("no assert record\n");
    }
    puts1("dhcsr=");
    puthex(SCB_DHCSR);
    puts1("\n");

    if (n == 0u)
        receive_frame(12);                                   /* в буфер влезает 8 */
    if (n == 1u)
        drain(3);                                            /* буфер после сброса пуст */
    puts1("halt\n");
    for (;;) {
    }
}

Дальше: раздел 110б «Плата на столе в конвейере: pytest, последовательный порт и свой исполнитель» — тот же конвейер запускает тесты на плате: фикстура pytest, порт с тайм-аутом и свой исполнитель с тегом bench.

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