GNU LD. Пытаюсь написать свой скрипт компоновки.

Кто любит RISC в жизни, заходим, не стесняемся.
Ответить
Аватара пользователя
YS
Друг Кота
Сообщения: 7518
Зарегистрирован: Вс мар 29, 2009 22:09:05
Контактная информация:

GNU LD. Пытаюсь написать свой скрипт компоновки.

Сообщение YS »

Здравствуйте, коллеги!

Я решил поучиться работать с GNU LD. Начать решил с малого: в стандартный файл скрипта для STM32L152RE, который идет в комплекте с EmBitz, добавить секции для размещения данных в EEPROM (по умолчанию их там нет).

Посмотрите, пожалуйста, я все правильно сделал? :)
Спойлер

Код: Выделить всё

OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
OUTPUT_ARCH(arm)
SEARCH_DIR(.)

/* Memory Spaces Definitions */
MEMORY
{
    ROM  (rx) : ORIGIN = 0x08000000, LENGTH = 512K
    RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 80K

/*
    ДОБАВИЛ определения для встроенной в контроллер EEPROM. Значения адресов и размеров взяты из даташита.
*/

    EEP1 (rw) : ORIGIN = 0x08080000, LENGTH = 8K
    EEP2 (rw) : ORIGIN = 0x08082000, LENGTH = 8K
}

SECTIONS
{
	.text :
	{
		KEEP(*(.isr_vector))
		*(.text*)

		KEEP(*(.init))
		KEEP(*(.fini))

		/* .ctors */
		*crtbegin.o(.ctors)
		*crtbegin?.o(.ctors)
		*(EXCLUDE_FILE(*crtend?.o *crtend.o) .ctors)
		*(SORT(.ctors.*))
		*(.ctors)

		/* .dtors */
 		*crtbegin.o(.dtors)
 		*crtbegin?.o(.dtors)
 		*(EXCLUDE_FILE(*crtend?.o *crtend.o) .dtors)
 		*(SORT(.dtors.*))
 		*(.dtors)

		*(.rodata*)

		KEEP(*(.eh_frame*))
	} > ROM

	.ARM.extab :
	{
		*(.ARM.extab* .gnu.linkonce.armextab.*)
	} > ROM

	__exidx_start = .;
	.ARM.exidx :
	{
		*(.ARM.exidx* .gnu.linkonce.armexidx.*)
	} > ROM
	__exidx_end = .;

	__etext = .;

	.data : AT (__etext)
	{
		__data_start__ = .;
		*(vtable)
		*(.data*)

		. = ALIGN(4);
		/* preinit data */
		PROVIDE_HIDDEN (__preinit_array_start = .);
		KEEP(*(.preinit_array))
		PROVIDE_HIDDEN (__preinit_array_end = .);

		. = ALIGN(4);
		/* init data */
		PROVIDE_HIDDEN (__init_array_start = .);
		KEEP(*(SORT(.init_array.*)))
		KEEP(*(.init_array))
		PROVIDE_HIDDEN (__init_array_end = .);


		. = ALIGN(4);
		/* finit data */
		PROVIDE_HIDDEN (__fini_array_start = .);
		KEEP(*(SORT(.fini_array.*)))
		KEEP(*(.fini_array))
		PROVIDE_HIDDEN (__fini_array_end = .);

		. = ALIGN(4);
		/* All data end */
		__data_end__ = .;

	} > RAM

	.bss (NOLOAD):
	{
		__bss_start__ = .;
		*(.bss*)
		*(COMMON)
		__bss_end__ = .;
	} > RAM

	.heap (NOLOAD):
	{
		__end__ = .;
		end = __end__;
		*(.heap*)
		__HeapLimit = .;
	} > RAM

	/* .stack_dummy section doesn't contains any symbols. It is only
	 * used for linker to calculate size of stack sections, and assign
	 * values to stack symbols later */
	.stack_dummy (NOLOAD):
	{
		*(.stack)
	} > RAM

/*
    ДОБАВИЛ две секции, соответствующие двум банкам EEPROM.
    Использовал KEEP, чтобы компоновщик не удалял данные, которые по той или иной причине должны быть записаны в память, но не используются в программе непосредственно.
*/
	.eeprom_bank_1 :
	{
        KEEP (*(.eep_bank_1));
	} > EEP1

	.eeprom_bank_2 :
	{
        KEEP (*(.eep_bank_2));
	} > EEP2

	/* Set stack top to end of RAM, and stack limit move down by
	 * size of stack_dummy section */
	__StackTop = ORIGIN(RAM) + LENGTH(RAM);
	__StackLimit = __StackTop - SIZEOF(.stack_dummy);
	PROVIDE(__stack = __StackTop);

	/* Check if data + heap + stack exceeds RAM limit */
	ASSERT(__StackLimit >= __HeapLimit, "region RAM overflowed with stack")
}
Судя по тому, что я наблюдаю в выходном бинарнике, все работает.

