DISCLAMER


Статья является шуточной, но с долей правды (программирование, же). Данная статья также содержит код, который может смертельно навредить вашему зрению. Читайте на ваш риск.

Вступление


Здравствуйте. Думаю многие сталкивались с неинформативностью большинства критических ошибок, вылетающих в программе. Давайте перечислим, какие ситуации могут приводить к аварийному завершению работы программы:

Исключение

Исключения — это очень мощная система обработки исключительных ситуаций, возникающих в программе. Но если исключение не было обработано — то оно роняет программу через std::terminate. Поэтому в хорошо написанных программах, исключение которое не было обработано зачастую означает баг в программе, который надо исправлять.

Данный вид ошибок является самым информативным, так как метод исключения what() выводится в stderr автоматически при падении программы.

Assert

Отключаемый метод контроля правильности использования функций. Отключение предоставляется в качестве инструмента увеличения производительности в функциях, в которых каждая наносекунда на счету. Если программа упала по assertу, значит программист где-то накосячил при использовании интерфейса какого то модуля. Но вполне возможно, что он просто не предусмотрел каких-то критических значений и по этой причине вышел из условий assertа.

Данный вид ошибок является не самым информативным, но при падении, выводит условие, которое было нарушено.

SIGSEGV

Вы, как профессионал своего дела разыменовали нулевой указатель и радостно записали в него какое-то значение. Программа не особо сопротивляясь упала.

Такое падение не сопровождается никакими сообщениями и является наверное самым не информативным и представленных, но оно есть и исключать его ни в коем случае нельзя.

Все виды ошибок, вне зависимости от их информативности, не очень помогают определить по какой же причине она появилась. В рамках этой статьи я попробую показать, что у меня получилось в порыве получить хоть какой-то stack trace во время отлова ошибок.

Смотрим по сторонам


Для начала надо понять, каким образом вообще отслеживать вызовы функций. Гуглинг выдал крайне неутешительные результаты. Очевидно, что кроссплатформенного решения нет. Под Linux и Mac OS есть заголовочный файл execinfo.h с помощью которого можно получить связный список стека вызовов. Под Windows есть функция WinAPI CaptureStackBackTrace, которая позволяет прогуляться по стеку и получить вызовы из фреймов. Но мы пойдем путем С++. Не будем использовать платформозависимые функции.

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

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

Но какой использовать интерфейс? Как написать более менее красивый код и при этом получить требуемую функциональность.
Единственное решение которое я смог найти — это макросы (возможно, это все также можно как-то реализовать через шаблоны, но я с шаблонами знаком крайне поверхностно и поэтому делаю так как умею).

Реализация


Для начала реализуем синглетон, который будет использоваться для работы со стеком. В качестве интерфейса пользователя реализуем только метод для получения строкового представления stack traceа.

class StackTracer
{
    friend class CallHolder;

public:

    static StackTracer& i()
    {
        static StackTracer s;
        return s;
    }

    std::string getStackTrace() const
    {
        std::stringstream ss;

        for (auto iterator = m_data.begin(), end = m_data.end();
             iterator != end;
             ++iterator)
            ss << iterator->file << ':' << iterator->line << " -> " << iterator->name << std::endl;

        return ss.str();
    }

private:
    void push(const std::string &name, const char *file, int line)
    {
        m_data.push_front({name, file, line});
    }

    void pop()
    {
        m_data.pop_front();
    }

    struct CallData
    {
        std::string name;
        const char *file;
        int line;
    };

    StackTracer() :
            m_data()
    {}

    std::list<CallData> m_data;
};

Нет возможности использовать std::stack, так как для того, что бы получить все элементы для вывода пришлось бы копировать весь контейнер.

Из проблем данного класса — полная потоковая небезопасность. Но с этим мы разберемся позже, а сейчас PoC.

Теперь реализуем класс, который будет регистрировать и удалять вызов функций.

class CallHolder
{
public:
    CallHolder(const std::string &name, const char *file, int line)
    {
        StackTracer::i().push(name, file, line);
    }

    ~CallHolder()
    {
        StackTracer::i().pop();
    }
};

Довольно нетривиальный код не так ли? Опять же, данный «регистратор» не учитывает многопоточность.

Теперь попробуем накидать небольшой пример, что бы проверить работоспособность такого Франкенштейна.

