четвер, 12 листопада 2015 р.

libcxxrt в ролі libsupc++ -- бібліотеки підтримки мовних засобів часу виконання


Клікабельно. :-)
Розглядаючи підтримку С++ на "голому залізі", в попередньому пості, ми орієнтувалися на штатні засоби --- мінімальний набір із libsupc++, яка підтримує синтаксичні засоби мови (зокрема, виключення та RTTI) та повну libstdc++, що дозволяє, принаймні -- в теорії, користуватися всіма засобами стандартної бібліотеки мови. Однак, і одна і друга страждають певною громіздкістю, тому іноді хотілося б мати більш компактну альтернативу, навіть якщо її можливості будуть дещо урізаними. В ролі альтернативи до libsupc++ часто пропонують libcxxrt, для повної бібліотеки можна подивитися на uClibc++.

Почнемо із розгляду libcxxrt. Вона прийшла із світу FreeBSD/x86, потім модифікувалася і для інших платформ. Потребує окрему бібліотеку розкрутки стеку, в ролі якої підходять як libgcc_s так і libunwind (якщо просто --- одна із них мала б бути з GCC, детальніше -- див. попередні пости або тут чи тут). Крім того, вона, все ж, розрахована на великі системи, де не потрібно економити кожен кілобайт, що проявляється в коді.

Зразу скажу, добитися повної функціональності мені поки не вдалося. Даний пост -- класичний Quick and Dirty опис способу почати працювати з бібліотекою, містить ряд доволі примітивних заглушок та не до кінця продуманих рішень. 

При тому, завдяки своїй простоті (відносній), бібліотека виявилася неоціненим джерелом подробиць про роботу C++ runtime, допомігши розібратися в нюансах для попереднього поста. У свою чергу, цінні підказки по перенесенню libcxxrt знайдено тут: "C++ Exception Support".

Попередивши, можна починати. Розгляд проводиться на прикладі все тієї ж STM32VLDiscovery, але для інших мікроконтролерів сімейства особливих відмінностей (крім доступного розміру RAM) бути не мало б.
Зауваження. Декілька раз компіляція бібліотеки із використанням LTO приводила до внутрішнього збою компілятора. Не вдалося створити мінімальний тестовий приклад -- програвив момент, коли, в результаті змін main() воно пропало. Але на увазі слід мати.

Адаптація бібліотеки


1. Скачуємо тим чи іншим способом бібліотеку на диск -- з використанням git, "Download ZIP", чи ще якось. Адреса: https://github.com/pathscale/libcxxrt.

Розархівовуємо, якщо це був архів, переходимо в директорію із клонованими текстами, якщо git. У ній -- директорії src, test, та службові файли, поміж яких вартий уваги CMakeLists.txt. Однак, ми штатною системою автоматичної побудови програм не користуватимемося. У директорію  test заглянути теж варто, безпосередньо файли бібліотеки знаходяться в src.

Беремо проект, описаний в одному із попередніх постів --- C-runtime нам буде потрібен! Створюємо у ньому директорію (скажімо,  libcxxrt), копіюємо, додаємо до проекту (думаю, варто створити відповідну групу-папку). 

Копіювати слід: всі *.cc (вони так назвали файли С++), *.c, *.h файли, крім unwind-itanium.h,  хоча щодо libelftc_dem_gnu3.c теж будуть зауваження.

2. Стандартну бібліотеку С ми їй надали (подробиці див. відповідний пост), однак libcxxrt цього мало. Вона використовує Pthreads та звертається до динамічного завантажувача (викликом dladdr) і системного виклику sched_yield.

З ldaddr просто -- використовується лише для відладки, тому викидаємо включення заголовку <dlfcn.h> з exception.cc:

#include <stdlib.h>
//#include <dlfcn.h> // <=======
#include <stdio.h>

та даємо порожню реалізацію цієї функції (яка завжди завершується із помилкою):

typedef struct
{
  __const char *dli_fname; /* File name of defining object.  */
  void *dli_fbase;  /* Load address of that object.  */
  __const char *dli_sname; /* Name of nearest symbol.  */
  void *dli_saddr;  /* Exact value of nearest symbol.  */
} Dl_info;

int dladdr(void*, Dl_info*) {
 return 0;
}

Аналогічно -- з sched_yield, у нас (поки) немає ні операційної системи, ні диспетчера, якому можна повернути залишок кванту часу, тому:

