Exception handling in rustc_codegen_cranelift
Panics in Rust by default unwind to run Drop
implementations for all values on the stack. Rust's stack unwinding is implemented using the same mechanism as C++ exceptions on most platforms. Recently the Cranelift code generator got support for "landingpad" style exceptions as detailed in the corresponding Cranelift RFC allowing me to add support for unwinding on panics to rustc_codegen_cranelift (cg_clif) for Unix systems.
In this article, I'll show you how exception handling works in cg_clif by walking you through several layers of the compilation process all the way to the runtime behavior.
Setup
We will use the following example to illustrate the various cases that commonly occur:
// For do_catch
#![feature(rustc_attrs, core_intrinsics)]
#![allow(internal_features)]
struct Droppable;
impl Drop for Droppable {
fn drop(&mut self) {}
}
// Unwind without running any drops
#[no_mangle]
fn do_panic() {
std::panic::panic_any(());
}
// Unwind while running a drop on the cleanup path
#[no_mangle]
fn some_func() {
let _a = Droppable;
do_panic();
}
// Catch a panic
#[no_mangle]
fn do_catch_panic() {
// This has a simplified version of std::panic::catch_unwind inlined for ease of understanding
unsafe {
if std::intrinsics::catch_unwind(do_call, 0 as *mut _, do_catch) == 0 {
std::process::abort(); // unreachable
} else {
// Caught panic
};
}
#[inline]
fn do_call(_data: *mut u8) {
some_func();
}
#[inline]
#[rustc_nounwind] // `intrinsic::catch_unwind` requires catch fn to be nounwind
fn do_catch(_data: *mut u8, _panic_payload: *mut u8) {}
}
fn main() {
do_catch_panic();
}
Let's first compile this using a version of cg_clif
with unwinding enabled:
dist/rustc-clif panic_example.rs -Cdebuginfo=2 --emit link,mir,llvm-ir
This command enables debuginfo, and emits three artifacts: link
emits the normal executable, mir
emits MIR, and llvm-ir
is repurposed with cg_clif
to emit Cranelift IR (clif ir for short). In any case with the executable now compiled, let's run it in a debugger:
$ gdb ./panic_example
Reading symbols from ./panic_example...
We begin by setting a breakpoint in do_panic
:
(gdb) break do_panic
Breakpoint 1 at 0x38794: file panic_example.rs, line 13.
And run the program:
(gdb) run
Downloading separate debug info for system-supplied DSO at 0xfffff7ffb000
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/aarch64-linux-gnu/libthread_db.so.1".
Breakpoint 1, panic_example::do_panic () at panic_example.rs:13
13 std::panic::panic_any(());
(gdb) backtrace
#0 panic_example::do_panic () at panic_example.rs:13
#1 0x0000aaaaaaad87b4 in panic_example::some_func () at panic_example.rs:20
#2 0x0000aaaaaaad8868 in panic_example::do_catch_panic::do_call () at panic_example.rs:37
#3 0x0000aaaaaaad8820 in panic_example::do_catch_panic () at panic_example.rs:28
#4 0x0000aaaaaaad888c in panic_example::main () at panic_example.rs:46
[...]
And we hit a breakpoint at the panic!()
. To learn more about Rust's panic infrastructure, read @FractalFir's "Implementation of Rust panics in the standard library" post. Here we'll skip past that and set a breakpoint on _Unwind_RaiseException
, which is the function that starts unwinding the stack.
(gdb) break _Unwind_RaiseException
Breakpoint 2 at 0xfffff7f975e8: file ../../../src/libgcc/unwind.inc, line 93
(gdb) continue
Continuing.
thread 'main' panicked at panic_example.rs:13:5:
Box<dyn Any>
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Breakpoint 2, _Unwind_RaiseException (exc=0xaaaaaace1ce0) at ../../../src/libgcc/unwind.inc:93
warning: 93 ../../../src/libgcc/unwind.inc: No such file or directory
(gdb) bt
#0 _Unwind_RaiseException (exc=0xaaaaaace1ce0) at ../../../src/libgcc/unwind.inc:93
#1 0x0000aaaaaabde42c in panic_unwind::imp::panic () at library/panic_unwind/src/gcc.rs:72
#2 0x0000aaaaaabdde3c in panic_unwind::__rust_start_panic () at library/panic_unwind/src/lib.rs:103
#3 0x0000aaaaaaae977c in std::panicking::rust_panic () at library/std/src/panicking.rs:894
#4 0x0000aaaaaaae959c in std::panicking::rust_panic_with_hook () at library/std/src/panicking.rs:858
#5 0x0000aaaaaaad7bcc in std::panicking::begin_panic::{closure#0}<()> () at /home/gh-bjorn3/cg_clif/build/stdlib/library/std/src/panicking.rs:770
#6 0x0000aaaaaaad7b20 in std::sys::backtrace::__rust_end_short_backtrace<std::panicking::begin_panic::{closure_env#0}<()>, !> ()
at /home/gh-bjorn3/cg_clif/build/stdlib/library/std/src/sys/backtrace.rs:168
#7 0x0000aaaaaaad7b70 in std::panicking::begin_panic<()> () at /home/gh-bjorn3/cg_clif/build/stdlib/library/std/src/panicking.rs:769
#8 0x0000aaaaaaad7b4c in std::panic::panic_any<()> () at /home/gh-bjorn3/cg_clif/build/stdlib/library/std/src/panic.rs:260
#9 0x0000aaaaaaad87a0 in panic_example::do_panic () at panic_example.rs:13
#10 0x0000aaaaaaad87b4 in panic_example::some_func () at panic_example.rs:20
#11 0x0000aaaaaaad8868 in panic_example::do_catch_panic::do_call () at panic_example.rs:37
#12 0x0000aaaaaaad8820 in panic_example::do_catch_panic () at panic_example.rs:28
#13 0x0000aaaaaaad888c in panic_example::main () at panic_example.rs:46
[...]
We can validate that the exception is in fact a Rust exception by running:
(gdb) print exc
$1 = (struct _Unwind_Exception *) 0xaaaaaace1ce0
(gdb) print *exc
$2 = {exception_class = 6076294132934528845, exception_cleanup = 0xaaaaaabde440 <panic_unwind::imp::panic::exception_cleanup>, private_1 = 0, private_2 = 0}
(gdb) print (char[8])(exc.exception_class)
$3 = "MOZ\000RUST"
That looks a lot like a Rust exception to me. The rest of the exception data is located directly after the _Unwind_Exception
struct.
Unwinding ABI crash course
There are nowadays two major unwinder ABIs still in use for C++ exceptions and Rust panics. These are:
- SEH (Structured Exception Handling) on Windows
- Itanium unwinding ABI (originating from the infamous Intel cpu architecture) on most Unix systems.
SEH and Itanium unwinding have a similar architecture: there is a table that indicates, for each instruction from which an exception may be thrown, which registers need to be restored to unwind the stack to the caller as well as contains a reference to a function (the so called personality function) which interprets a language-specific data format and a reference to some data in this format (called LSDA or language-specific data area for Itanium unwinding).
In most cases there is a single personality function for each language. Rust generally1 uses rust_eh_personality
as personality function. For the LSDA, Rust uses the exact same format as GCC and Clang use for C++ despite not needing half its features because LLVM doesn't allow frontends to specify a custom format.
Diagram showing how the various unwind tables interact with each other and with the machine code
Both SEH and Itanium unwinding implement two-phase unwinding. In other words, they first do a scan over the stack to see if any function catches the exception (phase one) before actually unwinding (phase two). For this in the first phase SEH and Itanium unwinders call the personality function to check if there is a catch for the exception around the given call site. To do this, the personality function parses the LSDA looking up the entry for the current instruction pointer.
In the second phase the personality function is called again and this time it is given the chance to divert execution to an exception handler. In the case of SEH this exception handler is a so-called "funclet": a function which gets the stack pointer of the stack frame currently being unwound as argument, and unwinding resumes when this funclet returns.
For Itanium unwinding on the other hand, execution gets diverted to a "landingpad" which runs in the context of the stack frame being unwound. Unwinding either resumes when the landingpad calls _Unwind_Resume
or in the case of a catch, the landingpad just continues execution as usual.
With the SEH method, all stack frames remain on the stack until unwinding has finished. It is also possible to unwind without removing any stack frames. Itanium unwinding instead removes each stack frame from the stack after it has been unwound, so effectively throwing an exception is an alternative return of the function. Cranelift currently only supports unwinding mechanisms that use landingpads, which is why cg_clif doesn't support unwinding on Windows.
While dwarfdump can be used to show part of the unwind info in a human-readable way, I'm not aware of any tool that is capable of showing the entire unwind info in a human-readable way: dwarfdump does not parse the LSDA, and there is no option to interleave assembly instructions and unwind instructions. As such I wrote my own tool for this, which I will use to show how exactly the unwinder sees our functions:
$ git clone https://github.com/bjorn3/rust_unwind_inspect.git
$ cd rust_unwind_inspect
$ cargo build
$ cp target/debug/rust_unwind_inspect ../
Unwinding without exception handlers
Now on to showing how Itanium unwinding support is actually implemented in cg_clif. I'm going to skip ahead to the second phase of the unwinding process -- the actual unwinding -- for the sake of simplicity.
Let's start with the do_panic
function:
//- panic_example.mir
// [snip]
fn do_panic() -> () {
let mut _0: ();
let _1: !;
bb0: {
_1 = panic_any::<()>(const ()) -> unwind continue;
}
}
// [snip]
This is a simple function which consists of nothing other than a panic_any
call which never returns, and when it unwinds, it continues to the caller.
;- panic_example.clif/do_panic.unopt.clif
function u0:28() system_v {
gv0 = symbol colocated userextname0
sig0 = (i64) system_v
fn0 = colocated u0:6 sig0 ; Instance { def: Item(DefId(1:5518 ~ std[a023]::panic::panic_any)), args: [()] }
block0:
jump block1
block1:
; _1 = std::panic::panic_any::<()>(const ())
v0 = global_value.i64 gv0
call fn0(v0)
trap user1
}
Nothing too exciting here. do_panic
gets lowered to a regular call of panic_any
. The argument is an implicit argument of type &std::panic::Location
because panic_any
is marked with #[track_caller]
. When unwinding out of a call
clif ir instruction, this will continue unwinding out of the current function. rust_unwind_inspect
shows the following:
$ ./rust_unwind_inspect panic_example do_panic
000000000003878c <do_panic>:
personality: 0x38db0 <rust_eh_personality+0x0>
LSDA: 0x1fd900 <.gcc_except_table+0x1e4>
0x3878c: stp x29, x30, [sp, #-0x10]!
CFA=SP+0x10 X29=Offset(-16) X30=Offset(-8)
0x38790: mov x29, sp
0x38794: adrp x0, #0x235000
0x38798: ldr x0, [x0, #0x578]
0x3879c: bl #0x37b40
call site 0x3879f..0x387a0 action=continue
Here personality and LSDA are as explained in the previous section. The CFA=SP+0x10 X29=Offset(-16) X30=Offset(-8)
line tells us that
- The
CFA
isSP+0x10
X29
can be found at offset -16 fromCFA
X30
can be found at offset -8 fromCFA
This information is all coming from the language independent half of the unwind tables which is found in .eh_frame
. This is what the unwinder itself parses. In addition there is a line call site 0x317a3..0x317a4 action=continue
which indicates that the previous instruction is a call which, if it throws an exception, should cause unwinding to continue to the caller of do_panic
. This information comes from the LSDA found in .gcc_except_table
at offset 0x1e0. If no call site is found for a call that threw an exception, the personality function will indicate to the unwinder that unwinding should abort.
Now to see it in action in the debugger:
First we define a macro that allows us to set a breakpoint for the personality function getting executed for a given call site:
(gdb) define break_on_personality_for
set language c
b rust_eh_personality if ((struct _Unwind_Context *)$x4).ra == $arg0
set language auto
end
And now we can set a breakpoint for do_panic
and continue:
(gdb) break_on_personality_for panic_example::do_panic+20
Breakpoint 4 at 0xaaaaaaad8dbc: file library/std/src/sys/personality/gcc.rs, line 307.
(gdb) continue
Continuing.
Breakpoint 4, std::sys::personality::gcc::rust_eh_personality () at library/std/src/sys/personality/gcc.rs:307
307 rust_eh_personality_impl(
(gdb) up
#1 0x0000fffff7f972d8 in _Unwind_RaiseException_Phase2 (exc=exc@entry=0xaaaaaace1ce0, context=context@entry=0xffffffffe9f0, frames_p=frames_p@entry=0xffffffffe628)
at ../../../src/libgcc/unwind.inc:64
warning: 64 ../../../src/libgcc/unwind.inc: No such file or directory
(gdb) print context.ra
$4 = (void *) 0xaaaaaaad87a0 <panic_example::do_panic+20>
(gdb) down
#0 std::sys::personality::gcc::rust_eh_personality () at library/std/src/sys/personality/gcc.rs:307
307 rust_eh_personality_impl(
And to show the return value:
(gdb) finish
Run till exit from #0 std::sys::personality::gcc::rust_eh_personality () at library/std/src/sys/personality/gcc.rs:307
_Unwind_RaiseException_Phase2 (exc=exc@entry=0xaaaaaace1ce0, context=context@entry=0xffffffffe9f0, frames_p=frames_p@entry=0xffffffffe628) at ../../../src/libgcc/unwind.inc:66
warning: 66 ../../../src/libgcc/unwind.inc: No such file or directory
Value returned is $5 = "\b\000\000"
(gdb) print (_Unwind_Reason_Code)$x0
$6 = _URC_CONTINUE_UNWIND
We had to explicitly read the return value from register x0 because cg_clif currently doesn't emit debuginfo for arguments and return types. We also had to use ((struct _Unwind_Context *)$x4).ra == $arg0
as condition for the breakpoint for this reason.
Unwinding with an exception handler
More exciting is the case where there is an exception handler in scope like our some_func
function.
//- panic_example.mir
// [snip]
fn some_func() -> () {
let mut _0: ();
let _1: Droppable;
let _2: ();
scope 1 {
debug _a => const Droppable;
}
bb0: {
_2 = do_panic() -> [return: bb1, unwind: bb3];
}
bb1: {
drop(_1) -> [return: bb2, unwind continue];
}
bb2: {
return;
}
bb3 (cleanup): {
drop(_1) -> [return: bb4, unwind terminate(cleanup)];
}
bb4 (cleanup): {
resume;
}
}
// [snip]
This function first calls do_panic
and then, no matter if it unwinds or not, it runs the drop glue for the Droppable
value in _a
. If the drop glue unwinds when called within the unwind path, the function will abort, otherwise it will unwind. And finally if the drop glue succeeds within the unwind path, unwinding will resume thanks to the resume
terminator.
; panic_example.clif/some_func.unopt.clif
function u0:29() system_v {
sig0 = () system_v
sig1 = (i64) system_v
sig2 = (i64) system_v
sig3 = () system_v
sig4 = (i64) system_v
fn0 = colocated u0:28 sig0 ; Instance { def: Item(DefId(0:7 ~ panic_example[4533]::do_panic)), args: [] }
fn1 = colocated u0:14 sig1 ; Instance { def: DropGlue(DefId(2:3040 ~ core[390d]::ptr::drop_in_place), Some(Droppable)), args: [Droppable] }
fn2 = colocated u0:14 sig2 ; Instance { def: DropGlue(DefId(2:3040 ~ core[390d]::ptr::drop_in_place), Some(Droppable)), args: [Droppable] }
fn3 = u0:47 sig3 ; "_ZN4core9panicking16panic_in_cleanup17hda9d23801310caf7E"
fn4 = u0:36 sig4 ; "_Unwind_Resume"
block0:
jump block1
block1:
; _2 = do_panic()
try_call fn0(), sig0, block6, [ tag0: block7(exn0) ]
block7(v0: i64) cold:
v4 -> v0
jump block4
block6:
jump block2
block2:
; drop(_1)
v1 = iconst.i64 1
call fn1(v1) ; v1 = 1
jump block3
block3:
return
block4 cold:
; drop(_1)
v2 = iconst.i64 1
try_call fn2(v2), sig2, block5, [ tag0: block9(exn0) ] ; v2 = 1
block9(v3: i64) cold:
; panic _ZN4core9panicking16panic_in_cleanup17hda9d23801310caf7E
call fn3()
trap user1
block5 cold:
; lib_call _Unwind_Resume
call fn4(v4)
trap user1
}
This is the clif ir produced for some_func
. The do_panic
call gets lowered to a try_call
rather than a regular call
because this time we want to divert execution to another code path in case of unwinding. In the try_call fn0(), sig0, block6, [ tag0: block7(exn0) ]
, block6
is where execution continues if the call returns normally, while block7
is where execution will continue when unwinding. The (exn0)
part indicates that block7
will get the first register set by the personality function as a block argument. In the case of Rust, this will be a pointer to the exception itself. Other languages may use additional "landingpad arguments". The tag0:
part is some opaque metadata that Cranelift will forward to cg_clif together with the position of all call sites and landingpads. cg_clif uses tag0
to indicate a cleanup block and tag1
to indicate that an exception should be caught. Once all cleanup code has run, a call to _Unwind_Resume
will be made with the exception pointer as argument to resume unwinding. _Unwind_Resume
will pop the stack frame of the caller and then continue unwinding as usual.
$ ./unwind_inspect/target/debug/rust_unwind_inspect ./panic_example some_func
00000000000387a4 <some_func>:
personality: 0x38db0 <rust_eh_personality+0x0>
LSDA: 0x1fd910 <.gcc_except_table+0x1f4>
0x387a4: stp x29, x30, [sp, #-0x10]!
CFA=SP+0x10 X29=Offset(-16) X30=Offset(-8)
0x387a8: mov x29, sp
0x387ac: str x20, [sp, #-0x10]!
CFA=X29+0x10 X29=Offset(-16) X30=Offset(-8) X20=Offset(-32)
0x387b0: bl #0x3878c
call site 0x387b3..0x387b4 landingpad=0x387c8 action=continue
0x387b4: mov x0, #1
0x387b8: bl #0x37dc0
call site 0x387bb..0x387bc action=continue
0x387bc: ldr x20, [sp], #0x10
0x387c0: ldp x29, x30, [sp], #0x10
0x387c4: ret
0x387c8: mov x20, x0
0x387cc: mov x0, #1
0x387d0: bl #0x37dc0
call site 0x387d3..0x387d4 landingpad=0x387e8 action=continue
0x387d4: adrp x1, #0x23f000
0x387d8: ldr x1, [x1, #0xdc8]
0x387dc: mov x0, x20
0x387e0: blr x1
call site 0x387e3..0x387e4 action=continue
Our do_panic
call has call site 0x387b3..0x387b4 landingpad=0x387c8 action=continue
as unwind info. This indicates that if the call unwinds, execution should jump to address 0x387c8
. The Rust personality function will also set x0
(aka exn0
in clif ir) to the exception pointer.
In the debugger we see:
(gdb) break_on_personality_for panic_example::some_func+16
Breakpoint 5 at 0xaaaaaaad8dbc: file library/std/src/sys/personality/gcc.rs, line 307.
(gdb) continue
Breakpoint 5, std::sys::personality::gcc::rust_eh_personality () at library/std/src/sys/personality/gcc.rs:307
307 rust_eh_personality_impl(
(gdb) up
#1 0x0000fffff7f972d8 in _Unwind_RaiseException_Phase2 (exc=exc@entry=0xaaaaaace1ce0, context=context@entry=0xffffffffe9f0, frames_p=frames_p@entry=0xffffffffe628)
at ../../../src/libgcc/unwind.inc:64
warning: 64 ../../../src/libgcc/unwind.inc: No such file or directory
(gdb) print context.ra
$7 = (void *) 0xaaaaaaad87b4 <panic_example::some_func+16>
down
#0 std::sys::personality::gcc::rust_eh_personality () at library/std/src/sys/personality/gcc.rs:307
307 rust_eh_personality_impl(
We got to the personality function call for some_func
. Now let's set a couple of breakpoints to see how the personality function causes execution to jump to the landingpad:
(gdb) break _Unwind_SetGR
Breakpoint 6 at 0xfffff7f9494c: file ../../../src/libgcc/unwind-dw2.c, line 275.
(gdb) break _Unwind_SetIP
Breakpoint 7 at 0xfffff7f949e0: file ../../../src/libgcc/unwind-dw2.c, line 369.
_Unwind_SetGR
and _Unwind_SetIP
are functions called by the personality function to tell the unwinder how to run the landingpad.
(gdb) continue
Breakpoint 6, _Unwind_SetGR (context=0xffffffffe9f0, index=0, val=187649986796768) at ../../../src/libgcc/unwind-dw2.c:275
warning: 275 ../../../src/libgcc/unwind-dw2.c: No such file or directory
(gdb) print *(struct _Unwind_Exception *)val
$8 = {exception_class = 6076294132934528845, exception_cleanup = 0xaaaaaabde440 <panic_unwind::imp::panic::exception_cleanup>, private_1 = 0, private_2 = 281474976706176}
(gdb) continue
Breakpoint 6, _Unwind_SetGR (context=0xffffffffe9f0, index=1, val=0) at ../../../src/libgcc/unwind-dw2.c:275
275 in ../../../src/libgcc/unwind-dw2.c
The first thing the personality function does is use _Unwind_SetGR
to set the aformentioned "landingpad arguments". x0 is set to the exception pointer, while x1 is set to zero. The latter isn't needed for cg_clif, but cg_llvm generates landingpads that take an additional i32 argument even though it doesn't do anything with it. I believe C++ uses it for the exception type. I suspect at some point LLVM didn't handle landingpads which are missing this extra argument.
(gdb) continue
Breakpoint 7, _Unwind_SetIP (context=0xffffffffe9f0, val=187649984661448) at ../../../src/libgcc/unwind-dw2.c:369
369 in ../../../src/libgcc/unwind-dw2.c
(gdb) set $landingpad=val
Next up _Unwind_SetIP
is used to set the address of the landingpad. We save this address here to set a breakpoint on it later on.
(gdb) up 2
#2 0x0000aaaaaaad8dc0 in std::sys::personality::gcc::rust_eh_personality () at library/std/src/sys/personality/gcc.rs:307
307 rust_eh_personality_impl(
(gdb) finish
_Unwind_RaiseException_Phase2 (exc=exc@entry=0xaaaaaace1ce0, context=context@entry=0xffffffffe9f0, frames_p=frames_p@entry=0xffffffffe628) at ../../../src/libgcc/unwind.inc:66
warning: 66 ../../../src/libgcc/unwind.inc: No such file or directory
Value returned is $9 = "\a\000\000"
(gdb) p (_Unwind_Reason_Code)$x0
$10 = _URC_INSTALL_CONTEXT
The personality function returns _URC_INSTALL_CONTEXT
to indicate that there is a landingpad.
(gdb) break *$landingpad
Breakpoint 8 at 0xaaaaaaad87c8: file panic_example.rs, line 21.
(gdb) continue
Breakpoint 8, 0x0000aaaaaaad87c8 in panic_example::some_func () at panic_example.rs:21
21 }
(gdb) disassemble
Dump of assembler code for function panic_example::some_func:
0x0000aaaaaaad87a4 <+0>: stp x29, x30, [sp, #-16]!
0x0000aaaaaaad87a8 <+4>: mov x29, sp
0x0000aaaaaaad87ac <+8>: str x20, [sp, #-16]!
0x0000aaaaaaad87b0 <+12>: bl 0xaaaaaaad878c <panic_example::do_panic>
0x0000aaaaaaad87b4 <+16>: mov x0, #0x1 // #1
0x0000aaaaaaad87b8 <+20>: bl 0xaaaaaaad7dc0 <_ZN4core3ptr45drop_in_place$LT$panic_example..Droppable$GT$17hb62d62884fcb8d11E>
0x0000aaaaaaad87bc <+24>: ldr x20, [sp], #16
0x0000aaaaaaad87c0 <+28>: ldp x29, x30, [sp], #16
0x0000aaaaaaad87c4 <+32>: ret
=> 0x0000aaaaaaad87c8 <+36>: mov x20, x0
0x0000aaaaaaad87cc <+40>: mov x0, #0x1 // #1
0x0000aaaaaaad87d0 <+44>: bl 0xaaaaaaad7dc0 <_ZN4core3ptr45drop_in_place$LT$panic_example..Droppable$GT$17hb62d62884fcb8d11E>
0x0000aaaaaaad87d4 <+48>: adrp x1, 0xaaaaaacdf000
0x0000aaaaaaad87d8 <+52>: ldr x1, [x1, #3528]
0x0000aaaaaaad87dc <+56>: mov x0, x20
0x0000aaaaaaad87e0 <+60>: blr x1
0x0000aaaaaaad87e4 <+64>: udf #49439
0x0000aaaaaaad87e8 <+68>: adrp x3, 0xaaaaaacdf000
0x0000aaaaaaad87ec <+72>: ldr x3, [x3, #2688]
0x0000aaaaaaad87f0 <+76>: blr x3
0x0000aaaaaaad87f4 <+80>: udf #49439
End of assembler dump.
And finally if we set a breakpoint on the registered landingpad value and continue execution, we indeed see that execution jumped to the landingpad.
Catching an exception
And finally to finish it up, let's catch a panic using intrinsics::catch_unwind
:
fn do_catch_panic() -> () {
let mut _0: ();
let mut _1: i32;
let mut _2: fn(*mut u8);
let mut _3: *mut u8;
let mut _4: fn(*mut u8, *mut u8);
let _5: !;
bb0: {
_2 = do_catch_panic::do_call as fn(*mut u8) (PointerCoercion(ReifyFnPointer, Implicit));
_3 = const 0_usize as *mut u8 (PointerWithExposedProvenance);
_4 = do_catch_panic::do_catch as fn(*mut u8, *mut u8) (PointerCoercion(ReifyFnPointer, Implicit));
_1 = std::intrinsics::catch_unwind(move _2, copy _3, move _4) -> [return: bb1, unwind unreachable];
}
bb1: {
switchInt(move _1) -> [0: bb2, otherwise: bb3];
}
bb2: {
_5 = std::process::abort() -> unwind continue;
}
bb3: {
return;
}
}
catch_unwind
calls the first function pointer with the second argument as argument. If this function unwinds, it will call the second function pointer with the same argument and additionally the exception pointer. And finally it returns 1 if an exception was caught and 0 otherwise.
function u0:30() system_v {
sig0 = (i64) system_v
sig1 = (i64, i64) system_v
sig2 = (i64) system_v
sig3 = (i64, i64) system_v
sig4 = () system_v
fn0 = colocated u0:31 sig0 ; Instance { def: Item(DefId(0:10 ~ panic_example[4533]::do_catch_panic::do_call)), args: [] }
fn1 = colocated u0:32 sig1 ; Instance { def: Item(DefId(0:11 ~ panic_example[4533]::do_catch_panic::do_catch)), args: [] }
fn2 = u0:44 sig4 ; Instance { def: Item(DefId(1:6188 ~ std[a023]::process::abort)), args: [] }
block0:
jump block1
block1:
; _2 = do_catch_panic::do_call as fn(*mut u8) (PointerCoercion(ReifyFnPointer, Implicit))
v0 = func_addr.i64 fn0
; _3 = const 0_usize as *mut u8 (PointerWithExposedProvenance)
v1 = iconst.i64 0
; _4 = do_catch_panic::do_catch as fn(*mut u8, *mut u8) (PointerCoercion(ReifyFnPointer, Implicit))
v2 = func_addr.i64 fn1
; _1 = std::intrinsics::catch_unwind(move _2, copy _3, move _4)
try_call_indirect v0(v1), sig2, block5, [ tag1: block6(exn0) ] ; v1 = 0
block5:
v3 = iconst.i32 0
jump block2(v3) ; v3 = 0
block6(v4: i64) cold:
call_indirect.i64 sig3, v2(v1, v4) ; v1 = 0
v5 = iconst.i32 1
jump block2(v5) ; v5 = 1
block2(v6: i32):
; switchInt(move _1)
brif v6, block4, block3
block3 cold:
; _5 = std::process::abort()
call fn2()
trap user1
block4:
return
}
In Cranelift IR this is implemented using a try_call_indirect
with tag1
rather than tag0
for the cleanup block. And additionally it won't call _Unwind_Resume
in the end, but rather continue execution after the intrinsic call. In the unwind tables the exception catching is represented using:
$ cargo run -- ../panic_example do_catch_panic
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.04s
Running `target/debug/rust_unwind_inspect ../panic_example do_catch_panic`
00000000000387f8 <do_catch_panic>:
personality: 0x38db0 <rust_eh_personality+0x0>
LSDA: 0x1fd930 <.gcc_except_table+0x214>
LSDA actions:
0x0: catch 0x0 next=None
0x387f8: stp x29, x30, [sp, #-0x10]!
CFA=SP+0x10 X29=Offset(-16) X30=Offset(-8)
0x387fc: mov x29, sp
0x38800: stp x20, x22, [sp, #-0x10]!
CFA=X29+0x10 X29=Offset(-16) X30=Offset(-8) X20=Offset(-32) X22=Offset(-24)
0x38804: adrp x11, #0x235000
0x38808: ldr x11, [x11, #0x4b8]
0x3880c: mov x0, #0
0x38810: mov x22, x0
0x38814: adrp x20, #0x235000
0x38818: ldr x20, [x20, #0x4c0]
0x3881c: blr x11
call site 0x3881f..0x38820 landingpad=0x38838 action=0
0x38820: mov w8, #0
0x38824: mov w15, w8
0x38828: cbz x15, #0x3884c
0x3882c: ldp x20, x22, [sp], #0x10
0x38830: ldp x29, x30, [sp], #0x10
0x38834: ret
0x38838: mov x1, x0
0x3883c: mov x0, x22
0x38840: blr x20
call site 0x38843..0x38844 action=continue
0x38844: mov w8, #1
0x38848: b #0x38824
0x3884c: adrp x0, #0x23e000
0x38850: ldr x0, [x0, #0xab0]
0x38854: blr x0
call site 0x38857..0x38858 action=continue
where action=0
references the 0x0: catch 0x0 next=None
LSDA action. In C++ the 0x0
would instead be the typeid of the caught exception and next
optionally representing another catch
block for the same try
block.
And finally one last debugger step through for completeness. It is not much different from the some_func
step through, so I won't discuss it in detail.
(gdb) break_on_personality_for panic_example::do_catch_panic+40
Breakpoint 9 at 0xaaaaaaad8dbc: file library/std/src/sys/personality/gcc.rs, line 307.
(gdb) continue
Breakpoint 9, std::sys::personality::gcc::rust_eh_personality () at library/std/src/sys/personality/gcc.rs:307
307 rust_eh_personality_impl(
(gdb) up
#1 0x0000fffff7f97354 in _Unwind_RaiseException_Phase2 (exc=exc@entry=0xaaaaaace1ce0, context=context@entry=0xffffffffea90, frames_p=frames_p@entry=0xffffffffe6c8)
at ../../../src/libgcc/unwind.inc:64
warning: 64 ../../../src/libgcc/unwind.inc: No such file or directory
(gdb) print context.ra
$11 = (void *) 0xaaaaaaad8820 <panic_example::do_catch_panic+40>
(gdb) down
#0 std::sys::personality::gcc::rust_eh_personality () at library/std/src/sys/personality/gcc.rs:307
307 rust_eh_personality_impl(
(gdb) continue
Breakpoint 6, _Unwind_SetGR (context=0xffffffffea90, index=0, val=187649986796768) at ../../../src/libgcc/unwind-dw2.c:275
warning: 275 ../../../src/libgcc/unwind-dw2.c: No such file or directory
(gdb) print *(struct _Unwind_Exception *)val
$12 = {exception_class = 6076294132934528845, exception_cleanup = 0xaaaaaabde440 <panic_unwind::imp::panic::exception_cleanup>, private_1 = 0, private_2 = 281474976706176}
(gdb) continue
Breakpoint 6, _Unwind_SetGR (context=0xffffffffea90, index=1, val=0) at ../../../src/libgcc/unwind-dw2.c:275
275 in ../../../src/libgcc/unwind-dw2.c
(gdb) continue
Breakpoint 7, _Unwind_SetIP (context=0xffffffffea90, val=187649984661560) at ../../../src/libgcc/unwind-dw2.c:369
369 in ../../../src/libgcc/unwind-dw2.c
(gdb) set $landingpad=val
(gdb) up 2
#2 0x0000aaaaaaad8dc0 in std::sys::personality::gcc::rust_eh_personality () at library/std/src/sys/personality/gcc.rs:307
307 rust_eh_personality_impl(
(gdb) finish
_Unwind_RaiseException_Phase2 (exc=exc@entry=0xaaaaaace1ce0, context=context@entry=0xffffffffea90, frames_p=frames_p@entry=0xffffffffe6c8) at ../../../src/libgcc/unwind.inc:66
warning: 66 ../../../src/libgcc/unwind.inc: No such file or directory
Value returned is $13 = "\a\000\000"
(gdb) print (_Unwind_Reason_Code)$x0
$14 = _URC_INSTALL_CONTEXT
(gdb) break *$landingpad
Breakpoint 10 at 0xaaaaaaad8838: file panic_example.rs, line 43.
(gdb) continue
Breakpoint 10, 0x0000aaaaaaad8838 in panic_example::do_catch_panic () at panic_example.rs:43
43 }
(gdb) disassemble
Dump of assembler code for function panic_example::do_catch_panic:
0x0000aaaaaaad87f8 <+0>: stp x29, x30, [sp, #-16]!
0x0000aaaaaaad87fc <+4>: mov x29, sp
0x0000aaaaaaad8800 <+8>: stp x20, x22, [sp, #-16]!
0x0000aaaaaaad8804 <+12>: adrp x11, 0xaaaaaacd5000
0x0000aaaaaaad8808 <+16>: ldr x11, [x11, #1208]
0x0000aaaaaaad880c <+20>: mov x0, #0x0 // #0
0x0000aaaaaaad8810 <+24>: mov x22, x0
0x0000aaaaaaad8814 <+28>: adrp x20, 0xaaaaaacd5000
0x0000aaaaaaad8818 <+32>: ldr x20, [x20, #1216]
0x0000aaaaaaad881c <+36>: blr x11
0x0000aaaaaaad8820 <+40>: mov w8, #0x0 // #0
0x0000aaaaaaad8824 <+44>: mov w15, w8
0x0000aaaaaaad8828 <+48>: cbz x15, 0xaaaaaaad884c <panic_example::do_catch_panic+84>
0x0000aaaaaaad882c <+52>: ldp x20, x22, [sp], #16
0x0000aaaaaaad8830 <+56>: ldp x29, x30, [sp], #16
0x0000aaaaaaad8834 <+60>: ret
=> 0x0000aaaaaaad8838 <+64>: mov x1, x0
0x0000aaaaaaad883c <+68>: mov x0, x22
0x0000aaaaaaad8840 <+72>: blr x20
0x0000aaaaaaad8844 <+76>: mov w8, #0x1 // #1
0x0000aaaaaaad8848 <+80>: b 0xaaaaaaad8824 <panic_example::do_catch_panic+44>
0x0000aaaaaaad884c <+84>: adrp x0, 0xaaaaaacde000
0x0000aaaaaaad8850 <+88>: ldr x0, [x0, #2736]
0x0000aaaaaaad8854 <+92>: blr x0
0x0000aaaaaaad8858 <+96>: udf #49439
End of assembler dump.
Conclusion
We've now seen how exception handling works in cg_clif. Currently, this feature is still disabled by default because I'm still in the process of finishing the implementation and fixing a performance regression caused by enabling it. Follow the tracking issue to stay up to date!
Appendix
The following gdb script can be used to reproduce the debugger session:
set debuginfod enabled on
set pagination off
b do_panic
run
bt
b _Unwind_RaiseException
c
bt
p exc
p *exc
p (char[8])(exc.exception_class)
b _Unwind_RaiseException_Phase2
c
del 3
define break_on_personality_for
set language c
b rust_eh_personality if ((struct _Unwind_Context *)$x4).ra == $arg0
set language auto
end
echo \ndo_panic\n===========================\n
break_on_personality_for panic_example::do_panic+20
c
up
p context.ra
down
finish
p (_Unwind_Reason_Code)$x0
echo \nsome_func\n===========================\n
break_on_personality_for panic_example::some_func+16
c
up
p context.ra
down
b _Unwind_SetGR
b _Unwind_SetIP
c
p *(struct _Unwind_Exception *)val
c
c
set $landingpad=val
up 2
finish
p (_Unwind_Reason_Code)$x0
b *$landingpad
c
disassemble
echo \ndo_catch_panic\n===========================\n
break_on_personality_for panic_example::do_catch_panic+40
c
up
p context.ra
down
c
p *(struct _Unwind_Exception *)val
c
c
set $landingpad=val
up 2
finish
p (_Unwind_Reason_Code)$x0
b *$landingpad
c
disassemble