Exception handling
During the "Memory layout" section, we decided to start out simple and leave out handling of
exceptions. In this section, we'll add support for handling them; this serves as an example of
how to achieve compile time overridable behavior in stable Rust (i.e. without relying on the
unstable #[linkage = "weak"]
attribute, which makes a symbol weak).
Background information
In a nutshell, exceptions are a mechanism the Cortex-M and other architectures provide to let applications respond to asynchronous, usually external, events. The most prominent type of exception, that most people will know, is the classical (hardware) interrupt.
The Cortex-M exception mechanism works like this: When the processor receives a signal or event associated to a type of exception, it suspends the execution of the current subroutine (by stashing the state in the call stack) and then proceeds to execute the corresponding exception handler, another subroutine, in a new stack frame. After finishing the execution of the exception handler (i.e. returning from it), the processor resumes the execution of the suspended subroutine.
The processor uses the vector table to decide what handler to execute. Each entry in the table contains a pointer to a handler, and each entry corresponds to a different exception type. For example, the second entry is the reset handler, the third entry is the NMI (Non Maskable Interrupt) handler, and so on.
As mentioned before, the processor expects the vector table to be at some specific location in memory,
and each entry in it can potentially be used by the processor at runtime. Hence, the entries must always
contain valid values. Furthermore, we want the rt
crate to be flexible so the end user can customize the
behavior of each exception handler. Finally, the vector table resides in read only memory, or rather in not
easily modified memory, so the user has to register the handler statically, rather than at runtime.
To satisfy all these constraints, we'll assign a default value to all the entries of the vector
table in the rt
crate, but make these values kind of weak to let the end user override them
at compile time.
Rust side
Let's see how all this can be implemented. For simplicity, we'll only work with the first 16 entries of the vector table; these entries are not device specific so they have the same function on any kind of Cortex-M microcontroller.
The first thing we'll do is create an array of vectors (pointers to exception handlers) in the
rt
crate's code:
$ sed -n 56,91p ../rt/src/lib.rs
#![allow(unused)] fn main() { pub union Vector { reserved: u32, handler: unsafe extern "C" fn(), } extern "C" { fn NMI(); fn HardFault(); fn MemManage(); fn BusFault(); fn UsageFault(); fn SVCall(); fn PendSV(); fn SysTick(); } #[link_section = ".vector_table.exceptions"] #[no_mangle] pub static EXCEPTIONS: [Vector; 14] = [ Vector { handler: NMI }, Vector { handler: HardFault }, Vector { handler: MemManage }, Vector { handler: BusFault }, Vector { handler: UsageFault, }, Vector { reserved: 0 }, Vector { reserved: 0 }, Vector { reserved: 0 }, Vector { reserved: 0 }, Vector { handler: SVCall }, Vector { reserved: 0 }, Vector { reserved: 0 }, Vector { handler: PendSV }, Vector { handler: SysTick }, ]; }
Some of the entries in the vector table are reserved; the ARM documentation states that they
should be assigned the value 0
so we use a union to do exactly that. The entries that must point
to a handler make use of external functions; this is important because it lets the end user
provide the actual function definition.
Next, we define a default exception handler in the Rust code. Exceptions that have not been assigned a handler by the end user will make use of this default handler.
$ tail -n4 ../rt/src/lib.rs
#![allow(unused)] fn main() { #[no_mangle] pub extern "C" fn DefaultExceptionHandler() { loop {} } }
Linker script side
On the linker script side, we place these new exception vectors right after the reset vector.
$ sed -n 12,25p ../rt/link.x
EXTERN(RESET_VECTOR);
EXTERN(EXCEPTIONS); /* <- NEW */
SECTIONS
{
.vector_table ORIGIN(FLASH) :
{
/* First entry: initial Stack Pointer value */
LONG(ORIGIN(RAM) + LENGTH(RAM));
/* Second entry: reset vector */
KEEP(*(.vector_table.reset_vector));
/* The next 14 entries are exception vectors */
KEEP(*(.vector_table.exceptions)); /* <- NEW */
} > FLASH
And we use PROVIDE
to give a default value to the handlers that we left undefined in rt
(NMI
and the others above):
$ tail -n8 ../rt/link.x
PROVIDE(NMI = DefaultExceptionHandler);
PROVIDE(HardFault = DefaultExceptionHandler);
PROVIDE(MemManage = DefaultExceptionHandler);
PROVIDE(BusFault = DefaultExceptionHandler);
PROVIDE(UsageFault = DefaultExceptionHandler);
PROVIDE(SVCall = DefaultExceptionHandler);
PROVIDE(PendSV = DefaultExceptionHandler);
PROVIDE(SysTick = DefaultExceptionHandler);
PROVIDE
only takes effect when the symbol to the left of the equal sign is still undefined after
inspecting all the input object files. This is the scenario where the user didn't implement the
handler for the respective exception.
Testing it
That's it! The rt
crate now has support for exception handlers. We can test it out with following
application:
NOTE: Turns out it's hard to generate an exception in QEMU. On real hardware a read to an invalid memory address (i.e. outside of the Flash and RAM regions) would be enough but QEMU happily accepts the operation and returns zero. A trap instruction works on both QEMU and hardware but unfortunately it's not available on stable so you'll have to temporarily switch to nightly to run this and the next example.
#![feature(core_intrinsics)] #![no_main] #![no_std] use core::intrinsics; use rt::entry; entry!(main); fn main() -> ! { // this executes the undefined instruction (UDF) and causes a HardFault exception intrinsics::abort() }
(gdb) target remote :3333
Remote debugging using :3333
Reset () at ../rt/src/lib.rs:7
7 pub unsafe extern "C" fn Reset() -> ! {
(gdb) b DefaultExceptionHandler
Breakpoint 1 at 0xec: file ../rt/src/lib.rs, line 95.
(gdb) continue
Continuing.
Breakpoint 1, DefaultExceptionHandler ()
at ../rt/src/lib.rs:95
95 loop {}
(gdb) list
90 Vector { handler: SysTick },
91 ];
92
93 #[no_mangle]
94 pub extern "C" fn DefaultExceptionHandler() {
95 loop {}
96 }
And for completeness, here's the disassembly of the optimized version of the program:
$ cargo objdump --bin app --release -- -d --no-show-raw-insn --print-imm-hex
app: file format elf32-littlearm
Disassembly of section .text:
<main>:
trap
trap
<Reset>:
push {r7, lr}
mov r7, sp
movw r1, #0x0
movw r0, #0x0
movt r1, #0x2000
movt r0, #0x2000
subs r1, r1, r0
bl 0x9c <__aeabi_memclr> @ imm = #0x3e
movw r1, #0x0
movw r0, #0x0
movt r1, #0x2000
movt r0, #0x2000
subs r2, r1, r0
movw r1, #0x282
movt r1, #0x0
bl 0x84 <__aeabi_memcpy> @ imm = #0x8
bl 0x40 <main> @ imm = #-0x40
trap
<UsageFault>:
$ cargo objdump --bin app --release -- -s -j .vector_table
app: file format elf32-littlearm
Contents of section .vector_table:
0000 00000120 45000000 83000000 83000000 ... E...........
0010 83000000 83000000 83000000 00000000 ................
0020 00000000 00000000 00000000 83000000 ................
0030 00000000 00000000 83000000 83000000 ................
The vector table now resembles the results of all the code snippets in this book so far. To summarize:
- In the Inspecting it section of the earlier memory chapter, we learned
that:
- The first entry in the vector table contains the initial value of the stack pointer.
- Objdump prints in
little endian
format, so the stack starts at0x2001_0000
. - The second entry points to address
0x0000_0045
, the Reset handler.- The address of the Reset handler can be seen in the disassembly above,
being
0x44
. - The first bit being set to 1 does not alter the address due to alignment requirements. Instead, it causes the function to be executed in thumb mode.
- The address of the Reset handler can be seen in the disassembly above,
being
- Afterwards, a pattern of addresses alternating between
0x83
and0x00
is visible.- Looking at the disassembly above, it is clear that
0x83
refers to theDefaultExceptionHandler
(0x84
executed in thumb mode). - Cross referencing the pattern to the vector table that was set up earlier
in this chapter (see the definition of
pub static EXCEPTIONS
) with the vector table layout for the Cortex-M, it is clear that the address of theDefaultExceptionHandler
is present each time a respective handler entry is present in the table. - In turn, it is also visible that the layout of the vector table data structure in the Rust code is aligned with all the reserved slots in the Cortex-M vector table. Hence, all reserved slots are correctly set to a value of zero.
- Looking at the disassembly above, it is clear that
Overriding a handler
To override an exception handler, the user has to provide a function whose symbol name exactly
matches the name we used in EXCEPTIONS
.
#![feature(core_intrinsics)] #![no_main] #![no_std] use core::intrinsics; use rt::entry; entry!(main); fn main() -> ! { intrinsics::abort() } #[no_mangle] pub extern "C" fn HardFault() -> ! { // do something interesting here loop {} }
You can test it in QEMU
(gdb) target remote :3333
Remote debugging using :3333
Reset () at /home/japaric/rust/embedonomicon/ci/exceptions/rt/src/lib.rs:7
7 pub unsafe extern "C" fn Reset() -> ! {
(gdb) b HardFault
Breakpoint 1 at 0x44: file src/main.rs, line 18.
(gdb) continue
Continuing.
Breakpoint 1, HardFault () at src/main.rs:18
18 loop {}
(gdb) list
13 }
14
15 #[no_mangle]
16 pub extern "C" fn HardFault() -> ! {
17 // do something interesting here
18 loop {}
19 }
The program now executes the user defined HardFault
function instead of the
DefaultExceptionHandler
in the rt
crate.
Like our first attempt at a main
interface, this first implementation has the problem of having no
type safety. It's also easy to mistype the name of the exception, but that doesn't produce an error
or warning. Instead the user defined handler is simply ignored. Those problems can be fixed using a
macro like the exception!
macro defined in cortex-m-rt
v0.5.x or the
exception
attribute in cortex-m-rt
v0.6.x.