С чего начинается прошивка. intvec.
Если коротко, то IAR особо ничем не порадует. Нам все так же нужны файлы для старта прошивки. Скрипт для компоновщика от GCC не подойдет, гранаты там не той системы.
Хорошо, все файлы для IAR под stm32 имеются в замечательном кубическом графическом генераторе кода. В icf файле можно заметить такие строчки:
На мой вкус выглядит проще, чем аналогичное обозначение в GCC. Эти две строчки просто вопят о том, что секция .intvec лежит в самом начале flash памяти. Несложно догадаться, эта секция и содержит вектор прерываний. Файл с обработчиками тоже прилагается
Предварительное объявление секции CSTACK не должно удивлять, нам нужно взять ее конечный адрес и положить в начало нашей секции, что и делает конструкция sfe(CSTACK).
Дальше объявляем секцию .intvec. Секция помечена как NOROOT. Обычно это дает знать компоновщику, что эти фрагменты кода можно отбросить, если их никто не использует. Здесь же мы избегаем назойливого предупреждения при сборке, когда заменяем weak-символы на свои обработчики прерывания. Собственно, вот и вся магия: __vector_table сформирован из адреса конца секции стека и коллекции обработчиков прерываний.
Если коротко, то IAR особо ничем не порадует. Нам все так же нужны файлы для старта прошивки. Скрипт для компоновщика от GCC не подойдет, гранаты там не той системы.
Хорошо, все файлы для IAR под stm32 имеются в замечательном кубическом графическом генераторе кода. В icf файле можно заметить такие строчки:
define symbol __ICFEDIT_intvec_start__ = 0x08000000;
...
place at address mem:__ICFEDIT_intvec_start__ { readonly section .intvec };
На мой вкус выглядит проще, чем аналогичное обозначение в GCC. Эти две строчки просто вопят о том, что секция .intvec лежит в самом начале flash памяти. Несложно догадаться, эта секция и содержит вектор прерываний. Файл с обработчиками тоже прилагается
MODULE ?cstartup
;; Forward declaration of sections.
SECTION CSTACK:DATA:NOROOT(3)
SECTION .intvec:CODE:NOROOT(2)
EXTERN __iar_program_start
EXTERN SystemInit
PUBLIC __vector_table
DATA
__vector_table
DCD sfe(CSTACK)
DCD Reset_Handler ; Reset Handler
...
Предварительное объявление секции CSTACK не должно удивлять, нам нужно взять ее конечный адрес и положить в начало нашей секции, что и делает конструкция sfe(CSTACK).
Дальше объявляем секцию .intvec. Секция помечена как NOROOT. Обычно это дает знать компоновщику, что эти фрагменты кода можно отбросить, если их никто не использует. Здесь же мы избегаем назойливого предупреждения при сборке, когда заменяем weak-символы на свои обработчики прерывания. Собственно, вот и вся магия: __vector_table сформирован из адреса конца секции стека и коллекции обработчиков прерываний.
👍7
С++ startup
С момента первого знакомства со стартап-файлами каждого разработчика волновал вопрос: "можно ли обойтись без всего этого ассемблерного ужаса?". Не поймите этих разработчиков неправильно, они беззаветно любят ассемблерные инструкции как и запах напалма по утрам, просто иногда хочется не испытывать приступы сильной тревожности при виде написанного заботливым вендором начального кода. Ладно скрипт для компоновщика, там сложно что-либо упростить. Можно только посоветовать пить валерьянку перед открытием файла. Вот с startup_*.s файлами все интереснее. Не раз было высказано предположение, что С++ достаточно выразительный для такой вот не самой сложной задачи: разместить некоторые данные в определенной области памяти. На плюсах это можно провернуть.
Что нужно разместить? Вектор прерываний. Не думаю, что стоит использовать вектор из стандартной библиотеки, а вот массив можно попробовать.
Ситуация незначительно осложняется тем, что первым элементом массива должен стать адрес стека, а остальные элементы - адреса функций. Казалось бы... но разница есть. Допустим, мы не хотим ломать код об колено или колено об код, пусть массив содержит объединения (union).
Сигнатура обработчика прерывания пусть будет такой:
Объединим указатель на функцию с самым прозаичным указателем:
Помещаем наши функци-обработчики прерываний в массив (по версии GCC, конечно)
Размер массива зависит от устройства, конечно. Для stm32l452re нашлась аж 101 штука. Атрибут section переносит наш vector прерываний (теперь уже массив прерываний) в секцию .isr_vector, который нам уже хорошо знаком и адрес его можно легко вычислить в скрипте компоновщика.
Да, немного утрированный пример для проверки работоспособности гипотезы, но если уж использовать плюсы, то уж и std::array притащим. Обработчиками прерывания могут быть с++ функции такого типа:
Верно, это билет в один конец, но возвращаться все одно не собираемся.
_estack - символ экспортируется из того самого скрипта для линкера, указывает на конец секции стека.
Итого, все нужные адреса в заданной секции, а секция размещена по нужному адресу. И да, это работает.
С момента первого знакомства со стартап-файлами каждого разработчика волновал вопрос: "можно ли обойтись без всего этого ассемблерного ужаса?". Не поймите этих разработчиков неправильно, они беззаветно любят ассемблерные инструкции как и запах напалма по утрам, просто иногда хочется не испытывать приступы сильной тревожности при виде написанного заботливым вендором начального кода. Ладно скрипт для компоновщика, там сложно что-либо упростить. Можно только посоветовать пить валерьянку перед открытием файла. Вот с startup_*.s файлами все интереснее. Не раз было высказано предположение, что С++ достаточно выразительный для такой вот не самой сложной задачи: разместить некоторые данные в определенной области памяти. На плюсах это можно провернуть.
Что нужно разместить? Вектор прерываний. Не думаю, что стоит использовать вектор из стандартной библиотеки, а вот массив можно попробовать.
Ситуация незначительно осложняется тем, что первым элементом массива должен стать адрес стека, а остальные элементы - адреса функций. Казалось бы... но разница есть. Допустим, мы не хотим ломать код об колено или колено об код, пусть массив содержит объединения (union).
Сигнатура обработчика прерывания пусть будет такой:
using Handler = void(*)();
Объединим указатель на функцию с самым прозаичным указателем:
union IrqElement {
Handler handler;
uint32_t *address;
};
Помещаем наши функци-обработчики прерываний в массив (по версии GCC, конечно)
std::array<IrqElement, 101> vector __attribute__((section (".isr_vector"))) {{
{.address = &_estack},
{&Reset_Handler},
{&NMI_Handler},
...
Размер массива зависит от устройства, конечно. Для stm32l452re нашлась аж 101 штука. Атрибут section переносит наш vector прерываний (теперь уже массив прерываний) в секцию .isr_vector, который нам уже хорошо знаком и адрес его можно легко вычислить в скрипте компоновщика.
Да, немного утрированный пример для проверки работоспособности гипотезы, но если уж использовать плюсы, то уж и std::array притащим. Обработчиками прерывания могут быть с++ функции такого типа:
[[noreturn]] void Default_Handler() {
while(true) {}
}
Верно, это билет в один конец, но возвращаться все одно не собираемся.
extern "C" {
extern uint32_t _estack;
...
_estack - символ экспортируется из того самого скрипта для линкера, указывает на конец секции стека.
Итого, все нужные адреса в заданной секции, а секция размещена по нужному адресу. И да, это работает.
👍7
__attribute__((naked))
Это что за атрибут?
Наконец-то коснемся темы для взрослых. В хорошем смысле! Любителей давать волю фантазии придется огорчить, этот атрибут применим только к функциям. Да и они будут голые только в том смысле, что отсутствуют пролог и эпилог. Хорошо, что это не литературное произведение.
Как всем хорошо известно, функции должны не только исполнять то странное, что мы пишем внутри нее, но и скрывать следы своей бурной деятельности. То есть, привести стек и регистры в то же состояние, каким оно было до вызова. Кроме того, неплохо бы и вернуться из функции. Напишем нечто простое для иллюстрации.
Забавно, но метод класса тоже может иметь атрибут naked. Для людей, знающих толк в развлечениях. В общем, область возможного применения обнаженки не такая уж и обширная. Некоторые обработчики прерывай, например, рутинные операции над регистрами в упаковке функции.
Показательно, что давным-давно GCC под архитектуру x86 высокомерно игнорировал этот атрибут, но последние версии выдают очень интересные инструкции:
Это что за атрибут?
Наконец-то коснемся темы для взрослых. В хорошем смысле! Любителей давать волю фантазии придется огорчить, этот атрибут применим только к функциям. Да и они будут голые только в том смысле, что отсутствуют пролог и эпилог. Хорошо, что это не литературное произведение.
Как всем хорошо известно, функции должны не только исполнять то странное, что мы пишем внутри нее, но и скрывать следы своей бурной деятельности. То есть, привести стек и регистры в то же состояние, каким оно было до вызова. Кроме того, неплохо бы и вернуться из функции. Напишем нечто простое для иллюстрации.
void dummy() {При отключенной оптимизации наша бесполезная функции будет состоять из приличного числа инструкций.
}
dummy():Сохраняем регистр R7 на стеке, сохраняем в этом же регистре указатель на стек. Потом ничего не делаем. Затем восстанавливаем значения указателя стека и регистра R7, переходим по бережно хранимому адресу в регистре LR (адрес возврата). Безусловно, оптимизатор может порезать каике-то действия, если сочтет их избыточными, но возврат-то оставит.
push {r7}
add r7, sp, #0
nop
mov sp, r7
ldr r7, [sp], #4
bx lr
dummy():Добавим атрибут naked:
bx lr
dummy():Это без оптимизаций! Иначе там вообще ни строчки не останется. Если говорить про архитектуру ARM, то после выполнения оголенки процессор просто шагнет на следующую за функций команду в памяти. Нам сильно повезет, если это будет что-то осмысленное. Предполагается, что до конца функции мы не дойдем или же пропишем возврат явно. Официальная документация настойчиво рекомендует использовать только базовый ассемблер внутри (инструкции без операндов). Расширенный ассемблер или инструкции C могут работать, но без гарантий, увы. Еще бы, если работы со стеком нет, возврата нет, ничего нет.
nop
Забавно, но метод класса тоже может иметь атрибут naked. Для людей, знающих толк в развлечениях. В общем, область возможного применения обнаженки не такая уж и обширная. Некоторые обработчики прерывай, например, рутинные операции над регистрами в упаковке функции.
Показательно, что давным-давно GCC под архитектуру x86 высокомерно игнорировал этот атрибут, но последние версии выдают очень интересные инструкции:
dummy():Кроме безделья в самом конце добавилась инструкция ud2. Undefined Instruction. Специальная инструкция. Применять, только когда все становится пугающе неопределенно. Генерирует исключение, и программа здесь падет с чем-то вроде "Illegal instruction". Это не милое плюсовое исключение, это будет какой-нибудь жесткий SIGILL. Попробуй перехвати. Хоть это и кажется странным, в этом есть смысл. Будет большой ошибкой покидать функцию не через адрес возврата, а просто выходя через окно или сквозь стену. Если уж в ход пошла исключительная инструкция, то где-то что-то пошло не так. Лучше сразу ткнуть в нас мизерикордией. Хорошо, что для ARM все не так... или нет?
nop
ud2
👍10
Deducing this
В конце жаркой рабочей недели можно немного расслабиться и вспомнить какие-нибудь уморительные и прохладные истории. Например, как частенько приходилось реализовывать в классе парные методы: первый полезный и второй константный. В иных случаях это часть интерфейса, которую придется описывать. Сразу вспоминается веселая возня с begin() и end(), что должны возвращать iterator или const_iterator в зависимости от контекста в котором были вызваны. Или метод data() у всяких там контейнеров. В детстве мы писали такие функции быстро и без затей:
В этом случае все просто, но иногда внутри функции нужны какие-то дополнительные телодвижения для достижения правильного результата. Поддавшись искушению просто скопировать строчки одного метода в другой, мы с удивлением обнаруживали, что код начинал предательски пахнуть. Потом уже, начитавшись книжек господина Скотта Майерса, открыли другой путь.
То есть, константный метод работает как обычно, а второй, допускающий изменение объекта, создает константную ссылку на самого себя. Потом через ссылку вызывается другой метод get(). Результат пропускаем через преобразование const_cast, чтоб привести объект к нужному нам типу. Ничего дурного не может случиться, мы точно знаем, объект x_ можно модифицировать. К сожалению, некоторым стандартам безопасности такие экзерсисы не внушают доверия, они не столь продвинутых взглядов. Если не горите желанием отстаивать корректность своего кода в сырых подвалах департамента безопасности, то можно подсмотреть иной подход хотя бы у Джейсона Тернера.
Темплейты! Они всегда готовы помочь. Здесь мы в разных методах вызываем некий общий, чаще всего приватный, шаблонный метод, где используем ссылку на самого себя. В разных контекстах тип Self будет разным: Test или Test const. Что даст нужный нам эффект и метод сам выведет возвращаемый тип. Хоть такой подход выглядит гораздо лучше на фоне остальных, но, скажем честно, AUTROSAR еще придется убедить приподзакрыть глаза на некоторые моменты.
И вот сейчас, хоть мы и стоим по колено в коде, не стоит отчаиваться, скоро выйдет C++23 и таки наступит конец страданиям. Ведь тогда мы наконец-то сможем передавать параметр this явно! Не думаю, что для кого-то является секретом скрытая передача этого параметра в метод класса. Можно даже отключить pedantic, инстинкт самосохранения и попробовать грязнейший хак.
Насильственное приведение метода класса к свободной функции. Результат вернется правильный, но за такой код коллеги не похвалят. Скорее, вас найдут потом в мусорном баке соседнего города со следами укусов на лице.
Что же предлагают в новом стандарте (proposal P0847R7, если кто-то хочет ознакомиться). Явно заявляем, что первым параметром пойдет универсальная ссылка на родной класс, только тип и возвращаемый тип компилятор должен вывести сам.
Вот теперь красиво и никаких плясок вокруг разного контекста. Просто выводим все типы и наслаждаемся лапидарностью подхода. Уже сейчас можно попробовать эту прекрасную фичу на MSVC (не забываем ключ /std:c++latest).
В конце жаркой рабочей недели можно немного расслабиться и вспомнить какие-нибудь уморительные и прохладные истории. Например, как частенько приходилось реализовывать в классе парные методы: первый полезный и второй константный. В иных случаях это часть интерфейса, которую придется описывать. Сразу вспоминается веселая возня с begin() и end(), что должны возвращать iterator или const_iterator в зависимости от контекста в котором были вызваны. Или метод data() у всяких там контейнеров. В детстве мы писали такие функции быстро и без затей:
struct Test {
int x_;
int &get() { return x_; }
int const &get() const { return x_; }
};
В этом случае все просто, но иногда внутри функции нужны какие-то дополнительные телодвижения для достижения правильного результата. Поддавшись искушению просто скопировать строчки одного метода в другой, мы с удивлением обнаруживали, что код начинал предательски пахнуть. Потом уже, начитавшись книжек господина Скотта Майерса, открыли другой путь.
struct Test {
int x_;
int & get() {
Test const &const_this {*this};
return const_cast<int &>(const_this.get());
}
int const &get() const { return x_; }
};
То есть, константный метод работает как обычно, а второй, допускающий изменение объекта, создает константную ссылку на самого себя. Потом через ссылку вызывается другой метод get(). Результат пропускаем через преобразование const_cast, чтоб привести объект к нужному нам типу. Ничего дурного не может случиться, мы точно знаем, объект x_ можно модифицировать. К сожалению, некоторым стандартам безопасности такие экзерсисы не внушают доверия, они не столь продвинутых взглядов. Если не горите желанием отстаивать корректность своего кода в сырых подвалах департамента безопасности, то можно подсмотреть иной подход хотя бы у Джейсона Тернера.
struct Test {
int x_;
template <typename Self>
static auto &get(Self &self) {
return self.x_;
}
int &get() { return get(*this); }
int const & get() const { return get(*this); }
Темплейты! Они всегда готовы помочь. Здесь мы в разных методах вызываем некий общий, чаще всего приватный, шаблонный метод, где используем ссылку на самого себя. В разных контекстах тип Self будет разным: Test или Test const. Что даст нужный нам эффект и метод сам выведет возвращаемый тип. Хоть такой подход выглядит гораздо лучше на фоне остальных, но, скажем честно, AUTROSAR еще придется убедить приподзакрыть глаза на некоторые моменты.
И вот сейчас, хоть мы и стоим по колено в коде, не стоит отчаиваться, скоро выйдет C++23 и таки наступит конец страданиям. Ведь тогда мы наконец-то сможем передавать параметр this явно! Не думаю, что для кого-то является секретом скрытая передача этого параметра в метод класса. Можно даже отключить pedantic, инстинкт самосохранения и попробовать грязнейший хак.
using free_func = int &(*)(Test *);
using mem_func = int &(Test::*)();
Test t {10};
auto cast = [](mem_func fn) { return reinterpret_cast<free_func>(fn); };
free_func func = cast(&Test::get);
assert(func(&t) == 10);
Насильственное приведение метода класса к свободной функции. Результат вернется правильный, но за такой код коллеги не похвалят. Скорее, вас найдут потом в мусорном баке соседнего города со следами укусов на лице.
Что же предлагают в новом стандарте (proposal P0847R7, если кто-то хочет ознакомиться). Явно заявляем, что первым параметром пойдет универсальная ссылка на родной класс, только тип и возвращаемый тип компилятор должен вывести сам.
struct Test {
int x_;
template <typename Self>
auto &get(this Self &&self) {
return std::forward<Self>(self).x_;
}
};
Вот теперь красиво и никаких плясок вокруг разного контекста. Просто выводим все типы и наслаждаемся лапидарностью подхода. Уже сейчас можно попробовать эту прекрасную фичу на MSVC (не забываем ключ /std:c++latest).
👍8
Reset_Handler
Думаю, все уже знают, чем занята прошивка сразу после включения питания. Вкратце, регистр указателя стека (SP) загружает значение по адресу 0x00000000, а программный счетчик (PC) выбирает значение по адресу 0x00000004. Эти странные адреса приведут нас точно в начало флеш-памяти, к хорошо знакомому нам адресу 0x08000000. Таков путь stm32. Другими словами, PC неотвратимо попадает в обработчик сброса Reset_Handler! Почему же не main? Нельзя просто взять и войти в главную (для пользователя) функцию без предварительной подготовки, а у нас конь еще не валялся.
Неплохо бы для начала инициализировать переменные с длительным временем жизни. Статические переменные, локальные статические переменные внутри функций и глобальные переменные. Всех этих долгожителей компилятор заботливо собирает в две могучие кучки: инициализированные переменные (те, которым мы присваиваем значение при объявлении) и неинициализированные переменные (те, про которые мы забыли). Компактное место проживания первых в оперативной памяти называется секцией памяти .data, а вторые ютятся в секции .bss (Block Started by Symbol, так сложилось исторически).
Что же, попытаемся преодолеть брезгливость и заглянем в полный ассемблера вендорский стартап-файл. Хорошо видны функции с говорящими названиями CopyDataInit и LoopCopyDataInit. Еще лучше говорят комментарии перед ними
Что еще там осталось? FillZerobss и LoopFillZerobss. Тут все проще. Секция неинициализированных переменных щедро заливается нулями. Если отдельные вендоры, которые пропускают этот шаг для поддержания разработчика в тонусе. Забыл инициализировать переменную - сыграл в рулетку. Там может быть все что угодно!
Для успешного обнуления нам потребуются символы начала и конца секции .bss: _sbss и _ebss.
Думаю, все уже знают, чем занята прошивка сразу после включения питания. Вкратце, регистр указателя стека (SP) загружает значение по адресу 0x00000000, а программный счетчик (PC) выбирает значение по адресу 0x00000004. Эти странные адреса приведут нас точно в начало флеш-памяти, к хорошо знакомому нам адресу 0x08000000. Таков путь stm32. Другими словами, PC неотвратимо попадает в обработчик сброса Reset_Handler! Почему же не main? Нельзя просто взять и войти в главную (для пользователя) функцию без предварительной подготовки, а у нас конь еще не валялся.
Неплохо бы для начала инициализировать переменные с длительным временем жизни. Статические переменные, локальные статические переменные внутри функций и глобальные переменные. Всех этих долгожителей компилятор заботливо собирает в две могучие кучки: инициализированные переменные (те, которым мы присваиваем значение при объявлении) и неинициализированные переменные (те, про которые мы забыли). Компактное место проживания первых в оперативной памяти называется секцией памяти .data, а вторые ютятся в секции .bss (Block Started by Symbol, так сложилось исторически).
Что же, попытаемся преодолеть брезгливость и заглянем в полный ассемблера вендорский стартап-файл. Хорошо видны функции с говорящими названиями CopyDataInit и LoopCopyDataInit. Еще лучше говорят комментарии перед ними
/* Copy the data segment initializers from flash to SRAM */Если долго глядеть на стройные ряды ассемблерных команд, до рвоты, до отвращения, то можно рассмотреть усердное копирование неких данных с адреса _sidata флеш-памяти в оперативную память от адреса _sdata до _edata. Все адреса легко определяются через скрипт компоновщика:
/* used by the startup to initialize data */То есть глобальные символы _sdata и _edata отмечают адреса начала и конца секции с переменными, которые должны быть инициализированы некими значениями. Настоящие данные для инициализации переменных хранятся, конечно, в энергонезависимой памяти по адресу символа _sidata. Без этого наложения в ваших глобальных переменных может вдруг обнаружиться мусор.
_sidata = LOADADDR(.data);
/* Initialized data sections goes into RAM, load LMA copy after code */
.data :
...
_sdata = .; /* create a global symbol at data start */
...
_edata = .; /* define a global symbol at data end */
Что еще там осталось? FillZerobss и LoopFillZerobss. Тут все проще. Секция неинициализированных переменных щедро заливается нулями. Если отдельные вендоры, которые пропускают этот шаг для поддержания разработчика в тонусе. Забыл инициализировать переменную - сыграл в рулетку. Там может быть все что угодно!
Для успешного обнуления нам потребуются символы начала и конца секции .bss: _sbss и _ebss.
.bss :Такая же техника, что и для .data, только без отображения на флеш-память, там все равно одни нули.
{
/* This is used by the startup in order to initialize the .bss secion */
_sbss = .; /* define a global symbol at bss start */
...
_ebss = .; /* define a global symbol at bss end */
👍12
Reset_Handler (продолжение)
Чувствую, как этот вопрос уже зародился в голове: "Можно ли сказать то же самое, но на языке C++?". Почему бы и нет?
Чувствую, как этот вопрос уже зародился в голове: "Можно ли сказать то же самое, но на языке C++?". Почему бы и нет?
extern "C" {Для начала утащим все нужные нам символы из скрипта компоновщика. Как видите, обработчику добавлен атрибут naked, поскольку сохранять значение регистров при входе бессмысленно, а выходить отсюда мы не собираемся. О чем красноречиво свидетельствует стандартный нынче атрибут [[noreturn]]. Все, что мы описали выше, заменим из хулиганских побуждений двумя строчками. Инициализацию секции .data исполним через memcpy, .bss заполняется нулями через memset. Стандартные функции глобальных или статических переменных не используют, все копируется правильно. Не проверял, работает ли это быстрее, но стало немного понятнее.
...
extern uint32_t _estack;
extern uint32_t _sidata;
extern uint32_t _sdata;
extern uint32_t _edata;
extern uint32_t _sbss;
extern uint32_t _ebss;
}
[[noreturn]] __attribute__((naked)) void Reset_Handler() {
std::memcpy(&_sdata, &_sidata, static_cast<size_t>(&_edata - &_sdata));
std::memset(&_sbss, 0x00, static_cast<size_t>(&_ebss - &_sbss));
...
main();
while(true) {}
}
👍10
IAR и снова Reset_Handler.
Настало время пристально рассмотреть начальный этап работы Reset_Handler для IAR. Сами понимаете, никаких чудес не будет и глобальные переменные в RAM придется инициализировать значениями из ROM. Своенравный компилятор здесь не стал выделяться и так же создает секции .data и .bss. Однако, вручную заполнять их не требуется, как это было сделано в обработчике сброса для GCC. Местный Reset_Handler становится совсем простым:
Атрибут __irq говорит о том, что функция не принимает параметров и не возвращает значений как и всякий уважающий себя обработчик прерываний. SystemInit из библиотеки HAL нежно настраивает нужные регистры для плавного старта, а вот вся соль с мякоткой сосредоточена в __iar_program_start. Функция эта стандартная для IAR, подтягивается из подготовленных библиотек (конкретно из
Что здесь происходит? Мы просто гуляем по сгенерированной компоновщиком таблице
Таких блоков в таблице может быть несколько, если IAR сочтет разумным использовать разный подход к отдельным областям RAM. Например, для секции .data применить __iar_copy_init, а для .bss вызвать __iar_zero_init. Компилятор может по своему усмотрению использовать алгоритмы сжатия для хранения данных в ROM (init_table) и распаковывать их в прямо в нужную область памяти: __iar_packbits_init_single, __iar_lz77_init_single. Что и говорить, многое он взял на себя, скоро и код сам писать будет.
Настало время пристально рассмотреть начальный этап работы Reset_Handler для IAR. Сами понимаете, никаких чудес не будет и глобальные переменные в RAM придется инициализировать значениями из ROM. Своенравный компилятор здесь не стал выделяться и так же создает секции .data и .bss. Однако, вручную заполнять их не требуется, как это было сделано в обработчике сброса для GCC. Местный Reset_Handler становится совсем простым:
[[noreturn]] __irq __naked void Reset_Handler() {
SystemInit();
__iar_program_start();
while(true) {}
}
Атрибут __irq говорит о том, что функция не принимает параметров и не возвращает значений как и всякий уважающий себя обработчик прерываний. SystemInit из библиотеки HAL нежно настраивает нужные регистры для плавного старта, а вот вся соль с мякоткой сосредоточена в __iar_program_start. Функция эта стандартная для IAR, подтягивается из подготовленных библиотек (конкретно из
cstartup_M.o
). Внутри можно найти много интересного, например, вызов функции __iar_data_init3 (3 - версия интерфейса инициализации elf). В ней до вызова main обязательно запускается адское колесо инициализации, принцип работы которого можно подсмотреть в любезно оставленных разработчиками исходных файлах компилятора.typedef struct { src_index_t mOff; } FAddr;
static init_fun_t * FAddr_GetPtr(FAddr const * me) {
return (init_fun_t *)((src_ptr_t)me + me->mOff);
}
void __iar_data_init3(void) {
FAddr const * pi = __section_begin("Region$$Table");
table_ptr_t pe = __section_end ("Region$$Table");
while (pi != pe) {
init_fun_t * fun = FAddr_GetPtr(pi);
++pi;
pi = fun(pi);
}
}
Что здесь происходит? Мы просто гуляем по сгенерированной компоновщиком таблице
Region$$Table
. Первым значением будет смещение относительно функции инициализации (__iar_copy_init3, например). Вычисляем указатель на нее и вызываем, передав как параметр указатель на второе значение. Это будет как раз размер области памяти, которую нужно облагодетельствовать этой функцией. Далее идет смещение относительно данных которые нужно записать: созданная компоновщиком область .iar.init_table
. Последним в блоке идет адрес куда данные нужно доставить. Возвращает функция адрес начала следующего блока инициализации, если он есть.<Region$$Table>:
ffffff57 <== __iar_copy_init3
00000030 <== init data size
ffffffc8 <== source .iar.init_table
20000000 <== destination address
00000000 <== next block?
Таких блоков в таблице может быть несколько, если IAR сочтет разумным использовать разный подход к отдельным областям RAM. Например, для секции .data применить __iar_copy_init, а для .bss вызвать __iar_zero_init. Компилятор может по своему усмотрению использовать алгоритмы сжатия для хранения данных в ROM (init_table) и распаковывать их в прямо в нужную область памяти: __iar_packbits_init_single, __iar_lz77_init_single. Что и говорить, многое он взял на себя, скоро и код сам писать будет.
👍6
Media is too big
VIEW IN TELEGRAM
Что может быть лучше, чем вечером сползти со стула и, удобно разнежившись на полу, посмотреть новый фильм о мужественных людях в странных нарядах, несущих возмездие во имя Луны? Только просмотр видео с конференции С++ Russia 2021! Наткнулся на запись доклада Владимира Вишневского (нет, не поэта, хотя...) "С++ в мире встраиваемых систем".
Доклад про общие понятия плавно перетекает в обсуждение любимых мозолей, т.е. применение C++ в нашей непростой сфере под гнетом отраслевых стандартов (MISRA, AUTOSAR). Конечно, не обошли стороной ни исключение, ни динамические выделения памяти.
Мне нравятся варианты решения задачи жонглирования битами в регистрах через шаблоны. Здесь есть симпатичное решение, довольно безопасное и с возможностью тестирования вне целевой платформы.
Если вы не любите изобретать велосипед, то имеются сторонние контейнеры (Boost, EASTL). Если вам совесть позволяет это использовать. Или отдел безопасности.
Отличный доклад, смотреть лучше вечером. Всю ночь будет сниться интрузивный контейнер.
Доклад про общие понятия плавно перетекает в обсуждение любимых мозолей, т.е. применение C++ в нашей непростой сфере под гнетом отраслевых стандартов (MISRA, AUTOSAR). Конечно, не обошли стороной ни исключение, ни динамические выделения памяти.
Мне нравятся варианты решения задачи жонглирования битами в регистрах через шаблоны. Здесь есть симпатичное решение, довольно безопасное и с возможностью тестирования вне целевой платформы.
Если вы не любите изобретать велосипед, то имеются сторонние контейнеры (Boost, EASTL). Если вам совесть позволяет это использовать. Или отдел безопасности.
Отличный доклад, смотреть лучше вечером. Всю ночь будет сниться интрузивный контейнер.
👍6
__attribute__ ((weak, alias))
Это что за атрибут?
Бывает так, что проводишь долгие часы в бесплодных попытках выбрать переменной выразительное и звучное имя. Уже потом, через пару лет, осознаешь всю ошибочность окончательного варианта. Хорошо, что некоторые компиляторы позволяют создать псевдоним. В частности для GCC это будет выглядеть как атрибут alias.
Сделаем вид, что у нас есть переменная max_gorky, но на самом деле alias с указанием имени реального объекта превратит ее в прокси-объект настоящей и полновесной переменной alex_peshkov. С этим псевдонимом можно работать как с настоящим именем, никто разницы не заметит. Только необходимо убедиться, что типы у обоих совпадают, иначе можно получить очень странное и неприятное поведение.
Для функций это тоже прекрасно работает:
Теперь можно вызвать функцию по псевдониму. В оригинальном объявлении extern "C" нужен для того, чтоб компилятор не исказил имя функции, не применил бы на нем свой ужасающий C++ name mangling. Тогда мы можем привязаться к его имени, иначе бы пришлось городить что-то вроде:
Хотя IAR понимает и имена c++, что приятно удивляет.
Ничего особенного в alias нет, за красивым названием кроется единственная директива объявляющая тождественность имен:
Может показаться, что в создании псевдонимов столько же смысла, как и в современном искусстве. Однако, alias в сочетании с атрибутом weak может дать интересный эффект. Weak ослабляет наш символ, и компоновщик с радостью пожертвует им, если встретит определенный пользователем сильный и здоровый символ с таким же именем в иной единице трансляции.
Это может облегчить жизнь, когда должно быть определено изрядное количество обязательных функций. Чтоб не писать лишнего, можно просто пометить все символы как weak и перенаправить на одну реализацию по-умолчанию. Тогда пользователь, если надумает, может переопределить любую из них.
Да, это идеально описывает ситуацию с обработчиками прерываний. Если нужно, любой обработчик можно переопределить, иначе мы проваливаемся в бесконечный цикл и ждем сторожевых псов.
Это что за атрибут?
Бывает так, что проводишь долгие часы в бесплодных попытках выбрать переменной выразительное и звучное имя. Уже потом, через пару лет, осознаешь всю ошибочность окончательного варианта. Хорошо, что некоторые компиляторы позволяют создать псевдоним. В частности для GCC это будет выглядеть как атрибут alias.
int32_t alex_peshkov = 0;
extern int32_t __attribute__ ((alias ("alex_peshkov"))) max_gorky;
Сделаем вид, что у нас есть переменная max_gorky, но на самом деле alias с указанием имени реального объекта превратит ее в прокси-объект настоящей и полновесной переменной alex_peshkov. С этим псевдонимом можно работать как с настоящим именем, никто разницы не заметит. Только необходимо убедиться, что типы у обоих совпадают, иначе можно получить очень странное и неприятное поведение.
Для функций это тоже прекрасно работает:
extern "C" int golikov() {
...
};
__attribute__ ((alias ("golikov"))) int gaidar();
Теперь можно вызвать функцию по псевдониму. В оригинальном объявлении extern "C" нужен для того, чтоб компилятор не исказил имя функции, не применил бы на нем свой ужасающий C++ name mangling. Тогда мы можем привязаться к его имени, иначе бы пришлось городить что-то вроде:
int golikov() {...}
__attribute__ ((alias ("_Z7golikovv"))) int gaidar();
Хотя IAR понимает и имена c++, что приятно удивляет.
Ничего особенного в alias нет, за красивым названием кроется единственная директива объявляющая тождественность имен:
.set max_gorky,alex_peshkov
.set gaidar(),golikov()
Может показаться, что в создании псевдонимов столько же смысла, как и в современном искусстве. Однако, alias в сочетании с атрибутом weak может дать интересный эффект. Weak ослабляет наш символ, и компоновщик с радостью пожертвует им, если встретит определенный пользователем сильный и здоровый символ с таким же именем в иной единице трансляции.
Это может облегчить жизнь, когда должно быть определено изрядное количество обязательных функций. Чтоб не писать лишнего, можно просто пометить все символы как weak и перенаправить на одну реализацию по-умолчанию. Тогда пользователь, если надумает, может переопределить любую из них.
[[noreturn]] void Default_Handler() {
while(true) {}
}
#define __attribute_default__ __attribute__((noreturn, naked, weak, alias("_Z15Default_Handlerv")))
void NMI_Handler() __attribute_default__;
void HardFault_Handler() __attribute_default__;
void MemManage_Handler() __attribute_default__;
...
Да, это идеально описывает ситуацию с обработчиками прерываний. Если нужно, любой обработчик можно переопределить, иначе мы проваливаемся в бесконечный цикл и ждем сторожевых псов.
👍4👎1
ref-qualifiers
Можно прожить в городе всю жизнь, но так и не побывать ни разу в метро. Ничего ужасного в этом нет, хотя в определенных затруднительных ситуациях этот вид транспорта может сильно выручить. Так и про ссылочные квалификаторы можно не задумываться. Хотя если вы пишете свои библиотеки с велосипедами, то, конечно, стоит изучить вопрос. А библиотеки не все могут разрабатывать, вернее, разрабатывать могут не только лишь все, любой С++ разработчик рано или поздно подползает к этому сомнительному развлечению.
Как вы уже знаете, функции-члены класса могут быть вызваны в двух разных контекстах, const и non-const. Однако язык различает и другие оттенки вкуса вызываемого метода.
Может случиться так, что ее для lvalue объекта нам требуется одно поведение, а для rvalue объекта - немного другое. В языке есть такая возможность, достаточно добавить & после списка аргументов для lvalue контекста или && для rvalue.
За примером далеко ходить не надо, вот optional стандартной библиотеки:
Очевидно, что метод value_or возвращает значение, если оно есть, или __u. Теперь вы видите сам механизм: метод в lvalue контексте возвращает значение через метод _M_get, в rvalue он передает владение std::move(this->_M_get()).
Сколько контекстов мы сможем распознать? Начнем с rvalue и lvalue:
Конечно же, никто не отменял const. Добавим еще функций
Это все разные функции со своими уникальными именами в с++
Заглянем в ABI, чтоб понимать, что компилятор там понаписал.
После обязательного префикса _Z следует nested-name.
где контекст вызова будет определяться следующим образом
Мы совсем забыли про volatile!
Каждую из определенных нами функций saul можно вызвать. Нам потребуется несложный шаблон
Все просто: через универсальную ссылку вызываем нашу функцию, при этом std::forward здесь важен, иначе мы потеряем rvalue контекст.
В зависимости от типа передаваемого значения будет вызываться соответствующий метод, например
Все, это успех. Пойду подумаю, где это можно будет бесконтрольно и безответственно применить.
Можно прожить в городе всю жизнь, но так и не побывать ни разу в метро. Ничего ужасного в этом нет, хотя в определенных затруднительных ситуациях этот вид транспорта может сильно выручить. Так и про ссылочные квалификаторы можно не задумываться. Хотя если вы пишете свои библиотеки с велосипедами, то, конечно, стоит изучить вопрос. А библиотеки не все могут разрабатывать, вернее, разрабатывать могут не только лишь все, любой С++ разработчик рано или поздно подползает к этому сомнительному развлечению.
Как вы уже знаете, функции-члены класса могут быть вызваны в двух разных контекстах, const и non-const. Однако язык различает и другие оттенки вкуса вызываемого метода.
Может случиться так, что ее для lvalue объекта нам требуется одно поведение, а для rvalue объекта - немного другое. В языке есть такая возможность, достаточно добавить & после списка аргументов для lvalue контекста или && для rvalue.
За примером далеко ходить не надо, вот optional стандартной библиотеки:
template<typename _Up>
constexpr _Tp value_or(_Up&& __u) const & {
...
return this->_M_is_engaged()
? this->_M_get()
: static_cast<_Tp>(std::forward<_Up>(__u));
}
template<typename _Up>
_Tp value_or(_Up&& __u) && {
...
return this->_M_is_engaged()
? std::move(this->_M_get())
: static_cast<_Tp>(std::forward<_Up>(__u));
}
Очевидно, что метод value_or возвращает значение, если оно есть, или __u. Теперь вы видите сам механизм: метод в lvalue контексте возвращает значение через метод _M_get, в rvalue он передает владение std::move(this->_M_get()).
Сколько контекстов мы сможем распознать? Начнем с rvalue и lvalue:
struct S {
void saul() & { std::cout<<__PRETTY_FUNCTION__<<std::endl; }
void saul() && { std::cout<<__PRETTY_FUNCTION__<<std::endl; }
};
Конечно же, никто не отменял const. Добавим еще функций
void saul() const & { std::cout<<__PRETTY_FUNCTION__<<std::endl; }
void saul() const && { std::cout<<__PRETTY_FUNCTION__<<std::endl; }
Это все разные функции со своими уникальными именами в с++
0000000000400a0c w F .text 000000000000002b _ZNKO1S4saulEv
00000000004009b4 w F .text 000000000000002b _ZNO1S4saulEv
00000000004009e0 w F .text 000000000000002b _ZNKR1S4saulEv
0000000000400988 w F .text 000000000000002b _ZNR1S4saulEv
Заглянем в ABI, чтоб понимать, что компилятор там понаписал.
После обязательного префикса _Z следует nested-name.
<nested-name> ::= N [<CV-qualifiers>] [<ref-qualifier>] <prefix> <unqualified-name> E
::= N [<CV-qualifiers>] [<ref-qualifier>] <template-prefix> <template-args> E
где контекст вызова будет определяться следующим образом
<CV-qualifiers> ::= [r] [V] [K] # restrict (C99), volatile, const
<ref-qualifier> ::= R # & ref-qualifier
<ref-qualifier> ::= O # && ref-qualifier
Мы совсем забыли про volatile!
void saul() volatile & { std::cout<<__PRETTY_FUNCTION__<<std::endl; } // _ZNVR1S4saulEv
void saul() volatile && { std::cout<<__PRETTY_FUNCTION__<<std::endl; } // _ZNVO1S4saulEv
void saul() volatile const & { std::cout<<__PRETTY_FUNCTION__<<std::endl; } // _ZNVKR1S4saulEv
void saul() volatile const && { std::cout<<__PRETTY_FUNCTION__<<std::endl; } // _ZNVKO1S4saulEv
Каждую из определенных нами функций saul можно вызвать. Нам потребуется несложный шаблон
template <class T>
void call_saul(T && t) {
std::forward<T>(t).saul();
}
Все просто: через универсальную ссылку вызываем нашу функцию, при этом std::forward здесь важен, иначе мы потеряем rvalue контекст.
В зависимости от типа передаваемого значения будет вызываться соответствующий метод, например
S const volatile s_cv{};
call_saul(s_cv); // void S::saul() const volatile &
call_saul(std::move(s_cv)); // void S::saul() const volatile &&
Все, это успех. Пойду подумаю, где это можно будет бесконтрольно и безответственно применить.
👍5😁1
__libc_init_array
Некоторое время назад мы рассматривали старт прошивки и функцию Reset_Handler, куда затесался таинственный вызов функции __libc_init_array. Все дело в том, что в функцию main мы должны войти полностью готовыми. Статические долгожители и глобальные объекты не просто заполняются данными из соответствующих секций, но и должны начать жить. Иными словами, кто-то должен вызвать их конструктор. Поскольку иных желающих нет, этом занимается рассматриваемая нами функция. Как это может быть исполнено в GCC для ARM.
В любом приличном скрипте для компоновщика можно заметить описание секции .init_array:
Заглянем в исходный код классической реализации функции __libc_init_array.
Немного магии от компоновщика
🔹 Вызывает конструкторы классов для глобальных объектов.
🔹 Регистрирует деструкторы классов через atexit (для нас неактуально).
У каждой единицы трансляции эта функция своя, а .init_array един, поэтому и понадобились обертки с веселыми и разными именами.
Некоторое время назад мы рассматривали старт прошивки и функцию Reset_Handler, куда затесался таинственный вызов функции __libc_init_array. Все дело в том, что в функцию main мы должны войти полностью готовыми. Статические долгожители и глобальные объекты не просто заполняются данными из соответствующих секций, но и должны начать жить. Иными словами, кто-то должен вызвать их конструктор. Поскольку иных желающих нет, этом занимается рассматриваемая нами функция. Как это может быть исполнено в GCC для ARM.
В любом приличном скрипте для компоновщика можно заметить описание секции .init_array:
.init_array:Еще там есть секция .preinit_array, но она по структуре не отличается от первой, и нас будет больше интересовать .init_array, именно там зарождается жизнь. В названии легко угадываются очертания массива указателей на функции, которые должны быть вызваны во что бы то ни стало перед main.
{
PROVIDE_HIDDEN (__init_array_start = .);
KEEP (*(SORT(.init_array.*)))
KEEP (*(.init_array*))
PROVIDE_HIDDEN (__init_array_end = .);
} >FLASH
Заглянем в исходный код классической реализации функции __libc_init_array.
Немного магии от компоновщика
/* These magic symbols are provided by the linker. */Экспортируем символы начала и конца массивов инициализации и пробегаем по всем указателям, вызывая каждую функцию.
extern void (*__init_array_start []) (void) __attribute((weak));
extern void (*init_array_end []) (void) __attribute((weak));
/* Iterate over all the init routines. */Если собирать прошивку с параметрами -specs=nano.specs -nostartfiles, то можно получить единственную функцию в массиве инициализации без всяких там малопонятных пока register_fini и frame_dummy.
void __libc_init_array (void) {
...
// вызов функций из массива preinit_array
_init ();
count = __init_array_end - __init_array_start;
for (int i = 0; i < count; i++) __init_array_start[i] ();
}
08000408 .init_array __init_array_startПравда, придется переопределить функцию _init, которая вызывается в перерыве между preinit_array и init_array, но это не проблема. Все равно сейчас в ней не осталось ничего полезного. Желающие могут скопировать код из исходников или просто сделать заглушку:
0800040c .init_array __init_array_end
Contents of section .init_array:
8000408 e5020008 00000000
extern "C" void _init() {}.
Единственный указатель в init_array ведет к функции _GLOBAL__sub_I_my_obj1 (_GLOBAL__sub_I_ - стандартный префикс для этой функции, my_obj1 - имя первой попавшейся глобальной переменной в этой единице трансляции). Это просто обертка для сгенерированной компилятором функции __static_initialization_and_destruction_0, которая служит двум целям:🔹 Вызывает конструкторы классов для глобальных объектов.
🔹 Регистрирует деструкторы классов через atexit (для нас неактуально).
У каждой единицы трансляции эта функция своя, а .init_array един, поэтому и понадобились обертки с веселыми и разными именами.
👍8
__attribute__((constructor))
Это что за атрибут?
В прошлый раз поверхностно коснулись того, как выполняется инициализация до главной функции main. Не спеша обходим все указатели в массиве .init_array и бездумно вызываем соответствующие им функции. Можно ли дополнить этот список своим очень нужным методом инициализации чего-либо? Конечно! Атрибут constructor даст нам нужный эффект.
Обратите внимание на фрагмент скрипта компоновщика, где говорится о секции .init_array:
В начале секции размещаются отсортированные по имени символы, а затем уже все остальные. Это важно, поскольку дает возможность приоритизировать вызовы функций:
Да, если вы рисковый разработчик или знаете, что делаете, то предупреждение можно отключить опцией
Время для небольшого примера:
Посмотрим, как оно легло на память:
Видно, атрибут constructor(101) компилятором понят как размещение указателя в секции
Следующий указатель из секции
Потом уже идут обычные скучные неприоритетные функции инициализации в общей секции
Вот теперь уже точно можно описать сложную инициализацию в заданном порядке. Осталось только придумать какую.
Это что за атрибут?
В прошлый раз поверхностно коснулись того, как выполняется инициализация до главной функции main. Не спеша обходим все указатели в массиве .init_array и бездумно вызываем соответствующие им функции. Можно ли дополнить этот список своим очень нужным методом инициализации чего-либо? Конечно! Атрибут constructor даст нам нужный эффект.
Обратите внимание на фрагмент скрипта компоновщика, где говорится о секции .init_array:
KEEP (*(SORT(.init_array.*)))
KEEP (*(.init_array*))
В начале секции размещаются отсортированные по имени символы, а затем уже все остальные. Это важно, поскольку дает возможность приоритизировать вызовы функций:
__attribute__((constructor(N)))
, где N - это приоритет вызова. Чем он меньше, тем раньше будет вызов. Однако приоритет от 0 до 100 зарезервирован под кое-какие внутренние нужды. Об этом вас предупредит GCC, если попытаетесь так сделать.warning: constructor priorities from 0 to 100 are reserved for the implementation [-Wprio-ctor-dtor]
Да, если вы рисковый разработчик или знаете, что делаете, то предупреждение можно отключить опцией
-Wno-prio-ctor-dtor
.Время для небольшого примера:
void __attribute__((constructor)) my_init() {}
void __attribute__((constructor(101))) my_init101() {}
void __attribute__((constructor(102))) my_init102() {}
Посмотрим, как оно легло на память:
*(SORT_BY_NAME(.init_array.*))
.init_array.00101
0x0000000008000498 0x4 /tmp/ccpvg6Df.o
.init_array.00102
0x000000000800049c 0x4 /tmp/ccpvg6Df.o
*(.init_array*)
.init_array 0x00000000080004a0 0x8 /tmp/ccpvg6Df.o
.init_array 0x00000000080004a8 0x4 /tmp/cc9th81t.o
Видно, атрибут constructor(101) компилятором понят как размещение указателя в секции
.init_array.00101
. Это указатель на самую приоритетную функцию:
08000224 g F .text my_init101()
Следующий указатель из секции
.init_array.00102
ведет к менее важной функции:08000232 g F .text my_init102()
Потом уже идут обычные скучные неприоритетные функции инициализации в общей секции
.init_array
.08000240 g F .text my_init()
... etc
Вот теперь уже точно можно описать сложную инициализацию в заданном порядке. Осталось только придумать какую.
👍7
AUTOSAR
Правило A12-8-7
Операторы присваивания должны быть объявлены с ref-квалификатором &.
Немного раздражающее правило, к счастью, его нарушение легко исправить. На самом деле эта рекомендация существует не только для того, чтоб позлить разработчика, есть причины ей следовать. Хотя бы для того, чтобы избежать малопонятных ошибок.
Когда мы используем встроенные типы, мы не предполагаем присваивания в rvalue контексте
Нонсенс! Это не только не имеет смысла, но и запрещено
Однако, например, у нас есть свой, более красивый тип для int с блэкджеком и операторами присваивания:
В таком случае вполне себе легальны странные и сбивающие с толку присваивания.
Добавив
Правило должно быть применено ко всем операторам присваивания в т.ч. operator=(), operator*=(), operator+=.
Правило A12-8-7
Операторы присваивания должны быть объявлены с ref-квалификатором &.
Немного раздражающее правило, к счастью, его нарушение легко исправить. На самом деле эта рекомендация существует не только для того, чтоб позлить разработчика, есть причины ей следовать. Хотя бы для того, чтобы избежать малопонятных ошибок.
Когда мы используем встроенные типы, мы не предполагаем присваивания в rvalue контексте
int x {};
std::move(x) = 1;
int {} = 1;
Нонсенс! Это не только не имеет смысла, но и запрещено
error: using rvalue as lvalue [-fpermissive]
Однако, например, у нас есть свой, более красивый тип для int с блэкджеком и операторами присваивания:
struct MyInt {
MyInt &operator = (int) { return *this; }
...
};
В таком случае вполне себе легальны странные и сбивающие с толку присваивания.
MyInt c{};
std::move(c) = 1;
MyInt {} = 1;
Добавив
&
к объявлению, мы можем пресечь все подобные действия, чтобы поведение нашего типа не отличалось от поведения встроенных в этом аспекте.MyInt &operator = (int) & { return *this; }
Правило должно быть применено ко всем операторам присваивания в т.ч. operator=(), operator*=(), operator+=.
👍8
__iar_init_core
Чем может порадовать нас IAR в плане инициализации? Вместо
Выглядит функция инициализации до безобразия просто:
В общем случае, не прилагая никаких усилий, можно получить такой код.
Перед ним
Куда делась
Что же, попробуем ввести:
Чем может порадовать нас IAR в плане инициализации? Вместо
__libc_init_array
у него есть функция __iar_program_start
. В стандартной реализации вектора прерываний эта функция заменяет Reset_Handler
, поскольку дополнительных действий по заполнению секций .data и .bss не требуется. __iar_program_start
обо всем позаботится, даже запустит main
.Выглядит функция инициализации до безобразия просто:
#pragma required=__vector_tableТакую реализацию можно найти в доступных исходниках. Здесь pragma required - это убедительная просьба к компоновщику не удалять символ, даже если кажется, что его никто не использует. Логично, ведь без вектора прерываний прошивка превратится в тыкву.
void __iar_program_start( void ) {
__iar_init_core();
__iar_init_vfp();
__cmain();
}
В общем случае, не прилагая никаких усилий, можно получить такой код.
0800027c <__iar_program_start>:Последним идет вызов
800027c: f3af 8000 nop.w
8000280: f000 f802 bl 8000288 <__iar_init_vfp>
8000284: f000 f82e bl 80002e4 <?main>
__cmain
, он же символ ?main
, так уж сложилось исторически. Там много интересного, в частности, разного рода стандартные инициализации, в т.ч. инициализации секций.Перед ним
__iar_init_vfp
- инициализация VFP.Куда делась
__iar_init_core
? Оказывается, если она не определена пользователем, то вместо нее будет красоваться команда NOP.Что же, попробуем ввести:
extern "C" void __iar_init_core(void) {}Ассемблерные инструкции забавно изменятся (не особо)
08000280 <__iar_program_start>:Вместо одной у нас теперь две инструкции для ничегонеделания. Только если функция выполняет какую-то полезную работу, у нее есть шанс не быть предательски оптимизированной. Например, поместить туда вызов
8000280: 46c0 nop ; (mov r8, r8)
8000282: 46c0 nop ; (mov r8, r8)
8000284: f000 f802 bl 800028c <__iar_init_vfp>
8000288: f000 f82e bl 80002e8 <?main>
SystemInit
из HAL.08000284 <__iar_program_start>:Вот теперь все правильно. Может быть полезно, если надо какие-нибудь биты в регистрах выставить перед началом работы.
8000284: f7ff ff90 bl 80001a8 <__iar_init_core>
8000288: f000 f802 bl 8000290 <__iar_init_vfp>
800028c: f000 f82e bl 80002ec <?main>
👍7
Работы пост.
Всем спасибо, кто уже подписан и читает (тем, кто отписался, уже поют анафему, слышите?).
Не секрет, что работаю я в одной хорошей аккредитованной компании, которая полагает, что если человеку были интересны посты на канале, то один из наших проектов тоже сможет заинтересовать. Кто знает, возможно, корреляция имеется... что точно есть, так это вакансия C/C++ разработчика.
Заказчик занимается разработкой программного обеспечения в области информационной безопасности. Одним из его продуктов является собственная микроядерная операционная система, спроектированная с нуля с учетом безопасности использования продуктов заказчика, основанных на этой ОС. Проект предполагает разработку драйверов и сопутствующих библиотек для различных устройств.
Что хотят:
- Опыт разработки на С и C++;
- Знание архитектуры операционных систем;
- Знание принципов взаимодействия ПО с железом;
- Отличное знание микроядерных операционных систем;
- Отличное знание архитектуры ARM;
- Знание внутреннего устройства ядра Unix (Linux) и стандартных библиотек.
Если заинтересовала вакансия, пишите мне @dandemidow
Если нет, то смотрите тут другие вакансии: https://hr.auriga.ru/
Всем спасибо, кто уже подписан и читает (тем, кто отписался, уже поют анафему, слышите?).
Не секрет, что работаю я в одной хорошей аккредитованной компании, которая полагает, что если человеку были интересны посты на канале, то один из наших проектов тоже сможет заинтересовать. Кто знает, возможно, корреляция имеется... что точно есть, так это вакансия C/C++ разработчика.
Заказчик занимается разработкой программного обеспечения в области информационной безопасности. Одним из его продуктов является собственная микроядерная операционная система, спроектированная с нуля с учетом безопасности использования продуктов заказчика, основанных на этой ОС. Проект предполагает разработку драйверов и сопутствующих библиотек для различных устройств.
Что хотят:
- Опыт разработки на С и C++;
- Знание архитектуры операционных систем;
- Знание принципов взаимодействия ПО с железом;
- Отличное знание микроядерных операционных систем;
- Отличное знание архитектуры ARM;
- Знание внутреннего устройства ядра Unix (Linux) и стандартных библиотек.
Если заинтересовала вакансия, пишите мне @dandemidow
Если нет, то смотрите тут другие вакансии: https://hr.auriga.ru/
👍9
__low_level_init
IAR, на самом деле, может порадовать любителей кастомной инициализации не только функцией __iar_init_core.
Много интересного происходит в функции
В общем случае результат работы компилятора может выглядеть так:
Выглядит странно, но все изменится, если мы напишем свою функцию низкоуровневой инициализации.
IAR, на самом деле, может порадовать любителей кастомной инициализации не только функцией __iar_init_core.
Много интересного происходит в функции
__cmain
, он же символ ?main
, так уж сложилось исторически.В общем случае результат работы компилятора может выглядеть так:
080002dc <?main>:На первый взгляд, полнейшая ерунда. Хитрый трюк на второй взгляд. Засунуть значение
80002dc: 2001 movs r0, #1
80002de: 46c0 nop ; (mov r8, r8)
80002e0: 2800 cmp r0, #0
80002e2: d001 beq.n 80002e8 <_call_main>
80002e4: f000 f820 bl 8000328 <__iar_data_init3>
080002e8 <_call_main>:
80002e8: f3af 8000 nop.w
80002ec: 2000 movs r0, #0
80002ee: f3af 8000 nop.w
80002f2: f7ff ff55 bl 80001a0 <main>
1
в регистр r0
, чтоб потом сравнить с нулем? Результат немного предсказуем. Команда условного перехода BEQ
не сработает, в _call_main
мы не попадем, а перенесемся прямо в __iar_data_init3
, где вызываются стандартные инициализации. Дальше, после возвращения из функции, мы плавно вкатывается в метод _call_main
. Здесь уже мы попадем в главную функцию main
.Выглядит странно, но все изменится, если мы напишем свою функцию низкоуровневой инициализации.
extern "C" int __low_level_init(void) { return 1; }Код немного изменится, бессмысленная инструкция
movs r0, #1
будет заменена на вызов __low_level_init
.080002e4 <?main>:Функция низкоуровневой инициализации возвращает значение, которое будет проанализировано. Благодаря этому можно отменить процедуры стандартной инициализации (предполагается, мы уже уладили эти вопросы), нужно просто вернуть нулевое значение, и сразу будет вызван
80002e4: f7ff ff5c bl 80001a0 <__low_level_init>
80002e8: 2800 cmp r0, #0
80002ea: d001 beq.n 80002f0 <_call_main>
80002ec: f000 f820 bl 8000330 <__iar_data_init3>
_call_main
. Уж из этой функции возврата не будет.👍5👎1
std::chrono::steady_clock
Можно ли научиться эффективно управлять временем? Нет! Зато реально повелевать часами из стандартной библиотеки.
Один из самых полезных хронометров -
Благодаря таким замечательным свойствам, на их основе можно исполнить интервальные таймеры, которым не будут страшны внезапные переводы стрелок реального времени.
Значение времени выдает статическая функция
tp - возвращаемое через указатель значение времени:
Что если подменить этот метод для тестов, например? Тогда возможно тестирование объектов, построенных на использовании стандартных часов.
Для этой аферы нам понадобятся опция компоновщика
Это значит, что оригинальная функция
Да, еще понадобится опция
Как это может выглядеть
Можно ли научиться эффективно управлять временем? Нет! Зато реально повелевать часами из стандартной библиотеки.
Один из самых полезных хронометров -
steady_clock
. Часы, которые монотонно идут вперед, т.е. ни пикосекунды назад. Обычно они считают время с момента включения устройства.Благодаря таким замечательным свойствам, на их основе можно исполнить интервальные таймеры, которым не будут страшны внезапные переводы стрелок реального времени.
Значение времени выдает статическая функция
std::chrono::steady_clock::now()GCC скрывает внутри этой функции вызов
clock_gettime
, который и возьмет показания у системы.int clock_gettime(clockid_t clk_id, struct timespec *tp);где clk_id - идентификатор часов, для
steady_clock
он всегда равен CLOCK_MONOTONIC
.tp - возвращаемое через указатель значение времени:
struct timespec {Метод возвращает 0, если все хорошо.
time_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
Что если подменить этот метод для тестов, например? Тогда возможно тестирование объектов, построенных на использовании стандартных часов.
Для этой аферы нам понадобятся опция компоновщика
-Wl,--wrap=clock_gettime
.Это значит, что оригинальная функция
clock_gettime
будет переименована в __real_clock_gettime
, и вместо нее будет вызвана __wrap_clock_gettime
, где мы вольны творить любой произвол.Да, еще понадобится опция
-static-libstdc++
, иначе мы не сможем перехватить clock_gettime
.Как это может выглядеть
extern "C" {Теперь все вызовы
int __wrap_clock_gettime(clockid_t clk_id, struct timespec *tp);
extern int __real_clock_gettime(clockid_t clk_id, struct timespec *tp);
int __wrap_clock_gettime(clockid_t clk_id, struct timespec *tp) {
int result = 0;
if (clk_id == CLOCK_MONOTONIC) {
tp->tv_sec = MySteadyClock::Get();
tp->tv_nsec = 0;
} else {
result = __real_clock_gettime(clk_id, tp);
}
return result;
}
}
steady_clock::now()
в тестах перенаправляются в мой класс MySteadyClock
, остальные часы будут работать как обычно. Таким нехитрым способом можно остановить ход времени, отмотать его назад или заставить бежать, как в сюите Свиридова.👍7
std::chrono::steady_clock для IAR
Собранные
Для этого нам опять нужно получить контроль над стандартными часами.
Посмотрим, как они устроены в IAR.
Перехватить функцию
Собранные
GCC
тесты можно запустить прямо на хост-машине. Это их очевидный плюс, как и то, что выполняются они относительно быстро. Если целевая машина существенно отличается от хоста, то опрометчиво всецело доверять только этим тестам. Сколько их полегло из-за разницы в размерах указателя... поэтому некоторые особо подозрительные разработчики хотят удостовериться, что результат тестов, собранных IAR
под целевую платформу, останется таким же.Для этого нам опять нужно получить контроль над стандартными часами.
Посмотрим, как они устроены в IAR.
class steady_clock: public system_clock {Это все. Внезапно оказалось, что
public:
static const bool is_monotonic = true; // retained
static constexpr bool is_steady = true;
};
steady_clock
и system_clock
это почти одно и то же! Что же тогда выдает метод now?static time_point now() _NOEXCEPT {Так, так... возвращаем
return time_point(duration(_Xtime_get_ticks()));
}
chrono::time_point
, созданный из chrono::duration
, в конструктор которому передают каике-то тики.clock_t _Xtime_get_ticks() {Все просто,
return clock();
}
_Xtime_get_ticks
оборачивает хорошо знакомую нам функцию clock
. clock_t clock()
Возвращает прошедшее с момента запуска программы время, что очень похоже на steady_clock
. Кстати, эта функция не обязательно возвращает секунды, есть возможность настройки.Перехватить функцию
clock
в IAR не составит труда. Достаточно использовать специальные префиксы $Sub$$
и $Super$$
. $Sub$$
определяет новую функцию, которая будет вызвана вместо оригинальной. Неповторимый оригинал будет скрываться под псевдонимом $Super$$
.extern clock_t $Super$$clock(void);Вот мы снова отправились за значением времени в свой же класс, где творится необходимый произвол.
clock_t $Sub$$clock(void) {
clock_t result {};
if (MySteadyClock::IsActive()) {
result = MySteadyClock::Get();
} else {
result = $Super$$clock();
}
return result;
}
👍7
AUTOSAR
Правило A8-5-2
Переменные должны быть инициализированы через фигурные скобки {} без использования оператора присваивания.
Инициализация только в фигурных скобках. Во-первых, это красиво. Во-вторых, это безопасно и однозначно. Иногда ведь смотришь, а там будто значение переменной присваивают при объявлении. Только никаких операторов там не звали, это все обман!
С простыми типами не легче.
Вот фигурные скобочки такого произвола не допустили бы:
В списке инициализации членов класса это тоже актуально: чтоб не допустить по недосмотру сужающего преобразования, лучше и там фигуристые скобы использовать.
Теперь стало понятно, почему для
Правило A8-5-2
Переменные должны быть инициализированы через фигурные скобки {} без использования оператора присваивания.
Инициализация только в фигурных скобках. Во-первых, это красиво. Во-вторых, это безопасно и однозначно. Иногда ведь смотришь, а там будто значение переменной присваивают при объявлении. Только никаких операторов там не звали, это все обман!
class Point {Неприемлемо! Ни копирующий конструктор, ни оператор присваивания не будет вызван. Не вводите коллег в заблуждение.
int32_t x_ {};
int32_t y_ {};
public:
Point() = default;
Point(int32_t x, int32_t y) : x_{x}, y_{y} {}
};
Point x1 = {1, 2.F};
Point x1();Так тем более не пойдет. Компилятор подумает, что
x1
- это объявление функции, и не вызовет никакого конструктора. Особенно опасно, если должен отработать основанный на RAII защитный механизм типа std::lock_guard
, где все интересное происходит в конструкторе и деструкторе. Мы заметим, что не работает наша защита, только после долгих посиделок с отладчиком. С простыми типами не легче.
float v{25.999999F};Здесь тихо-тихо пройдет преобразование типов, и значение обеих переменных будет 25.
uint8_t x1 = v;
uint8_t x2 (v);
Вот фигурные скобочки такого произвола не допустили бы:
uint8_t x2 {v};Надеюсь, вы не игнорируете предупреждения компилятора? Он вам не фешн-дизайнер, его слушать надо.
warning: narrowing conversion of 'v' from 'float' to 'uint8_t' {aka 'unsigned char'} [-Wnarrowing]
В списке инициализации членов класса это тоже актуально: чтоб не допустить по недосмотру сужающего преобразования, лучше и там фигуристые скобы использовать.
class Point {Само собой, в шаблонах только скобки и только с фигурой.
int32_t x_, y_;
...
Point(float x, float y) :
x_{x}, // тут будет предупреждение
y_(y) // маскирует ошибку
Теперь стало понятно, почему для
auto
это правило не особо полезно, но и на него управа найдется.👍14
__iar_init_vfp
Если вам повезло разжиться процессором ARM с модулем операций с плавающей точкой FPU (floating point unit), то почему бы и не скинуть скучную работу по делению, умножению и т.п. всяких
Для генерации кода с привлечением FPU в IAR существует специальный флаг. Называется он совершенно неожиданно:
Внутри функции
Из неожиданного и приятного: в функции задаются значения по-умолчанию в регистре FPSCR (floating-point status and control register).
Вообще, это очень трогательная забота о пользователе компилятора IAR. К сожалению, не бескорыстная.
Если вам повезло разжиться процессором ARM с модулем операций с плавающей точкой FPU (floating point unit), то почему бы и не скинуть скучную работу по делению, умножению и т.п. всяких
float
? Кому доводилось реализовывать операции для чисел стандарта IEEE 754, тот представляет себе, какой это объем вычислений, и чувствует боль.Для генерации кода с привлечением FPU в IAR существует специальный флаг. Называется он совершенно неожиданно:
--fpu
. По-умолчанию компилятор собирает с --fpu=none
, то есть предполагает, что никакого специального модуля нет, а есть много памяти для весьма объемных функций. Если же в качестве параметра передается имя архитектуры (для моего Cortex-M это VFPv4_sp, например), то так уж и быть, IAR включит вам хардварные операции с плавающей точкой. Заодно еще инициализирует FPU, вдруг мы забудем это сделать?Внутри функции
__iar_init_vfp
мы можем найти такие инструкции:movw r1, #60808 ; 0xed88Смысл этих команд сводится к простому действию: в регистре по адресу
movt r1, #57344 ; 0xe000
ldr r0, [r1, #0]
orr.w r0, r0, #15728640 ; 0xf00000
str r0, [r1, #0]
0xe000ed88
устанавливаем биты [20:23] в состояние 1
. Совершенно случайно на этом месте находится Coprocessor Access Control Register (CPACR). Судя по названию, регистр контролирует доступ с сопроцессору. Биты [20:21] - это некое поле CP10, биты [22:23] - поле CP11. Эти поля должны иметь одинаковые значения, чтоб конфигурация считалась правильной. Для разрешения работы туда нужно записать 0xf
(все единицы). Это, так сказать, откроет полный доступ к модулю. Иначе какое-нибудь вшивое деление чисел с плавающей точкой внезапно может привести к UsageFault.Из неожиданного и приятного: в функции задаются значения по-умолчанию в регистре FPSCR (floating-point status and control register).
Вообще, это очень трогательная забота о пользователе компилятора IAR. К сожалению, не бескорыстная.
👍11