The gory details of an implementation
Why?
How?
setjmp/longjmp, as used by GCC where there's no other optionsetjmp/longjmp
Quick overview of semantics
jmp_buflongjmp causes a non-local transfer of control - restores registers from jmp_buf, including stack pointer and program countersetjmp, but now with the return value as specified by the longjmp calllongjmp non-local transfer
Dealing with cleanup: Maintain a runtime-stack of clean-ups and catch blocks
try sets up an exception handling "frame" with a jmp_buf embedded in itlongjmp
As implemented in FreeBSD 4.11 (c 2004)
As implemented in FreeBSD 4.11 (c 2004)
Note the happy path overhead ->
As implemented in FreeBSD 4.11 (c 2004)
What's wrong with it?
Affects the happy path in two distinct ways:
g, we need to keep track of what we need to clean upcatch block, we need to call setjmp, to get a copy of the machine state, and store it somewherejmp_buf is 200 bytes on amd64, 156 on i386
The compiler associates the table with the function in the generated assembler as a "Language Specific Data Area" (LSDA), and also associates a "personality routine" with the function that can interpret that table (i.e., __gxx_personality_v0 - more later)
Our table-based approach only tells us what to do for the local function. How do we find out where we are in g() when h() throws an exception?
| ... | |
| ESP | return-address back to g |
| ESP | "there" |
| ESP | "here" |
| ESP | return address back to f |
| ESP | arguments to g |
| ESP | local variables in f |
| ESP | return address back to main |
| ESP | arguments to f |
| ESP | main's local variables |
| ESP | return address back to C startup |
| ESP | argc |
| ESP | argv |
Use a register to point to the current stack frame: the "frame pointer"
On function entry push frame pointer, copy stack pointer to frame pointer, so new frame pointer points to old frame pointer
i386: ENTER and LEAVE instructions; use ebp for FP.
| ... | ||
| ESP | return-address back to g | |
| ESP | "there" | |
| ESP | "here" | |
| EBP | ESP | old EBP |
| EBP | ESP | return address back to f |
| EBP | ESP | arguments to g |
| EBP | ESP | local variables in f |
| EBP | ESP | old EBP |
| ESP | return address back to main | |
| ESP | arguments to f | |
| ESP | main's local variables | |
| EBP | ESP | old EBP |
| EBP | ESP | return address back to C startup |
| EBP | ESP | argc |
| EBP | ESP | argv |
| EBP | ESP | EBP somewhere down here |
Output from modern 32-bit compiler for our sample
Table Contents
Stack on entry to g:
Local stack space for g |
|
| ESP-> | return address into f |
| CFA ("previous frame ESP") -> | arguments into g |
| Addr | R0 | R1 | R2 | R3(rbx) | ... | CFA | RAR |
|---|---|---|---|---|---|---|---|
g |
- | - | - | - | ... | ESP+8 | *(CFA-8) |
g+1 |
- | - | - | *(CFA -16) | ... | ESP + 16 | *(CFA - 8) |
g+5 |
- | - | - | *(CFA -16) | ... | ESP + 32 | *(CFA - 8) |
| ... | |||||||
g+59 |
- | - | - | *(CFA -16) | ... | ESP + 16 | *(CFA - 8) |
g+60 |
- | - | - | *(CFA -16) | ... | ESP + 8 | *(CFA - 8) |
.eh_frame contains Frame Description Entrys (FDEs) for each function in the objectthrow to catch
.eh_frame
h calls __cxa_throw to actually raise the exception
__cxa_throw is part of the C++ runtime; stack unwinding is handled by _Unwind_RaiseException
First phase - Search - find the frame where execution will resume
Second phase - cleanup - handle the exception, cleaning up any stack resources.
finally, our personality routine: handle the C++ side
_Unwind* interface was originally documented for Itanium here.eh_frame format is an extension of what's specified by int the DWARF