void func1();

void func2()
{
    CallHolder __f("func2()", __FILE__, __LINE__);

    func1();
}

void func1()
{
    CallHolder __f("func1()", __FILE__, __LINE__);

    static int i = 1;
    if (i-- == 1)
        func2();
    else
        std::cout << StackTracer::i().getStackTrace() << std::endl;
}

int main()
{
    func1();

    return 0;
}

Результат:


Рисунок 3.1 — «Оно живое!!»

Отлично! Но надо же как-то упаковать вызов CallHolder, а то не красиво как-то получается ручками вызывать и два раза прописывать название метода.

Для реализаций функций и методов получился такой вот макрос:

#define MEM_IMPL(func_name, args)func_name args{    CallHolder __f("" #func_name #args "", __FILE__, __LINE__);

Теперь нашего Франкенштейна можно модифицировать и получить что-то вроде этого. Уже более похоже на «обычный» код:

void func1();

void MEM_IMPL(func2, ())

    func1();
}

void MEM_IMPL(func1, ())

    static int i = 1;
    if (i-- == 1)
        func2();
    else
        std::cout << StackTracer::i().getStackTrace() << std::endl;
}

int main()
{
    func1();

    return 0;
}

Результат выполнения ровно такой же, как и ранее. Но в данном подходе есть явная проблема. Пропадает открывающая фигурная скобка, которую скрывает макрос. Это усложняет чтение кода. Хотя люди, которые придерживаются идеологии с открывающей фигурной скобкой в строке с заголовком не сочтут это сильным минусом. Более сильный минус, что среда разработки, которой я пользуюсь не умеет работать с такими изворотливыми случаями и считает только фигурные скобки вне макросов.

Но мы отвлеклись от нашей вакханалии. Что же делать, если у нас класс? Ну если реализация вне класса — то ничего. Пример:

void func1();

void MEM_IMPL(func2, ())

    func1();
}

void MEM_IMPL(func1, ())

    static int i = 1;
    if (i-- == 1)
        func2();
    else
        std::cout << StackTracer::i().getStackTrace() << std::endl;
}

class EpicClass
{
public:
    void someFunc();
};

void MEM_IMPL(EpicClass::someFunc, ())
    func1();
}

int main()
{
    EpicClass a;
    
    a.someFunc();

    return 0;
}

Результат:


Рисунок 3.2 — Вывод из класса

А что, если вы пишете реализацию прямо в объявлении класса? Тогда требуется другой макрос:

#define CLASS_IMPL(class_name, func_name, args)func_name args{    CallHolder __f("" #class_name "::" #func_name "", __FILE__, __LINE__);

Но у такого подхода есть проблема. В нем надо отдельно указывать имя класса, что не очень хорошо. Это можно обскакать, если мы используем С++11. Я использую найденное на stack overflow решение. Это type_name<decltype(i)>(). Где type_name это

#include <type_traits>
#include <typeinfo>
#ifndef _MSC_VER
#   include <cxxabi.h>
#endif
#include <memory>
#include <string>
#include <cstdlib>

template <class T>
std::string
type_name()
{
    typedef typename std::remove_reference<T>::type TR;
    std::unique_ptr<char, void(*)(void*)> own
            (
#ifndef _MSC_VER
            abi::__cxa_demangle(typeid(TR).name(), nullptr,
                                nullptr, nullptr),
#else
            nullptr,
#endif
            std::free
    );
    std::string r = own != nullptr ? own.get() : typeid(TR).name();
//    if (std::is_const<TR>::value)
//        r += " const";
//    if (std::is_volatile<TR>::value)
//        r += " volatile";
//    if (std::is_lvalue_reference<T>::value)
//        r += "&";
//    else if (std::is_rvalue_reference<T>::value)
//        r += "&&";
    return r;
}

Часть с модификаторами закомментирована по той причине, что результат обработки (*this) тогда будет в конце иметь знак ссылки — амперсанд (&).

Хитрожопый макрос выглядит так:

#define CLASS_IMPL(func_name, args)func_name args{    CallHolder __f(type_name<decltype(*this)>() + "::" + #func_name + #args, __FILE__, __LINE__);

Подредактируем нашего франка и посмотрим на результат:

void func1();

