Скрипт компоновщика GNU ld для микроконтроллеров
Скрипт компоновщика решает, где в памяти микроконтроллера окажутся код, константы, переменные, таблица векторов и стек. Разбираем команды MEMORY и SECTIONS, счётчик адреса, VMA и LMA, KEEP и PROVIDE, шаблон ST для STM32F103 и стандартный скрипт avr-gcc, map-файл и сообщения о переполнении памяти — с расчётом свободного ОЗУ под стек.
Обновлено Редакция GAW
Содержание
- Как компоновщик собирает программу
- MEMORY: области памяти
- SECTIONS: входные и выходные секции
- VMA и LMA: где секция живёт и где хранится
- Разбор шаблона ST для STM32F103
- Стек и куча: что проверяет компоновщик
- Проверка: map-файл, size -A и --print-memory-usage
- Пример: сколько ОЗУ остаётся стеку на STM32F103
- Стандартный скрипт avr-gcc: что в нём иначе
- Своя секция: .noinit, функция в ОЗУ
- Ошибки компоновщика и как их читать
Скрипт компоновщика (linker script, файл .ld) — текстовое описание для компоновщика GNU ld: какая память есть у микроконтроллера, в каком порядке и в какие её области сложить код и данные из объектных файлов и какие адреса сообщить коду запуска. Компилятор раскладывает функции и переменные по входным секциям (.text, .rodata, .data, .bss и их вариантам), а компоновщик по скрипту собирает из них выходные секции, назначает адреса и проверяет, что всё поместилось. У STM32 скрипт — файл проекта, его передают ключом -T. Компилятор avr-gcc пользуется скриптом, встроенным в binutils, поэтому его редко видят, но устроен он так же.
Коротко
- MEMORY описывает области памяти: имя, атрибуты, начало
ORIGINи размерLENGTH. Атрибуты вроде(rx)только подсказывают, в какую область класть секцию, если для неё не указана область>REGION; от записи они ничего не защищают. - SECTIONS собирает выходные секции из входных по шаблонам вроде
*(.text*); каждая входная секция достаётся первому подходящему правилу. >RAM AT>FLASH— переменные работают в ОЗУ (VMA), а их начальные значения хранятся во флеш-памяти (LMA). Код запуска копирует их по символам_sidata,_sdata,_edataи обнуляет.bssот_sbssдо_ebss.- KEEP защищает секцию от удаления ключом
--gc-sections: таблица векторов не нужна никакому коду, и без KEEP её можно потерять. - Переполнение компоновщик сообщает строкой
region `RAM' overflowed by N bytes; карту памяти пишет в map-файл (-Wl,-Map=…), сводку по областям — по ключу-Wl,--print-memory-usage. - У AVR адреса ОЗУ в скрипте сдвинуты на 0x800000, а константы без
PROGMEMна классических AVR попадают в.dataи копируются в ОЗУ.
Как компоновщик собирает программу
Каждый файл исходного текста компилятор превращает в объектный файл с набором секций. Код функций ложится в .text, константы и строки — в .rodata, переменные с ненулевым начальным значением — в .data, переменные без начального значения или с нулевым — в .bss. С ключами -ffunction-sections и -fdata-sections каждая функция и каждая переменная получают собственную секцию, названную по их имени, например .text.main или .bss.counter. Это позволяет ключу компоновщика --gc-sections выбросить неиспользуемые функции поштучно: он оставляет секцию с точкой входа и рекурсивно всё, на что есть ссылки, а остальное удаляет.
Компоновщик читает скрипт, собирает выходные секции, раскладывает их по областям памяти подряд, вычисляет значения символов, подставляет адреса в команды и пишет файл ELF. Как устроен ELF и что из него попадает в HEX и BIN, разобрано в статье о файлах прошивки.
Откуда берётся скрипт:
- STM32 и другие Cortex-M. Скрипт — файл проекта, ключ GCC
-T script.ldпередаёт его компоновщику. Документация GCC отмечает, что для «голых» платформ без операционной системы-Tбывает обязателен. Скрипт из-Tзаменяет стандартный, а не дополняет его, поэтому в нём должно быть описано всё. - AVR. avr-gcc по ключу
-mmcuвыбирает эмуляцию компоновщика, для ATmega328P этоavr5, и ld использует встроенный скрипт, собранный из шаблонаscripttempl/avr.scи параметровemulparams/avr5.sh. Ключ--verboseзаставляет ld напечатать скрипт, которым он пользуется:avr-gcc -mmcu=atmega328p … -Wl,--verbose.
В скрипте три вида содержимого: описание памяти (MEMORY), правила размещения секций (SECTIONS) и команды с присваиваниями символов: ENTRY, PROVIDE, ASSERT и выражения вида _estack = …;.
MEMORY: области памяти
Команда MEMORY перечисляет области памяти. Так она выглядит в шаблоне ST для STM32F103xB (выдержка из STM32F103XB_FLASH.ld):
MEMORY
{
FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 128K
RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 20K
}
ORIGIN — начальный адрес, LENGTH — размер в байтах; суффиксы K и M умножают число на 1024 и 1024 × 1024. Имена областей существуют только внутри скрипта и с именами символов и секций не пересекаются. Значения подставляются в выражения функциями ORIGIN(RAM) и LENGTH(RAM).
В скобках после имени — атрибуты:
| Буква | Какие секции подходят |
|---|---|
| R | только для чтения |
| W | для чтения и записи |
| X | исполняемые |
| A | размещаемые в памяти (allocatable) |
| I, L | инициализированные |
| ! | инвертирует проверку для следующих букв |
С атрибутами связано частое заблуждение. Они не делают область доступной только на чтение и ничего не запрещают. Компоновщик использует их, чтобы выбрать область для выходной секции, у которой не указаны ни адрес, ни область >REGION: он берёт первую область с подходящими атрибутами. Сюда относятся и «сироты» — входные секции, не упомянутые ни в одном правиле SECTIONS, для которых компоновщик сам создаёт выходную секцию с тем же именем. Запись RW!X подходит секции для чтения или записи, но только если она не исполняемая. Для секций, направленных в область явно (>FLASH), атрибуты не проверяются.
Область нужна компоновщику для двух вещей. Он ставит очередную секцию на следующий свободный адрес области и проверяет, что секции в неё помещаются; переставлять секции, чтобы они влезли, компоновщик не будет. Если не помещаются — сборка заканчивается ошибкой. Только эта проверка и связывает скрипт с реальным микроконтроллером: если LENGTH больше настоящей памяти, программа соберётся и сломается уже на плате.
SECTIONS: входные и выходные секции
Правило SECTIONS описывает выходную секцию: имя, список входных секций и область памяти. Общий вид:
SECTIONS
{
.output_name :
{
*(.input_name) /* input sections from all files */
*(.input_name*) /* .input_name and .input_name.anything */
symbol = .; /* a symbol gets the current address */
} >VMA_REGION AT>LMA_REGION
}
Что важно знать о шаблонах входных секций:
*(.text)— секции.textиз всех входных файлов;*(.text*)— ещё и.text.main,.text.Reset_Handlerи прочие, которые дают-ffunction-sections.*(.text .rodata)перемешивает секции в порядке входных файлов, а*(.text) *(.rodata)кладёт сначала все.text, потом все.rodata.- Вместо
*можно указать файл:data.o(.data); аEXCLUDE_FILEисключает файлы из шаблона. COMMON— так называемые общие символы, неинициализированные глобальные переменные старого стиля; их кладут в.bss:*(.bss) *(COMMON).- Если входная секция подходит под несколько правил, она достаётся первому из них по тексту скрипта.
Счётчик адреса «.»
Точка — счётчик адреса, текущее место размещения. symbol = .; создаёт символ со значением текущего адреса, . = ALIGN(4); выравнивает счётчик вверх до кратного 4, . = . + 0x400; оставляет дыру в 1 КБ. Назад внутри выходной секции счётчик двигать нельзя. Есть тонкость: внутри описания секции «.» — смещение от её начала, а не абсолютный адрес, так что . = 0x200 внутри .text означает «секция длиной 0x200 байт», а не «перейти на адрес 0x200».
Символы скрипта в программе на C
Символ, определённый в скрипте, — это адрес без значения: компоновщик создаёт запись в таблице символов, но память под переменную не выделяет. Поэтому в C его объявляют внешним объектом и берут адрес, а не значение:
extern uint32_t _sdata, _edata; /* defined in the linker script */
size_t data_size(void)
{
return (size_t)((uintptr_t)&_edata - (uintptr_t)&_sdata); /* &symbol, never symbol */
}
Второй способ — объявить символ массивом: extern char _sdata[];, тогда имя само даёт адрес. Обращение _sdata без & прочитает слово по адресу начала .data, то есть значение первой переменной, — частая ошибка.
PROVIDE, KEEP, ENTRY, /DISCARD/ и ASSERT
PROVIDE(symbol = …)определяет символ, только если на него ссылаются и он не определён в программе. Если программа определит символ сама, используется её определение, без ошибки о повторном определении.PROVIDE_HIDDENделает то же, но символ получает скрытую видимость ELF и не экспортируется из выходного файла; для прошивки разницы нет.KEEP(*(.isr_vector))не даёт--gc-sectionsудалить секцию. Компоновщик оставляет то, до чего можно дойти по ссылкам от точки входа, а на таблицу векторов не ссылается ни одна функция: её читает ядро. То же касается.init_arrayи.fini_arrayсо списками конструкторов.ENTRY(Reset_Handler)задаёт точку входа ELF. Микроконтроллер её не читает — после сброса он берёт адрес из таблицы векторов, — но она нужна отладчику и служит корнем для сборки мусора./DISCARD/— специальная выходная секция: всё, что в неё попало, в файл не записывается.ASSERT(выражение, "сообщение")останавливает компоновку с сообщением, если выражение равно нулю. В стандартном скрипте AVR так проверяется, что.progmemне выходит за первые 64 КБ.
VMA и LMA: где секция живёт и где хранится
У каждой секции два адреса: VMA — где она находится во время работы, и LMA — куда её записывают при прошивке. Для кода и констант они совпадают, для .data — нет: переменные работают в ОЗУ, а начальные значения должны пережить отключение питания и хранятся во флеш-памяти. В скрипте это записывается так:
>RAMзадаёт область для VMA;AT>FLASHзадаёт область для LMA: секция получит следующий свободный адрес во флеш-памяти;AT(выражение)задаёт LMA конкретным адресом;- функция
LOADADDR(.data)возвращает LMA секции, по ней код запуска узнаёт, откуда копировать.
Если ни AT, ни AT> нет, компоновщик выбирает LMA сам: при явно заданном VMA берёт LMA равным ему, иначе сохраняет ту же разницу VMA − LMA, что у предыдущей секции в той же области. Счётчик «.» всегда считает VMA. Подробно о том, как два адреса хранятся в ELF и почему ошибка с LMA раздувает BIN-файл до сотен мегабайт, — в статье о файлах прошивки.
Разбор шаблона ST для STM32F103
Для разбора возьмём шаблон STM32F103XB_FLASH.ld из репозитория STMicroelectronics cmsis-device-f1 (коммит c8e9a4a от 21 марта 2025 года): он рассчитан на STM32F103xB со 128 КБ флеш-памяти и 20 КБ ОЗУ. В заголовке файла указаны авторские права Ac6 и запрет распространять файл, поэтому ниже — описание и короткие выдержки, а не текст целиком. По порядку сверху вниз:
| Фрагмент | Что делает |
|---|---|
ENTRY(Reset_Handler) | точка входа ELF — обработчик сброса из стартового файла |
_estack = 0x20004FFF; | вершина стека, о значении — ниже |
_Min_Heap_Size = 0x200; _Min_Stack_Size = 0x400; | минимальные куча 512 байт и стек 1 КБ, только для проверки |
| MEMORY | FLASH 128K с 0x08000000, RAM 20K с 0x20000000 |
.isr_vector с KEEP(*(.isr_vector)) | таблица векторов первой во FLASH, с адреса 0x08000000 |
.text | код: *(.text), *(.text*), служебные .glue_7, .glue_7t, .eh_frame, KEEP(*(.init)), KEEP(*(.fini)); в конце символ _etext |
.rodata | константы и строки во FLASH |
.ARM.extab, .ARM | служебные таблицы ARM с символами __exidx_start и __exidx_end |
.preinit_array, .init_array, .fini_array | списки функций, которые библиотека вызывает до main() (конструкторы) и после; границы — PROVIDE_HIDDEN(__init_array_start = .) и подобные |
_sidata = LOADADDR(.data); | адрес начальных значений .data во флеш-памяти |
.data … >RAM AT> FLASH | переменные в ОЗУ, начальные значения во FLASH; символы _sdata, _edata |
.bss | обнуляемые переменные: *(.bss), *(.bss*), *(COMMON); символы _sbss, _ebss и их синонимы __bss_start__, __bss_end__ |
._user_heap_stack | PROVIDE(end = .), PROVIDE(_end = .), затем . = . + _Min_Heap_Size и . = . + _Min_Stack_Size с выравниванием на 8 |
/DISCARD/ | отбрасывает остатки стандартных библиотек libc.a, libm.a и libgcc.a |
.ARM.attributes 0 | служебная секция с атрибутами сборки ARM, по адресу 0 |
Пара замечаний к таблице. Правило /DISCARD/ стоит после всех правил размещения, а входная секция достаётся первому подходящему правилу, поэтому код и данные библиотек, уже размещённые выше, оно не трогает. Выравнивание . = ALIGN(4) вокруг _sdata, _edata и _ebss нужно потому, что код запуска копирует и обнуляет память словами по 4 байта.
Код запуска и символы скрипта
Стартовый файл startup_stm32f103xb.s из того же репозитория в обработчике сброса по порядку вызывает SystemInit, копирует словами начальные значения из _sidata в область от _sdata до _edata, записывает нули от _sbss до _ebss, вызывает __libc_init_array (функции из .init_array) и затем main. Таблица векторов g_pfnVectors лежит в секции .isr_vector: слово 0 — _estack, слово 1 — Reset_Handler, дальше адреса обработчиков; у STM32F103xB это 67 слов, 0x10C байт. Символы стартовый файл не определяет, а берёт из скрипта, поэтому их имена должны совпадать — undefined reference to `_sidata' означает, что скрипт и стартовый файл взяты из разных комплектов.
Почему _estack = 0x20004FFF — плохая идея
ОЗУ STM32F103xB занимает адреса от 0x20000000 до 0x20004FFF, и _estack = 0x20004FFF выглядит как «последний байт ОЗУ». Но стек Cortex-M полный нисходящий: SP указывает на последний сохранённый элемент, и перед записью он сначала уменьшается. Поэтому правильная вершина — адрес на единицу больше последнего байта, 0x20005000: первое слово ляжет в 0x20004FFC.
Что происходит с 0x20004FFF: при сбросе ядро читает слово 0 таблицы векторов и обнуляет у него два младших бита (псевдокод сброса в ARMv7-M ARM, разд. B1.5.5). MSP получает 0x20004FFC, верхние 4 байта ОЗУ пропадают, а SP кратен 4, но не 8. Стандарт вызовов AAPCS требует, чтобы на входе в функцию SP был кратен 8 (ARMv7-M ARM, разд. B1.5.7; AAPCS32, «Stack constraints at a public interface»), а обработчик сброса стартового файла SP заново не устанавливает, так что уже SystemInit и main вызываются с нарушенным выравниванием. Сломаться может то, что рассчитывает на выравнивание 8: double и uint64_t в стеке и особенно функции с переменным числом аргументов. По AAPCS32 объект с выравниванием 8 в списке аргументов лежит по адресу, кратному 8, и va_arg в printf на это полагается, поэтому характерный симптом — мусор вместо числа в printf("%f", x). В шаблоне для платы Nucleo из пакета STM32CubeF1 (SW4STM32) стоит правильное 0x20005000. Надёжнее не писать число руками, а вычислить его от описания памяти:
_estack = ORIGIN(RAM) + LENGTH(RAM); /* one past the last RAM byte */
У STM32F1 есть и второй источник того же сбоя. В его ядре Cortex-M3 ревизии r1p1 бит STKALIGN регистра CCR после сброса равен 0 (в r2p0 значение по умолчанию сменили на 1 — TRM Cortex-M3 r2p1, разд. 1.6.3), и при входе в прерывание ядро выравнивает кадр только на 4 байта. Поэтому даже с правильной вершиной стека обработчики получают SP, кратный лишь 4. Исправляется одной строкой в начале программы, до разрешения прерываний: SCB->CCR |= SCB_CCR_STKALIGN_Msk; — подробнее в статье об архитектуре Cortex-M3.
Стек и куча: что проверяет компоновщик
Стек в скрипте Cortex-M не размещается как секция: его вершина — _estack в слове 0 таблицы векторов, и стек растёт вниз до тех пор, пока что-нибудь не остановит. Куча начинается сразу после статических данных: в _sbrk() из примеров STM32CubeF1 она начинается с символа end и растёт вверх, пока не дойдёт до текущего значения SP. Секция ._user_heap_stack ничего не выделяет, она лишь сдвигает счётчик адреса на _Min_Heap_Size + _Min_Stack_Size после .bss: если этот запас не помещается в RAM, компоновка закончится ошибкой. Реальный стек ею не ограничен и не измерен.
0x20005000 ← _estack: вершина стека (правильное значение)
│ стек растёт вниз ↓
│
│ свободно
│
│ куча растёт вверх ↑ (если используется malloc)
0x20000D28 ← конец проверочного запаса ._user_heap_stack (0x600 байт)
0x20000728 ← _ebss, end, _end
│ .bss 0x6B0 = 1712 байт
0x20000078 ← _sbss, _edata
│ .data 0x78 = 120 байт
0x20000000 ← _sdata, начало ОЗУ
Числа здесь из примера ниже. У AVR стандартный скрипт стек не резервирует вовсе: указатель стека код запуска avr-libc ставит на __stack, по умолчанию равный RAMEND, а всё, что выше __heap_start, делят стек и куча.
Проверка: map-файл, size -A и --print-memory-usage
Map-файл — подробный отчёт компоновщика. Его включают ключом -Wl,-Map=firmware.map: GCC передаёт компоновщику всё, что после -Wl,, разбивая по запятым. В файле есть список отброшенных входных секций («Discarded input sections»), таблица областей памяти («Memory Configuration») и карта: каждая выходная секция с адресом и размером, под ней входные секции с именами объектных файлов и значения символов. Если VMA и LMA различаются, в строке секции стоит load address. Выдержка для примера ниже:
Memory Configuration
Name Origin Length Attributes
FLASH 0x08000000 0x00020000 xr
RAM 0x20000000 0x00005000 xrw
*default* 0x00000000 0xffffffff
Linker script and memory map
.data 0x20000000 0x78 load address 0x08002d78
.bss 0x20000078 0x6b0
._user_heap_stack
0x20000728 0x600
Короткую сводку даёт ключ -Wl,--print-memory-usage: для каждой области MEMORY занятый объём, размер и процент. Секции из AT>FLASH засчитываются во FLASH по LMA, так что начальные значения .data в ней учтены:
Memory region Used Size Region Size %age Used
FLASH: 11760 B 128 KB 8.97%
RAM: 3368 B 20 KB 16.45%
Третий способ — arm-none-eabi-size -A -x firmware.elf: таблица всех секций с размерами и адресами (выдержка, отладочные секции и секции нулевого размера опущены):
firmware.elf :
section size addr
.isr_vector 0x10c 0x8000000
.text 0x2a64 0x800010c
.rodata 0x1f8 0x8002b70
.ARM 0x8 0x8002d68
.init_array 0x4 0x8002d70
.fini_array 0x4 0x8002d74
.data 0x78 0x20000000
.bss 0x6b0 0x20000078
._user_heap_stack 0x600 0x20000728
Чем size -A отличается от строки text data bss по умолчанию и почему её суммы приблизительны, разобрано в статье о файлах прошивки.
Пример: сколько ОЗУ остаётся стеку на STM32F103
Возьмём таблицу size -A выше (числа условные) и шаблон ST с исправленной вершиной стека 0x20005000.
Флеш-память. Последняя секция во FLASH, .fini_array, кончается на 0x08002D74 + 0x4 = 0x08002D78. С этого адреса компоновщик кладёт начальные значения .data (это и есть _sidata), ещё 0x78 байт:
Статические данные в ОЗУ — .data и .bss: 0x78 + 0x6B0 = 0x728 = 1832 байта, они кончаются на 0x20000728.
Под стек и кучу остаётся всё от конца .bss до вершины стека:
Если программа не вызывает malloc, все 18 648 байт доступны стеку. Сводка --print-memory-usage показывает для RAM 3368 байт: 1832 байта данных плюс проверочный запас 0x600 = 1536 байт из ._user_heap_stack. С неисправленным _estack = 0x20004FFF стек начнётся с 0x20004FFC, и останется 18 644 байта.
Когда сборка упадёт. Пусть .bss вырос до 0x4A00 байт: он займёт ОЗУ до 0x20004A78, проверочный запас кончится на 0x20004A78 + 0x600 = 0x20005078, что на 0x78 байт дальше конца RAM. Компоновщик напишет:
…/ld: firmware.elf section `._user_heap_stack' will not fit in region `RAM'
…/ld: region `RAM' overflowed by 120 bytes
Обратите внимание: названа секция ._user_heap_stack, хотя вырос .bss, — первой не поместилась последняя секция области. Число 120 — сколько не хватило с учётом запаса. Если уменьшить _Min_Stack_Size, сборка пройдёт, но памяти программе от этого не прибавится.
Сколько стека нужно на самом деле, компоновщик не знает. Ключ GCC -fstack-usage пишет для каждой функции размер её кадра стека в файл .su с пометкой static, dynamic или dynamic,bounded; глубину стека оценивают, складывая кадры по самой длинной цепочке вызовов и добавляя кадры прерываний, которые могут вложиться друг в друга. Про кадр прерывания на Cortex-M — в статье о прерываниях и общих данных.
Стандартный скрипт avr-gcc: что в нём иначе
AVR — микроконтроллер гарвардской архитектуры: у памяти программ и памяти данных разные адресные пространства, и адрес 0x0100 существует в обоих. Компоновщику же нужна одна шкала адресов. Поэтому стандартный скрипт binutils сдвигает данные: область data начинается не с 0, а с 0x800000 плюс начало ОЗУ, EEPROM — с 0x810000, фьюзы — с 0x820000, биты защиты — с 0x830000, сигнатура — с 0x840000, пользовательские сигнатуры — с 0x850000. Программа использует только младшие 16 бит адреса, так что сдвиг в машинный код не попадает. В выводе avr-size -A, где адреса десятичные, начало .data у ATmega328P выглядит как 8388864 — это 0x800100.
Как выглядят основные правила (фрагмент шаблона avr.sc из binutils 2.47 после подстановки переменных; многоточия и комментарии наши, часть строк опущена):
.text :
{
*(.vectors)
KEEP(*(.vectors))
*(.progmem)
*(.progmem.*) /* PROGMEM data, lower 64 KB */
*(.init0) … *(.init9) /* startup code, each with KEEP */
*(.text) *(.text.*)
_etext = . ;
} > text
.data :
{
PROVIDE (__data_start = .) ;
*(.data) *(.data*)
*(.rodata) *(.rodata*) /* classic AVR: constants go to RAM */
_edata = . ;
PROVIDE (__data_end = .) ;
} > data AT> text
.bss ADDR(.data) + SIZEOF(.data) : AT (ADDR (.bss)) { … } > data
__data_load_start = LOADADDR(.data);
.noinit ADDR(.bss) + SIZEOF(.bss) : AT (ADDR (.noinit)) { … PROVIDE (__heap_start = .) ; } > data
Чем это отличается от STM32:
| STM32F103, шаблон ST | ATmega328P, стандартный скрипт avr5 | |
|---|---|---|
| Где скрипт | файл проекта, -T | встроен в ld, выбирается по -mmcu |
| Области | FLASH 0x08000000, RAM 0x20000000 | text с 0; data — в avr5.sh с 0x800060, но .data ATmega328P начинается с 0x800100: адрес передаёт avr-gcc ключом -Tdata 0x800100 из спецификации устройства; eeprom 0x810000, fuse 0x820000, lock 0x830000, signature 0x840000, user_signatures 0x850000 |
| Длины областей | записаны в скрипте | для text и data по умолчанию 128 КБ и 0xFFA0 байт; точные значения для чипа (32 КБ и 2048 байт) задаёт код запуска avr-libc, для data — при binutils 2.40 и новее |
| Таблица векторов | .isr_vector: адреса обработчиков | .vectors: команды перехода на __vector_N |
| Константы | .rodata во FLASH | .rodata внутри .data, в ОЗУ; во флеш-памяти — только с PROGMEM |
| Код запуска | Reset_Handler в стартовом файле | секции .init0–.init9: в .init2 ставится указатель стека, в .init4 копируется .data и обнуляется .bss, в .init9 вызывается main |
| Символы для кода запуска | _sidata, _sdata, _edata, _sbss, _ebss | __data_load_start, __data_start, __data_end, __bss_start, __bss_end |
| Стек | вершина _estack, запас ._user_heap_stack для проверки | __stack = RAMEND (0x8FF), ничего не резервируется |
.noinit | правила нет | есть, сразу после .bss; за ней __heap_start |
| EEPROM, фьюзы | — | секции .eeprom, .fuse, .lock, .signature в своих областях |
Две детали стоят отдельного внимания. Первая — константы. Команды LD классических AVR не читают флеш-память, поэтому шаблон включает .rodata в .data, если для архитектуры не задан сдвиг RODATA_PM_OFFSET или механизм FLMAP. У avr5 ни того, ни другого нет, и любая строка без PROGMEM занимает и флеш-память, и ОЗУ. У архитектуры avrxmega3 (ATtiny816, ATmega4809) задан сдвиг RODATA_PM_OFFSET, по умолчанию 0x8000, и .rodata остаётся во флеш-памяти, которую эти чипы видят в адресах данных. У ATmega4809 флеш-память видна с адреса 0x4000, поэтому avr-gcc переопределяет сдвиг ключом --defsym=__RODATA_PM_OFFSET__=0x4000 из спецификации устройства. Подробнее о PROGMEM — в статье о языке C для микроконтроллеров.
Вторая — адреса и длины областей. Начало .data у ATmega328P (0x800100) задаёт не скрипт, а драйвер avr-gcc: в спецификации устройства есть правило *link_data_start, которое передаёт компоновщику -Tdata 0x800100, если начало ОЗУ чипа отличается от умолчания архитектуры (у avr5 это 0x60, у ATmega328P — 0x100). Длины скрипт берёт из символов __TEXT_REGION_LENGTH__ и __DATA_REGION_LENGTH__, а значения по умолчанию у avr5 рассчитаны на самый большой чип архитектуры: 128 КБ и 0xFFA0 байт. Код запуска avr-libc 2.3.2 определяет эти символы по заголовку устройства: для ATmega328P это FLASHEND + 1 = 32 768 байт и RAMEND − RAMSTART + 1 = 2048 байт. Начало и длину области data код запуска задаёт, только если avr-libc собрана с binutils версии 2.40 или новее, где появился __DATA_REGION_ORIGIN__. Тогда переполнение статических данных компоновщик ловит сам. Проверка Arduino IDE по upload.maximum_data_size из boards.txt работает отдельно и описана в статье о файлах прошивки.
Пример: свободное ОЗУ ATmega328P
Пусть avr-size -A показывает .data 22 байта по адресу 8388864 (0x800100) и .bss 184 байта по адресу 8388886 (0x800116), .noinit пуста. Тогда:
.dataзанимает 0x100–0x115,.bss— 0x116–0x1CD;__heap_start= 0x100 + 22 + 184 = 0x1CE;- указатель стека после старта = RAMEND = 0x8FF;
- стеку и куче остаётся 0x8FF − 0x1CE + 1 = 0x732 = 1842 байта.
Это та же цифра, которую Arduino IDE называет «оставляя 1842 байт для локальных переменных». Сколько из них стек занимает на деле, можно измерить. В avr-libc 2.3 есть функция _get_ram_unused() из <util/ram-usage.h>: если она есть в программе, код запуска в .init3 закрашивает ОЗУ от __heap_start до RAMEND значением 0xAA, а функция считает, сколько байт подряд от __heap_start остались нетронутыми. Если после прогона всех режимов и прерываний она вернула 1630, стек занимал как минимум 1842 − 1630 = 212 байт. С malloc этот способ несовместим: куча растёт как раз из зоны раскраски. У malloc в avr-libc своя защита: без явной границы кучи он не заходит выше текущего SP минус __malloc_margin, по умолчанию 32 байта, — но и это не гарантирует, что стек потом не вырастет в кучу.
Своя секция: .noinit, функция в ОЗУ
Атрибут GCC section("имя") кладёт функцию или глобальную переменную в указанную секцию, а куда эта секция попадёт в памяти, решает скрипт. Два частых применения.
Переменная, которая переживает сброс. Код запуска обнуляет .bss и заполняет .data при каждом старте, поэтому для счётчика перезапусков или кода ошибки перед сбросом от сторожевого таймера нужна секция, которую он не трогает. В стандартном скрипте AVR такая секция есть — .noinit, без начальных значений и без обнуления. У GCC 16 для этого есть и атрибут noinit, который полагается на скрипт, размещающий секции .noinit. В шаблоне ST для STM32F1 правила для .noinit нет, и секция оказалась бы «сиротой» с местом, которое выбирает эвристика компоновщика. Правило лучше добавить явно, например после .bss:
.noinit (NOLOAD) :
{
. = ALIGN(4);
*(.noinit)
*(.noinit.*)
. = ALIGN(4);
} >RAM
NOLOAD помечает секцию как незагружаемую: место в ОЗУ под неё при работе отводится, но содержимое при загрузке не записывается. Переменная:
#define BOOT_MAGIC 0x5AA5C33Cu
__attribute__((section(".noinit"))) static uint32_t boot_magic;
__attribute__((section(".noinit"))) static uint32_t boot_count;
void boot_counter_update(void)
{
if (boot_magic != BOOT_MAGIC) { /* power-on: RAM holds garbage */
boot_magic = BOOT_MAGIC;
boot_count = 0;
}
boot_count++; /* survives watchdog and software resets */
}
После включения питания в .noinit случайные значения, поэтому рядом хранят «магическое» число. Начальное значение такой переменной задать нельзя: avr-libc прямо пишет, что в .noinit можно класть только данные без инициализатора.
Функция в ОЗУ. Иногда код должен выполняться не из флеш-памяти, например пока флеш-память стирается. Ядро Cortex-M3 может выполнять код из области SRAM. Функцию кладут в свою секцию, а в скрипте добавляют эту секцию в правило .data, чтобы код запуска скопировал её в ОЗУ вместе с переменными:
__attribute__((section(".ramfunc"), noinline))
void flash_wait_ready(void)
{
/* runs from RAM: copied there by the startup code together with .data */
}
/* inside the existing .data rule, between *(.data*) and the final ALIGN(4) before _edata */
*(.ramfunc)
*(.ramfunc*)
Расстояние от флеш-памяти 0x08000000 до ОЗУ 0x20000000 больше дальности команды BL (±16 МБ), но вызов всё равно работает: компоновщик ARM сам вставляет промежуточную заглушку для слишком далёкого вызова. Атрибут long_call делает дальний вызов явным. Выходная секция .data с кодом внутри становится одновременно записываемой и исполняемой, поэтому ld выдаст предупреждение о сегменте с правами RWX — здесь оно ожидаемо.
Своя секция бывает нужна и для данных по фиксированному адресу, например для блока настроек в последней странице флеш-памяти: в MEMORY под неё заводят отдельную область, уменьшив FLASH, и направляют туда секцию правилом с KEEP.
Ошибки компоновщика и как их читать
| Сообщение или симптом | Что случилось | Что делать |
|---|---|---|
region `FLASH' overflowed by N bytes | код, константы и начальные значения .data не влезли во флеш-память на N байт | -Os; -ffunction-sections -fdata-sections и -Wl,--gc-sections; найти крупные функции и таблицы в map-файле; проверить LENGTH |
region `RAM' overflowed by N bytes и перед ним section `._user_heap_stack' will not fit in region `RAM' | статические данные плюс проверочный запас не влезли в ОЗУ | искать крупные массивы в .bss и .data по map-файлу; на AVR — строки без PROGMEM |
address 0x… of firmware.elf section `.x' is not within region `RAM' | секции задан явный адрес, и она выходит за область; в сообщении — адрес конца секции | исправить адрес или область |
section .x LMA […] overlaps section .y LMA […] (или VMA) | две секции получили пересекающиеся адреса загрузки или выполнения, обычно из-за адресов, заданных вручную через AT(…) или . = … | убрать ручные адреса или раздвинуть секции |
undefined reference to `_estack' или _sidata | имена символов в скрипте и в стартовом файле не совпадают | взять скрипт и стартовый файл из одного комплекта |
warning: firmware.elf has a LOAD segment with RWX permissions | загружаемый сегмент одновременно доступен на чтение, запись и выполнение | предупреждение, а не ошибка; выключается -Wl,--no-warn-rwx-segments |
| сборка без ошибок, прошивка не стартует | таблица векторов выброшена --gc-sections или лежит не с начала флеш-памяти | KEEP в правиле .isr_vector; адрес .isr_vector в map-файле |
| сборка без ошибок, случайные сбои при работе | LENGTH больше реальной памяти или стек врезался в данные | сверить MEMORY с документацией на чип, оценить стек |
| BIN-файл в сотни мегабайт | у секции с содержимым LMA оказалась в ОЗУ | AT> FLASH, см. файлы прошивки |
Последние две строки таблицы — самые неприятные: компоновщик ошибки не видит. Map-файл стоит открывать не только после ошибки, но и после любой правки скрипта: там видно, куда легла каждая секция, сколько места съели библиотечные функции и что выбросила сборка мусора.
Частые вопросы
Что такое скрипт компоновщика и где он в проекте STM32?
Это текстовый файл с расширением .ld, по которому компоновщик GNU ld раскладывает код и данные по памяти микроконтроллера: какие есть области памяти (MEMORY), какие секции и в каком порядке в них лежат (SECTIONS) и какие символы получит код запуска. В проектах для STM32 он лежит в папке проекта и передаётся компоновщику ключом -T. У avr-gcc скрипт встроен в компоновщик и выбирается по архитектуре микроконтроллера; посмотреть его можно, добавив к сборке -Wl,--verbose.
Что значит ошибка «region `FLASH' overflowed by N bytes»?
Секции, назначенные в область FLASH, не поместились в её LENGTH, и не хватает ровно N байт. Перед этой строкой компоновщик называет первую секцию, которая не влезла. Во флеш-память идут код, константы и начальные значения переменных .data. Помогают сборка с -Os, ключи -ffunction-sections и -fdata-sections вместе с -Wl,--gc-sections, отказ от тяжёлых функций библиотеки, например printf с плавающей точкой. Сначала проверьте, что LENGTH соответствует вашему микроконтроллеру.
Зачем в скрипте компоновщика KEEP?
Чтобы компоновщик не выбросил секцию при сборке мусора --gc-sections. Он оставляет только секции, достижимые по ссылкам от точки входа, а на таблицу векторов прерываний и на списки конструкторов .init_array код не ссылается — их читает ядро или код запуска по адресам. Без KEEP(*(.isr_vector)) таблица векторов может исчезнуть, и прошивка не запустится, хотя сборка пройдёт без ошибок.
Почему адреса ОЗУ AVR в map-файле начинаются с 0x800000?
У AVR флеш-память и ОЗУ — разные адресные пространства, и адрес 0x100 есть в обоих. Компоновщику нужна одна линейная шкала, поэтому стандартный скрипт binutils сдвигает область data на 0x800000: ОЗУ ATmega328P с адреса 0x100 видно компоновщику как 0x800100. Программа использует только младшие 16 бит адреса, так что сдвиг в код не попадает. EEPROM в той же шкале начинается с 0x810000, фьюзы — с 0x820000.
Как сохранить значение переменной после сброса микроконтроллера?
Положить её в секцию, которую код запуска не обнуляет: __attribute__((section(".noinit"))) или атрибут noinit в GCC. В стандартном скрипте avr-gcc секция .noinit есть, в шаблоне ST для STM32F1 её нет, и правило нужно добавить в .ld самому. После включения питания там мусор, поэтому рядом хранят «магическое» число и проверяют его. Переживает такая переменная только сброс без отключения питания: от сторожевого таймера, кнопки или программный.
Проверяет ли компоновщик, хватит ли памяти для стека?
Нет. Компоновщик знает только статические данные. Шаблоны ST резервируют после .bss секцию ._user_heap_stack размером _Min_Heap_Size + _Min_Stack_Size, и сборка падает, если такой запас не помещается, но реальный стек этим не ограничен и не измерен. У AVR стандартный скрипт стек вообще не резервирует. Глубину стека оценивают по файлам -fstack-usage или измеряют на работающей программе, например функцией _get_ram_unused() из avr-libc.
Источники
- GNU ld 2.47 Documentation: MEMORY Command, SECTIONS (Input Section Basics, Wildcards, Common, Keep), Location Counter, Output Section Address, LMA, Type и Discarding, PROVIDE, Source Code Reference, Builtin Functions, Constants, ASSERT, Options, ARM — атрибуты областей памяти R, W, X, A, I, L и «!» выбирают область для выходной секции без адреса и без >region, в том числе для секций, не упомянутых в скрипте; ошибка, если секции не помещаются в область; входная секция попадает в первое подходящее правило; KEEP защищает от --gc-sections, который оставляет секцию с точкой входа и всё, на что есть ссылки; «.» внутри описания секции — смещение от её начала; AT и AT>; NOLOAD; /DISCARD/; PROVIDE определяет символ, только если на него ссылаются и он не определён; символ скрипта — адрес без значения; суффиксы K и M; -T заменяет стандартный скрипт; -Map, --print-memory-usage, --verbose; заглушки для слишком далёких вызовов на ARM
- GNU Binutils 2.47, исходные тексты: ld/ldlang.c, ld/ldlang.h, ld/ldmisc.c, bfd/elf.c, ld/scripttempl/avr.sc, ld/emulparams/avr5.sh, avrxmega3.sh — тексты сообщений region `%s' overflowed by %lu bytes, section `%s' will not fit in region `%s', address … is not within region, VMA overlaps, LOAD segment with RWX permissions; формат map-файла и --print-memory-usage; выбор области по атрибутам для секции без >region (lang_memory_default); стандартный скрипт AVR: области text, data (avr5: 0x800060), eeprom 0x810000, fuse, lock, signature, user_signatures 0x850000, .rodata внутри .data у классических AVR, .noinit и __heap_start
- STMicroelectronics. cmsis-device-f1, коммит c8e9a4a: Source/Templates/gcc/linker/STM32F103XB_FLASH.ld и Source/Templates/gcc/startup_stm32f103xb.s — шаблон скрипта для STM32F103xB (128 КБ флеш-памяти, 20 КБ ОЗУ, © Ac6, распространение файла не разрешено — в статье только короткие выдержки): _estack = 0x20004FFF, _Min_Heap_Size = 0x200, _Min_Stack_Size = 0x400, KEEP(*(.isr_vector)), .data >RAM AT> FLASH, ._user_heap_stack; код запуска копирует .data словами и обнуляет .bss, вызывает __libc_init_array и main; таблица векторов из 67 слов
- STMicroelectronics. STM32CubeF1, коммит c7656f1: шаблон SW4STM32 STM32F103VBIx_FLASH.ld и syscalls.c примеров — в шаблоне для платы Nucleo _estack = 0x20005000; _sbrk() в syscalls.c начинает кучу с символа end и не даёт ей подняться выше текущего SP
- AVR-LibC 2.3.2: Memory Sections (doc/api/sections.dox), Memory Areas and Using malloc() (malloc.dox), crt1/gcrt1.S, avr/iom328p.h, util/ram-usage.h — смещение 0x800000 для адресов ОЗУ в представлении компоновщика; области и выходные секции стандартного скрипта; секции .init0–.init9 (указатель стека в .init2, копирование .data и обнуление .bss в .init4); символы __data_load_start, __bss_end, __heap_start; .noinit; код запуска задаёт длины областей по устройству; __stack = RAMEND; __malloc_margin = 32; _get_ram_unused() и раскраска ОЗУ значением 0xAA; ATmega328P: RAMSTART 0x100, RAMEND 0x8FF, FLASHEND 0x7FFF, E2END 0x3FF
- GCC 16.2 Manual: Common Attributes (section, noinit), Link Options (-T, -Wl), Optimize Options (-ffunction-sections), Developer Options (-fstack-usage) — атрибут section кладёт функцию или переменную в указанную секцию; noinit полагается на скрипт, который размещает секции .noinit; -Wl,-Map=файл; -ffunction-sections и -fdata-sections дают каждой функции и переменной свою секцию; -fstack-usage пишет размер кадра стека каждой функции в файл .su
- GCC 15.2, исходные тексты: gcc/config/avr/gen-avr-mmcu-specs.cc и avr-mcus.def — спецификация устройства: *link_data_start передаёт -Tdata 0x800000 + начало ОЗУ, если оно отличается от умолчания архитектуры (avr5 — 0x60, ATmega328P — 0x100); --defsym=__RODATA_PM_OFFSET__, если сдвиг флеш-памяти чипа отличается от архитектурного (ATmega4809 — 0x4000 при 0x8000 у avrxmega3)
- Arm. Procedure Call Standard for the Arm Architecture (AAPCS32), github.com/ARM-software/abi-aa, коммит 7850367 — на публичном интерфейсе SP mod 8 = 0; аргумент с выравниванием 8 кладётся в стек по адресу, кратному 8; va_list рассчитывает, что такие объекты выровнены на 8
- Arm. Cortex-M3 Processor Technical Reference Manual, revision r2p1 (100165) — разд. 1.6.3 — в r2p0 значение STKALIGN после сброса сменили на 1 (у r1p1, ядра STM32F1, — 0)
- Arm. ARMv7-M Architecture Reference Manual, DDI 0403E.e, 2021 — B1.5.5 — при сбросе SP_main = слово 0 таблицы векторов AND 0xFFFFFFFC; B1.5.7 — AAPCS требует выравнивания SP на 8 байт на входе в функцию
- Arm. Cortex-M3 Devices Generic User Guide, DUI 0552A, 2010 — полный нисходящий стек: SP указывает на последний сохранённый элемент; область SRAM с 0x20000000 исполняемая; дальность команды BL — ±16 МБ
Нашли ошибку?
Смотрите также
- Программирование МКC для МК, битовые операции, фиксированная точка, регистры
- Файлы прошивки: HEX, BIN, ELFELF, Intel HEX, S-record, BIN и UF2: адреса, секции, objcopy, size
- Прерывания и общие данныеISR на AVR и Cortex-M, атомарность, критические секции, кольцевой буфер
- C для микроконтроллеровТипы, stdint, volatile, атомарность, регистры, константы во Flash
- Flash и загрузчик STM32Страницы и секторы, BOOT0, системный загрузчик, AN3155, IAP, option bytes, RDP
- Cortex-M3Регистры, карта памяти, bit-banding, NVIC, SysTick, MPU, старт, пример
- Архитектура AVRЯдро, конвейер, память, порты, прерывания, сброс, такты и сон
- СтекОбласть ОЗУ для адресов возврата и локальных переменных