Pull to refresh

Особенности обработки исключений в Windows

Reading time4 min
Views25K
Прочитав недавний топик "Использование 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());
 }

Замечу, что обработка исключений может быть дорогостоящей операцией в плане производительности, но полезной при отладке.
Tags:
Hubs:
Total votes 40: ↑37 and ↓3+34
Comments4

Articles