int sched_yield(void)
{
 return -1;
}

З Pthreads складніше -- надання (чи перенесення) вимагає великих зусиль (і, будемо чесні, кваліфікації, якої в мене немає). Однак, якщо багатопоточність не використовується (принаймні, одночасно із використанням runtime --- скажімо, обробники переривань не виділяють пам'яті і не кидають виключень С++, що, на загал -- хороша ідея!), то можна обійтися простими заглушками (взято, із модифікаціями, тут: "C++ Exception Support"), додатково вказавши при компіляції наступний макрос препроцесора: _POSIX_THREADS, щоб pthread.h включив необхідні оголошення (виніс їх в окремий файл, libcxxrt/bare_metal_support.c):

#include <pthread.h>
#include <assert.h>

// namespace { void* threadDataTable[64]; int freeEntry = 0;}

static void* threadDataTable[64];
static int freeEntry = 0;

int pthread_key_create(pthread_key_t* key, void (*fptr)(void*)) {
 assert(freeEntry < 64);

 *key = freeEntry;
 freeEntry++;
 return 0;
}

#if 0
typedef struct {
  int   is_initialized;  /* is this structure initialized? */
  int   init_executed;   /* has the initialization routine been run? */
} pthread_once_t;       /* dynamic package initialization */
#endif

int pthread_once(pthread_once_t* control, void (*init)(void)) {
 if ( control->is_initialized == 0) {
  (*init)();
  control->is_initialized = 1;
 }
 return 0;
}

void* pthread_getspecific(pthread_key_t key) {
 return threadDataTable[key];
}

int pthread_setspecific(pthread_key_t key, const void* data) {
 threadDataTable[key] = (void*)data;
 return 0;
}

int pthread_mutex_init(pthread_mutex_t* mutex, const pthread_mutexattr_t* ptr) {
 *mutex = 0;
 return 0;
}

int pthread_mutex_lock(pthread_mutex_t* mutex) {
 assert(*mutex == 0);
 *mutex = 1;
 return 0;
}

int pthread_mutex_unlock(pthread_mutex_t* mutex) {
 assert(*mutex != 0);
 *mutex = 0;
 return 0;
}

int pthread_cond_wait(pthread_cond_t* optr, pthread_mutex_t* ptr) {
 return 0;
}

int pthread_cond_signal(pthread_cond_t* ptr) {
 return 0;
}

Тепер програма спробує скомпілюватися. І їй майже вдасться. Однак лінкер заявить, що прошивка потребує значно більше оперативної пам'яті, ніж її взагалі є в контролері -- з на десяток кілобайт.

3. Причина такої потреби в пам'яті (RAM) проста -- аварійні буфери, на випадок, коли malloc не зможе виділити пам'ять, необхідну для обробки виключень. Файл exception.cc містить наступні рядки:

/**
 * An emergency allocation reserved for when malloc fails.  This is treated as
 * 16 buffers of 1KB each.
 */
static char emergency_buffer[16384];

/**
 * Flag indicating whether each buffer is allocated.
 */
static bool buffer_allocated[16];

Ми таких витрат собі дозволити не можемо. Взагалі, вирішення проблеми вимагає акуратного та вдумливого аналізу коду. Зокрема, мотивів бібліотеки для вибору розміру буферу в один кілобайт. Бібліотека ускладнює ситуацію, всюди користуючись магічними константами ( 16384 = 16*1024, 16 -- кількість аварійних буферів, 4 -- максимальна кількість буферів на потік, тощо). Прямолінійно вирішено, створивши константу, яка задає кількість однокілобайтових буферів. Рішення не раціональне -- кілобайт, автоматично, втрачається майже намарно, крім того, бібліотека багато де вважає, що буферів більше одного. Але як Quick and Dirty -- зійде. Замінюємо код вище на:

const int emergency_buffers = 1;
static char emergency_buffer[1024*emergency_buffers];
/**
 * Flag indicating whether each buffer is allocated.
 */
static bool buffer_allocated[emergency_buffers];

А у функціях, що користуються цими буферами, emergency_malloc() та emergency_malloc_free(), замінив цикли:

for (int i=0 ; i<16 ; i++)

на:

for (int i=0 ; i<emergency_buffers; i++)

(Файли із всіма змінами є в демонстраційному проекті).

Поки зійде, хоча тримати в голові потенційне місце проблем і потенційне місце оптимізації використання пам'яті слід!

4. Тепер програма, що користується бібліотекою, успішно компілюватиметься. Однак, з виключеннями проблеми залишаються. Перш ніж перейти до них, розглянемо ще одну "оптимізацію". Файл  використовує функцію __cxa_demangle_gnu3() для "розплутування" імен С++, перетворення їх в форму, читабельну для людини. Реалізації цієї функції знаходиться в файлі libelftc_dem_gnu3.c. Компіляція цього файлу з проектом збільшує розмір прошивки на ~22Kb!

Подивимося, де вона використовується. В typeinfo.cc є функція __cxa_demangle(), котра для своєї роботи потребує згадану  __cxa_demangle_gnu3(), та займається "шаманізмом" на тему, що ж робити із результатом  __cxa_demangle_gnu3(). Якщо остання повернула нульовий вказівник -- повідомляє про помилку та повертає той же нульовий вказівник. У свою чергу, вона використовується в exception.cc, лише в функції report_failure(), призначеній повідомляти про помилки кидання виключень, котра, якщо demanigling пройшов успішно, виводить нормальне, не заплутане ім'я функції, інакше просто виводить заплутане.

Як на мене, можливість бачити у (відсутній, зазвичай) консолі розплутане ім'я функції замість заплутаного -- не вартує зростання розміру прошивки на 22336 байта (із ключем -Os!).

Тому, надавши мінімальну заглушку цієї функції, сам файл libelftc_dem_gnu3.c викидаю:

char    *__cxa_demangle_gnu3(const char *){
 return 0;
}

5. Бібліотека не містить всі необхідні варіанти оператора delete, (про те, для чого саме ці оператори, та про повну їх колекцію, див., наприклад, попередній пост). Їх можна додати самостійно:

void operator delete(void* ptr, const std::nothrow_t& nothrow_constant) {
 ::operator delete(ptr);
}

void operator delete[](void* ptr, const std::nothrow_t& nothrow_constant) {
 ::operator delete(ptr);
}

(Цей код потребуватиме #include <new>, а ті випадки, де він використовується -- існування константи "const nothrow_t nothrow;", див. попередній пост за подробицями).

Все, бібліотека компілюється, і за винятком виключень, працює. Про них поговоримо нижче, зауважу тільки, що та сама програма, займаючи із використанням libsupc++ ~61Кб, тут потребуватиме всього ~44Кб -- на 17Кб менше, (і на пару сотень байт менше RAM). Якщо функціональності libcxxrt достатньо, то це дуже значущий виграш (при сумарному розмірі пам'яті програм 128Кб і оперативної пам'яті  8Кб).

Рекомендую подивитися на реалізацію new в в файлі memory.cc -- вона більш послідовна, ніж запропонована мною в попередньому пості, поважає new_handler(), хоч і більш громіздка. Див. реалізацію в файлі memory.cc.

Обробка виключень

Поки обробка виключень не працює. На жаль, мені не вдалося знайти рішення, тому опишу пройдений шлях, в надії, що він може допомогти комусь чи й мені самому, в подальших спробах.

Навіть найпростіший код, який чудово компілюється:

class myException{};

................

 try
 {
  throw myException();
 }
 catch(myException &e)
 {
  puts("We have myException");
 }
 catch(...)
 {
  puts("Unknown exception");
 }

приводить до Hard Reset.

Беремо в руки дебагер, дивимося. Наявність джерельних текстів libcxxrt  дуже в цьому допомагає, хоча офіційна документація + виконання окремих машинних команд, яким доводилося займатися для написання попереднього поста, теж дозволяє відтворити цю картину. Почнемо із виконання команди throw.
  1. Виділяється пам'ять під об'єкт виключення, викликом __cxa_allocate_exception().
  2. Далі -- __cxa_throw(), котра викликає throw_exception(), яка, у свою чергу -- _Unwind_RaiseException().
  3. _Unwind_RaiseException(), якщо не сталося якоїсь помилки, не повертається --- вона зразу переходить до розкрутки стеку. Ця функція буде в двійковому коді --- вона не частина libcxxrt, а частина бібліотеки розкрутки. Викликає           __gnu_Unwind_RaiseException, та, кілька раз викликавши get_eit_entry, стрибає назад, в код  libcxxrt, до функції __gxx_personality_v0() із exception.cc. Для подальшої роботи зручно тут поставити точку зупинки -- щоб не блукати без потреби в асемблерному коді розкрутки. 
  4. В процесі роботи ця функція викликає check_action_record(). Вона, якщо я правильно зрозумів коментарі, повертає true, якщо є обробник саме для цього виключення, false, якщо тільки очищення. (Поправляйте мене, якщо помиляюся.)
  5. Вона, check_action_record(), викликає get_type_info_entry() для отримання інформації про тип, який тут ловитиметься (сподіваюся, знову не плутаю), і ця інформація, разом із самим виключенням, передається check_type_signature().
  6.  Задача check_type_signature() перевірити, чи ми ловимо дане виключення. Воно б було просто -- порівняти його із тим, що дає get_type_info_entry(), якби все було просто. Але, скажімо, ми можемо ловити виключення за його предковим класом. Всілякими такими фокусами check_type_signature() й займається.
  7. Тут виявляється, що саме виключення функції приходить коректне. Див. вміст аргументу ex, зокрема, поле __type_name містить вказівник на символ '1' чи '2' чи ще щось схоже. Зрозуміло, що це сильно залежить від конкретної програми, але воно виглядає розумним і поводиться послідовно -- додавши ще виключення, отримаєш наступне число. А от інформація про виключення, яке перехоплюється в даному місці коду, містить дурниці (аргумент type), те ж __type_name вказує на щось типу -75 (не символи - число). Відповідно, далі код іде в рознос, при спробі виклику type->__do_catch(...) стрибає невідомо куди і завершується Hard Reset.
  8. Тобто, причина в get_type_info_entry(), яка повертає некоректну інформацію. Розібратися в її функціонуванні я не зміг. Вся необхідна інформація доступна, див. наприклад, "Exception Handling ABI for the ARM® Architecture" -- EHABI, та взагалі, розділ "Література по реалізації виключень" попереднього поста, однак воно потребуватиме більше ресурсів, ніж можу зараз приділити.
  9. Чисто в ролі лотереї, (не робіть так - це погана практика!), побачивши в тілі цієї ф-ції закоментований рядок "// record -= 4;", прибрав коментар. Зразу подумав -- сталося чудо. Здалося, що виключення почали опрацьовуватися і ловитися. Як "прості" порожні класи чи фундаментальні типи, так і класи з віртуальними функціями та успадкуванням, коли кидається виключення нащадок, а ловиться як посилання на предка. На перший погляд, все добре.
  10. Однак, халяви немає. Таким чином вдається написати лише один try-catch блок... Як би це дивно не звучало. За наступного кидання виключення бібліотека зависає намертво. Точніше, закономірність якась складніша, щось типу: тривіальні класи можна кидати і ловити декілька раз, однак після кинутого і зловленого тривіального, ловлення нетривіального зависне. Нетривіальний можна кинути лише раз -- після нього висне будь-який try-catch (ну, як -- будь-який, всі комбінації я не перебрав, але типові виснуть). Аналогічно, якщо першим catch ловиться не те виключення, яке кидається -- висне.
  11. Взявши все той же дебаггер, бачимо, що за другого виклику, (у ситуації, коли зависне), get_type_info_entry() хоче повернути не 0, як раніше, в процесі викликає resolve_indirect_value(), яка робить якусь хитру магію, і на ось такій конструкції: "v = static_cast<uint64_t>(reinterpret_cast<uintptr_t>(*reinterpret_cast<void**>(v)));" засипається...  Очевидно, робить щось заборонене контролером.Після багатьох прогонів варіантів з попереднього пункту, склалося враження, що коли get_type_info_entry() не читає нічого хитрого -- просто 0, все ОК, як тільки вона пробує передати якусь реальну інформацію -- все. (При тому, RTTI, так виглядає, працює. Може я занадто грубо його тестував, але типові конструкції -- див. код, працюють.)
  12. На цьому етапі шаманство більше не допомогло, поки залишаю як є.

Резюме

Бібліотека цікава. Плюси її частково розглянуто вище -- компактність і простота, мінуси -- орієнтація на трохи більші системи та проблеми із виключеннями. Чи використовувати -- залежить від ситуації, але сенс може бути!

Скачати проект із експериментами, описаними вище, можна тут. Особливих відмінностей, в порівнянні із описаним раніше, крім використання libcxxrt, він не має.

Наступного разу спробуємо глянути на повну заміну  libstdc++, uClibc++, поки:

Дякую за увагу!



Немає коментарів:

Дописати коментар