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

Сегодня никого не удивить возможностью разрабатывать на 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 -v
Using built-in specs.
COLLECT_GCC=arm-none-eabi-gcc
COLLECT_LTO_WRAPPER=/home/alexander/apt/gcc-arm-none-eabi-9-2020-q2-update/bin/../lib/gcc/arm-none-eabi/9.3.1/lto-wrapper
Target: arm-none-eabi
Configured with: ***     --with-gnu-as --with-gnu-ld --with-newlib   ***
Thread model: single
gcc 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: single
gcc 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. Весь код естественно свободный.

Let’s block ads! (Why?)

Read More

Recent Posts

Apple возобновила переговоры с OpenAI и Google для интеграции ИИ в iPhone

Apple возобновила переговоры с OpenAI о возможности внедрения ИИ-технологий в iOS 18, на основе данной операционной системы будут работать новые…

15 часов ago

Российская «дочка» Google подготовила 23 иска к крупнейшим игрокам рекламного рынка

Конкурсный управляющий российской «дочки» Google подготовил 23 иска к участникам рекламного рынка. Общая сумма исков составляет 16 млрд рублей –…

22 часа ago

Google завершил обновление основного алгоритма March 2024 Core Update

Google завершил обновление основного алгоритма March 2024 Core Update. Раскатка обновлений была завершена 19 апреля, но сообщил об этом поисковик…

1 день ago

Нейросети будут писать тексты объявления за продавцов на Авито

У частных продавцов на Авито появилась возможность составлять текст объявлений с помощью нейросети. Новый функционал доступен в категории «Обувь, одежда,…

1 день ago

Объявлены победители международной премии Workspace Digital Awards-2024

24 апреля 2024 года в Москве состоялась церемония вручения наград международного конкурса Workspace Digital Awards. В этом году участниками стали…

2 дня ago

Яндекс проведет гик-фестиваль Young Con

27 июня Яндекс проведет гик-фестиваль Young Con для студентов и молодых специалистов, которые интересуются технологиями и хотят работать в IT.…

2 дня ago