void MEM_IMPL(func2, ())

    func1();
}

void MEM_IMPL(func1, ())

    static int i = 1;
    if (i-- == 1)
        func2();
    else
        std::cout << StackTracer::i().getStackTrace() << std::endl;
}

class EpicClass
{
public:
    void someFunc();

    void CLASS_IMPL(insideFunc, ())
        func1();
    }
};

void MEM_IMPL(EpicClass::someFunc, ())
    func1();
}

int main()
{
    EpicClass a;

//    a.someFunc();
    a.insideFunc();

    return 0;
}

Результат:


Рисунок 3.3 — Объявленный внутри метод класса

Хорошо, но что там с информативностью? Каким образом можно получить хоть какую-нибудь полезную информацию при падении. Ведь сейчас при возникновении того же Seg Fault все просто упадет. Ну для начала реализуем свой int main, который будет ловить ошибки. В заголовке объявляем:

int safe_main(int argc, char *argv[]);

В cpp реализуем наш «безопасный» main, который уже вызовет safe_main.

void signal_handler(int signum)
{
    std::cerr << "Death signal has been taken. Stack trace:" << std::endl << StackTracer::i().getStackTrace() << std::endl;
    signal(signum, SIG_DFL);
    exit(3);
}

int MEM_IMPL(main, (int argc, char * argv[]))
    signal(SIGSEGV, signal_handler);
    signal(SIGTERM, signal_handler);
    signal(SIGABRT, signal_handler);

    return safe_main(argc, argv);
}

Думаю стоит объясниться. Функцией signal мы устанавливаем обработчик, который вызовется при появлении сигналов SIGSEGV, SIGTERM и SIGABRT. В котором уже будет выведен в stderr stack trace. (Последний требуется для assert).

Попробуем сломать программу SIGSEGV. Опять изменим наш «тестовый стенд»:

void func1();

void MEM_IMPL(func2, ())

    func1();
}

void MEM_IMPL(func1, ())

    static int i = 1;
    if (i-- == 1)
        func2();
    else
    {
        int *i = nullptr;
        (*i) = 12;
    }
}

class EpicClass
{
public:
    void someFunc();

    void CLASS_IMPL(insideFunc, ())
        func1();
    }
};

void MEM_IMPL(EpicClass::someFunc, ())
    func1();
}

int MEM_IMPL(safe_main, (int argc, char *argv[]))
    EpicClass a;

//    a.someFunc();
    a.insideFunc();

    return 0;
}

Результат:


Рисунок 3.4 — Работа безопасного main

Но как обстоят дела с исключениями? Ведь если вызывать исключение — то оно просто поразрушает все имеющиеся CallHolder и в stack trace мы не получим ничего обстоятельного. Для этого создаем собственный THROW макрос, который бы получал stack trace в момент выброса исключения:

#define THROW(exception, explanation)throw exception(explanation + std::string("\n\rStack trace:\n\r") + StackTracer::i().getStackTrace());

Так же модифицируем немного наш «тестовый стенд»:

void func1();

void MEM_IMPL(func2, ())

    func1();
}

void MEM_IMPL(func1, ())

    static int i = 1;
    if (i-- == 1)
        func2();
    else
    {
//        int *i = nullptr;
//        (*i) = 12;
        THROW(std::runtime_error, "Some cool error");
    }
}

class EpicClass
{
public:
    void someFunc();

    void CLASS_IMPL(insideFunc, ())
        func1();
    }
};

void MEM_IMPL(EpicClass::someFunc, ())
    func1();
}

int MEM_IMPL(safe_main, (int argc, char *argv[]))
    EpicClass a;

//    a.someFunc();
    a.insideFunc();

    return 0;
}

И получаем результат:


Рисунок 3.5 — THROW не прощает

Хорошо. Мы добились полного базового функционала, но что там с многопоточностью? Будем ли мы с ней что-то делать?
Ну по крайней мере попробуем!

Для начала редактируем StackTracer, что бы он начал работать с разными потоками:

class StackTracer
{
    friend class CallHolder;

public:

    static StackTracer& i()
    {
        static StackTracer s;
        return s;
    }

