Pull to refresh

Comments 19

Делал такое в MSVC2005. Отличия следующие:
— макрос шел в теле функции первой строчкой _FUNCTION_PROLOGUE(m_sl, ClassName::FuncName);
— m_sl — паттерн ServiceLocator, который хранил стек-трейсы по тредам;
— кроме того, был собственный класс SourcedError, к которому приводились ошибки из всего зоопарка модулей.
макрос шел в теле функции первой строчкой _FUNCTION_PROLOGUE(m_sl, ClassName::FuncName);

Да, в моем текущем проекте в тестовой ветке примерно так все и есть. Только без аргументов. В общем то это было сделано для сохранения нормального внешнего вида кода и решения проблемы в операторными скобками.

Но третье отличие не совсем понял?
Код оборачивался в еще пару макросов _BEGIN_ERROR(err) / _CATCH_ERROR(err), где переменная «err» имела тип SourcedError и начинался блок try/catch. Все выбрасываемые исключения и коды ошибок приводились к этому типу с указанием модуля, файла и номера строки.
Хм, очень интересный механизм. То есть и изначальное исключение сохраняется и дополнительные данные через него передаются. Имеет смысл во время реализации дополнительного препроцессора взять эту функцию на вооружение.
UFO just landed and posted this here
Да, это вполне решение, но я что-то об этом не подумал. Как появится время — углублюсь в этот вопрос и по получении более менее нормальных результатов — представлю его.
Пока что оно мне нравится гораздо больше, чем моя поделка.
Есть ещё два варианта, более хардкорных:
— встраиваться в платформу (LLVM) и вставлять код там при сборке
— патчить бинарные файлы

Что касается вашего варианта, то лучше взять такой код:
#if defined(__GNUC__)
#	define FUNCTION __PRETTY_FUNCTION__
#elif defined(_MSC_VER)
#	define FUNCTION __FUNCSIG__
#else
#	define FUNCTION __func__
#endif

И хранить не дорогие std::string, а const char *. Также можно использовать вместо map'ы глобальную thread_local (тоже понадобится несколько if'ов в макросах, если вдруг iOS/Android/MSVS 2013) переменную для стека, а сам стек заменить с list'а на array<CallData, *много, всяко хватит*>.
По поводу хранения const char* у меня в начале так и было, но позже когда пришлось конкатенировать имя функции с вычисляемым именем класса я изменил на std::string. Хотя, вполне логично, что хранить указатель гораздо дешевле, чем целый объект std::string, который в некоторых реализациях имеет pimpl и счетчик копий.
На счет препроцессорного кода — большое спасибо. По причине того, что я не пользовался никогда VS компилятором — я не знаю достоверно как там работает этот макрос (а в стандарте он не указан, что делает его не кросскомпиляторным), ваш код решает эту проблему.
По поводу thread_local написали ниже, обязательно в следующей реализации буду использовать глобальный thread_local переменную.
Как я понимаю при использовании array — это будет массив на стеке?
В вашей реализации есть много проблем:

1. Обработчик сигналов не AS-Safe (https://www.gnu.org/software/libc/manual/html_node/POSIX-Safety-Concepts.html)
2. Обработчик исключений выделяет память, а что если она закончилась и произошло исключение std::bad_alloc?
3. Такое решение дает накладные расходы на каждый вызов.
4. Нет возможности получить стек вызова при использовании сторонних библиотек.
5.…

Не проще ли воспользоваться backtrace/backtrace_symbols в Linux или WinGdb на Windows?
Да, проблем очень много. На счет накладных расходов — это очевидно. Правда расходы на создание объекта на стеке и добавление элемента в связный список не очень велики и в большинстве программ (которые не используют сложные вычисления и не имеют явно очень горячих ф-ций) ничего особо страшного не происходит.
На счет AS-Safe, честно говоря вообще никогда не думал о такой проблеме. Хотя это довольно очевидная проблема разделяемых «глобальных» данных. Спасибо за ссылку и обязательно учту это в будущем.
Касательно std::bad_alloc сказать ничего не могу. Я хоть примерно знаю как работают исключения, но конкретно сказать не могу. На сколько я помню под bad_alloc память выделяется сразу, что бы при возникновении проблем с памятью ее не выделять заново. Но я скорее всего ошибаюсь, поправьте меня если я не прав. Поэтому при вызове std::bad_alloc программа просто вылетит и все. Оно же бросается не через THROW.
На счет отсутствия возможности получения stacktrace из библиотек — я думал, что я написал об этом в статье, но оказалось что нет. Сейчас исправлю.

Проще ли вызывать backtarce/backtrace_symbols под Linux? Да, проще. WinGdb тогда требуется тащить за программой и надеяться yа его вызов при падении.
Но вообще, если интересует, зачем вообще тот маразм, что я написал в статье — то для того, что бы если программа у пользователя упала и он вообще не может объяснить как он этого добился — то можно достать у него лог и посмотреть что вызывалось во время падения. Преимущественно для отлова ошибок во время написания кода.
Можно ещё объяснить пользователю как включить формирование coredump (если он не включен) и попросить отослать вам этот дамп.
По дампу будет значительно проще понять что произошло, при этом конечно ваша программа должна быть скомпилирована с отладочными символами.
У меня, в свое время попроще получилось. Идея в том, что объект создается на стеке, в конструкторе пишет в лог, и при удалении в деструкторе так же пишет в лог.
some.cpp

#include «tracer.h»
void f()
{
TRACE_;
}

tracer.h
#define TRACE_ tracer tracer__( std::string( __PRETTY_FUNCTION__ )+" "+std::string( __FILE__ ) )
#define TRACE_R(i) tracer tracer__( std::string( __PRETTY_FUNCTION__ )+" "+std::string( __FILE__ ), i )

template
class tracer
{
string f;
public:

tracer( const string& s ) :f( s )
{
string tab( thread_trace_counter::inst().get(), ' ' );
OUT_TO_FILE( tab + ">> " + f );
thread_trace_counter::inst().increment();
}

~tracer()
{
thread_trace_counter::inst().decrement();
string tab( thread_trace_counter::inst().get(), ' ' );
OUT_TO_FILE( tab + "
Проблема этого подхода в том, что постоянно идет запись в лог. Как известно это довольно затратная операция. И в результате даже после успешного завершения программы лог просто завален вызовами.
Обрезался мой комментарий. Там в самом низу было сказано, что в релизе трассировка отключается простой заменой макросов на пустышки. Трасса нужна только для отладки. В релизе ей ни в каком виде не место.
Используем сходный велосипед в рабочем проекте. Отличия:
1) Для создания stack-фреймов не используется выделение памяти на куче
2) В stack-фреймы можно напихать сколько угодно и какой угодно информации, не только имя функции и номер строки
3) Функции не заворачиваются в макросы, скорее наоборот — макросы кладутся внутрь функций, причем только тех, которые реально нужны
4) Поддержка многопоточность реализована через thread-local переменные
Вижу пару недостатков такого подхода:
1. Нужно псиать макрос в каждом вызове.
2. Есть только callstack
3. Невозможно учесть все случаи (а ведь может упасть в сторонней библе)

У нас в проекте под Win Embedded была подобная задача. Нужн было отлавливать все падения софта и писать дамп.
В винде для этого есть набор функций:
::SetUnhandledExceptionFilter
std::set_unexpected
std::set_terminate
_set_purecall_handler
_set_invalid_parameter_handler
_set_new_handler
_set_abort_behavior

Нужно только задать свою callback-функцию. Пока не удалось отловить только heap corruption, но с Win8 вроде тоже API для этого появился.
Кроме того в Windows есть встроенный механизм отслеживания падений и создания дампов. Настраивается через реестр.
Насчет Linux ничего сказать не могу, но думаю там тоже есть более удобные решения.
Вообще я старался не использовать кроссплатформенные функции, но сейчас я начинаю считать что это не очень честно по отношению к программе.
Для решения первой проблемы, которую вы указали выше предложили написать собственный препроцессор, который бы кушал обычные исходники, а выдавал исходники с макросами.
Но вот решить остальные проблемы невозможно без использования платформозависимых функций.
Справедливости ради стоит сказать, что я не знал об указанных вами функциях, так как не очень разбираюсь в winapi. Но я сейчас больше склоняюсь к тому, что бы написать обертку вокруг встроенных и в Linux и в Windows алгоритмов получения stack trace и отслеживания падений.
Кстати на счет Linux. В моем коде была представлена функция signal, работа которой не обязательно должна быть такой, какой я ее ожидал. Нигде не сказано, что Windows, так же как и Linux, получает сигнал SIGSEGV при падении. Пока что он его получает, в будущем может перестать.
P.S.
Как же я ненавижу приложения Habr для Android. Совершенно не удобно писать комментарии и стоит хотя бы попытаться взглянуть на статью или не дай б-г вернуть приложение — все. Комментарий утерян безвозвратно. Пришлось бросить все и идти за компьютер.
Уровень «быдлокодовости» зашкаливает. Сама идея оборачивать всё функции никуда не годится. Проблема решается гораздо проще.

> Под Windows есть функция WinAPI CaptureStackBackTrace, которая позволяет прогуляться по стеку и получить вызовы из фреймов.

А под линукс есть backtrace. А под другую платформу есть что-либо ещё. Может даже libunwind. Вот так и надо действовать, а не засерать свой код тупыми макросами.
Sign up to leave a comment.

Articles