Проверял, объявляя массив вот так:

Код: Выделить всё

volatile uint32_t a[10] __attribute__ ((section(".eep_bank_2"))) = {1,2,3,4,5,6,7,8,9,10};
И да, его правда видно в бинарнике начиная с адреса 0x08082000.

Но, тем не менее, у меня есть вопросы.

Вопрос номер один. Я просто добавил новые секции в конец перечисления секций. Этого достаточно, чтобы они встали, как надо? Если да, то зачем при объявлении секции .data используется атрибут AT?

Вопрос номер два. Я правильно понимаю, что, по сути, две записи, которые я привожу ниже, эквивалентны?

Код: Выделить всё

MEMORY
{
    ...

    EEP1 (rw) : ORIGIN = 0x08080000, LENGTH = 8K

    ...
}

...

	.eeprom_bank_1 :
	{
            KEEP (*(.eep_bank_1));
	} > EEP1

Код: Выделить всё


        .eeprom_bank_1 0x08080000 :
        {
            KEEP(*(.eep_bank_1));
        }

Я так понимаю, что в первом варианте компоновщик кроме всего прочего будет проверять размер, но вроде бы во втором он должен положить данные этой секции по тому же адресу.
Разница между теорией и практикой на практике гораздо больше, чем в теории.
Аватара пользователя
Мурик
Друг Кота
Сообщения: 3383
Зарегистрирован: Пн окт 11, 2010 19:00:08

Re: GNU LD. Пытаюсь написать свой скрипт компоновки.

Сообщение Мурик »

YS писал(а):зачем при объявлении секции .data используется атрибут AT?
Потому что данные для ОЗУ, но хранятся во флеше.
https://ftp.gnu.org/old-gnu/Manuals/ld- ... /ld_3.html
YS писал(а):EEP1 (rw) : ORIGIN = 0x08080000, LENGTH = 8K
EEP2 (rw) : ORIGIN = 0x08082000, LENGTH = 8K
EEPROM в STM32 поддерживает прямую запись?
Аватара пользователя
YS
Друг Кота
Сообщения: 7518
Зарегистрирован: Вс мар 29, 2009 22:09:05
Контактная информация:

Re: GNU LD. Пытаюсь написать свой скрипт компоновки.

Сообщение YS »

Спасибо. :beer:

Кроме всего прочего, я тут нашел замечательную публикацию, которая мне многое пояснила.

Для тех, кто найдет эту тему поиском, напишу, в чем дело.

Как обычно, всему виной историческая терминология. Там есть два термина - LMA (load memory address) и VMA (virtual memory address). Правда, в наши дни уже давно ничего никуда не загружается. По сути, VMA - это тот стартовый адрес, который компоновщик будет учитывать при обращении к переменным, расположенным в этой секции. LMA - это адрес, по которому данные этой секции будут записаны в файл образа прошивки, а в нашем случае - и во FLASH контроллера, потому что прошивка записывается туда.

Адреса в случае выше указываются как

<имя секции> : AT [LMA]
{
...
} [ > <указанная память определяет VMA>]

