Exception Handling in Rustc_codegen_cranelift

4 months ago 7

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 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 is SP+0x10
  • X29 can be found at offset -16 from CFA
  • X30 can be found at offset -8 from CFA

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
Read Entire Article