knurling-rs / defmt

Efficient, deferred formatting for logging on embedded systems
https://defmt.ferrous-systems.com/
Apache License 2.0
864 stars 80 forks source link

[RFC] defmt::abort #595

Open japaric opened 3 years ago

japaric commented 3 years ago

Summary

add an alternative panicking mechanism, defmt::abort, that does NOT use the Logger infrastructure but still reports some information to the user when used with probe-run.

Motivation

currently, there are two places where one can run into "silent" panics:

struct S;

impl Format for S { fn format(&self, _: defmt::Formatter) { defmt::error!("S") } }

[cortex_m_rt::entry]

fn main() -> ! { defmt::error!("{}", S); loop { asm::nop() } }


- "nested" panicking with [`panic-probe`](https://github.com/knurling-rs/defmt/blob/e021e7d013b9202f70bc762f6909401f85142c89/firmware/panic-probe/src/lib.rs#L50-L54). this can be triggered with:

``` rust
// src/bin/nested-panic.rs

use panic_probe as _;

struct S;

impl Format for S {
    fn format(&self, _: defmt::Formatter) {
        panic!()
    }
}

#[cortex_m_rt::entry]
fn main() -> ! {
    defmt::panic!("{}", S)
}

both of these failure modes don't report any information to the end user and thus are hard to debug.

This document proposes using defmt::abort in those two cases to provide information to the end user

Detailed design

User API

defmt::abort is a "diverging" (fn() -> !) macro like defmt::panic. It takes a single argument: a string literal. Example usage:

// Guard against infinite recursion, just in case.
if PANICKED.load(Ordering::Relaxed) {
    defmt::abort!("nested panic")
}

When defmt::abort is reached, probe-run will report the string literal argument, the location of the defmt::abort call, a stack backtrace and then will exit with non-zero exit code. This will work regardless of what Logger implementation (e.g. defmt-rtt) the program uses.

$ cargo run --bin nested-panic
(HOST) INFO  flashing program (6.64 KiB)
(HOST) INFO  success!
────────────────────────────────────────────────────────────────────────────────
────────────────────────────────────────────────────────────────────────────────
(HOST) ERROR the program aborted with message "nested panic" at /home/japaric/.cargo/registry/src/github.com-1ecc6299db9ec823/panic-probe-0.2.2/src/lib.rs:51:9
stack backtrace:
   0: HardFaultTrampoline
      <exception entry>
   1: lib::inline::__udf
        at ./asm/inline.rs:172:5
   2: __udf
        at ./asm/lib.rs:49:17
   3: cortex_m::asm::udf
(..)

$ echo $?
6

when probe-run is not used. the program will execute the UDF instruction

hand-wavy implementation details

The expansion of defmt::abort!("string literal") will roughly look like this:

{
    let istr = {
        #[link_section = ".defmt.{\"package\":\"app\",\"tag\":\"defmt_abort\",\"data\":\"string literal\",\"disambiguator\":\"3648903125280595818\"}"]
        #[export_name = "{\"package\":\"app\",\"tag\":\"defmt_abort\",\"data\":\"string literal\",\"disambiguator\":\"3648903125280595818\"}"]
        static DEFMT_LOG_STATEMENT: u8 = 0;
        &DEFMT_LOG_STATEMENT as *const u8
    };
    expose_to_debugger(istr);
    defmt::export::udf()
}

When unwinding the code, probe-run will identify the above pattern and report "string literal" as the abort reason.

The tricky part is the implementation of expose_to_debugger. The simplest thing to do would be to use custom assembly that:

(+) there are different machine instruction encodings that are considered "undefined" instructions. Another option could be using the immediate value of the BKPT instruction, e.g. BKPT 0x42

Alternatives

Dirbaio commented 3 years ago

One disadvantage is this will only work when directyl attached to the device with SWD. It won't work when using a custom Logger impl that sends the defmt data over e.g. the network. It's also highly specific to embedded (#463)

For the "nested acquire" problem: with rzcobs encoding, it is possible to write a 0x00 byte to "force-terminate" the previous frame (corrupting it) and then write the panic message frame, so that it is always sent. This works in the "defmt over the network" and non-embedded cases.

Not sure how this fits in the Logger trait though. Can it be done backwards-compatibly?