rust-embedded / cortex-m

Low level access to Cortex-M processors
Apache License 2.0
825 stars 148 forks source link

add virtual unwind info (.debug_frame / CFI) to external assembly #215

Open japaric opened 4 years ago

japaric commented 4 years ago

virtual unwinders (*), for example based on gimli::{DebugFrame,UnwindSection}, are not able to unwind through external assembly subroutines like __nop because these subroutines lack CFI (Call Frame Information), which -- when present -- gets encoded in the .debug_frame (**) section of the ELF.

$ # no `.debug_frame` in the output
$ arm-none-eabi-size -Ax bin/thumbv6m-none-eabi.a
.text                 0x0    0x0
.data                 0x0    0x0
.bss                  0x0    0x0
(..)
.debug_line         0x160    0x0
.debug_info          0x22    0x0
.debug_abbrev        0x12    0x0
.debug_aranges       0xb0    0x0
.debug_str           0x2d    0x0
.debug_ranges        0xa8    0x0
.ARM.attributes      0x1c    0x0
(..)

For subroutines that do not touch the stack pointer or link register adding CFI with no register rules is sufficient to make virtual unwinding work. Adding that CFI looks like this:

   .section .text.__nop
+  .cfi_sections .debug_frame
   .global __nop
   .thumb_func
+  .cfi_startproc
 __nop:
   bx lr
+  .cfi_endproc
   .size __nop, . - __nop

And yeah bx lr does not count as "touching LR" because it does not modify LR.

What needs to be done:

To test this part run the following command on the archives (bin/*.a*):

$ arm-none-eabi-readelf --debug-dump=frames thumbv7em-none-eabi.a
File: thumbv7em-none-eabi.a(asm.o)
Contents of the .debug_frame section:

00000000 0000000c ffffffff CIE
  Version:               1
  Augmentation:          ""
  Code alignment factor: 2
  Data alignment factor: -4
  Return address column: 14

  DW_CFA_def_cfa: r13 ofs 0

00000010 0000000c 00000000 FDE cie=00000000 pc=00000000..00000004

00000020 0000000c 00000000 FDE cie=00000000 pc=00000000..00000004

00000030 0000000c 00000000 FDE cie=00000000 pc=00000000..00000004

00000040 0000000c 00000000 FDE cie=00000000 pc=00000000..00000004

00000050 0000000c 00000000 FDE cie=00000000 pc=00000000..00000004

You should see one "FDE" for each subroutine that now has CFI.

(*) GDB doesn't seem to require this info (.debug_frame) to print stack backtraces but GDB also contains a full blown disassembler so I guess it can figure out the missing info on its own

(**) AFAIK, for proper stack unwinding, where stack frames are popped and destructors are called as the stack is unwound, one needs .eh_frame instead of .debug_frame. But we are dealing with panic=abort targets (which also lack the other unwinding ingredient: "landing pads" big question mark) here so we don't need the "full" version (.eh_frame).

P.S. cortex-m is not the only crate that's missing this info. cortex-m-rt, cortex-m-semihosting, etc. also need these changes.

whitequark commented 4 years ago

GDB doesn't seem to require this info (.debug_frame) to print stack backtraces but GDB also contains a full blown disassembler so I guess it can figure out the missing info on its own

Having interacted with that code I would strongly suggest you not rely on it but rather use the CFI directives. There's nothing more unpleasant than debugging your debugger when it becomes confused by an especially strange stack frame and either gets lost or crashes. (Though I guess the problem might be less acute on ARMv7 than on OR1K.)

Tiwalun commented 4 years ago

Other crates with external assembly:

Tiwalun commented 4 years ago

@japaric Regarding the psp_w and msp_w functions:

I'm not sure if it is possible to properly unwind after overwriting the stack pointer, I don't see a way how the debugger could recover the previous call frame address, it's not stored anywhere.

jonas-schievink commented 4 years ago

The functions that overwrite the stack pointer should probably use .cfi_undefined directives after the instruction

jonas-schievink commented 4 years ago

This interacts with https://github.com/rust-embedded/cortex-m/pull/259 – I've removed all the directives there, since rustc should generate them automatically for all Rust functions. But I don't know if that can see through inline assembly and emit unwind info for individual instructions in there.