    std::string getStackTrace() const
    {
        std::stringstream ss;

        std::lock_guard<std::mutex> guard(m_readMutex);
        for (auto mapIterator = m_data.begin(), mapEnd = m_data.end();
             mapIterator != mapEnd;
             ++mapIterator)
        {
            ss << "Thread: 0x" << std::hex << mapIterator->first << std::dec << std::endl;

            for (auto listIterator = mapIterator->second.begin(), listEnd = mapIterator->second.end();
                 listIterator != listEnd;
                 ++listIterator)
                ss << listIterator->file << ':' << listIterator->line << " -> " << listIterator->name << std::endl;
            ss << std::endl;
        }

        return ss.str();
    }

private:
    void push(const std::string &name, const char *file, int line, std::thread::id thread_id)
    {
        m_data[thread_id].push_front({name, file, line});
    }

    void pop(std::thread::id thread_id)
    {
        m_data[thread_id].pop_front();
    }

    struct CallData
    {
        std::string name;
        const char *file;
        int line;
    };

    StackTracer() :
            m_data()
    {}

    mutable std::mutex m_readMutex;
    std::map<std::thread::id, std::list<CallData> > m_data;
};

Аналогично меняем CallHolder, что бы в него передавался thread_id:

class CallHolder
{
public:
    CallHolder(const std::string &name, const char *file, int line, std::thread::id thread_id)
    {
        StackTracer::i().push(name, file, line, thread_id);
        m_id = thread_id;
    }

    ~CallHolder()
    {
        StackTracer::i().pop(m_id);
    }

private:
    std::thread::id m_id;
};

Ну и модифицируем немного макросы:

#define CLASS_IMPL(func_name, args)func_name args{    CallHolder __f(type_name<decltype(*this)>() + "::" + #func_name + #args, __FILE__, __LINE__, std::this_thread::get_id());


#define MEM_IMPL(func_name, args)func_name args{    CallHolder __f("" #func_name #args "", __FILE__, __LINE__, std::this_thread::get_id());


Тестируем. Подготовим такой «стенд»:
void MEM_IMPL(sleepy, ())
    std::this_thread::sleep_for(std::chrono::seconds(3));

    THROW(std::runtime_error, "Thread exception");
}

void MEM_IMPL(thread_func, ())
    sleepy();
}

int MEM_IMPL(safe_main, (int argc, char *argv[]))

    std::thread th(&thread_func);
    th.detach();

    std::this_thread::sleep_for(std::chrono::seconds(20));

    return 0;
}

И попробуем запустить:


Рисунок 3.6 — Смерть наступила в 1:10 по московскому времени

Вот мы и получили многопоточный stack trace. Эксперимент окончен, подопытный мертв. Из очевидных проблем данной реализации
  • Мы не можем получить вызовы из библиотек не написанных нами;
  • Дополнительные накладные расходы на каждый вызов функции.

Заключение


К сожалению без серьезной компиляторной поддержки реализовать отладочный stack trace крайне затруднительно и приходится прибегать к костылям. Но в любом случае, спасибо за прочтение данной статьи.
Поделиться с друзьями
-->

