77. Компиляция и компиляторы
Стадии сборки — препроцессор, компилятор, ассемблер, компоновщик — разобраны в разделе 63. Здесь — про сам компилятор как инструмент: какие бывают для STM32, как код узнаёт, каким компилятором и под какое ядро его собирают, какие ключи обязательны и какие предупреждения стоит включить. А в конце — что лежит в файле прошивки и что по нему видно.
Всё сказанное о ключах и предупреждениях снято с armclang из Keil —
того самого, которым проверен весь справочник. Где GCC ведёт себя иначе,
это сказано отдельно.
Как это называется в документации
Оригинал Перевод toolchain набор инструментов сборки predefined macro предопределённое имя target, CPU, FPU options ключи цели, ядра, FPU ABI, float ABI соглашение о двоичном интерфейсе, о передаче вещественных warning, -Werrorпредупреждение, превращение их в ошибки section, garbage collection секция, удаление неиспользуемого debug information (DWARF) отладочная информация ELF, Intel HEX, binary форматы образа symbol table таблица символов strip удаление символов
77.1 Компиляторы для STM32
Для Cortex-M в ходу три семейства:
- Arm Compiler 6 —
armclangв Keil. Построен на clang и LLVM; в этом справочнике — версия 6.24 на основе clang 20; - GCC —
arm-none-eabi-gcc, его использует CubeIDE; - IAR — коммерческий, со своими средой и библиотекой.
Язык C у них общий, а различаются расширения (__attribute__, встроенные
функции), стандартные библиотеки (раздел 74), описание раскладки памяти
для компоновщика и стартовые файлы — поэтому в паке устройства для каждого
компилятора свой вариант стартового файла (раздел 65.9).
77.2 Как код узнаёт компилятор и ядро
Компилятор сам определяет несколько имён, по которым код может понять, где его собирают:
| Имя | M0 | M3 | M4 | Что значит |
|---|---|---|---|---|
__ARMCC_VERSION |
6240002 | 6240002 | 6240002 | Arm Compiler 6.24 |
__clang_major__ |
20 | 20 | 20 | версия clang внутри |
__ARM_ARCH |
6 | 7 | 7 | поколение архитектуры |
__ARM_ARCH_6M__ / _7M__ / _7EM__ |
6M | 7M | 7EM | профиль |
__ARM_FEATURE_CLZ |
нет | есть | есть | команда clz (раздел 76.8) |
__ARM_FEATURE_IDIV |
нет | есть | есть | деление в железе (раздел 75.4) |
__ARM_FEATURE_DSP |
нет | нет | есть | команды обработки сигналов |
__ARM_FP |
нет | нет | есть | FPU |
Так пишут код, который сам выбирает быстрый путь: с clz, если он есть,
и с запасным вариантом для M0.
Одна ловушка: armclang определяет и __GNUC__ — для совместимости с
кодом, написанным под GCC. Поэтому проверка «#ifdef __GNUC__ — значит
GCC» ошибочна: сначала проверяют __ARMCC_VERSION и __clang__. Именно
так поступает cmsis_compiler.h из CMSIS: по этим именам он подключает
cmsis_armclang.h или cmsis_gcc.h, и функции вроде __REV и __CLZ
выглядят одинаково под любым компилятором.
77.3 Ключи под ядро и FPU
Главные ключи — ядро (-mcpu) и всё, что касается вещественных чисел.
Отдельно стоит редакция языка, ключ -std=: от неё зависит смысл кода.
В C23 int f() означает int f(void) (раздел 66.2), а bool, true
и false стали ключевыми словами и спорят со старыми typedef. armclang
из MDK 5.43 и arm-none-eabi-gcc 12.2 по умолчанию собирают как C17, а новые
версии GCC перешли на C23 — поэтому редакцию в проекте пишут явно,
например -std=c17.
Функция float scale(float x) { return x * 2.5f; } на Cortex-M4 с
разными ключами:
| Ключи | Что получилось |
|---|---|
-mcpu=cortex-m4 |
команды FPU, но x приходит в r0: переложить в s0, умножить, переложить обратно — 5 команд |
-mfloat-abi=softfp |
то же самое |
-mfloat-abi=hard |
x сразу в s0 — 3 команды |
-mfpu=none |
вызов библиотечной __aeabi_fmul |
-mfloat-abi меняет соглашение о вызовах: где передаются вещественные
аргументы — в обычных регистрах или в регистрах FPU. Отсюда опасное
следствие. Функция собрана с -mfloat-abi=hard, а вызывающий её файл —
без этого ключа. armlink собрал такую пару молча, без ошибки и без
предупреждения. А по машинному коду видно, что вызывающая сторона кладёт
аргумент 1.0 в r0, тогда как функция читает его из s0: она получит не
то число. Все файлы проекта и все библиотеки должны быть собраны с
одинаковым -mfloat-abi. В Keil это проверяют сами. GNU ld, которым
собирает CubeIDE, такую пару не соберёт: «uses VFP register arguments,
… does not» — один объект передаёт вещественные в регистрах FPU, другой
нет. Поэтому стандартная библиотека GCC лежит в нескольких вариантах, и
arm-none-eabi-gcc по ключам сам подставляет подходящий.
Выбранный вариант код видит в именах: __ARM_PCS_VFP появляется только
с -mfloat-abi=hard, __SOFTFP__ — только без FPU. Как FPU включается при
старте — раздел 69.8.
77.4 Предупреждения: сводная таблица
По ходу части IX набралась коллекция ошибок, которые компилятор умеет находить. Вот они вместе — проверены на одном файле, по строке на ошибку:
| Ошибка | armclang без ключей |
armclang -Wall -Wextra |
GCC: с какого ключа | Раздел |
|---|---|---|---|---|
if (a = 5) |
да | да | -Wall |
65.4 |
x & 1 == 0 |
да | да | -Wall |
65.3 |
if (e); |
да | да | -Wextra |
65.4 |
sizeof массива-параметра |
да | да | без ключей | 67.5 |
| адрес локальной переменной из функции | да | да | без ключей | 67.10 |
strcpy длинной строки в короткий буфер |
да | да | без ключей | 74.5 |
| неинициализированный указатель | — | да | -Wall |
67.10 |
| знаковое сравнивается с беззнаковым | — | да | -Wextra |
64.6 |
| знаковое делится на беззнаковое | — | — | -Wsign-conversion |
64.6 |
провал в switch |
— | — | -Wextra |
65.6 |
| затенение глобальной | — | — | -Wshadow |
66.4 |
| массив переменной длины | — | — | -Wvla |
68.5 |
неявный double |
— | — | -Wdouble-promotion |
69.7 |
У armclang нижние пять не ловят даже -Wall -Wextra. У GCC
(arm-none-eabi-gcc 12; CubeIDE собирает тем же GCC) картина другая: без
ключей он ловит только три ошибки, а верхнюю часть таблицы — лишь с
-Wall и -Wextra, так что в CubeIDE ключи нужны тем более. Поэтому для
проектов на камне стоит сразу включить расширенный набор:
-Wall -Wextra -Wshadow -Wdouble-promotion -Wimplicit-fallthrough -Wvla -Wsign-conversion
С ним тот же файл дал предупреждение на каждой из тринадцати строк —
и у armclang, и у GCC.
Ключ -Werror превращает предупреждения в ошибки: сборка
останавливается. В новом проекте его стоит включить с первого дня — тогда
предупреждения не накапливаются. В старом, где их сотни, — только после
того, как их разобрали.
77.5 Компоновщик выбрасывает лишнее
armclang кладёт каждую функцию в свою секцию — .text.used,
.text.unused, — и компоновщик по умолчанию выбрасывает секции, на
которые никто не ссылается. В карте компоновки это видно прямо:
Removing g.o(.text.unused). Поэтому неиспользуемые функции драйверов и
библиотек не занимают Flash. Ключ --no_remove оставляет всё.
У GCC так не делается само: функция unused без специальных ключей
осталась в программе, а с -ffunction-sections при компиляции и
-Wl,--gc-sections при компоновке — исчезла. В проектах CubeIDE эти ключи
включены по умолчанию; в своём Makefile про них легко забыть.
77.6 Отладочная информация не попадает в прошивку
Ключ -g добавляет в объектный файл описание исходника: строки, типы,
имена переменных — всё, что нужно отладчику (раздел 35). В проверенном
примере отладочных данных в объектном файле стало почти в десять раз
больше; точное число зависит даже от длины пути к исходнику.
А код и данные прошивки не изменились ни на байт — 340 байт кода в
обоих вариантах. Отладочная информация живёт в файле образа на
компьютере и во Flash не записывается. Поэтому -g оставляют включённым
всегда, в том числе в выпускных сборках: прошивка от него не растёт, а
разбирать отказ с ним гораздо проще.
На компьютере похожую роль играет strip: он убирает таблицу символов из
исполняемого файла. Программа уменьшилась с 16 016 до 14 472 байт и
продолжила работать — символы нужны людям и отладчику, а не процессору.
77.7 Из образа — в файл прошивки
Компоновщик создаёт файл образа ELF (в Keil — .axf). Программатору
чаще отдают один из двух форматов:
| Формат | Что внутри |
|---|---|
| Intel HEX | текстовые записи с адресом и контрольной суммой (раздел 48) |
| bin | голые байты подряд, без адресов |
fromelf --i32 превратил проверенный образ в HEX: первая запись —
:020000040800F2, то есть старшая половина адреса 0x0800 (Flash
начинается с 0x0800 0000); дальше данные; последняя — :00000001FF, конец
файла. Контрольная сумма каждой записи сходится.
fromelf --bin дал 356 байт — ровно код и константы подряд. Адреса в bin
нет: где эти байты должны лежать, программатору говорят отдельно.
77.8 Инструменты разбора
| Задача | Keil | GNU |
|---|---|---|
| дизассемблировать | fromelf -c |
objdump -d |
| таблица символов | fromelf -s |
nm |
| размеры секций | fromelf -z |
size |
| файл прошивки | fromelf --i32, --bin |
objcopy -O ihex, -O binary |
| убрать символы | — | strip |
nm показывает, где что определено. В объектном файле с main,
вызывающей fact, у fact стоит U — не определена здесь, а у main —
T, определена. В файле с телом fact у неё T. Связывает их
компоновщик (раздел 63.7), и после компоновки программа выдаёт 5! = 120.
Какой объектный файл дал каждое имя, видно в карте компоновки (раздел
71.6).
77.9 Что видно в прошивке
Строка, записанная в программе, лежит в файле прошивки открытым
текстом. Функция сравнения пароля со строкой "hunter2-secret" — и в
bin-файле эта строка находится простым поиском. То же увидит любой, кто
прочитает Flash.
Отсюда два правила:
- секретов открытым текстом в прошивке не хранят: вместо пароля — его хеш, вместо ключа — ключ в защищённой области или вовсе в отдельной микросхеме (раздел 43);
- закрывают чтение Flash байтами конфигурации (раздел 11.6): тогда обычный отладчик прошивку не прочитает. Но уровень 1 защищает от любопытных, а не от лаборатории: у F0 и F1 его обходят известными приёмами через отладочный интерфейс. Уровень 2 навсегда отключает отладку и прочнее (у F1 его нет), но и он против сбоя питания не абсолютен (раздел 2, вопрос 8). Ключ, утечка которого стоит дорого, держат в отдельной микросхеме.
И обратное: условный переход в машинном коде легко заменить на противоположный, превратив «пароль неверен» в «пароль верен». Проверка, которую можно обойти правкой одного байта, — не защита, пока образ можно прочитать и записать.
77.10 Грабли
Код «под GCC» ведёт себя странно в Keil. Проверка __GNUC__:
armclang его тоже определяет.
Вещественная функция из библиотеки получает не те аргументы.
Библиотека собрана с другим -mfloat-abi; armlink этого не замечает,
GNU ld откажет при компоновке.
Вещественная функция на M4 медленнее, чем ожидалось. Не включён
-mfloat-abi=hard: аргументы перекладываются между регистрами.
Ошибку поймал коллега, а у вас сборка чистая. Предупреждение выключено; включите расширенный набор.
Старый проект не собирается с -Werror. Сначала разобрать накопленные
предупреждения.
Прошивка на GCC больше, чем в Keil. Не включены -ffunction-sections
и --gc-sections.
Выключили -g, чтобы прошивка стала меньше. Она не изменилась:
отладочная информация во Flash не попадает.
Программатор записал bin не туда. В bin нет адреса; его задают программатору.
Пароль из прошивки нашли за минуту. Лежал открытым текстом, чтение Flash не закрыто.
77.11 Итоги
- Для STM32 — Arm Compiler 6 (
armclang), GCC и IAR; язык общий, расширения, библиотеки и стартовые файлы — свои. - Компилятор сообщает версию, архитектуру и возможности ядра
предопределёнными именами: у M3 есть
clzи деление, у M0 — нет.armclangопределяет и__GNUC__. -mfloat-abi=hardпередаётfloatв регистрах FPU; объекты с разным соглашениемarmlinkсобирает молча, и они передают друг другу не те числа; GNU ld такую пару не соберёт.- Без ключей
armclangловит шесть частых ошибок,-Wall -Wextra— ещё две, остальные пять — только расширенный набор; GCC без ключей — лишь три, с-Wall -Wextra— девять.-Werrorделает их ошибками. armlinkвыбрасывает неиспользуемые функции сам; GCC — с-ffunction-sectionsи--gc-sections.-gне увеличивает прошивку ни на байт.- HEX несёт адреса и контрольные суммы, bin — только байты.
- Строки из программы читаются в прошивке открытым текстом: секреты так не хранят, чтение Flash закрывают.
Дальше: раздел 78 «Сокеты и веб-сервер на C».