Back to Home

Handling Multiple SIGSEGV-Like Errors

sigsegv · segmentation fault · access violation · signal · seh · veh · setjmp · longjmp

Handling Multiple SIGSEGV-Like Errors

    The theme was driven out and not a few copies were broken because of it. One way or another, people continue to wonder whether an application written in C / C ++ can not fall after dereferencing a null pointer, for example. The short answer is yes, even on Habré there are articles on this subject.


    One of the most common answers to this question is the phrase "Why? This simply should not happen!". The true reasons why people continue to be interested in this topic may be different, one of them may be laziness. In the case when it is lazy or expensive to check everything and everything, and exceptional situations are extremely rare, you can, without complicating the code, wrap the potentially falling pieces of code in some try/ catchwhich will allow you to beautifully minimize the application or even recover and continue to work as if nothing had happened. The most abnormal thing just might seem to be the desire to catch again and again the errors that usually lead to the crash of the application, process them and continue to work.


    So let's try to create something to solve the problem of processing SIGSEGV-like errors. The solution should be cross-platform to the maximum, work on all the most common desktop and mobile platforms in single-threaded and multi-threaded environments. We also make possible the existence of nested try/ catchsections. We will process the following types of exceptional situations: access to memory at incorrect addresses, executing invalid instructions, and dividing by zero. The apotheosis will be that the hardware exceptions that have occurred will turn into regular C ++ exceptions.


    Most often, to solve similar tasks, it is recommended to use POSIX signals on not Windows systems, but on Windows Structured Exception Handling (SEH). We will do something like this, but instead of SEH we will use Vectored Exception Handling (VEH), which are often deprived of attention. In general, according to Microsoft, VEH is an extension of SEH, i.e. something more functional and modern. VEH is somewhat similar to POSIX signals, in order to start catching any events, the handler must be registered. However, unlike signals for VEH, several handlers can be registered, which will be called in turn until one of them processes the event.


    In addition to the signal processors, we will adopt a pair of setjmp/ longjmp, which will allow us to return to where we want after an emergency and in any way handle this very exceptional situation. Also, for our craft to work in multi-threaded environments, we need the good old thread local storage (TLS), which is also available in all the environments we are interested in.


    The simplest thing to do to just not fall in the event of an emergency is to write your handler and register it. In most cases, people just need to collect the necessary amount of information and beautifully collapse the application. One way or another, the signal processor is registered in a known manner. For POSIX-compatible systems, this is as follows:


    stack_t ss;
    ss.ss_sp = exception_handler_stack;
    ss.ss_flags = 0;
    ss.ss_size = SIGSTKSZ;
    sigaltstack(&ss, 0);
    struct sigaction sa;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_ONSTACK;
    sa.sa_handler = signalHandler;
    for (int signum : handled_signals)
        sigaction(signum, &sa, &prev_handlers[signum - MIN_SIGNUM]);

    The above code snippet above registers a handler for the following signals: SIGBUS, SIGFPE, SIGILL, SIGSEGV. In addition, using a call sigaltstack, it is indicated that the signal handler should be launched on an alternative stack of its own. This allows the application to survive even in stack overflow conditions, which can easily occur in the case of infinite recursion. If you do not specify an alternative stack, then this kind of error cannot be processed, the application will simply crash, because to call and execute the handler, there simply will not be a stack and nothing can be done with it. Pointers to previously registered handlers are also stored, which will allow them to be called if our handler understands that he has nothing to do.


    For Windows, the code is much shorter:


    exception_handler_handle = AddVectoredExceptionHandler(1, vectoredExceptionHandler);

    There is only one handler, it catches all events at once (not only hardware exceptions must be said) and there is no way to do anything with the stack like in Linux, for example. The unit supplied by the first argument to the function AddVectoredExceptionHandlerindicates that our handler should be called first, before any other existing ones. This gives us a chance to be the first and take the necessary actions.


    The handler itself for POSIX systems is as follows:


    static void signalHandler(int signum)
    {
        if (execution_context) {
            sigset_t signals;
            sigemptyset(&signals);
            sigaddset(&signals, signum);
            sigprocmask(SIG_UNBLOCK, &signals, NULL);
            reinterpret_cast(static_cast(execution_context))->exception_type = signum;
            longjmp(execution_context->environment, 0);
        }
        else if (prev_handlers[signum - MIN_SIGNUM].sa_handler) {
            prev_handlers[signum - MIN_SIGNUM].sa_handler(signum);
        }
        else {
            signal(signum, SIG_DFL);
            raise(signum);
        }
    }

    I must say that in order for our signal processor to become reusable, i.e. could be called again and again in the event of new errors, we must unlock the triggered signal at each call. This is necessary in cases where the handler knows that an exception has occurred in a section of code that is wrapped in some try/ catchwhich will be discussed later. If the emergency happened where we did not expect it at all, the cases will be transferred to the previously registered signal handler, if there is none, then the default handler is called, which will end the application that is crashing.


    The handler for Windows is as follows:


    static LONG WINAPI vectoredExceptionHandler(struct _EXCEPTION_POINTERS *_exception_info)
    {
        if (!execution_context ||
            _exception_info->ExceptionRecord->ExceptionCode == DBG_PRINTEXCEPTION_C ||
            _exception_info->ExceptionRecord->ExceptionCode == 0xE06D7363L /* C++ exception */
        )
            return EXCEPTION_CONTINUE_SEARCH;
        reinterpret_cast(static_cast(execution_context))->dirty = true;
        reinterpret_cast(static_cast(execution_context))->exception_type = _exception_info->ExceptionRecord->ExceptionCode;
        longjmp(execution_context->environment, 0);
    }

    As mentioned above, the VEH handler on Windows catches a lot of other things besides hardware exceptions. For example, a call throws OutputDebugStringan exception with a code DBG_PRINTEXCEPTION_C. We will not handle such events and simply return EXCEPTION_CONTINUE_SEARCH, which will lead to the OS going to look for the next handler that will handle this event. Also, we do not want to handle C ++ exceptions that correspond to magic code that 0xE06D7363Ldoes not have a normal name.


    Both on POSIX-compatible systems and on Windows, a handler is called at the end longjmp, which allows us to return up the stack to the very beginning of the section tryand bypass it once in a branch catchwhere you can do everything necessary to restore the operation and continue working as as if nothing terrible had happened.


    In order for ordinary C ++ to trystart catching unusual situations that are not peculiar to it, it is necessary to place a small macro at the very beginning HW_TO_SW_CONVERTER:


    #define HW_TO_SW_CONVERTER_UNIQUE_NAME(NAME, LINE) NAME ## LINE
    #define HW_TO_SW_CONVERTER_INTERNAL(NAME, LINE) ExecutionContext HW_TO_SW_CONVERTER_UNIQUE_NAME(NAME, LINE); if (setjmp(HW_TO_SW_CONVERTER_UNIQUE_NAME(NAME, LINE).environment)) throw HwException(HW_TO_SW_CONVERTER_UNIQUE_NAME(NAME, LINE))
    #define HW_TO_SW_CONVERTER() HW_TO_SW_CONVERTER_INTERNAL(execution_context, __LINE__)

    It looks pretty curly, but in fact a very simple thing is done here:


    1. Called setjmp, which allows us to remember the place where we started and where we need to return in the event of an accident.
    2. If a hardware exception occurred along the execution path, it setjmpwill return a non-zero value after it was called somewhere along the path longjmp. This will cause a C ++ exception to be thrown of type HwException, which will contain information about what kind of error occurred. The thrown exception without problems is caught by the standard catch.

    To simplify the above macro, it is expanded into the following pseudocode:


    if (setjmp(environment))
        throw HwException();

    The setjmp/ approach longjmphas one major drawback. In the case of ordinary C ++ exceptions, the stack is unwound at which the destructors of all objects created along the path are called. In the case with, longjmpwe immediately jump to the starting position, no unwinding of the stack occurs. This imposes appropriate restrictions on the code that is inside such sections try, you cannot allocate any resources there because there is a risk of losing them forever, which will lead to leaks.


    Another limitation is that it setjmpcannot be used in functions / methods declared as inline. This is a limitation of itself setjmp. In the best case, the compiler will simply refuse to collect such code, in the worst it will collect it, but the resulting binary will simply crash.


    The most abnormal action to take after handling a hardware exception on Windows is to call RemoveVectoredExceptionHandler. If this is not done, then after each entry into our VEH handler and execution longjmpthere will be a situation as if our handler was registered one more time. This leads to the fact that with each subsequent emergency, the handler will be called more and more times in a row, which will lead to disastrous consequences. This solution was found exclusively through numerous magical experiments and has not been documented anywhere.


    In order for the solution to work in multi-threaded environments, it is necessary that each thread has its own place where you can save the execution context with setjmp. For these purposes, TLS is used, in the use of which there is nothing tricky.


    The execution context itself is designed as a simple class having the following constructor and destructor:


    ExecutionContext::ExecutionContext() : prev_context(execution_context)
    {
    #if defined(PLATFORM_OS_WINDOWS)
        dirty = false;
    #endif
        execution_context = this;
    }
    ExecutionContext::~ExecutionContext()
    {
    #if defined(PLATFORM_OS_WINDOWS)
        if (execution_context->dirty)
            RemoveVectoredExceptionHandler(exception_handler_handle);
    #endif
        execution_context = execution_context->prev_context;
    }

    This class has a field prev_contextthat allows us to create chains from nested sections try/ catch.


    A full listing of the product described above is available on GitHub:
    https://github.com/kutelev/hwtrycatch


    To prove that everything works as described, there is an automatic assembly and tests for the Windows, Linux, Mac OS X and Android platforms:


    https://ci.appveyor.com/project/kutelev/hwtrycatch
    https://travis-ci.org/kutelev/hwtrycatch


    Under iOS, this also works, but for lack of a device for testing, there are no automatic tests.


    In conclusion, we can say that a similar approach can be used in regular C. You just need to write a few macros that will simulate the work of try/ catchfrom C ++.


    It is also worth saying that the use of the described methods in most cases is a very bad idea, especially when you consider that at the signal level it is impossible to find out what led to SIGSEGVor SIGBUS. This is equally likely to be as reading at the wrong addresses or writing. If reading at arbitrary addresses is not a destructive operation, then writing can lead to disastrous results such as breaking the stack, heap, or even the code itself.

    Read Next