C++ Embedded
425 subscribers
2 photos
16 videos
3 files
14 links
Леденящие душу прохладные истории про С++ в embedded проектах. Зарисовки из разработки встраиваемых систем.
Download Telegram
С чего начинается прошивка. intvec.
Если коротко, то 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).
Сигнатура обработчика прерывания пусть будет такой:
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))
Это что за атрибут?
Наконец-то коснемся темы для взрослых. В хорошем смысле! Любителей давать волю фантазии придется огорчить, этот атрибут применим только к функциям. Да и они будут голые только в том смысле, что отсутствуют пролог и эпилог. Хорошо, что это не литературное произведение.
Как всем хорошо известно, функции должны не только исполнять то странное, что мы пишем внутри нее, но и скрывать следы своей бурной деятельности. То есть, привести стек и регистры в то же состояние, каким оно было до вызова. Кроме того, неплохо бы и вернуться из функции. Напишем нечто простое для иллюстрации.
void dummy() {
}
При отключенной оптимизации наша бесполезная функции будет состоять из приличного числа инструкций.
dummy():
push {r7}
add r7, sp, #0
nop
mov sp, r7
ldr r7, [sp], #4
bx lr
Сохраняем регистр R7 на стеке, сохраняем в этом же регистре указатель на стек. Потом ничего не делаем. Затем восстанавливаем значения указателя стека и регистра R7, переходим по бережно хранимому адресу в регистре LR (адрес возврата). Безусловно, оптимизатор может порезать каике-то действия, если сочтет их избыточными, но возврат-то оставит.
dummy():
bx lr
Добавим атрибут naked:
dummy():
nop
Это без оптимизаций! Иначе там вообще ни строчки не останется. Если говорить про архитектуру ARM, то после выполнения оголенки процессор просто шагнет на следующую за функций команду в памяти. Нам сильно повезет, если это будет что-то осмысленное. Предполагается, что до конца функции мы не дойдем или же пропишем возврат явно. Официальная документация настойчиво рекомендует использовать только базовый ассемблер внутри (инструкции без операндов). Расширенный ассемблер или инструкции C могут работать, но без гарантий, увы. Еще бы, если работы со стеком нет, возврата нет, ничего нет.
Забавно, но метод класса тоже может иметь атрибут naked. Для людей, знающих толк в развлечениях. В общем, область возможного применения обнаженки не такая уж и обширная. Некоторые обработчики прерывай, например, рутинные операции над регистрами в упаковке функции.
Показательно, что давным-давно GCC под архитектуру x86 высокомерно игнорировал этот атрибут, но последние версии выдают очень интересные инструкции:
dummy():
nop
ud2
Кроме безделья в самом конце добавилась инструкция ud2. Undefined Instruction. Специальная инструкция. Применять, только когда все становится пугающе неопределенно. Генерирует исключение, и программа здесь падет с чем-то вроде "Illegal instruction". Это не милое плюсовое исключение, это будет какой-нибудь жесткий SIGILL. Попробуй перехвати. Хоть это и кажется странным, в этом есть смысл. Будет большой ошибкой покидать функцию не через адрес возврата, а просто выходя через окно или сквозь стену. Если уж в ход пошла исключительная инструкция, то где-то что-то пошло не так. Лучше сразу ткнуть в нас мизерикордией. Хорошо, что для ARM все не так... или нет?
👍10
Deducing this
В конце жаркой рабочей недели можно немного расслабиться и вспомнить какие-нибудь уморительные и прохладные истории. Например, как частенько приходилось реализовывать в классе парные методы: первый полезный и второй константный. В иных случаях это часть интерфейса, которую придется описывать. Сразу вспоминается веселая возня с 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. Еще лучше говорят комментарии перед ними
/* Copy the data segment initializers from flash to SRAM */
Если долго глядеть на стройные ряды ассемблерных команд, до рвоты, до отвращения, то можно рассмотреть усердное копирование неких данных с адреса _sidata флеш-памяти в оперативную память от адреса _sdata до _edata. Все адреса легко определяются через скрипт компоновщика:
/* used by the startup to initialize data */
_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 */
То есть глобальные символы _sdata и _edata отмечают адреса начала и конца секции с переменными, которые должны быть инициализированы некими значениями. Настоящие данные для инициализации переменных хранятся, конечно, в энергонезависимой памяти по адресу символа _sidata. Без этого наложения в ваших глобальных переменных может вдруг обнаружиться мусор.
Что еще там осталось? FillZerobss и LoopFillZerobss. Тут все проще. Секция неинициализированных переменных щедро заливается нулями. Если отдельные вендоры, которые пропускают этот шаг для поддержания разработчика в тонусе. Забыл инициализировать переменную - сыграл в рулетку. Там может быть все что угодно!
Для успешного обнуления нам потребуются символы начала и конца секции .bss: _sbss и _ebss.
  .bss :
{
/* 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 */
Такая же техника, что и для .data, только без отображения на флеш-память, там все равно одни нули.
👍12
Reset_Handler (продолжение)
Чувствую, как этот вопрос уже зародился в голове: "Можно ли сказать то же самое, но на языке C++?". Почему бы и нет?
extern "C" {
...
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) {}
}
Для начала утащим все нужные нам символы из скрипта компоновщика. Как видите, обработчику добавлен атрибут naked, поскольку сохранять значение регистров при входе бессмысленно, а выходить отсюда мы не собираемся. О чем красноречиво свидетельствует стандартный нынче атрибут [[noreturn]]. Все, что мы описали выше, заменим из хулиганских побуждений двумя строчками. Инициализацию секции .data исполним через memcpy, .bss заполняется нулями через memset. Стандартные функции глобальных или статических переменных не используют, все копируется правильно. Не проверял, работает ли это быстрее, но стало немного понятнее.
👍10
IAR и снова Reset_Handler.
Настало время пристально рассмотреть начальный этап работы 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). Если вам совесть позволяет это использовать. Или отдел безопасности.
Отличный доклад, смотреть лучше вечером. Всю ночь будет сниться интрузивный контейнер.
👍6
__attribute__ ((weak, alias))
Это что за атрибут?
Бывает так, что проводишь долгие часы в бесплодных попытках выбрать переменной выразительное и звучное имя. Уже потом, через пару лет, осознаешь всю ошибочность окончательного варианта. Хорошо, что некоторые компиляторы позволяют создать псевдоним. В частности для 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 стандартной библиотеки:
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:
.init_array:
{
PROVIDE_HIDDEN (__init_array_start = .);
KEEP (*(SORT(.init_array.*)))
KEEP (*(.init_array*))
PROVIDE_HIDDEN (__init_array_end = .);
} >FLASH
Еще там есть секция .preinit_array, но она по структуре не отличается от первой, и нас будет больше интересовать .init_array, именно там зарождается жизнь. В названии легко угадываются очертания массива указателей на функции, которые должны быть вызваны во что бы то ни стало перед main.
Заглянем в исходный код классической реализации функции __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.  */
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] ();
}
Если собирать прошивку с параметрами -specs=nano.specs -nostartfiles, то можно получить единственную функцию в массиве инициализации без всяких там малопонятных пока register_fini и frame_dummy.
08000408       .init_array    __init_array_start
0800040c .init_array __init_array_end
Contents of section .init_array:
8000408 e5020008 00000000
Правда, придется переопределить функцию _init, которая вызывается в перерыве между preinit_array и init_array, но это не проблема. Все равно сейчас в ней не осталось ничего полезного. Желающие могут скопировать код из исходников или просто сделать заглушку: 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:
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 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 в плане инициализации? Вместо __libc_init_array у него есть функция __iar_program_start. В стандартной реализации вектора прерываний эта функция заменяет Reset_Handler, поскольку дополнительных действий по заполнению секций .data и .bss не требуется. __iar_program_start обо всем позаботится, даже запустит main.
Выглядит функция инициализации до безобразия просто:
#pragma required=__vector_table
void __iar_program_start( void ) {
__iar_init_core();
__iar_init_vfp();
__cmain();
}
Такую реализацию можно найти в доступных исходниках. Здесь pragma required - это убедительная просьба к компоновщику не удалять символ, даже если кажется, что его никто не использует. Логично, ведь без вектора прерываний прошивка превратится в тыкву.
В общем случае, не прилагая никаких усилий, можно получить такой код.
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/
👍9
__low_level_init
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
Можно ли научиться эффективно управлять временем? Нет! Зато реально повелевать часами из стандартной библиотеки.
Один из самых полезных хронометров - 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 {
time_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
Метод возвращает 0, если все хорошо.
Что если подменить этот метод для тестов, например? Тогда возможно тестирование объектов, построенных на использовании стандартных часов.
Для этой аферы нам понадобятся опция компоновщика -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
Собранные 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
Переменные должны быть инициализированы через фигурные скобки {} без использования оператора присваивания.
Инициализация только в фигурных скобках. Во-первых, это красиво. Во-вторых, это безопасно и однозначно. Иногда ведь смотришь, а там будто значение переменной присваивают при объявлении. Только никаких операторов там не звали, это все обман!
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};
uint8_t x1 = v;
uint8_t x2 (v);
Здесь тихо-тихо пройдет преобразование типов, и значение обеих переменных будет 25.
Вот фигурные скобочки такого произвола не допустили бы:
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), то почему бы и не скинуть скучную работу по делению, умножению и т.п. всяких 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