Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
2 tasks
japaric opened this issue May 5, 2020 · 5 comments
Open
2 tasks

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

japaric opened this issue May 5, 2020 · 5 comments

Comments

@japaric
Copy link
Member

japaric commented May 5, 2020

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:

  • the easy part: add this "no register rules" CFI prologue and epilogue to all assembly subroutines that do *not *modify the stack pointer (SP) or the linker register (LR) and then re-run assemble.sh. Some instructions that do modify SP / LR are push, pop, stmia, ldmia, mov/msr where SP/LR is the destination (first argument) -- do not modify subroutines that contain those instructions (see "hard part" below)

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.

  • the hard part: figure out how to write register rules for instructions that do modify SP / LR. Then add proper CFI to subroutines with weird ABIs like cortex_m_rt::HardFaultTrampoline

(*) 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
Copy link
Contributor

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.)

bors bot added a commit that referenced this issue May 8, 2020
216: Add unwind information to external assembly r=jonas-schievink a=Tiwalun

Add unwind information to the external assembly files, as discussed in #215.

The `.debug_frame` section is now present:
```

cortex-m.o   (ex bin/thumbv6m-none-eabi.a):
section              size   addr
.text                 0x0    0x0
.data                 0x0    0x0
.bss                  0x0    0x0
.text.__bkpt          0x4    0x0
.text.__control_r     0x6    0x0
.text.__control_w     0x6    0x0
.text.__cpsid         0x4    0x0
.text.__cpsie         0x4    0x0
.text.__delay         0x8    0x0
.text.__dmb           0x6    0x0
.text.__dsb           0x6    0x0
.text.__isb           0x6    0x0
.text.__msp_r         0x6    0x0
.text.__msp_w         0x6    0x0
.text.__nop           0x2    0x0
.text.__primask       0x6    0x0
.text.__psp_r         0x6    0x0
.text.__psp_w         0x6    0x0
.text.__sev           0x4    0x0
.text.__udf           0x2    0x0
.text.__wfe           0x4    0x0
.text.__wfi           0x4    0x0
.debug_line         0x161    0x0
.debug_info          0x22    0x0
.debug_abbrev        0x12    0x0
.debug_aranges       0xb0    0x0
.debug_str           0x30    0x0
.debug_ranges        0xa8    0x0
.debug_frame        0x140    0x0
.ARM.attributes      0x1c    0x0
Total               0x4d9
```


Co-authored-by: Dominik Boehi <[email protected]>
@Tiwalun
Copy link
Contributor

Tiwalun commented May 11, 2020

@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
Copy link
Contributor

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

@jonas-schievink
Copy link
Contributor

This interacts with #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.

bors bot added a commit that referenced this issue Mar 1, 2022
423: Swap to just-stabilised asm!() and global_asm!() macros r=thejpster a=adamgreig

Once Rust 1.59 is released in a couple of days, the `asm!()` and `global_asm!()` macros will become stable, and we will no longer require the various precompiled binaries we've used until now in cortex-m and cortex-m-rt. cc #420.

This PR uses `asm!()` in cortex-m, and removes the inline-asm feature, since I anticipate this going into cortex-m 0.8 and so we don't need to leave it behind for compatibility. In various places the previous version would call an extern C method when built for the native target, which would only fail at link time; to preserve the ability to build on x86 I've either made the whole method require the `cortex_m` configuration, or where appropriate/convenient simply skipped the `asm!()` call.

This PR replaces the old gcc-preprocessed `asm.S` in cortex-m-rt with use of `global_asm!()`, although since you can't normally use `#[cfg(...)]` attributes with `global_asm!()`, there's also a slightly scary macro modified from one invented by `@Dirbaio` for a similar purpose. I considered putting the initialisation of LR behind an armv6m flag, but since we want to restore it after calling `__pre_init` it seemed better to just leave it the same on both targets. I added Cargo features to optionally set SP and VTOR at startup, which has been variously requested but would previously have required multiplicatively more pre-built binaries. Now: no problem. Relevant issues:

* rust-embedded/cortex-m-rt#283
* rust-embedded/cortex-m-rt#55
* rust-embedded/cortex-m-rt#254
* rust-embedded/cortex-m-rt#102
* rust-embedded/cortex-m-rt#338

I've tested these on a couple of targets (and updated the CI): on the whole there's a small improvement in code size due to everyone getting inlined asm, especially in `cortex_m::interrupt::free()`.

The major downside is we bump our MSRV from 1.42 (March 2020) to 1.59 (Feb 2022). For cortex-m, I propose putting these changes in the upcoming 0.8 release (which is technically what the master branch is already on) and not backporting. For cortex-m-rt I'm not sure: we don't have any other pending breaking changes, so we could consider a patch release. Anyway, this PR doesn't commit to any particular releases, so we can decide that later.

For cortex-m-semihosting/panic-semihosting I think a patch release would be ideal, especially since we had to yank the last c-m-sh release due to conflicting prebuilt binaries (a problem that should now vanish).

Also tagging these issues that I think might also benefit from new inline asm:
* #265
* #215
* #406

Co-authored-by: Adam Greig <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants