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

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 в ходу три семейства:

Язык 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.

Что умеет ядро по именам компилятора: у Cortex-M0 с архитектурой 6 и профилем 6M нет ни clz, ни деления, ни DSP, ни FPU; у M3 с профилем 7M есть clz и деление; у M4 с профилем 7EM есть все четыре; ловушка — armclang определяет и GNUC

Одна ловушка: 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.

Файл прошивки: записи HEX, bin без адресов и строка пароля в дампе

Отсюда два правила:

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


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 Итоги

Дальше: раздел 78 «Сокеты и веб-сервер на C».

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