Search code examples
cexceptionstack-unwindinglibunwind

Using libunwind for implementing exceptions


Working on a compiler, need some assistance understanding and working with libunwind. Here's what I have so far:

#define UNW_LOCAL_ONLY
#include <libunwind.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>

typedef void *data_t;
typedef struct exception_stack_st *exception_stack_t;

struct exception_stack_st {
  unw_cursor_t catch_block;
  exception_stack_t prev;
};

/* PROTOTYPES */
void foo(void);
void bar(void);
void set_try(void);
void clear_try(void);
bool check_exception(void);
void throw_exception(data_t);
void print_backtrace();

/* GLOBALS */
exception_stack_t exception_stack = NULL;
data_t curr_exception = NULL;

int main(void) {
  foo();
}

void foo(void) {
  printf("In foo\n");
  set_try();
  if(check_exception()) {
    printf("EXCEPTION: %s\n", (char*)curr_exception);
    goto CATCH;
  }
  bar();
  printf("This should never run\n");
 CATCH:
  clear_try();
  printf("Leaving foo\n");
}

void bar(void) {
  printf("In bar\n");
  throw_exception("Throwing an exception in bar");
  printf("Leaving bar\n");
}

void set_try(void) {
  unw_cursor_t cursor;
  unw_context_t context;
  unw_word_t ip, offp;
  char buf[1024];
  unw_getcontext(&context);
  unw_init_local(&cursor, &context);
  unw_step(&cursor);
  unw_get_reg(&cursor, UNW_REG_IP, &ip);
  unw_get_proc_name(&cursor, buf, 1024, &offp);
  printf("%s+0x%lx  IP %lx\n", buf, offp, ip);

  exception_stack_t cb = malloc(sizeof(struct exception_stack_st));
  cb->catch_block = cursor;
  cb->prev = exception_stack;
  exception_stack = cb;
}

void clear_try(void) {
  if (exception_stack != NULL)
    exception_stack = exception_stack->prev;
  curr_exception = NULL;
}

void throw_exception(data_t exception) {
  unw_word_t ip, offp;
  char buf[1024];
  curr_exception = exception;
  unw_get_reg(&(exception_stack->catch_block), UNW_REG_IP, &ip);
  unw_get_proc_name(&(exception_stack->catch_block), buf, 1024, &offp);
  printf("%s+0x%lx  IP %lx\n", buf, offp, ip);
  unw_resume(&(exception_stack->catch_block));
  printf("PANIC: unw_resume returned.\n");
  exit(1);
}

bool check_exception(void) {
  return curr_exception != NULL;
}

void print_backtrace() {
  unw_cursor_t cursor;
  unw_context_t context;
  char buf[1024];
  unw_getcontext(&context);
  unw_init_local(&cursor, &context);
  printf("BACKTRACE:\n");
  while(unw_step(&cursor) > 0) {
    unw_get_proc_name(&cursor, buf, 1024, NULL);
    printf("%s\n", buf);
  }
}

Alright, this is already pretty messy, but some context might help justify the weird choices. What I would like to do is call throw_exception at any point down the call stack after calling set_try in foo in order to unwind the stack and restore the CPU state to right after the call to set_try but before the conditional. While this is currently just a small C program, I'm intending on using the general structure of these functions in a compiler that will generate the function calls necessary (similar to how exceptions are done in C++ using g++), which is why I have the labels+goto as a way to quickly mimic the assembly I would be generating. I've tried using libunwind's setjmp implementation, but it doesn't quite fit my use case well enough.

The issue I'm having has to do with where unw_resume resumes after unwinding the call stack. The printf("This should never run\n") seems to run every time no matter what. My understanding is that it should restore the stack and CPU state to whatever was stored when the call to unw_getcontext was made, and I think the state that gets stored is correct because the value of the IP register (or PC register, since this is x86_64) is exactly the same in the cursor when I call set_try and throw_exception. I've even jumped into gdb a number of times to look at the PC register right after the call to set_try and before the conditional, and it matched the printed output every time.

My questions are:

  • Am I misunderstanding unw_resume?
  • Do I need to be modifying the PC (UNW_REG_IP) register?
  • Is there somewhere (aside from nongnu.org docs) else I can look to for help with libunwind?

Thanks in advance!


Solution

  • First the good news: after fixes, your program produces (what I assume expected) output:

    ./a.out
    In foo
    foo+0x31  IP 557615f273a1
    In bar
    foo+0x31  IP 557615f273a1
    foo+0x31  IP 557615f273a1
    EXCEPTION: Throwing an exception in bar
    Leaving foo
    

    Now the bad news: there are a few separate issues with your original program:

    1. The context (machine state) that was used to initialize the cursor must remain valid for the duration for which the cursor is used. This is explicitly spelled out in the unw_init_local man page.

      Violation of this rule caused your program to SIGSEGV on my machine during the unw_resume call.

    2. The unw_step updates the cursor, but not the context, and it is the latter that is actually used to restore machine state in unw_resume.

      This could be made clearer in the unw_resume documentation.

    To fix problem 1, simply move unw_context_t into exception_stack_st like so:

    struct exception_stack_st {
      unw_context_t context;
      unw_cursor_t cursor;
      exception_stack_t prev;
    };
    

    And initialize them together:

      exception_stack_t cb = malloc(sizeof(*cb));
      unw_getcontext(&cb->context);
      unw_init_local(&cb->cursor, &cb->context);
    

    To fix problem 2, you need to establish the machine context in the frame to which you intend to return.

    That requires either turning set_try into a macro, or an always inlined function, and getting rid of the unw_step.