Search code examples
cwindowsgccseh

Using Windows SEH and GCC label-as-value to jump to local error handler


I need to unit test some code where the failure cases raise an exception. In a nutshell, I need to handle the exception by either unwinding a stack frame or making a local jump over the error. Using MSVC is not an option.

The MSDN example for AddVectoredExceptionHandler indicates that I can modify eip and then return EXCEPTION_CONTINUE_EXECUTION to perform a local jump. The obvious question is what to what address to jump. GCC's Label as Value feature seems like it should be just the thing.

The example below works if there is a single return from the erroring function. However, if a second return statement is added, the offset of the jump is suspiciously small and the jump fails. Why?

#include <assert.h>
#include <stdbool.h>
#include <stdio.h>

#ifndef _WIN32_WINNT
#define _WIN32_WINNT 0x0502
#endif

#define WIN32_LEAN_AND_MEAN
#include <windows.h>

static bool handler_called;
static DWORD handler_eip;

static LONG WINAPI local_handler(struct _EXCEPTION_POINTERS* ExceptionInfo) {
   handler_called = true;
   PCONTEXT Context = ExceptionInfo->ContextRecord;
   Context->Eip = handler_eip;
   return EXCEPTION_CONTINUE_EXECUTION;
}

static void badcall(void) {
   handler_called = false;
   handler_eip = &&fail;

   int value = 100;
   int zero = 0;
   value = value/0;
   printf("not handled.\n");
   assert(false);
   //return; // Uncomment this to break the handler

   fail:
   printf("error handled.\n");
   return;
}

int main(int argc, char* argv[]) {
   void* old_handler = SetUnhandledExceptionFilter(&local_handler);
   badcall();
   SetUnhandledExceptionFilter(old_handler);
   return(0);
}

Solution

  • GCC's dead code elimination seems to be the culprit -- nothing to do with SEH. When the bottom block of the function is not reachable, those statements are eliminated. GCC then arbitrarily creates a label at the location where we took the address.

    Since we're manipulating program flow directly, the rules of C tell us that we're not allowed to complain when the compiler does something wacky like this.

    Here is a more minimal example:

    static void* handler_eip;
    static void badcall(void) {
       handler_eip = &&fail;
    
       int value = 100/0;
       printf("not handled.\n");
       //goto fail;
       return;
    
       fail:
       printf("error handled.\n");
       return;
    }

    Note the location of L2 with the goto in place:

    _badcall:
        pushl    %ebp
        movl    %esp, %ebp
        subl    $56, %esp
        movl    $L2, _handler_eip
        movl    $100, %eax
        movl    $0, -28(%ebp)
        movl    %eax, %edx
        sarl    $31, %edx
        idivl    -28(%ebp)
        movl    %eax, -12(%ebp)
        movl    $LC0, (%esp)
        call    _puts
        nop
    L2:
        movl    $LC1, (%esp)
        call    _puts
        nop
        leave
        ret

    And without:

    _badcall:
        pushl    %ebp
        movl    %esp, %ebp
        subl    $56, %esp
    L2:
        movl    $L2, _handler_eip
        movl    $100, %eax
        movl    $0, -28(%ebp)
        movl    %eax, %edx
        sarl    $31, %edx
        idivl    -28(%ebp)
        movl    %eax, -12(%ebp)
        movl    $LC0, (%esp)
        call    _puts
        nop
        leave
        ret