В файле выше смысл в том, что RAM начинается с 0x20000000, и в тех местах, где производится обращение к переменным, расположенным в этой секции, компоновщик будет подставлять смещения от этого адреса. А вот константы, предназначенные для инициализации переменных, он положит сразу после кода (секция .text), то есть, во FLASH.

Если этого не сделать, то выходной .hex-файл будет инструктировать программатор записывать значения прямо в RAM. Он, конечно, запишет, это технически возможно, но смысл, заключающийся в резервировании памяти в RAM и хранении инициализационных констант во FLASH, будет потерян.

К слову, атрибут NOLOAD говорит компоновщику, что секцию надо всего лишь учитывать при расстановке смещений, но в выходной файл ее содержимое писать не надо.
EEPROM в STM32 поддерживает прямую запись?
Да. Сначала надо разблокировать запись в специальном регистре, и дальше можно писать по указателю. Ну а в нашем случае, если переменная расположена в новой секции, можно будет писать как в обычную переменную.
Разница между теорией и практикой на практике гораздо больше, чем в теории.
Аватара пользователя
AVI-crak
Прорезались зубы
Сообщения: 202
Зарегистрирован: Сб янв 09, 2016 15:51:17
Контактная информация:

Re: GNU LD. Пытаюсь написать свой скрипт компоновки.

Сообщение AVI-crak »

Оно конечно будет работать, но радости не принесёт.
Размечать EEPROM как область памяти - означает позволить компилятору самому назначить место хранения переменных. Ну в том смысле что он будет там всё оптимизировать и складывать в аккуратную кучку. Это удобно, но не долговечно.
iddqd
Нашел транзистор. Понюхал.
Сообщения: 156
Зарегистрирован: Вс сен 06, 2020 16:06:10

Re: GNU LD. Пытаюсь написать свой скрипт компоновки.

Сообщение iddqd »

Более того - все время работать с разблокированным на запись EEPROM не очень удачная идея с точки зрения сохранности данных в нем. С всем что флеша касается вообще работают обычно так: разблокировали - записали что хотели - заблокировали обратно. Иначе записанного можно однажды лишиться при отклонениях от идеала.

И это, при рестарте переменные инициализируются же. Это чего, каждый ребут будет цикл еепром давать? Или оно как uninitialized идет? А как вы начальные значения пропишете только 1 раз? И вообще как оно будет non-volatile при этом, если так в лоб? Можно const'ами обвесить, но записывать как? :)

Кроме того - доступ на запись может bus stall на довольно почтенное время. Вам совсем плевать на реальное время и вы готовы с неконтролируемым (вами явно) тупняком чипа от такого доступа жить? Флеш так то довольно долго пишется (по микроконтроллерным меркам) и в это время модуль флеша не может код прошивки отдавать. В 2-банковых чипах, конечно, не занятый записью банк дееспособен пока другой пишется - но этим надо явно заморочиться, положив код в один и поюзав еепром в другом.

Ну и если хочется видеть это напрямую в памяти - например, завести typedef struct config какой-нибудь, переменную (-ые) которые заявлены - как _указатели_ на это, и по мере желания можно указатель назначать куда там охота (ram vs eeprom) например (но осторожно, указатели они такие). На еепром лучше указывать с полями const везде, во избежание. А записывать чем-то типа memcpy этого (из версии в RAM) - сняв лок, записав как надо, вернув лок - и вызывается в момент когда ОК что чип может довольно долго тупануть и за это ничего не будет. На 2-банковом в принципе можно код в 1 банк, еепром в другой, тогда это не проблема вроде, но потребует специального внимания.
Аватара пользователя
YS
Друг Кота
Сообщения: 7518
Зарегистрирован: Вс мар 29, 2009 22:09:05
Контактная информация:

Re: GNU LD. Пытаюсь написать свой скрипт компоновки.

Сообщение YS »

