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_buf
longjmp
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