📜 ⬆️ ⬇️

Windows exception handling

After reading the recent topic " Using try-catch for debugging, " I decided to share my experience as an add-on.

In this article, I propose to consider getting the callstack of the place where an exception was thrown if you work with
structural exceptions (MS Windows). We will not go into the details of the work of exceptions, since This draws on a separate series of articles (for those interested, I recommend Richter, MSDN and wasm.ru). Of course, there are many ready-made projects for generating minidump (for example, CrashRpt or google-breakpad ), so this article is more educational in nature.

What to do with the received call stack is up to you. You can watch the debugger, you can write to a file and watch a third-party program (for this, do not forget to write down the list of loaded modules with their addresses, and you also need debugging symbols).

Theoretical part


Immediately I would like to note that getting a call stack in the exception constructor is not the best option. Not all exceptions are yours, and there is also a class of hardware exceptions that are not caught using the try-catch construct, but using __try-__except .
')
We will go to the solution in an iterative manner so that it becomes clear how this works.

In the case of hardware exclusion, everything is simple. The stack is not unwound, and we can get a stack of calls in the exception filter. In the case of a software exception, when we get into the catch stack is already promoted, and in the constructor of the exception, we agreed not to receive the stack. But it turns out that if try-catch wraps up __try-__except , then even in the case of a program exception, we first go into the filter passed to __except . Here we can get a call stack, but what should the filter return? If the filter returns EXCEPTION_EXECUTE_HANDLER , then we will not get to try-catch . Well, let's return EXCEPTION_CONTINUE_SEARCH , which will cause the handler to look for the next filter, which will return EXCEPTION_EXECUTE_HANDLER . In this case, with a software exception, we will go to try-catch , and in the case of hardware exclusion, the exception-handling mechanism will go looking for a handler further down the stack, skip try-catch and so on until it meets __except with the argument EXCEPTION_EXECUTE_HANDLER . Well, then we wrap try-catch in __try-__except(EXCEPTION_EXECUTE_HANDLER) .

By the way, in this case the block
 __except(filter()/*-> EXCEPTION_CONTINUE_SEARCH*/) { /* */ } 
never be fulfilled.

So, we sketch out what happened (this construction is not compiled, because within the same function you cannot use different forms of exception handling):
 __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) { } 

Useful wrapper


Now you can make a convenient wrapper, so as not to fence such designs every time and to be able to control it all in one place. In my opinion, this kind of wrapper is better than a jumble of preprocessor directives.

Wrap Requirements:

If your method is a class method, and / or which returns something and / or accepts arguments, then all these constructions can always be represented as a delegate, which returns nothing and does not require arguments.

Wrap highlights:

Our wrapper interface:
  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; }; 

Implementation:

The Filter function, in which we have to get a call stack, and which returns EXCEPTION_CONTINUE_SEARCH :
  LONG Filter( PEXCEPTION_POINTERS pep ) { // pep->ExceptionRecord->ExceptionCode // pep->ExceptionRecord->ExceptionAddress // GetModules(); // GetCallStack(); return EXCEPTION_CONTINUE_SEARCH; } 

The topmost wrapper that prevents further propagation of hardware exceptions.
 bool SafeExecutor::Do() { bool AbnornalTermination = false; bool IsExecSuccessful = true; { __try { IsExecSuccessful = DoCPlusPlusExceptionWrapper(); } __except(EXCEPTION_EXECUTE_HANDLER) { AbnornalTermination = true; } } return !AbnornalTermination && IsExecSuccessful; } 

The second wrapper - we catch "C ++ - exceptions" and prevent further propagation of program exceptions.
 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; } 

And the third wrapper that calls the delegate passed and the Filter function in which we need to get the call stack.
 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; } 

Examples of use on the google test framework .

Hardware Exception:
  int HWUnsafe() { int z = 0; return 1/z; } TEST(HWUnsafe, SafeExecutor) { SafeExecutorNS::SafeExecutor se(HWUnsafe); ASSERT_FALSE(se.Do()); } 

Software exception:
  int SWUnsafe1() { int z = 1; throw std::exception(); return 1/z; } TEST(SW_std_ex, SafeExecutor) { SafeExecutorNS::SafeExecutor se(SWUnsafe1); ASSERT_FALSE(se.Do()); } 

Note that exception handling can be a costly operation in terms of performance, but useful when debugging.

Source: https://habr.com/ru/post/163745/


All Articles