все время работать с разблокированным на запись EEPROM не очень удачная идея с точки зрения сохранности данных в нем.
С чего вы взяли, что я буду так делать? :)
И это, при рестарте переменные инициализируются же.
Инициализируются только переменные из сегмента .data. Для этого в startup-коде, который по умолчанию добавляется при компоновке, есть специальный цикл. Для всех остальных сегментов никакой инициалиации по умолчанию нет.
А как вы начальные значения пропишете только 1 раз?
То, что помещено в заданные сегменты, попадает в образ прошивки по соответствующим адресам и, соответственно, записывается программатором при прошивке чипа. В этом и есть прелесть задания области сегментом в скрипте компоновщика.
И вообще как оно будет non-volatile при этом, если так в лоб?
Эта область памяти non-volatile аппаратно.
Кроме того - доступ на запись может bus stall на довольно почтенное время. Вам совсем плевать на реальное время и вы готовы с неконтролируемым (вами явно) тупняком чипа от такого доступа жить?
FLASH находится в другом банке относительно EEPROM. Кроме того, запись туда имеет смысл производить только при сохранении настроек, например.
но осторожно, указатели они такие
Именно от этой проблемы избавляет введение специального сегмента. В этом случае компоновщик сам проверяет адреса.
Разница между теорией и практикой на практике гораздо больше, чем в теории.
SgtPepper_91
Первый раз сказал Мяу!
Сообщения: 28
Зарегистрирован: Пн ноя 30, 2015 03:52:00

Re: GNU LD. Пытаюсь написать свой скрипт компоновки.

Сообщение SgtPepper_91 »

Я вот тоже хочу научиться понимать и работать с линкером. Задача такая - есть кастомный бутлоадер и есть основное приложение. Как это делать и как отредактировать линкер скрипты для обеих частей в интернете разжёвано. Моя пробелма - и загрузчик, и приложение использую ST-шную библиотеку для криптографии STM32cryptographic...GCC.a. Вернее используют они конечно не всю библиотеку целиком, а только несколько функций. Как можно сделать, чтобы эти функции при линковке загрузчика ложились в определенное место в прошивке, а в приложении эти функции подхватывались из того же места?
Professor Chaos
Открыл глаза
Сообщения: 41
Зарегистрирован: Вс мар 21, 2021 11:06:04

Re: GNU LD. Пытаюсь написать свой скрипт компоновки.

Сообщение Professor Chaos »

Использовать одни и те же данные или код и в бутлоадере и в основной прошивке плохая идея. В идеале, они должны быть абсолютно независимыми и не связанными. Единственное, что бутлоадер может знать об основной программе - её начальный адрес. Не более. А вот основная программа о бутлоадере не должна знать ничего. Вообще. Для неё его не существует.
SgtPepper_91
Первый раз сказал Мяу!
Сообщения: 28
Зарегистрирован: Пн ноя 30, 2015 03:52:00

Re: GNU LD. Пытаюсь написать свой скрипт компоновки.

Сообщение SgtPepper_91 »

[uquote="Professor Chaos",url="/forum/viewtopic.php?p=4021270#p4021270"]Использовать одни и те же данные или код и в бутлоадере и в основной прошивке плохая идея. В идеале, они должны быть абсолютно независимыми и не связанными. Единственное, что бутлоадер может знать об основной программе - её начальный адрес. Не более. А вот основная программа о бутлоадере не должна знать ничего. Вообще. Для неё его не существует.[/uquote]
В иделе да, но на секундочку давайте предположим, что я знаю, что делаю (всего лишь не знаю, как :D). Мы говорим о МК, и хотя сегодня они уже тоже дают много памяти, хранить вещи два раза - можно и обойтись.

Чёрт, да это уже на уровне практикума интересно попытаться реализовать.
Professor Chaos
Открыл глаза
Сообщения: 41
Зарегистрирован: Вс мар 21, 2021 11:06:04

Re: GNU LD. Пытаюсь написать свой скрипт компоновки.

Сообщение Professor Chaos »

В соседней теме я уже давал ссылку на эту книжку. Вам она тоже пригодится. Тут глава про загрузчик, а в конце её раздел про API загрузчика. Это то, что вам нужно.
Но я бы все-таки создал константную структуру указателей во флеш-памяти загрузчика по фиксированному адресу, который будет известен основной программе. Она будет обращаться к ней по этому адресу и вычитывать из неё указатели на нужные функции. Это избавит от траты ОЗУ.
SgtPepper_91
Первый раз сказал Мяу!
Сообщения: 28
Зарегистрирован: Пн ноя 30, 2015 03:52:00

Re: GNU LD. Пытаюсь написать свой скрипт компоновки.

Сообщение SgtPepper_91 »

[uquote="Professor Chaos",url="/forum/viewtopic.php?p=4021308#p4021308"]В соседней теме я уже давал ссылку на эту книжку. Вам она тоже пригодится. Тут глава про загрузчик, а в конце её раздел про API загрузчика. Это то, что вам нужно.
Но я бы все-таки создал константную структуру указателей во флеш-памяти загрузчика по фиксированному адресу, который будет известен основной программе. Она будет обращаться к ней по этому адресу и вычитывать из неё указатели на нужные функции. Это избавит от траты ОЗУ.[/uquote]
Ок, почитаю на выхах.

Пока мне удалось только положить функцию в загрузчике в отдельную секцию, её адрес - 0x080000c0. это void func(void) {}; но вызвать её из основной программы не получается, вызываю вот так ((void(*)(void))0x080000c0)(); Это было слишком наивно видимо :) буду читать
Professor Chaos
Открыл глаза
Сообщения: 41
Зарегистрирован: Вс мар 21, 2021 11:06:04

Re: GNU LD. Пытаюсь написать свой скрипт компоновки.

Сообщение Professor Chaos »

И как? Работает или нет?
Обратите внимание на эту страничку. Там сказано, чему должен быть равен младший бит в адресе перехода при вызове функции. А у вас он какой? Как адрес секции - с нулем в младшем бите?

1. Объявите тип структуры с полями-указателями на функции с требуемой сигнатурой.
2. В загрузчике создайте такую константную структуру, инициализируйте её указателями на требуемые функции, и поместите её в RO-секцию по фиксированному адресу.
3. В основной программе объявите константный указатель на эту структуру и инициализируйте его её фиксированным адресом.
4. В основной программе вызывайте функции, обращаясь к полям этой структуры через указатель на неё.
Аватара пользователя
YS
Друг Кота
Сообщения: 7518
Зарегистрирован: Вс мар 29, 2009 22:09:05
Контактная информация:

Re: GNU LD. Пытаюсь написать свой скрипт компоновки.

Сообщение YS »

Как можно сделать, чтобы эти функции при линковке загрузчика ложились в определенное место в прошивке, а в приложении эти функции подхватывались из того же места?
Я не великий гуру линкер-скриптинга, но предположу.

В скрипте компоновщика пишем:

Код: Выделить всё

MEMORY
{
    ...
    SHARED_CODE_REGION (rx) : ORIGIN <где должен начинаться разделяемый код>, LENGTH = <сколько выделим под разделяемый код>
}

...

.shared_code_section:
{
    __shared_code_start__ = .;
    KEEP(*(.shared_code));
} > SHARED_CODE_REGION;
Функции объявляем так:

Код: Выделить всё

<тип> function(...) __attribute__ ((section(".shared_code")))
{
    ...
}
В той программе, которая эти функции должна только использовать, но не содержать, к секции .shared_code_section добавляем атрибут NOLOAD.
Разница между теорией и практикой на практике гораздо больше, чем в теории.
Professor Chaos
Открыл глаза
Сообщения: 41
Зарегистрирован: Вс мар 21, 2021 11:06:04

Re: GNU LD. Пытаюсь написать свой скрипт компоновки.

Сообщение Professor Chaos »

YS писал(а):В той программе, которая эти функции должна только использовать, но не содержать, к секции .shared_code_section добавляем атрибут NOLOAD.
При таком подходе существует отличная от нуля вероятность, что в двух разных проектах линкер положит функции в секцию в разном порядке. Имеет полное право тасовать их внутри секции как хочет. И тогда в программе, которая их только использует, при вызове этих функций будет неопределенное поведение. Скорее всего хардфолт или еще какой-нибудь фолт.
Ответить

Вернуться в «ARM»