Открытый канал ID 72525990801502

C++ Embedded

Вся правда о C++ в embedded проектах

Публикации за период

По реакциям

Поиск и вся лента — после бесплатной регистрации. Свои даты — по подписке; владелец канала видит их бесплатно. Сортировка по реакциям — по подписке. Зарегистрироваться

Загрузка…

__cxa_atexit
Еще как причем! Эта __cxa_atexit — не просто ценный мех, а еще внутренняя низкоуровневая функция рантайма C++. С ее помощью компилятор тихой сапой регистрирует деструкторы глобальных и статических объектов, чтобы они были корректно вызваны при завершении работы программы или при выгрузке динамической библиотеки.
Есть похожая функция в чистом C - atexit, но она не очень умеет с динамическими объектами работать, да и с деструкторами в общем-то не особо.
Вот, например, пишем такой глобальный объект

struct MyClass { ~MyClass() {}};
MyClass global_object;

Тогда GCC, никого не спрашивая, нагенерирует неявных вызовов под это дело

:
__cxa_atexit((void (*)(void *))&MyClass::~MyClass, &global_object, &__dso_handle);

В so-библиотеке этот деструктор сохраняется во внутренний список с соответствующим маркером __dso_handle.
Если мы вообще закрываем приложение, будет вызвана функция __cxa_finalize(&__dso_handle), а __dso_handle в этом случае нулевая. В таком случае Itanium C++ ABI велит вызвать последовательно все функции из расстрельного списка.
Гораздо веселее выгружать только so-шник. Это вызывает кратковременный dlclose, который в свою очередь дергает __cxa_finalize с __dso_handle, и тут уже она нулю не равна. Вот тогда мы вытаскиваем из списка все деструкторы с таким маркером и начинаем яростно, дико вызывать в порядке живой очереди.
Для сурового embedded я видел советы отключить этот механизм как избыточный, поскольку микроконтроллерам все равно не хочется жонглировать so-либами.
В GCC (и даже в clang) можно явно отключить этот механизм через флаг -fno-use-cxa-atexit.
В этом случае компилятор перестанет использовать __cxa_atexit, а указатели на деструкторы запишет в специальную секцию бинарного файла .fini_array (или .dtors), которые будут последовательно вызваны при выходе из программы самым простым циклом.
Впрочем, я не проверял. Должно работать.
Просмотры: 10
Перейти в пост Открыть в MAX Сообщить о неточности
__dso_handle
Таинственная незнакомка неизменно появлялась в Начале. Тенью мелькала она среди невзрачных рабочих и прочих безликих ассистентов. Если специально не искать ее взглядом, то и не заметишь. Впрочем, никому и в голову не приходило искать ее специально. Серые мыши никого не интересуют.
Так думал и я, пока не столкнулся с ней в Конце. После этой встречи я не переставал думать о ней. Меня не обманула ее ложная скромность. Это была кротость омута, таящего в себе мрачную тайну. Кто она, какова ее роль во всем этом? Зачем она приходит в Начале и что делает в Конце? Никто не знал.
Я стал следить за ней, записывать, когда и где она появляется. Долгими бессонными ночами я строил планы, где и как подкараулить ее.
Сегодня наконец-то она попалась в мои силки. Теперь она расскажет нам все!
Я пристально смотрел на нее, а она - на меня. Таинственная глобальная переменная __dso_handle.
Если попробовать вывести ее значение в обычном исполняемом файле

printf("%p", __dso_handle);

Она покажется нам банальной пустышкой: (nil).
Нулевой указатель. Тогда зачем она вообще нужна? Совсем эти яйцеголовые разработчики накурились своих мануалов и айда сувать всякую гадость в свои __cxa_thread_atexit.
Однако я сам плюсовик, разрабатываю уже сто лет, внук председателя колхоза. Просто поверьте, тут не все так однозначно!..
Я специально __dso_handle не объявлял, вы, наверняка, тоже. Тогда кто же это сделал?
Компилятор, конечно! Он же обеспечивает вызов и управление глобальными конструкторами и деструкторами в файлах crtbegin (C Runtime Begin) и там же определяет эту магическую переменную. Для тривиальной сборки исполняемого файла компилятор будет применять файлы crtbegin, где

void *__dso_handle = 0;

Для динамических библиотек будет применен особый файл crtbeginS.o, где уже


void *__dso_handle __attribute__ ((__visibility__ ("hidden"))) = &__dso_handle;

