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

от автора

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

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

ссылка на оригинал статьи http://habrahabr.ru/post/163745/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *