Русский
Русский
English
Статистика
Реклама

Запуск сложных C приложений на микроконтроллерах

image Сегодня никого не удивить возможностью разрабатывать на C++ под микроконтроллеры. Проект mbed полностью ориентирован на этот язык. Ряд других RTOS предоставляют возможности разработки на С++. Это удобно, ведь программисту доступны средства объектно-ориентированного программирования. Вместе с тем, многие RTOS накладывают различные ограничения на использование C++. В данной статье мы рассмотрим внутреннюю организацию C++ и выясним причины этих ограничений.

Сразу хочу отметить, что большинство примеров будут рассмотрены на RTOS Embox. Ведь в ней на микроконтроллерах работают такие сложные C++ проекты как Qt и OpenCV. OpenCV требует полной поддержки С++, которой обычно нет на микроконтроллерах.

Базовый синтаксис


Синтаксис языка C++ реализуется компилятором. Но в рантайм необходимо реализовать несколько базовых сущностей. В компиляторе они включаются в библиотеку поддержки языка libsupc++.a. Наиболее базовой является поддержка конструкторов и деструкторов. Существуют два типа объектов: глобальные и выделяемые с помощью операторов new.

Глобальные конструкторы и деструкторы


Давайте взглянем на то как работает любое C++ приложение. Перед тем как попасть в main(), создаются все глобальные C++ объекты, если они присутствуют в коде. Для этого используется специальная секция .init_array. Еще могут быть секции .init, .preinit_array, .ctors. Для современных компиляторов ARM, чаще всего секции используются в следующем порядке .preinit_array, .init и .init_array. С точки зрения LIBC это обычный массив указателей на функции, который нужно пройти от начала и до конца, вызвав соответствующий элемент массива. После этой процедуры управление передается в main().

Код вызова конструкторов для глобальных объектов из Embox:

void cxx_invoke_constructors(void) {    extern const char _ctors_start, _ctors_end;    typedef void (*ctor_func_t)(void);    ctor_func_t *func = (ctor_func_t *) &_ctors_start;    ....    for ( ; func != (ctor_func_t *) &_ctors_end; func++) {        (*func)();    }}

Давайте теперь посмотрим как устроено завершение C++ приложения, а именно, вызов деструкторов глобальных объектов. Существует два способа.

Начну с наиболее используемого в компиляторах через __cxa_atexit() (из C++ ABI). Это аналог POSIX функции atexit, то есть вы можете зарегистрировать специальные обработчики, которые будут вызваны в момент завершения программы. Когда при старте приложения происходит вызов глобальных конструкторов, как описано выше, там же есть и сгенерированный компилятором код, который регистрирует обработчики через вызов __cxa_atexit. Задача LIBC здесь сохранить требуемые обработчики и их аргументы и вызвать их в момент завершения приложения.

Другим способом является сохранение указателей на деструкторы в специальных секциях .fini_array и .fini. В компиляторе GCC это может быть достигнуто с помощью флага -fno-use-cxa-atexit. В этом случае во время завершения приложения деструкторы должны быть вызваны в обратном порядке (от старшего адреса к младшему). Этот способ менее распространен, но может быть полезен в микроконтроллерах. Ведь в этом случае на момент сборки приложения можно узнать сколько обработчиков потребуется.

Код вызова деструкторов для глобальных объектов из Embox:

int __cxa_atexit(void (*f)(void *), void *objptr, void *dso) {    if (atexit_func_count >= TABLE_SIZE) {        printf("__cxa_atexit: static destruction table overflow.\n");        return -1;    }    atexit_funcs[atexit_func_count].destructor_func = f;    atexit_funcs[atexit_func_count].obj_ptr = objptr;    atexit_funcs[atexit_func_count].dso_handle = dso;    atexit_func_count++;    return 0;};void __cxa_finalize(void *f) {    int i = atexit_func_count;    if (!f) {        while (i--) {            if (atexit_funcs[i].destructor_func) {                (*atexit_funcs[i].destructor_func)(atexit_funcs[i].obj_ptr);                atexit_funcs[i].destructor_func = 0;            }        }        atexit_func_count = 0;    } else {        for ( ; i >= 0; --i) {            if (atexit_funcs[i].destructor_func == f) {                (*atexit_funcs[i].destructor_func)(atexit_funcs[i].obj_ptr);                atexit_funcs[i].destructor_func = 0;            }        }    }}void cxx_invoke_destructors(void) {    extern const char _dtors_start, _dtors_end;    typedef void (*dtor_func_t)(void);    dtor_func_t *func = ((dtor_func_t *) &_dtors_end) - 1;    /* There are two possible ways for destructors to be calls:     * 1. Through callbacks registered with __cxa_atexit.     * 2. From .fini_array section.  */    /* Handle callbacks registered with __cxa_atexit first, if any.*/    __cxa_finalize(0);    /* Handle .fini_array, if any. Functions are executed in teh reverse order. */    for ( ; func >= (dtor_func_t *) &_dtors_start; func--) {        (*func)();    }}

