Прочитав недавний топик "Использование try — catch для отладки" решил все таки в качестве дополнения поделиться и своим опытом.
В этой статье предлагаю рассмотреть получение callstack’а места, где было брошено исключение в случае работы со
структурными исключениями (MS Windows). В детали работы исключений вдаваться не будем, т.к. это тянет на отдельный цикл статей (для интересующихся рекомендую Рихтера, MSDN и wasm.ru). Конечно, есть много уже готовых проектов для генерации minidump
’ов (например CrashRpt или google-breakpad), так что эта статья носит больше образовательный характер.
Что делать с полученным стеком вызовов — решать вам. Можно смотреть отладчиком, можно записать в файл и смотреть сторонней программой (для этого не забудьте записать список загруженных модулей с их адресами, а так же вам понадобятся отладочные символы).
Теоретическая часть
Сразу же хотелось бы заметить, что получать стек вызовов в конструкторе исключения — не самый лучший вариант. Не все исключения ваши, и есть еще класс аппаратных исключений, которые отлавливаются не с помощью конструкции try-catch
, а с помощью __try-__except
.
Будем идти к решению в итеративном порядке, чтобы стало понятно, как это работает.
В случае с аппаратным исключением все просто. Стек не раскручивается, и мы можем в фильтре исключения получить стек вызовов. В случае с программным исключением, когда мы попадаем в блок catch
стек уже раскручен, а в конструкторе исключения мы договорились стек не получать. Но, оказывается, что если try-catch
оборачивает __try-__except
, то, даже в случае программного исключения, мы сначала заходим в фильтр, переданный в __except
. Тут мы можем получить стек вызовов, но что должен возвращать фильтр? Если фильтр вернет EXCEPTION_EXECUTE_HANDLER
, тогда мы не дойдем до try-catch
. Что же, вернем EXCEPTION_CONTINUE_SEARCH
, который побудит обработчик искать следующий фильтр, который вернет EXCEPTION_EXECUTE_HANDLER
. В этом случае с программным исключением мы дойдем до try-catch
, а в случае с аппаратным исключением механизм обработки исключений пойдет искать обработчик дальше по стеку, пропустит try-catch
и так до тех пор, пока не встретит __except
с аргументом EXCEPTION_EXECUTE_HANDLER
. Хорошо, тогда обернем try-catch
в __try-__except(EXCEPTION_EXECUTE_HANDLER)
.
Кстати, в этом случае блок
__except(filter()/*-> EXCEPTION_CONTINUE_SEARCH*/)
{
/*этот блок*/
}
никогда не выполнится.
Итак, изобразим схематично, что получилось (это конструкция не компилируется, потому что внутри одной функции нельзя использовать разные формы обработки исключений):
__try
{
try
{
__try
{
useful_unsafe_function();
}
__except(filter()/*-> EXCEPTION_CONTINUE_SEARCH*/)
{
// this block will be never executed
}
}
catch(const your_lib::Exception& ex)
{
}
catch(const std::exception& ex)
{
}
catch(...)
{
}
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
}
Полезная обертка
Теперь можно сделать удобную обертку, чтобы не городить такие конструкции каждый раз и чтобы иметь возможность контролировать это все в одном месте. На мой взгляд такого рода обертка лучше, чем нагромождение из директив препроцессора.
Требования к обертке:
- В простейшем случае ни одно исключение не должно распространяться за пределы обертки.
- Принимает делегат, который ничего не возвращает и не принимает аргументы (
function<void()>
) - Считаем, что если произошло исключение, то обертка должна об этом сообщить, вернув
false
.
Если ваш метод — это метод класса, и/или который что-то возвращает и/или принимает аргументы, то все эти конструкции всегда можно представить в виде делегата, который ничего не возвращает и не требует аргументы.
Основные моменты обертки:
Интерфейс нашей обертки:
struct SafeExecutor
{
typedef boost::function<void()> TDoDelegate;
SafeExecutor(TDoDelegate doDelegate);
// true - the everything is successful
// false - otherwise
bool Do();
private:
bool DoCPlusPlusExceptionWrapper();
bool DoWorkWrapper();
private:
TDoDelegate m_DoDelegate;
};
Реализация:
Функция Filter
, в которой мы должны получить стек вызовов, и которая возвращает EXCEPTION_CONTINUE_SEARCH
:
LONG Filter( PEXCEPTION_POINTERS pep )
{
// pep->ExceptionRecord->ExceptionCode
// pep->ExceptionRecord->ExceptionAddress
// GetModules();
// GetCallStack();
return EXCEPTION_CONTINUE_SEARCH;
}
Самая верхняя обертка, которая предотвращает дальнейшее распространение аппаратных исключений.
bool SafeExecutor::Do()
{
bool AbnornalTermination = false;
bool IsExecSuccessful = true;
{
__try
{
IsExecSuccessful = DoCPlusPlusExceptionWrapper();
}
__except(EXCEPTION_EXECUTE_HANDLER)
{
AbnornalTermination = true;
}
}
return !AbnornalTermination && IsExecSuccessful;
}
Вторая обертка — отлавливаем «C++-исключения» и предотвращаем дальнейшее распространение программных исключений.
bool SafeExecutor::DoCPlusPlusExceptionWrapper()
{
bool res = true;
try
{
res = DoWorkWrapper();
}
catch(std::exception& /*ex*/)
{
// smth like log(ex.what());
//assert(false);
res = false;
}
catch(...)
{
// smth like log("unknown sw-exception);
//assert(false);
res = false;
}
return res;
}
И третья обертка, которая вызывает переданный делегат и функцию Filter
, в которой мы должны получить стек вызовов.
bool SafeExecutor::DoWorkWrapper()
{
bool res = false;
if (!m_DoDelegate.empty())
{
__try
{
m_DoDelegate();
res = true;
} __except(Filter(GetExceptionInformation())) // we must dump callstack inside this Filter
{
// never be executed because Filter always returns `CONTINUE_SEARCH`
}
}
return res;
}
Примеры использования на google test framework.
Аппаратное исключение:
int HWUnsafe()
{
int z = 0;
return 1/z;
}
TEST(HWUnsafe, SafeExecutor)
{
SafeExecutorNS::SafeExecutor se(HWUnsafe);
ASSERT_FALSE(se.Do());
}
Программное исключение:
int SWUnsafe1()
{
int z = 1;
throw std::exception();
return 1/z;
}
TEST(SW_std_ex, SafeExecutor)
{
SafeExecutorNS::SafeExecutor se(SWUnsafe1);
ASSERT_FALSE(se.Do());
}
Замечу, что обработка исключений может быть дорогостоящей операцией в плане производительности, но полезной при отладке.
Автор: Talyutin