Exception handling in rustc_codegen_cranelift

Björn
Systems engineer

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 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
1: For SEH on the *-windows-msvc targets, rustc uses the MSVC C++ __CxxFrameHandler3 personality function instead, due to LLVM determining which format of unwind tables to generate based on the personality function, while the *-windows-msvc targets have to specifically use pure SEH unwind tables.

Stay up-to-date

Stay up-to-date with our work and blog posts?

Related articles

While using a full-blown filesystem for storing your data in non-volatile memory is common practice, those filesystems are often too big, not to mention annoying to use, for the things I want to do. My solution?

I've been hard at work creating the sequential-storage crate. In this blog post I'd like to go over what it is, why I created it and what it does.

In Dutch we have a saying 'meten is weten', which translates to 'to measure is to know'. That sentiment is frequently overlooked in setting up computers and networks.
Ever wanted to have a quickly put together command-line tool to delete large chunks of your project automatically? Me neither, but my colleague Marc made a pretty convincing argument as to why such a tool could be useful. So we went ahead and made it. Here are the results.