In my previous post I mentioned work on a new register allocator. Something that was missing from my Church-State project was error recovery. Most languages today use some form of try-catch exception handling (although the Go language uses deferred statements to perform cleanup).
The biggest challenge in implementing exception handling is that functions that use “callee-saved” registers may save these register values on the stack and then use them to store private values. Under normal control flow these values will be loaded from the stack and restored when the function returns. In the case of an exception, the runtime has to inspect each stack frame on the call stack between the point where the exception was thrown and the point where it will be caught and restore the values of these callee saved registers.
The implementation I chose was to emit a label for the function ‘epilogue’, the final sequence of statements before the return instruction, and store this in the object file. At exception handling time I have a routine implemented in assembly which will walk the stack and arrange to invoke this epilogue stub for each function (adjusting the stack pointer appropriately so that the function can find the saved values). Finally it jumps to the exception handler in the context of the function with the try-catch handler.
One complication arises in that it is not possible for variables that are used in the catch block to be stored temporarily in registers in the try block. The risk is that control is transferred out of the try block to the catch block and the catch block won’t know which register holds the current value. This can be avoided by treating every single statement that could raise an exception as having an extra control flow edge to the catch block. I implemented a simpler solution which was to mark all variables used in the catch block for stack allocation only. These variables are effectively permanently spilled to the stack.
The assembly routine to handle exceptions and restore callee saved values from the stack is pretty hairy but I’m quite happy with the solution.
7 ;; // Thread local storage has the following layout
8 ;; // (some other thread's heap) <------------ limit-pointer
9 ;; // heap
10 ;; // heap
11 ;; // heap
12 ;; // heap <------------ allocation-pointer
13 ;; // heap
14 ;; // heap
15 ;; // heap start of heap %r15
16 ;; // limit pointer -0x8 (%r15)
17 ;; // allocation pointer (initially points to start of heap) -0x10(%r15)
18 ;; // saved exception object -0x18(%r15)
19 ;; // saved return address when calling unwind stub -0x20(%r15)
20 ;; // saved frame pointer for exception trace -0x28(%r15)
21 ;; // working stack pointer to be used during stack trace traversal -0x30(%r15)
22 ;; // saved traversal stack pointer -0x38(%r15)
...
;; //--------------------------------------------------------------------------------
57 ;; // Exception handling
58 ;; //--------------------------------------------------------------------------------
59
60
61 ;; //transfer to exception handler
62 .globl __l_throw_exception
63
64
65 .macro switch_to_original_stack
66 mov %rsp, -0x30(%r15)
67 mov -0x38(%r15), %rsp
68 .endm
69
70 .macro switch_to_working_stack
71 mov %rsp, -0x38(%r15)
72 mov -0x30(%r15), %rsp
73 .endm
74
75 __l_throw_exception:
76 ;; // first argument is the exception object
77 ;; // save it in thread local memory
78 mov %rdi, -0x18(%r15)
79 ;; // also save the frame pointer so that we can generate a stack trace later if necessary
80 mov %rbp, -0x28(%r15)
81 ;; // and keep a pointer to a 'working stack' so that we call helper routines without destroying the original stack frames
82 mov %rsp, -0x30(%r15)
83 __find_exception_handler:
84 ;; // get the return address from the stack
85 ;; // and check if there is an exception handler installed around that address
86 mov (%rsp), %rdi
87 switch_to_working_stack
88 call __l_find_exception_handler
89 switch_to_original_stack
90 test %rax,%rax
91 jnz found_exception_handler
92 ;; // if there is no exception handler, unwind this frame by finding the unwind stub for this function
93 mov (%rsp), %rdi
94 switch_to_working_stack
95 call __l_find_unwind_stub
96 switch_to_original_stack
97 test %rax,%rax
98 jz no_unwind
99 ;; // pop off the return address (restore the stack to the calling function's expected value)
100 pop %rcx
101 ;; // get the return address of the previous frame and save it to thread local memory
102 mov 0x8(%rbp), %rdx
103 mov %rdx, -0x20(%r15)
104 ;; // do some magic to get the address of 'after_unwind_stub'
105 lea 0x6(%rip), %rcx
106 ;; // save it in place of the normal return address
107 mov %rcx, 0x8(%rbp)
108 ;; // branch to the unwind stub
109 jmp *%rax
110 after_unwind_stub:
111 ;; // now restore the real return address that we stored in thread local memory
112 mov -0x20(%r15), %rax
113 push %rax
114 ;; // repeat from the top until we find an exception handler
115 jmp __find_exception_handler
116
117 found_exception_handler:
118 ;; // load the exception object that we stored in thread local memory
119 mov -0x18(%r15), %rdi
120 ;; // pop old return address
121 pop %rcx
122 ;; // branch to exception handler
123 jmp *%rax
124
125 no_unwind:
126 no_exception_handler:
127 mov -0x28(%r15), %rdi
128 mov -0x18(%r15), %rsi
129 switch_to_working_stack
130 call __l_top_level_exception_handler
131 ;; // no return?
132 int $3
133