Комментарии (18)


  1. 0x3f00
    30.05.2016 14:20

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


    1. Megaxela
      30.05.2016 14:35

      макрос шел в теле функции первой строчкой _FUNCTION_PROLOGUE(m_sl, ClassName::FuncName);

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

      Но третье отличие не совсем понял?


      1. 0x3f00
        30.05.2016 14:47

        Код оборачивался в еще пару макросов _BEGIN_ERROR(err) / _CATCH_ERROR(err), где переменная «err» имела тип SourcedError и начинался блок try/catch. Все выбрасываемые исключения и коды ошибок приводились к этому типу с указанием модуля, файла и номера строки.


        1. Megaxela
          30.05.2016 15:59

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


  1. numitus2
    30.05.2016 14:30

    А почему бы не реализовать это в виде отдельного препроцессора, чтобы на входе были человеческие исходники, а на выходе с макросами?


    1. Megaxela
      30.05.2016 14:32

      Да, это вполне решение, но я что-то об этом не подумал. Как появится время — углублюсь в этот вопрос и по получении более менее нормальных результатов — представлю его.
      Пока что оно мне нравится гораздо больше, чем моя поделка.


      1. Woodroof
        31.05.2016 18:38

        Есть ещё два варианта, более хардкорных:
        — встраиваться в платформу (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, *много, всяко хватит*>.


        1. Megaxela
          31.05.2016 20:09

          По поводу хранения const char* у меня в начале так и было, но позже когда пришлось конкатенировать имя функции с вычисляемым именем класса я изменил на std::string. Хотя, вполне логично, что хранить указатель гораздо дешевле, чем целый объект std::string, который в некоторых реализациях имеет pimpl и счетчик копий.
          На счет препроцессорного кода — большое спасибо. По причине того, что я не пользовался никогда VS компилятором — я не знаю достоверно как там работает этот макрос (а в стандарте он не указан, что делает его не кросскомпиляторным), ваш код решает эту проблему.
          По поводу thread_local написали ниже, обязательно в следующей реализации буду использовать глобальный thread_local переменную.
          Как я понимаю при использовании array — это будет массив на стеке?


          1. Woodroof
            01.06.2016 21:51

            Да, именно.


  1. nekipelov
    30.05.2016 14:48
    +4

    В вашей реализации есть много проблем:

    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?


    1. Megaxela
      30.05.2016 15:04
      -2

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

      Проще ли вызывать backtarce/backtrace_symbols под Linux? Да, проще. WinGdb тогда требуется тащить за программой и надеяться yа его вызов при падении.
      Но вообще, если интересует, зачем вообще тот маразм, что я написал в статье — то для того, что бы если программа у пользователя упала и он вообще не может объяснить как он этого добился — то можно достать у него лог и посмотреть что вызывалось во время падения. Преимущественно для отлова ошибок во время написания кода.


      1. naodesu
        30.05.2016 18:49
        +2

        Можно ещё объяснить пользователю как включить формирование coredump (если он не включен) и попросить отослать вам этот дамп.
        По дампу будет значительно проще понять что произошло, при этом конечно ваша программа должна быть скомпилирована с отладочными символами.


  1. psplus2016
    30.05.2016 18:49

    У меня, в свое время попроще получилось. Идея в том, что объект создается на стеке, в конструкторе пишет в лог, и при удалении в деструкторе так же пишет в лог.
    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. Megaxela
      30.05.2016 18:51
      +1

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


      1. psplus2016
        31.05.2016 12:06

        Обрезался мой комментарий. Там в самом низу было сказано, что в релизе трассировка отключается простой заменой макросов на пустышки. Трасса нужна только для отладки. В релизе ей ни в каком виде не место.


  1. Xop
    30.05.2016 19:26

    Используем сходный велосипед в рабочем проекте. Отличия:
    1) Для создания stack-фреймов не используется выделение памяти на куче
    2) В stack-фреймы можно напихать сколько угодно и какой угодно информации, не только имя функции и номер строки
    3) Функции не заворачиваются в макросы, скорее наоборот — макросы кладутся внутрь функций, причем только тех, которые реально нужны
    4) Поддержка многопоточность реализована через thread-local переменные


  1. fareloz
    31.05.2016 11:19

    Вижу пару недостатков такого подхода:
    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 ничего сказать не могу, но думаю там тоже есть более удобные решения.


    1. Megaxela
      31.05.2016 11:41

      Вообще я старался не использовать кроссплатформенные функции, но сейчас я начинаю считать что это не очень честно по отношению к программе.
      Для решения первой проблемы, которую вы указали выше предложили написать собственный препроцессор, который бы кушал обычные исходники, а выдавал исходники с макросами.
      Но вот решить остальные проблемы невозможно без использования платформозависимых функций.
      Справедливости ради стоит сказать, что я не знал об указанных вами функциях, так как не очень разбираюсь в winapi. Но я сейчас больше склоняюсь к тому, что бы написать обертку вокруг встроенных и в Linux и в Windows алгоритмов получения stack trace и отслеживания падений.
      Кстати на счет Linux. В моем коде была представлена функция signal, работа которой не обязательно должна быть такой, какой я ее ожидал. Нигде не сказано, что Windows, так же как и Linux, получает сигнал SIGSEGV при падении. Пока что он его получает, в будущем может перестать.

      P.S.
      Как же я ненавижу приложения Habr для Android. Совершенно не удобно писать комментарии и стоит хотя бы попытаться взглянуть на статью или не дай б-г вернуть приложение — все. Комментарий утерян безвозвратно. Пришлось бросить все и идти за компьютер.