Обязательно нужно пометить символ как hidden, чтоб не засветить снаружи динамической библиотеки. В итоге, даже если мы загрузим 100500 so-объектов, у всех будет разная __dso_handle. Это наводит нас на очевидную мысль, что подобный символ можно использовать как некий маркер бинарного модуля.
Эта же мысль пришла в головы создателям требований Itanium C++ ABI.
В разделе 3.3.6.3
Runtime
API в пункте C. Termination ангельским языком писано:
При компоновке любого разделяемого объекта (DSO), содержащего вызов __cxa_atexit, компоновщик должен определить скрытый символ __dso_handle, значением которого является адрес в одном из сегментов объекта. Конкретный адрес не важен, если для разных DSO они различаются.
Казалось бы, причем тут какая-то функция?
Просмотры: 17
Перейти в пост Открыть в MAX Сообщить о неточности
__cxa_thread_atexit
Вот если посмотреть пристально на обычную сосиску. Длинная, похожа на палку. А что говорят древние философы?
Палка о двух концах! А это о чем говорит? О том, что концы-то у неё есть, а начала — нет! Значит, по науке, сосиска есть предмет безначальный!
О чем это говорит? Сосиска - это революция. Потому как есть у революции начало, нет у революции конца!
Что же касается потоков, мы можем точно сказать, что они не революция. Конец у них есть, и мы можем легко его обнаружить!
Стоит лишь внимательнее присмотреться к thread_local, ведь такие объекты будут удалять самыми последними.
Обычный объект в потоке - дело скучное:

void thread_worker(int id) {
Signal s{1};
...

Не случится ничего интересного, просто в конце потока вызовется деструктор объекта.

bl Signal::~Signal() [complete object destructor]

Теперь чуть разнообразия в томный вечер:

void thread_worker(int id) {
thread_local Signal tls{2};
...

Жизнь заиграла новыми красками.
Во-первых, пропадет вызов деструктора Signal.
Во-вторых, при создании объекта произойдет следующая удивительная метаморфоза:

movw r2, #:lower16:__dso_handle
movt r2, #:upper16:__dso_handle
mov r1, r3
movw r0, #:lower16:Signal::~Signal() [complete object destructor]
movt r0, #:upper16:Signal::~Signal() [complete object destructor]
bl __cxa_thread_atexit

Что же за непотребство здесь творится?!
Да это просто компилятор регистрирует деструктор thread_local объекта через функцию __cxa_thread_atexit.
Это специальная внутренняя плюсовая функция, в которую можно вписать функцию очистки, и она гарантированно будет вызвана при завершении работы потока.
Для деструкторов работает особая уличная магия компилятора, но почему бы не пошалить и не засунуть туда какой-нибудь свой обработчик? Кто нам запретит?
Например, желаю в самом конце потока удалить кое-какие чувствительные данные, ну и отладочную печать для наглядности тиснуть.

void my_thread_cleanup(void* arg) {
int* id = static_cast<int*>(arg);
std::cout << "[Cleanup] Поток " << *id << " завершился. Чистим память...\n";
delete id;
}

В качестве аргумента получаем указатель. Допустим, мы знаем, что это некий int. В функции приведем его в надлежащий вид и удалим.
Теперь вручную зарегистрируем наш уничтожитель. Для этого нам потребуется несколько магических пассов. Призовем в код таинственную функцию __cxa_thread_atexit и служебную переменную для линкера:

extern "C" int __cxa_thread_atexit(void (*func)(void *), void *obj, void *dso_handle);
extern void* __dso_handle;
void thread_worker(int id) {
thread_local Signal tls{2};
int* thread_data = new int(id);
__cxa_thread_atexit(my_thread_cleanup, thread_data, &__dso_handle);
std::cout << "[Thread] Поток " << id << " выполнил работу.\n";
}

Соответственно, на экране мы увидим

+sig2
[Thread] Поток 1 выполнил работу.
[Cleanup] Поток 1 завершился. Чистим память...
-sig2

Ой, наш чистильщик пришел слишком рано, перед деструктором Signal.
Все дело в порядке вызовов __cxa_thread_atexit: Поскольку объект Signal создается первый, то и деструктор он регистрирует первым. А в конце потока специально обученная функция вызывает все втиснутые в список функции в обратном порядке.
Если вызвать регистратор перед созданием Signal,

__cxa_thread_atexit(my_thread_cleanup, thread_data, &__dso_handle);
thread_local Signal tls{2};

то все будет выглядеть иначе:

+sig2
[Thread] Поток 1 выполнил работу.
-sig2
[Cleanup] Поток 1 завершился. Чистим память...

Теперь порядок, my_thread_cleanup выполнится в самом конце потока. А поток, как известно, не сосиска.
Просмотры: 25
Перейти в пост Открыть в MAX Сообщить о неточности
dangling reference
Освежающая легкость летнего утра давно сменилась вязкими предрассветными холодами. Потом, если едкая прохлада и отступала, то день все равно оставался придавлен низкими серыми облаками. Тяжесть эта отдавалась в теле, распадаясь внутри на тоску и туман. Такой же поднимался вечерами в поле от безымянной речки. Плотный и сырой, огромными ватными клочьями выползал на берег.
От этой картины сами собой закрывались глаза, голова чугунела и тянула вниз.
"Возраст наверное, - подумал я, и, лихо по-стариковски крякнув, оторвался от стула, - четверть века прожить - чай, не поле перейти..."
Тут же закружилась голова.
"И вот третьего дня в спину вступило, вчера в ухе стреляло, - мысленно стал перебирать неизлечимые болезни, зевнул, - э-эх, и указатели ломит, деструктор отваливается... а ссылки-то! Ссылки прям висят..."
Да, такое бывает. Многие мужчины старше сорока лет сталкиваются с такой проблемой. Не конец света.
По-научному она называется "проблема синхронизации времени жизни объектов". Чаще всего проявляется у тех, кто любит баловаться потоками.
Тут легче всего поймать рассинхрон, особенно когда есть какие-нибудь thread_local переменные.


void thread_worker(int id) { Signal s{1}; thread_local Signal tls{2};}Класс

Signal тут просто для иллюстрации


struct Signal { int x; Signal(int x) : x{x} { std::cout << "+sig" << x << "\r\n"; } ~Signal() { std::cout << "-sig" << x << "\r\n"; }};Просто п

осмотрим, кто, где и когда:

+sig1+sig2Эт

о вполне себе ожидаемо, создание идет в порядке живой очереди.
А вот уничтожение немного удивляет.

-sig1-sig2То

есть, тут концепция RAII подводит нас под монастырь. Вы ожидаете... ну хорошо, я ожидаю, что деструктор s будет вызван после уничтожения tls, а выходит наоборот.
Казалось бы, ерунда. Однако, если постараться, то можно поймать серьезные проблемы.
Например, при использовании эксклюзивных ресурсов и отсоединенных потоков, вы же любите так делать?


std::mutex mtx;bool is_done = false;struct ExclusiveResource {...} gasoline;
void thread_worker() { thread_local Vehicle track{gasoline}; ... std::lock_guard<std::mutex> lock{mtx}; is_done = true;
}Обычный рабочий п

оток. Допустим, нам очень интересно, когда он завершится, поэтому синхронизируемся глобальным мьютексом.
Еще деталь, - объект track при уничтожении вызывает метод gasoline.unload().

int main() { std::thread t(thread_worker); t.detach();
while(true) { std::lock_guard<std::mutex> lock{mtx}; if (is_done) break; } gasoline.spoil(); // Но поток еще работает! return 0
;}Теперь выпускаем пот

ок на свободу через detach и ждем его завершения. Выглядит глупо, конечно, но это для примера, я так не пишу на самом деле. Обычно не пишу. А если и пишу, то не я.
Так вот, lock в thread_worker разрушается ближе к концу, но поток, как и Ленин, все еще живой.
Зато в главном потоке мы выходим из цикла и портим из вредности наш эксклюзивный ресурс - gasoline.spoil().
Допустим, объект тотально разрушен, но в рабочем потоке наступает очередь деструктора track, который попытается выполнить gasoline.unload() на римских развалинах. Дальше, сами понимаете, случается катастрофа.
Просмотры: 27
Перейти в пост Открыть в MAX Сообщить о неточности
back to the std::future
Привели мы как-то нашего ПМ-а в цирк. Никогда до этого не был. А там по арене наматывают круги кони, бегемот сквозь горящие кольца прыгает, на простынях под куполом гимнасты корчатся. Вот дрессированные коты ходят строем, олень на табуретке кеглями жонглирует, бегают клоуны и обливают друг друга желтой водицей. Три часа как на одном дыхании.
За время представления ПМ ни разу не улыбнулся, постоянно отвлекался, записывал что-то в телефоне.
Мы его спрашиваем:
- Не понравилось, да?
- Почему? - строго взглянул на нас. - Понравилось, спасибо.
И опять что-то в сотике набирает.
На следующий день назначает митинг про новую стратегию управления.
- Вчера, наконец-то, я понял, как должны выглядеть процессы в нашей компании, - говорит с чикатильской улыбкой, - нам нужна Continuous Incremental Resilient Collaborative User-centric Sustainable Strategy!
И открывает презентацию:
"CIRCUS Strategy — это подход к управлению разработкой, основанный на непрерывной и инкрементальной поставке ценности через устойчивую к изменениям и высококоллаборативную среду, с фокусом на потребности пользователя и сохранением стабильного темпа команды".
Три часа как на одном дыхании. В конце всем сказал поставить себе цели на год. Не к добру это.
Пока поставил себе целью разобраться окончательно в кружевах и подпорочках std::future. В прошлый раз успели только убедиться, что результат std::promise на самом деле принадлежит разделяемому состоянию. Это значит, что в отличие от депутатов, promise раздает не только обещания, но и возвращает вожделенное значение.
В роли обещания выступает объект std::future, и получить его проще, чем отказ на собеседовании.
Достаточно вызвать простой надежный советский метод get_future(), чтоб получить немного надежды на будущий результат.


future<_Res> get_future(){ return future<_Res>(_M_future); }

Для друзей у std::future есть особый конструктор:

explicit future(const __state_type& __state) : _Base_type(__state) { }

Где тип аргумента __state_type это тип разделяемого состояния и есть.

typedef shared_ptr<_State_base> __state_type;

Можно ошибочно предположить, что фьючерсы можно раздавать всем подряд, shared_ptr все стерпит. Однако взгляните на функцию извлечения:

_Res get() { typename _Base_type::_Reset __reset(*this); return std::move(this->_M_get_result()._M_value());}Метод

этот в Химках уже видели. Кажется, мы вернулись к тому, с чего начинали. Как вы хорошо помните, _Res - это тип шаблонного аргумента std::future. Возвращаем мы не просто копию объекта типа _Res, а через move отдаем владение результатом страждущему. Более того, разделяемое состояние сбрасывается через RAII __reset.
Все эти факты мягко намекают нам, что метод get одноразовый!
При повторном вызове функция проверки в методе _M_get_result бросит исключение std::future_error с кодом no_state.

template<typename _Tp>static void _S_check(const shared_ptr<_Tp>& __p) { if (!static_cast<bool>(__p)) __throw_future_error((int)future_errc::no_state);}Но ско

рее всего, так далеко мы зайти не успеем. Будем остановлены на стадии получения второго std::future.
Особый конструктор для друга у std::future есть, но он с подвохом.
Конструктор future вызывает конструктор базового класса __basic_future


explicit __basic_future(const __state_type& __state) { _State_base::_S_check(_M_state); _M_state->_M_set_retrieved_flag();}Провер

ка _S_check ничего вам не сделает, а вот _M_set_retrieved_flag...
Сей атомный флаг как раз отслеживает, передавалось ли уже это разделяемое состояние в какой-нибудь std::future. Такой интересный костыль для shared_ptr:

void _M_set_retrieved_flag() { if (_M_retrieved.test_and_set()) __throw_future_error(int(future_errc::future_already_retrieved));}Если

уже передавался, то бросаем исключение с кодом future_already_retrieved.
А, ну и цели почти во всех компаниях - это карго-культ.
Просмотры: 35
Перейти в пост Открыть в MAX Сообщить о неточности
ВКонтакте Телеграм Одноклассники
Избранное в datool.ru

Следите за каналом в личном кабинете

Зарегистрируйтесь в datool.ru, чтобы добавить канал «C++ Embedded» в избранное и отслеживать его показатели.

Зарегистрироваться
Для владельцев каналов

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

Войдите или зарегистрируйтесь в datool.ru, затем добавьте нашего бота в канал по инструкции в личном кабинете. После проверки владения замок исчезнет и вам станет доступно управление каналом.

Войти или зарегистрироваться

Сообщить о неточности

Опишите, что нужно исправить. Если есть источник, добавьте ссылку.

Вы покидаете datool.ru

Ссылка ведёт на внешний сайт. datool.ru не отвечает за его содержимое — проверьте адрес, прежде чем вводить пароли или платёжные данные.

Переход через 10 с

Перейти