Пришла пора привести в понятный вид скрипт линковки для прошивки под STM32 (или любой другой ARM Cortex-M).
Прошлый раз мы писали как попало и не очень понятно было, где и что лежит. Будем устранять непонятности.
Я все время говорю – символы, секции. Давайте займемся терминологией.
Первое и главное – это символ. Для компоновщика символ это именованный адрес не более того. (правда похоже на переменную в любом языке программирования?). Причем не важно, что скрывается под этим символом – адрес переменной, код функции, все что угодно. Плюс некоторые «атрибуты», такие как принадлежность к секции, размер. Когда в коде написано uint32_t var_a = 12345;
, при компиляции создается символ var_a
, в атрибуты помещается размер в 4 байта и принадлежность к секции .data
(мы уже говорили, что инициализированные переменные попадают в эту секцию). Для функций – примерно тоже самое, только секция будет .text
, ну и размер нельзя вот прямо так сразу назвать, но после компиляции он конечно же будет известен.
Секции, на самом деле под этим словом скрываются два понятия, с точки зрения компоновщика есть два типа секций – входные и выходные. Входные секции, это значение того самого атрибута символа, про который мы говорили выше, не более того. Выходные секции — это именованные логические области памяти в выходном (исполняемом и бинарном) файле. Тут не следует путать с регионами памяти (описываемые в блоке MEMORY
), регион памяти — это физический блок (флешка, SRAM, внешняя память и так далее). А выходная секция логический блок внутри региона. Выходные секции, как, наверное, стало понятно, описываются в разделе SECTIONS
скрипта линковки.
Еще одно очень важное понятие, которым компоновщик оперирует — это «текущий адрес». В начале скрипта он устанавливается на начало первого описанного региона и двигается при размещении данных. В скриптах линковки к этому адресу всегда можно обратиться через оператор «.» (Точка). Можно получать его значение (и назначать символам), можно двигать в абсолютное значение (например, . = ORIGIN(RAM);
— помещает в текущий адрес начало региона RAM), или относительное (например, . = ALIGN(16);
— подвинет, если требуется, текущий адрес так, чтобы его значение стало кратно 16). Так же, можно подвинуть на заданное количество байт (. += 10
— подвинет текущий адрес на 10 байт вперед). Самое главное, что там где адрес не указан явно – всегда используется текущий адрес, а там где разворачиваются какие-то данные, текущий адрес всегда двигается вперед на размер этих данных.
Давайте перепишем скрипт линковки с учетом этих знаний и приведем в понятный не только компьютеру вид.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm") OUTPUT_ARCH(arm) MEMORY { ROM (rx) : ORIGIN = 0x08000000, LENGTH = 64K /* Объявляем регион ROM */ RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K /* Объявляем регион RAM */ } SECTIONS { .isr_vector ORIGIN(ROM): /* Объявляем выходную секцию .isr_vector, адрес укажем явно: начало региона ROM */ { KEEP(*(.isr_vector)) /* помещаем сюда все символы с атрибутом section(".isr_vector") */ } .text ALIGN(16): /* Объявляем выходную секцию .text, попросим компоновщик выровнять секцию кратно 16 байт */ { *(.text*); /* Развернем сюда все символы с атрибутом section начинающимся со слова .text */ } .data_rom ALIGN(4): /* Выходная секция для значений инициализации данных из .data */ { /* Создадим символ __data_rom__ с адресом равным текущему */ PROVIDE(__data_rom__ = .); /* Подвинем указатель на текущий адрес на размер выходной секции .data, она будет объявлена позже */ . += SIZEOF(.data); } /* Секция для неинициализированных переменных, адрес - начало региона RAM Тип NOLOAD, говорит компоновщику, что нужно только распределить адреса, данные нам не важны*/ .common ORIGIN(RAM) (NOLOAD) : { *(COMMON*) } .bss ALIGN(4) (NOLOAD): { PROVIDE(__bss_start__ = .); *(.bss*) PROVIDE(__bss_end__ = .); } /* Секция .data, для инициализированных переменных, оператор AT() назначает LMA для этой секции */ .data ALIGN(4): AT(__data_rom__) { PROVIDE(__data_start__ = .); *(.data*) PROVIDE(__data_end__ = .); } >RAM /* Создадим символ __stack_top__ */ PROVIDE(__stack_top__ = ORIGIN(RAM) + LENGTH(RAM)); } |
Небольшое примечание: символ __data_lma__ мы больше не используем, для сборки с этим скриптом потребуется немного поправить исходник из прошлого занятия. Теперь адрес начала секции данными мы указали как __data_rom__
Теперь все адреса известны и явно указаны, мы всегда знаем где находится какая секция. А мы разберем вопрос, почему символы указываются как *(.text*)
и зачем столько звездочек. Во-первых напомню формат инструкции, документация говорит о трёх возможных форматах:
filename( section ) filename( section, section, ... ) filename( section section ... )
Мы пользуемся первым (одна секция на строку). В качестве filename мы указываем * — любой файл, а в качестве section (входной секции) – мы указываем .text*
, что значит «любая секция, начинающаяся на .text
. Так нужно делать, потому что один из базовых способов оптимизации размера программы это помещение КАЖДОГО символа в свою собственную секцию вида .section.symbolname
, например для переменной int var_a = 0;
при компиляции с ключом GCC -fdata-sections
в выводе arm-none-eabi-nm
можно будет увидеть что-то похожее на:
> arm-none-eabi-nm -a main.o ... 00000000 b .bss.var_a ...
Для того, чтобы все такие символы из всех файлов попали в одну нужную выходную секцию и указывается *(.section*)