Select Git revision
rtic_bare1.rs
Forked from
Per Lindgren / e7020e_2021
Source project has a limited visibility.
rtic_bare1.rs 13.98 KiB
//! bare1.rs
//!
//! Inspecting the generated assembly
//!
//! What it covers
//! - Rust panic on arithmetics
//! - assembly calls and inline assembly
#![no_main]
#![no_std]
use panic_semihosting as _;
use stm32f4;
#[rtic::app(device = stm32f4)]
const APP: () = {
#[init]
#[inline(never)] // avoid inlining of this function/task
#[no_mangle] // to strip hash from symbols (easier to read)
fn init(_cx: init::Context) {
let mut x = core::u32::MAX - 1;
loop {
cortex_m::asm::bkpt();
x = x.wrapping_add(1);
cortex_m::asm::bkpt();
// prevent optimization by read-volatile (unsafe)
unsafe {
//core::ptr::read_volatile(&x);
}
}
}
};
// 0. Setup
// Make sure that your repository is updated (pull from upstream).
//
// 1. Build in debug mode and run the application in vscode (Cortex Debug)
//
// Continue until you hit a breakpoint.
//
// Now select OUTPUT and Adapter Output.
//
// You should have encountered a Rust panic.
//
// Paste the error message:
//
// panicked at 'attempt to add with overflow', examples/rtic_bare1.rs:24:13
//
// Explain in your own words why the code panic:ed.
//
// The program tried to add to 1 to an integer variable which already
// were on the integer max (2^32-1) thus resulting in a panic
//
// Commit your answer (bare1_1)
//
// 2. Inspecting what caused the panic.
//
// Under CALL STACK you find the calls done to reach the panic:
//
// You can get the same information directly from GDB
//
// Select the DEBUG CONSOLE and enter the command
//
// > backtrace
//
// Paste the backtrace:
//
// #0 lib::__bkpt () at asm/lib.rs:49
// #1 0x0800104e in cortex_m::asm::bkpt () at /home/frappe/.cargo/registry/src/github.com-1ecc6299db9ec823/cortex-m-0.7.1/src/asm.rs:15// #2 panic_semihosting::panic (info=0x2000fed8) at /home/frappe/.cargo/registry/src/github.com-1ecc6299db9ec823/panic-semihosting-0.5.6/src/lib.rs:92
// #3 0x0800039a in core::panicking::panic_fmt () at /rustc/cb75ad5db02783e8b0222fee363c5f63f7e2cf5b//library/core/src/panicking.rs:92
// #4 0x08000374 in core::panicking::panic () at /rustc/cb75ad5db02783e8b0222fee363c5f63f7e2cf5b//library/core/src/panicking.rs:50
// #5 0x08000ebe in rtic_bare1::init (_cx=...) at /home/frappe/document/E7020E/software_labs/e7020e_2021/examples/rtic_bare1.rs:24
// #6 0x08000f08 in rtic_bare1::APP::main () at /home/frappe/document/E7020E/software_labs/e7020e_2021/examples/rtic_bare1.rs:15
//
// Explain in your own words the chain of calls.
//
// App calls init, init initializes a mutable variable x to int max -2
// init loops while adding to x, x goes over max, rust calls panic
// panic calls panic_format, panic_fmt calls semihosting_panic
// panic semihosting calls brakpoint.
//
// Commit your answer (bare1_2)
//
// 3. Now let's try to break it down to see what caused the panic.
//
// Put a breakpoint at line 24 (x += 1;)
// (Click to the left of the line marker, you get a red dot.)
//
// Restart the debug session, and continue until you hit the breakpoint.
//
// What is the value of `x`?
//
// 4294967294
//
// Explain in your own words where this value comes from.
//
// core::u32::MAX = 4294967295 = 2^32-1; we assign x to be MAX-1
//
// Now continue the program, since you are in a loop
// the program will halt again at line 24.
//
// What is the value of `x`?
//
// Explain in your own words why `x` now has this value.
//
// we added 1 to x
//
// Now continue again.
//
// At this point your code should panic.
//
// You can navigate the CALL STACK.
// Click on rtic_bare::init@0x08.. (24)
//
// The line leading up to the panic should now be highlighted.
// So you can locate the precise line which caused the error.
//
// Explain in your own words why a panic makes sense at this point.
//
// Since x had the value of MAX which is the maximum value x can't get bigger.
// since the purpose of adding to a variable is to make it bigger,
// and if it can't get any bigger rust has chosen to panic.
//
// Commit your answer (bare1_3)
//
// 4. Now lets have a look at the generated assembly.
//
// First restart the debug session and continue to the first halt (line 24).
//
// Select DEBUG CONSOLE and give the command
//
// > disassemble
//
// The current PC (program counter is marked with an arrow)
// => 0x08000f18 <+20>: ldr r0, [sp, #0]
//
// Explain in your own words what this assembly line does.
//// load the value of the address sp + offset 0 to register address r0
//
// In Cortex Registers (left) you can see the content of `r0`
//
// What value do you observe?
//
// -2 (MAX-2)
//
// You can also get the register info from GDB directly.
//
// > register info
//
// Many GDB commands have short names try `i r`.
//
// So now, time to move on, one assembly instruction at a time.
//
// > stepi
// > disassemble
//
// Now you should get
// => 0x08000f1a <+22>: adds r0, #1
//
// Explain in your own words what is happening here.
//
// add to the register r0 the immidiate constant 1
//
// We move to the next assembly instruction:
//
// > si
// > i r
//
// What is the reported value for `r0`
//
// -1 (MAX) (0xffffffff)
//
// So far so good.
//
// We can now continue to the next breakpoint.
//
// > continue
// (or in short >c, or press the play button, or press F5, many options here ...)
// > disassemble
// (or in short >disass)
//
// You should now be back at the top of the loop:
//
// => 0x08000f18 <+20>: ldr r0, [sp, #0]
//
// and the value of `r0` should be -1 (or 0xffffffff in hexadecimal)
//
// Now we can step an instruction again.
//
// > si
// => 0x08000f1a <+22>: adds r0, #1
//
// So far so good, and another one.
//
// > si
// => 0x08000f1c <+24>: bcs.n 0x8000f28 <rtic_bare::init+36>
//
// lookup the arm instruction set: https://developer.arm.com/documentation/ddi0210/c/Introduction/Instruction-set-summary/Thumb-instruction-summary
//
// What does BCS do?
//
// BCS.n branches if there was a carry bit from the last addition. (.n makes the instruction to be 16 bits)
//
// Now let's see what happens.
//
// > si
// => 0x08000f28 <+36>: movw r0, #6128 ; 0x17f0// 0x08000f2c <+40>: movw r2, #6112 ; 0x17e0
// 0x08000f30 <+44>: movt r0, #2048 ; 0x800
// 0x08000f34 <+48>: movt r2, #2048 ; 0x800
// 0x08000f38 <+52>: movs r1, #28
// 0x08000f3a <+54>: bl 0x8000346 <_ZN4core9panicking5panic17h6c8437680724f6d0E>
//
// Explain in your own words where we are heading.
//
// We are heading toward a branch to the panic "function"
//
// To validate that your answer, let's let the program continue
//
// > c
//
// Look in the OUTPUT/Adapter Output console again.
//
// Explain in your own words what the code
// 0x08000f28 .. 0x08000f38 achieves
//
// Hint 1, look at the error message?
// Hint 2, look at the call stack.
// Hint 3, the code is generated by the Rust compiler to produce the error message.
// there is no "magic" here, just a compiler generating code...
//
// it is to generate the error message.
//
// Commit your answer (bare1_4)
//
// 5. Now we can remove the break point (click the `Remove All Breakpoints`),
// and instead uncomment the two breakpoint instructions (on lines 23 and 25).
//
// Close the debug session and press F5 again to re-compile and launch the app.
//
// Continue until you hit the firs breakpoint.
//
// The disassembly should look like this:
//
//
// 0x08000f18 <+20>: bl 0x800103e <lib::__bkpt>
// => 0x08000f1c <+24>: ldr r0, [sp, #0]
// 0x08000f1e <+26>: adds r0, #1
// 0x08000f20 <+28>: bcs.n 0x8000f30 <rtic_bare::init+44>
// 0x08000f22 <+30>: str r0, [sp, #0]
// 0x08000f24 <+32>: bl 0x800103e <lib::__bkpt>
// 0x08000f28 <+36>: mov r0, r4
// 0x08000f2a <+38>: bl 0x8000fde <_ZN4core3ptr13read_volatile17hea5ef1c780562e1fE>
//
// In stable Rust we cannot currently write inline assembly, thus we do a "workaround"
// and call a function that that contains the assembly instruction.
//
// In this code:
// 0x08000f18 <+20>: bl 0x800103e <lib::__bkpt>
// and
// 0x08000f24 <+32>: bl 0x800103e <lib::__bkpt>
//
// In cases, this is not good enough (if we want exact cycle by cycle control).
// We can overcome this by letting the linker inline the code.
//
// Let's try this, build and run the code in release mode (Cortex Release).
// Continue until you hit the first assembly breakpoint.
//
// The disassembly now should look like this:
//
// => 0x0800024c <+12>: bkpt 0x0000
// 0x0800024e <+14>: adds r0, #1
// 0x08000250 <+16>: str r0, [sp, #4]
// 0x08000252 <+18>: bkpt 0x0000
// 0x08000254 <+20>: ldr r0, [sp, #4]
// 0x08000256 <+22>: b.n 0x800024c <rtic_bare::init+12>
//// Now let's compare the two assembly snippets.
// We now see that the breakpoints have been inlined (offsets +12, +18).
//
// But something else also happened here!
//
// Do you see any way this code may end up in a panic?
//
// No since there is no branching when there is a carry.
//
// So clearly, the "semantics" (meaning) of the program has changed.
// This is on purpose, Rust adopts "unchecked" (wrapping) additions (and subtractions)
// by default in release mode (to improve performance).
//
// The downside, is that programs change meaning. If you intend the operation
// to be wrapping you can explicitly express that in the code.
//
// Change the x += 1 to x = x.wrapping_add(1).
//
// And recompile/run/the code in Debug mode
//
// Paste the generated assembly:
//
// 0x08000e84 <+0>: push {r4, r6, r7, lr}
// 0x08000e86 <+2>: add r7, sp, #8
// 0x08000e88 <+4>: sub sp, #16
// 0x08000e8a <+6>: movw r0, #5808 ; 0x16b0
// 0x08000e8e <+10>: mov r4, sp
// 0x08000e90 <+12>: movt r0, #2048 ; 0x800
// 0x08000e94 <+16>: ldr r0, [r0, #0]
// 0x08000e96 <+18>: str r0, [sp, #0]
// 0x08000e98 <+20>: bl 0x8000fa6 <lib::__bkpt>
// => 0x08000e9c <+24>: ldr r0, [sp, #0]
// 0x08000e9e <+26>: adds r0, #1
// 0x08000ea0 <+28>: str r0, [sp, #0]
// 0x08000ea2 <+30>: bl 0x8000fa6 <lib::__bkpt>
// 0x08000ea6 <+34>: mov r0, r4
// 0x08000ea8 <+36>: bl 0x8000f46 <_ZN4core3ptr13read_volatile17hb977623ea709e27cE>
// 0x08000eac <+40>: b.n 0x8000e98 <rtic_bare1::init+20>
//
// Can this code generate a panic?
//
// No, no branch to a panic "function"
//
// Is there now any reference to the panic handler?
// If not, why is that the case?
//
// There is no reference since there is no need for it as we have stated that
// wrapping is the intended outcome
//
// commit your answers (bare1_5)
//
// Discussion:
// In release (optimized) mode the addition is unchecked,
// so there is a semantic difference here in between
// the dev and release modes. This is motivated by:
// 1) efficiency, unchecked/wrapping is faster
// 2) convenience, it would be inconvenient to explicitly use
// wrapping arithmetics, and wrapping is what the programmer
// typically would expect in any case. So the check
// in dev/debug mode is just there for some extra safety
// if your intention is NON-wrapping arithmetics.
//
// The debug build should have additional code that checks if the addition
// wraps (and in such case call panic). In the case of the optimized
// build there should be no reference to the panic handler in the generated
// binary. Recovering from a panic is in general very hard. Typically
// the best we can do is to stop and report the error (and maybe restart).
//
// Later we will demonstrate how we can get guarantees of panic free execution.
// This is very important to improve reliability.//
// 6. Now comment out the `read_volatile`.
//
// Rebuild and run the code in Release mode.
//
// Dump the generated assembly.
//
// 0x08000240 <+0>: push {r7, lr}
// 0x08000242 <+2>: mov r7, sp
// => 0x08000244 <+4>: bkpt 0x0000
// 0x08000246 <+6>: bkpt 0x0000
// 0x08000248 <+8>: b.n 0x8000244 <rtic_bare1::init+4>
//
// Where is the local variable stored?
// What happened, and why is Rust + LLVM allowed to optimize out your code?
//
// Since the local variable is no longer read then the compiler can
// optimize away the variable.
//
// Commit your answers (bare1_6)
//
//
// 7. *Optional
// You can pass additional flags to the Rust `rustc` compiler.
//
// `-Z force-overflow-checks=off`
//
// Under this flag, code is never generated for overflow checking even in
// non optimized (debug/dev) builds.
// You can enable this flag in the `.cargo/config` file.
//
// What is now the disassembly of the loop (in debug/dev mode):
//
// ** your answer here **
//
// commit your answers (bare1_7)
//
// Now restore the `.cargo/config` to its original state.
//
// 8. *Optional
// There is another way to conveniently use wrapping arithmetics
// without passing flags to the compiler.
//
// https://doc.rust-lang.org/std/num/struct.Wrapping.html
//
// Rewrite the code using this approach.
//
// What is now the disassembly of the code in dev mode?
//
// ** your answer here **
//
// What is now the disassembly of the code in release mode?
//
// ** your answer here **
//
// commit your answers (bare1_8)
//
// Final discussion:
//
// Embedded code typically is performance sensitive, hence
// it is important to understand how code is generated
// to achieve efficient implementations.
//
// Moreover, arithmetics are key to processing of data,
// so its important that we are in control over the
// computations. E.g. computing checksums, hashes, cryptos etc.
// all require precise control over wrapping vs. overflow behavior.
//
// If you write a library depending on wrapping arithmetics
// do NOT rely on a compiler flag. (The end user might compile// it without this flag enabled, and thus get erroneous results.)
//