Перейти к содержанию

Скрипт компоновщика GNU ld для микроконтроллеров

Скрипт компоновщика решает, где в памяти микроконтроллера окажутся код, константы, переменные, таблица векторов и стек. Разбираем команды MEMORY и SECTIONS, счётчик адреса, VMA и LMA, KEEP и PROVIDE, шаблон ST для STM32F103 и стандартный скрипт avr-gcc, map-файл и сообщения о переполнении памяти — с расчётом свободного ОЗУ под стек.

Обновлено Редакция GAW

Скрипт компоновщика (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 КБ, только для проверки
MEMORYFLASH 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_stackPROVIDE(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 байт:

FLASH=0x2D78+0x78=0x2DF0=11 760 байт  (8,97 % из 131 072)\text{FLASH} = 0x2D78 + 0x78 = 0x2DF0 = 11\,760\ \text{байт} \;(8{,}97\,\%\ \text{из}\ 131\,072)

Статические данные в ОЗУ — .data и .bss: 0x78 + 0x6B0 = 0x728 = 1832 байта, они кончаются на 0x20000728.

Под стек и кучу остаётся всё от конца .bss до вершины стека:

0x20005000−0x20000728=0x48D8=18 648 байт0x20005000 - 0x20000728 = 0x48D8 = 18\,648\ \text{байт}

Если программа не вызывает 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.

Память данных ATmega328P: регистры, I/O, расширенный I/O и SRAMАдреса 0x0000–0x001F — регистры r0–r31, 0x0020–0x005F — 64 регистра ввода-вывода, 0x0060–0x00FF — 160 регистров расширенного ввода-вывода, 0x0100–0x08FF — 2048 байт SRAM. Масштаб не соблюдён.адрес данных32 регистраr0–r310x0000I/O 0x00–0x1Fбиты: SBI, CBI, SBIC, SBIS0x0020I/O 0x20–0x3F0x0040160 регистроврасширенного I/O0x0060SRAM, 2048 байтпеременные и стек0x01000x08FFстек64 регистра I/O: IN, OUT;адрес I/O = адрес данных − 0x20только LD, LDD, LDSи ST, STD, STSSP после сброса = RAMEND = 0x08FF;стек растёт к меньшим адресамLD/ST, LDD/STD и LDS/STS адресуют всё пространство 0x0000–0x08FF
Адресное пространство данных ATmega328P: регистры, ввод-вывод и 2 КБ SRAM с адреса 0x0100. В скрипте компоновщика эти же адреса сдвинуты на 0x800000.

Как выглядят основные правила (фрагмент шаблона 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, шаблон STATmega328P, стандартный скрипт avr5
Где скриптфайл проекта, -Tвстроен в ld, выбирается по -mmcu
ОбластиFLASH 0x08000000, RAM 0x20000000text с 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.

Источники

  1. 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
  2. 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
  3. 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 слов
  4. STMicroelectronics. STM32CubeF1, коммит c7656f1: шаблон SW4STM32 STM32F103VBIx_FLASH.ld и syscalls.c примеров — в шаблоне для платы Nucleo _estack = 0x20005000; _sbrk() в syscalls.c начинает кучу с символа end и не даёт ей подняться выше текущего SP
  5. 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
  6. 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
  7. 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)
  8. 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
  9. Arm. Cortex-M3 Processor Technical Reference Manual, revision r2p1 (100165) — разд. 1.6.3 — в r2p0 значение STKALIGN после сброса сменили на 1 (у r1p1, ядра STM32F1, — 0)
  10. Arm. ARMv7-M Architecture Reference Manual, DDI 0403E.e, 2021 — B1.5.5 — при сбросе SP_main = слово 0 таблицы векторов AND 0xFFFFFFFC; B1.5.7 — AAPCS требует выравнивания SP на 8 байт на входе в функцию
  11. Arm. Cortex-M3 Devices Generic User Guide, DUI 0552A, 2010 — полный нисходящий стек: SP указывает на последний сохранённый элемент; область SRAM с 0x20000000 исполняемая; дальность команды BL — ±16 МБ

Нашли ошибку?