Глобальные деструкторы необходимы, чтобы иметь возможность перезапускать C++ приложения. Большинство RTOS для микроконтроллеров предполагает запуск единственного приложения, которое не перезагружается. Старт начинается с пользовательской функции main, единственной в системе. Поэтому в небольших RTOS зачастую глобальные деструкторы пустые, ведь их использование не предполагается.

Код глобальный деструкторов из Zephyr RTOS:

/** * @brief Register destructor for a global object * * @param destructor the global object destructor function * @param objptr global object pointer * @param dso Dynamic Shared Object handle for shared libraries * * Function does nothing at the moment, assuming the global objects * do not need to be deleted * * @return N/A */int __cxa_atexit(void (*destructor)(void *), void *objptr, void *dso){    ARG_UNUSED(destructor);    ARG_UNUSED(objptr);    ARG_UNUSED(dso);    return 0;}

Операторы new/delete


В компиляторе GCC реализация операторов new/delete находится в библиотеке libsupc++, А их декларации в заголовочном файле .

Можно использовать реализации new/delete из libsupc++.a, но они достаточно простые и могут быть реализованы например, через стандартные malloc/free или аналоги.

Код реализации new/delete для простых объектов Embox:

void* operator new(std::size_t size)  throw() {    void *ptr = NULL;    if ((ptr = std::malloc(size)) == 0) {        if (alloc_failure_handler) {            alloc_failure_handler();        }    }    return ptr;}void operator delete(void* ptr) throw() {    std::free(ptr);}

RTTI & exceptions


Если ваше приложение простое, вам может не потребоваться поддержка исключений и динамическая идентификация типов данных (RTTI). В этом случае их можно отключить с помощью флагов компилятора -no-exception -no-rtti.

Но если эта функциональность С++ требуется, ее нужно реализовать. Сделать это куда сложнее чем new/delete.

Хорошая новость заключается в том что эти вещи не зависят от ОС и уже реализованы в кросс-компиляторе в библиотеке libsupc++.a. Соответственно, самый простой способ добавить поддержку это использовать библиотеку libsupc++.a из кросс компилятора. Сами прототипы находятся в заголовочных файлах и .

Для использования исключений из кросс-компилятора есть небольшие требования, которые нужно реализовать при добавлении собственного метода загрузки C++ рантайма. В линкер скрипте должна быть предусмотрена специальная секция .eh_frame. А перед использованием рантайма эта секция должна быть инициализирована с указанием адреса начала секции. В Embox используется следующий код:

void register_eh_frame(void) {    extern const char _eh_frame_begin;    __register_frame((void *)&_eh_frame_begin);}

Для ARM архитектуры используются другие секции с собственной структурой информации .ARM.exidx и .ARM.extab. Формат этих секция определяется в стандарте Exception Handling ABI for the ARM Architecture EHABI. .ARM.exidx это таблица индексов, а .ARM.extab это таблица самих элементов требуемых для обработки исключения. Чтобы использовать эти секции для обработки исключений, необходимо включить их в линкер скрипт:

    .ARM.exidx : {        __exidx_start = .;        KEEP(*(.ARM.exidx*));        __exidx_end = .;    } SECTION_REGION(text)    .ARM.extab : {        KEEP(*(.ARM.extab*));    } SECTION_REGION(text)

Чтобы GCC мог использовать эти секции для обработки исключений, указывается начало и конец секции .ARM.exidx __exidx_start и __exidx_end. Эти символы импортируются в libgcc в файле libgcc/unwind-arm-common.inc:
extern __EIT_entry __exidx_start;extern __EIT_entry __exidx_end;

Более подробно про stack unwind на ARM написано в статье.

Стандартная библиотека языка (libstdc++)


Собственная реализация стандартной библиотеки


В поддержку языка C++ входит не только базовый синтаксис, но и стандартная библиотека языка libstdc++. Ее функциональность, так же как и для синтаксиса, можно разделить на разные уровни. Есть базовые вещи типа работы со строками или C++ обертка setjmp . Они легко реализуются через стандартную библиотеку языка C. А есть более продвинутые вещи, например, Standard Template Library (STL).

Стандартная библиотека из кросс-компилятора


Базовые вещи реализованы в Embox. Если этих вещей достаточно, то можно не подключать внешнюю стандартную библиотеку языка C++. Но если нужна, например, поддержка контейнеров, то самым простым способом является использование библиотеки и заголовочных файлов из кросс-компилятора.

При использовании стандартной библиотеки С++ из кросс-компилятора существует особенность. Взглянем на стандартный arm-none-eabi-gcc:

