使用 libunwind 实现异常

Using libunwind for implementing exceptions

在编译器上工作,需要一些帮助来理解和使用 libunwind。这是我目前所拥有的:

#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);
  }
}

好吧,这已经很混乱了,但一些上下文可能有助于证明这些奇怪的选择是合理的。我想做的是在 foo 中调用 set_try 之后在调用堆栈中的任何一点调用 throw_exception 以展开堆栈并将 CPU 状态恢复到正确的状态在调用 set_try 之后但在有条件之前。虽然这目前只是一个小的 C 程序,但我打算在编译器中使用这些函数的一般结构,该编译器将生成必要的函数调用(类似于在 C++ 中使用 g++ 完成异常的方式),这就是为什么我有labels+goto 作为一种快速模拟我将生成的程序集的方法。我试过使用 libunwind 的 setjmp 实现,但它不太适合我的用例。

我遇到的问题与展开调用堆栈后 unw_resume 在哪里恢复有关。无论如何,printf("This should never run\n") 似乎每次都是 运行。我的理解是它应该将堆栈和 CPU 状态恢复到调用 unw_getcontext 时存储的任何状态,我认为存储的状态是正确的,因为 IP 寄存器的值(或 PC 寄存器,因为这是 x86_64) 在我调用 set_trythrow_exception 时光标中的完全相同。在调用 set_try 之后和条件之前,我什至跳入 gdb 多次查看 PC 寄存器,并且每次都与打印输出相匹配。

我的问题是:

提前致谢!

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.