$ arm-none-eabi-gcc -vUsing built-in specs.COLLECT_GCC=arm-none-eabi-gccCOLLECT_LTO_WRAPPER=/home/alexander/apt/gcc-arm-none-eabi-9-2020-q2-update/bin/../lib/gcc/arm-none-eabi/9.3.1/lto-wrapperTarget: arm-none-eabiConfigured with: ***     --with-gnu-as --with-gnu-ld --with-newlib   ***Thread model: singlegcc version 9.3.1 20200408 (release) (GNU Arm Embedded Toolchain 9-2020-q2-update)

Он собран с поддержкой --with-newlib.Newlib реализация стандартной библиотеки языка C. В Embox используется собственная реализация стандартной библиотеки. Для этого есть причина, минимизация накладных расходов. И следовательно для стандартной библиотеки С можно задать требуемые параметры, как и для других частей системы.

Так как стандартные библиотеки C отличаются, то для поддержки рантайма нужно реализовать слой совместимости. Приведу пример реализации из Embox одной из необходимых но неочевидных вещей для поддержки стандартной библиотеки из кросс-компилятора

struct _reent {    int _errno;           /* local copy of errno */  /* FILE is a big struct and may change over time.  To try to achieve binary     compatibility with future versions, put stdin,stdout,stderr here.     These are pointers into member __sf defined below.  */    FILE *_stdin, *_stdout, *_stderr;};struct _reent global_newlib_reent;void *_impure_ptr = &global_newlib_reent;static int reent_init(void) {    global_newlib_reent._stdin = stdin;    global_newlib_reent._stdout = stdout;    global_newlib_reent._stderr = stderr;    return 0;}

Все части и их реализации необходимые для использования libstdc++ кросс-компилятора можно посмотреть в Embox в папке third-party/lib/toolchain/newlib_compat/

Расширенная поддержка стандартной библиотеки std::thread и std::mutex


Стандартная библиотека C++ в компиляторе может иметь разный уровень поддержки. Давайте еще раз взглянем на вывод:

$ arm-none-eabi-gcc -v***Thread model: singlegcc version 9.3.1 20200408 (release) (GNU Arm Embedded Toolchain 9-2020-q2-update)

Модель потоков Thread model: single. Когда GCC собран с этой опцией, убирается вся поддержка потоков из STL (например std::thread и std::mutex). И, например, со сборкой такого сложного С++ приложение как OpenCV возникнут проблемы. Иначе говоря, для сборки приложений, которые требуют подобную функциональность, недостаточно этой версии библиотеки.

Решением, которые мы применяем в Embox, является сборка собственного компилятора ради стандартной библиотеки с многопоточной моделью. В случае Embox модель потоков используется posix Thread model: posix. В этом случае std::thread и std::mutex реализуются через стандартные pthread_* и pthread_mutex_*. При этом также отпадает необходимость подключать слой совместимости с newlib.

Конфигурация Embox


Хотя пересборка компилятора и является самым надежным и обеспечивает максимально полное и совместимое решение, но при этом оно занимает достаточно много времени и может потребовать дополнительных ресурсов, которых не так много в микроконтроллере. Поэтому данный метод не целесообразно использовать везде.

В Embox для оптимизации затрат на поддержку введены несколько абстрактных классов (интерфейсов) различные реализации которых можно задать.

  • embox.lib.libsupcxx определяет какой метод для поддержки базового синтаксиса языка нужно использовать.
  • embox.lib.libstdcxx определяет какую реализацию стандартной библиотеки нужно использовать

Есть три варианта libsupcxx:

  • embox.lib.cxx.libsupcxx_standalone базовая реализация в составе Embox.
  • third_party.lib.libsupcxx_toolchain использовать библиотеку поддержки языка из кросс-компилятора
  • third_party.gcc.tlibsupcxx полная сборка библиотеки из исходников

Минимальный вариант может работать даже без стандартной библиотеки С++. В Embox есть реализация базирующаяся на простейших функциях из стандартной библиотеки языка С. Если этой функциональности не хватает, можно задать три варианта libstdcxx.

  • third_party.STLport.libstlportg стандартная библиотека вслкючающая STL на основе проекта STLport. Не требует пересборки gcc. Но проект давно не поддерживается
  • third_party.lib.libstdcxx_toolchain стандартная библиотека из кросс-компилятора
  • third_party.gcc.libstdcxx полная сборка библиотеки из исходников

Если есть желание у нас на wiki описано как можно собрать и запустить Qt или OpenCV на STM32F7. Весь код естественно свободный.
Источник: habr.com
К списку статей
Опубликовано: 27.01.2021 20:23:33
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Блог компании embox

C++

Системное программирование

Программирование микроконтроллеров

Embox

Opencv

Qt

Mcu

Mcus

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru