├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── changelog.yml │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── src ├── backtrace │ ├── mod.rs │ ├── pp.rs │ ├── symbolicate.rs │ └── unwind.rs ├── canary.rs ├── cli.rs ├── cortexm.rs ├── dep │ ├── cratesio.rs │ ├── mod.rs │ ├── rust_repo.rs │ ├── rust_std.rs │ ├── rust_std │ │ └── toolchain.rs │ └── rustc.rs ├── elf.rs ├── main.rs ├── probe.rs ├── registers.rs ├── stacked.rs └── target_info.rs └── tests ├── README.md ├── snapshot.rs ├── snapshots ├── snapshot__case_1_successful_run_has_no_backtrace.snap ├── snapshot__case_1_without_time.snap ├── snapshot__case_2_raw_encoding.snap ├── snapshot__case_2_with_time.snap ├── snapshot__case_3_successful_run_can_enforce_backtrace.snap ├── snapshot__case_3_with_time_but_no_impl.snap ├── snapshot__case_4_stack_overflow_is_reported_as_such.snap ├── snapshot__case_4_without_time_but_with_impl.snap ├── snapshot__case_5_host_without_time.snap ├── snapshot__case_5_panic_is_reported_as_such.snap ├── snapshot__case_6_host_with_timestamp.snap ├── snapshot__case_6_panic_verbose.snap ├── snapshot__case_7_unsuccessful_run_can_suppress_backtrace.snap ├── snapshot__case_8_stack_overflow_can_suppress_backtrace.snap ├── snapshot__case_9_canary.snap └── snapshot__ctrl_c_by_user_is_reported_as_such.snap └── test_elfs ├── hello-raw ├── hello-rzcobs ├── hello.rs ├── levels-rzcobs ├── levels-with-timestamp ├── overflow-no-flip-link ├── overflow-rzcobs ├── overflow.rs ├── panic-rzcobs ├── panic.rs ├── silent-loop-rzcobs └── silent-loop.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [knurling-rs] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | *Example* 17 | 1. Write `src/bin/abort.rs` 18 | ``` rust 19 | // .. 20 | fn main() -> ! { 21 | cortex_m::asm::udf() 22 | } 23 | ``` 24 | 2. Run it with `cargo run --bin abort` 25 | 26 | **Expected and observed behavior** 27 | A clear and concise description of what you expected to happen. Please include relevant console output. 28 | 29 | *Example*: I expected to see a backtrace but instead the `cargo run` command hanged / stopped responding. 30 | ``` console 31 | $ cargo run --bin hello 32 | Finished dev [optimized + debuginfo] target(s) in 0.03s 33 | Running `probe-run --chip nrf52840 --defmt target/thumbv7em-none-eabihf/debug/hello` 34 | (HOST) INFO flashing program 35 | (HOST) INFO success! 36 | ──────────────────────────────────────────────────────────────────────────────── 37 | stack backtrace: 38 | (.. probe-run hangs here ..) 39 | ``` 40 | 41 | **config.toml** 42 | The contents of your project's `.cargo/config.toml` file 43 | 44 | *Example*: 45 | 46 | ``` toml 47 | [target.'cfg(all(target_arch = "arm", target_os = "none"))'] 48 | runner = "probe-run --chip nrf52840 --defmt" 49 | rustflags = [ 50 | "-C", "linker=flip-link", 51 | "-C", "link-arg=-Tlink.x", 52 | "-C", "link-arg=-Tdefmt.x", 53 | ] 54 | 55 | [build] 56 | target = "thumbv7em-none-eabihf" # Cortex-M4F and Cortex-M7F (with FPU) 57 | ``` 58 | 59 | **Probe details** 60 | You can get this information from [`probe-rs-cli`](https://crates.io/crates/probe-rs-cli). Your microcontroller must be connected to your PC / laptop when you run the command below. 61 | 62 | *Example:* 63 | 64 | ``` console 65 | $ probe-rs-cli list 66 | [0]: DAPLink CMSIS-DAP (VID: 0d28, PID: 0204, Serial: abc, DAPLink) 67 | ``` 68 | 69 | **Operating System:** 70 | [e.g. Linux] 71 | 72 | **ELF file (attachment)** 73 | 74 | Please attach to this bug report the ELF file you passed to `probe-run`. The path to this file will appear in the output of `cargo run`. If you'd prefer not to upload this file please keep it around as we may ask to run commands like [`nm` or `objdump`](https://crates.io/crates/cargo-binutils) on it. 75 | 76 | *Example*: 77 | 78 | ``` console 79 | $ cargo run 80 | Running `probe-run --chip nrf52840 --defmt target/thumbv7em-none-eabihf/debug/hello` 81 | ``` 82 | 83 | Attach the file `target/thumbv7em-none-eabihf/debug/hello` to the bug report. 84 | 85 | **Additional context** 86 | Add any other context about the problem here. 87 | -------------------------------------------------------------------------------- /.github/workflows/changelog.yml: -------------------------------------------------------------------------------- 1 | # Check that the changelog is updated for all changes. 2 | # 3 | # This is only run for PRs. 4 | 5 | on: 6 | pull_request: 7 | # opened, reopened, synchronize are the default types for pull_request. 8 | # labeled, unlabeled ensure this check is also run if a label is added or removed. 9 | types: [opened, reopened, synchronize, labeled, unlabeled] 10 | merge_group: 11 | 12 | name: Changelog 13 | 14 | jobs: 15 | changelog: 16 | name: Changelog 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout sources 20 | uses: actions/checkout@v2 21 | 22 | - name: Check that changelog updated 23 | uses: dangoslen/changelog-enforcer@v3 24 | with: 25 | changeLogPath: CHANGELOG.md 26 | skipLabels: "skip-changelog" 27 | missingUpdateErrorMessage: "Please add a changelog entry in the CHANGELOG.md file." 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main, staging, trying ] 6 | pull_request: 7 | branches: [ main ] 8 | merge_group: 9 | 10 | jobs: 11 | test: 12 | strategy: 13 | matrix: 14 | os: 15 | - ubuntu-latest 16 | - macOS-latest 17 | - windows-latest 18 | runs-on: ${{ matrix.os }} 19 | timeout-minutes: 20 20 | steps: 21 | - uses: actions/checkout@v3 22 | - name: Use the latest stable release 23 | run: rustup update stable && rustup default stable 24 | - name: Install C libraries for tooling 25 | if: matrix.os == 'ubuntu-latest' 26 | run: sudo apt-get update && sudo apt-get install libudev-dev libusb-1.0-0-dev 27 | 28 | - run: cargo build 29 | - run: cargo test 30 | 31 | static: 32 | runs-on: ubuntu-latest 33 | steps: 34 | - uses: actions/checkout@v3 35 | - name: Use the latest stable release 36 | run: rustup update stable && rustup default stable 37 | - name: Install C libraries for tooling 38 | run: sudo apt-get update && sudo apt-get install libudev-dev libusb-1.0-0-dev 39 | 40 | - run: cargo fmt --check 41 | - run: cargo clippy --all-targets -- -D warnings 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). 6 | 7 | ## [Unreleased] 8 | 9 | ## [v0.3.11] - 2024-01-29 10 | 11 | - [#423] Add better defaults for log format when timestamp is available 12 | 13 | [#423]: https://github.com/knurling-rs/probe-run/pull/423 14 | 15 | ## [v0.3.10] - 2023-08-01 16 | 17 | - [#417] Release `probe-run v0.3.10` 18 | - [#416] Add support for log_format option for defmt decoder 19 | - [#410] Simplify canary 20 | - [#405] Also enable merge queue for changelog enforcer 21 | - [#404] Switch from bors to merge queue 22 | - [#402] Do not panic if locations do not contain the frame 23 | 24 | [#417]: https://github.com/knurling-rs/probe-run/pull/417 25 | [#416]: https://github.com/knurling-rs/probe-run/pull/416 26 | [#410]: https://github.com/knurling-rs/probe-run/pull/410 27 | [#405]: https://github.com/knurling-rs/probe-run/pull/405 28 | [#404]: https://github.com/knurling-rs/probe-run/pull/404 29 | [#402]: https://github.com/knurling-rs/probe-run/pull/402 30 | 31 | ## [v0.3.9] - 2023-05-05 32 | 33 | - [#403] Release `probe-run v0.3.9` 34 | - [#399] Update link shown in error message 35 | - [#398] Handle reset vector not pointing to ELF symbol 36 | 37 | [#403]: https://github.com/knurling-rs/probe-run/pull/403 38 | [#399]: https://github.com/knurling-rs/probe-run/pull/399 39 | [#398]: https://github.com/knurling-rs/probe-run/pull/398 40 | 41 | ## [v0.3.8] - 2023-04-12 42 | 43 | - [#395] Bump `defmt-decoder` to `0.3.6` due to yanked version `0.3.5` 44 | - [#389] Clean `fn run_target_program` up 45 | 46 | [#395]: https://github.com/knurling-rs/probe-run/pull/395 47 | [#389]: https://github.com/knurling-rs/probe-run/pull/389 48 | 49 | ## [v0.3.7] - 2023-03-29 50 | 51 | - [#390] Release `probe-run v0.3.7` 52 | - [#385] Update snapshot tests and test stack canary 53 | - [#384] Satisfy clippy 54 | - [#383] Fix unwinding 55 | - [#381] Add feature `ftdi` to enable `ftdi` debuggers 56 | - [#380] Add `--erase-all` flag 57 | - [#379] Update to `probe-rs v0.17` 58 | - [#378] Update to `probe-rs v0.16` 59 | 60 | [#390]: https://github.com/knurling-rs/probe-run/pull/390 61 | [#385]: https://github.com/knurling-rs/probe-run/pull/385 62 | [#384]: https://github.com/knurling-rs/probe-run/pull/384 63 | [#383]: https://github.com/knurling-rs/probe-run/pull/383 64 | [#381]: https://github.com/knurling-rs/probe-run/pull/381 65 | [#380]: https://github.com/knurling-rs/probe-run/pull/380 66 | [#379]: https://github.com/knurling-rs/probe-run/pull/379 67 | [#378]: https://github.com/knurling-rs/probe-run/pull/378 68 | 69 | ## [v0.3.6] - 2023-01-23 70 | 71 | - [#375] Release `probe-run v0.3.6` 72 | - [#373] Update to `probe-rs v0.14` 73 | - [#371] Add "missing drivers" to troubleshooting section 74 | - [#366] Update CI 75 | 76 | [#375]: https://github.com/knurling-rs/probe-run/pull/375 77 | [#373]: https://github.com/knurling-rs/probe-run/pull/373 78 | [#371]: https://github.com/knurling-rs/probe-run/pull/371 79 | [#366]: https://github.com/knurling-rs/probe-run/pull/366 80 | 81 | ## [v0.3.5] - 2022-10-07 82 | 83 | - [#357] Update to `clap 4.0` 84 | - [#349] Add `PROBE_RUN_SPEED` env variable 85 | - [#345] Update dev-dependency `serial_test` 86 | - [#344] Replace `pub(crate)` with `pub` 87 | - [#343] Mark `v0.3.4` as released in `CHANGELOG.md` 88 | - [#353] Add `--verify` option to verify written flash 89 | 90 | [#357]: https://github.com/knurling-rs/probe-run/pull/357 91 | [#349]: https://github.com/knurling-rs/probe-run/pull/349 92 | [#345]: https://github.com/knurling-rs/probe-run/pull/345 93 | [#344]: https://github.com/knurling-rs/probe-run/pull/344 94 | [#343]: https://github.com/knurling-rs/probe-run/pull/343 95 | [#353]: https://github.com/knurling-rs/probe-run/pull/353 96 | 97 | ## [v0.3.4] - 2022-08-10 98 | 99 | - [#342] Release `probe-run 0.3.4` 100 | - [#339] Simplify `fn round_up` 101 | - [#337] Clean snapshot tests 102 | - [#335] Add unit tests for `fn round_up` 103 | - [#334] Simplify snapshot tests 104 | - [#333] Clean up `enum Outcome` 105 | - [#331] Refactor stack painting 106 | - [#330] Fix `fn round_up` 107 | - [#329] Update probe-rs to 0.13.0 (does not yet implement 64-bit support) 108 | - [#328] Simplify, by capturing identifiers in logging macros 109 | - [#327] Optimize stack usage measuring 110 | - [#326] Make use of i/o locking being static since rust `1.61`. 111 | - [#321] CI: Add changelog-enforcer 112 | - [#320] Disable terminal colorization if `TERM=dumb` is set 113 | - [#319] Warn on target chip mismatch 114 | - [#317] Clarify "can't determine stack overflow" error message 115 | - [#314] Clarify documentation in README 116 | - [#305] Add option to disable double buffering while loading firmware 117 | - [#293] Update snapshot tests to new TRACE output 118 | 119 | [#342]: https://github.com/knurling-rs/probe-run/pull/342 120 | [#339]: https://github.com/knurling-rs/probe-run/pull/339 121 | [#337]: https://github.com/knurling-rs/probe-run/pull/337 122 | [#335]: https://github.com/knurling-rs/probe-run/pull/335 123 | [#334]: https://github.com/knurling-rs/probe-run/pull/334 124 | [#333]: https://github.com/knurling-rs/probe-run/pull/333 125 | [#331]: https://github.com/knurling-rs/probe-run/pull/331 126 | [#330]: https://github.com/knurling-rs/probe-run/pull/330 127 | [#329]: https://github.com/knurling-rs/probe-run/pull/329 128 | [#328]: https://github.com/knurling-rs/probe-run/pull/328 129 | [#327]: https://github.com/knurling-rs/probe-run/pull/327 130 | [#326]: https://github.com/knurling-rs/probe-run/pull/326 131 | [#321]: https://github.com/knurling-rs/probe-run/pull/321 132 | [#320]: https://github.com/knurling-rs/probe-run/pull/320 133 | [#319]: https://github.com/knurling-rs/probe-run/pull/319 134 | [#317]: https://github.com/knurling-rs/probe-run/pull/317 135 | [#314]: https://github.com/knurling-rs/probe-run/pull/314 136 | [#305]: https://github.com/knurling-rs/probe-run/pull/305 137 | [#293]: https://github.com/knurling-rs/probe-run/pull/293 138 | 139 | ## [v0.3.3] - 2022-03-14 140 | 141 | ### Fixed 142 | 143 | - [#311] fixed "probe-run does not work with some programs that have less than 10 KiB of stack space _unless_ `--measure-stack` is available" 144 | 145 | [#311]: https://github.com/knurling-rs/probe-run/pull/311 146 | 147 | ## [v0.3.2] - 2022-03-10 - YANKED 148 | 149 | - [#303] Don't bail, but only warn if using `--no-flash` with `defmt` 150 | - [#302] Make stack painting fast again! 🇪🇺 151 | - [#301] Add way to pass chip description file 152 | - [#300] Simplify verbose matching 153 | - [#299] Fix and refactor `fn extract_stack_info` 154 | - [#296] turn some `println!` into `writeln!` 155 | - [#295] probe-run json output 156 | - [#294] Update `Cargo.lock` 157 | 158 | [#303]: https://github.com/knurling-rs/probe-run/pull/303 159 | [#302]: https://github.com/knurling-rs/probe-run/pull/302 160 | [#301]: https://github.com/knurling-rs/probe-run/pull/301 161 | [#300]: https://github.com/knurling-rs/probe-run/pull/300 162 | [#299]: https://github.com/knurling-rs/probe-run/pull/299 163 | [#296]: https://github.com/knurling-rs/probe-run/pull/296 164 | [#295]: https://github.com/knurling-rs/probe-run/pull/295 165 | [#294]: https://github.com/knurling-rs/probe-run/pull/294 166 | 167 | ## [v0.3.1] - 2021-11-26 168 | 169 | - [#287]: unwind: skip FDEs with initial address of 0 170 | - [#286]: Update dependencies 171 | - [#285]: Update `probe-rs` and `probe-rs-rtt` to `0.12` 172 | - [#282]: Include program counter value in backtrace when -v is passed 173 | - [#281]: Report flashing size using probe-rs's FlashProgress system 174 | - [#280]: Turn symbol demangling back on 175 | 176 | [#287]: https://github.com/knurling-rs/probe-run/pull/287 177 | [#286]: https://github.com/knurling-rs/probe-run/pull/286 178 | [#285]: https://github.com/knurling-rs/probe-run/pull/285 179 | [#282]: https://github.com/knurling-rs/probe-run/pull/282 180 | [#281]: https://github.com/knurling-rs/probe-run/pull/281 181 | [#280]: https://github.com/knurling-rs/probe-run/pull/280 182 | 183 | ## [v0.3.0] - 2021-11-09 184 | 185 | - [#273]: Update to Rust 2021 🎉 186 | - [#267]: Minimize dependencies by disabling default-features 187 | - [#266]: Recover from decoding-errors 188 | - [#247]: Print troubleshooting information on "probe not found" error 189 | - [#264]: Use new stream decoder API 190 | - [#263]: Set blocking mode to `0b10` (`BLOCK_IF_FULL`) 191 | - [#257]: Split "set rtt to blocking-mode" into function 192 | - [#259]: Update snapshot tests 193 | - [#260]: Fix CI on nightly 194 | - [#254]: Add support for measuring the program's stack usage 195 | - [#248]: Print dedicated message on control + c 196 | - [#250]: Backtrace options 197 | - [#253]: feat: add help message for JtagNoDeviceConnected 198 | - [#251]: Removes call to fill in user survey from readme. 199 | - [#246]: Enable deactivated tests for Windows 200 | 201 | [#273]: https://github.com/knurling-rs/probe-run/pull/273 202 | [#267]: https://github.com/knurling-rs/probe-run/pull/267 203 | [#266]: https://github.com/knurling-rs/probe-run/pull/266 204 | [#247]: https://github.com/knurling-rs/probe-run/pull/247 205 | [#264]: https://github.com/knurling-rs/probe-run/pull/264 206 | [#263]: https://github.com/knurling-rs/probe-run/pull/263 207 | [#257]: https://github.com/knurling-rs/probe-run/pull/257 208 | [#259]: https://github.com/knurling-rs/probe-run/pull/259 209 | [#260]: https://github.com/knurling-rs/probe-run/pull/260 210 | [#254]: https://github.com/knurling-rs/probe-run/pull/254 211 | [#248]: https://github.com/knurling-rs/probe-run/pull/248 212 | [#250]: https://github.com/knurling-rs/probe-run/pull/250 213 | [#253]: https://github.com/knurling-rs/probe-run/pull/253 214 | [#251]: https://github.com/knurling-rs/probe-run/pull/251 215 | [#246]: https://github.com/knurling-rs/probe-run/pull/246 216 | 217 | ## [v0.2.5] - 2021-08-02 218 | 219 | - [#244] Fix clippy warnings 220 | - [#240] Link Knurling User Survey in `README` 221 | - [#235] Update to `probe-rs` `0.11` 222 | - [#234] Feature-gate windows tests 223 | - [#233] Some clarifications and corrections 224 | 225 | [#244]: https://github.com/knurling-rs/probe-run/pull/244 226 | [#240]: https://github.com/knurling-rs/probe-run/pull/240 227 | [#235]: https://github.com/knurling-rs/probe-run/pull/235 228 | [#234]: https://github.com/knurling-rs/probe-run/pull/234 229 | [#233]: https://github.com/knurling-rs/probe-run/pull/233 230 | 231 | ## [v0.2.4] - 2021-06-17 232 | 233 | - [#212] make `unwind::target()` infallible 234 | - [#216] Fix `EXC_RETURN` detection on thumbv8 235 | - [#218] add first, user-triggered snapshot tests 236 | - [#219] add more explicit hint if elf path doesn't lead to an existing file 237 | - [#221] Obtain git-version from macro, instead of custom build-script 238 | - [#222] refactor the huge "main" function into smaller functions + modules 239 | - [#224] target_info: print ram region again 240 | - [#225] `cli::tests`: rstest-ify tests for `fn extract_git_hash` 241 | - [#226] `CI`: Run tests and clippy 242 | - [#228] Remove unused file `utils.rs` 243 | 244 | [#212]: https://github.com/knurling-rs/probe-run/pull/212 245 | [#216]: https://github.com/knurling-rs/probe-run/pull/216 246 | [#218]: https://github.com/knurling-rs/probe-run/pull/218 247 | [#219]: https://github.com/knurling-rs/probe-run/pull/219 248 | [#221]: https://github.com/knurling-rs/probe-run/pull/221 249 | [#222]: https://github.com/knurling-rs/probe-run/pull/222 250 | [#224]: https://github.com/knurling-rs/probe-run/pull/224 251 | [#225]: https://github.com/knurling-rs/probe-run/pull/225 252 | [#226]: https://github.com/knurling-rs/probe-run/pull/226 253 | [#228]: https://github.com/knurling-rs/probe-run/pull/228 254 | 255 | ## [v0.2.3] - 2021-05-21 256 | 257 | ### Improvements 258 | - [#193] Check `PROBE_RUN_IGNORE_VERSION` on runtime 259 | - [#199] Add column info to backtrace 260 | - [#200] Highlight frames that point to local code in backtrace 261 | - [#203] + [#209] + [#210] Add `--shorten-paths` 262 | - [#204] Make 'stopped due to signal' force a backtrace 263 | - [#207] Read as little stacked registers as possible during unwinding 264 | 265 | ### Docs 266 | - [#190] `README`: Replace ${PROBE_RUN_CHIP} in code example 267 | - [#192] + [#194] `README`: Add installation instructions for Fedora and Ubuntu 268 | 269 | ### Fixes 270 | - [#206] Fix unwinding exceptions that push FPU registers onto the stack 271 | 272 | ### Internal improvements 273 | - [#197] Refactor "print backtrace" code 274 | - [#211] `mv backtrace.rs backtrace/mod.rs` 275 | 276 | [#193]: https://github.com/knurling-rs/probe-run/pull/193 277 | [#199]: https://github.com/knurling-rs/probe-run/pull/199 278 | [#200]: https://github.com/knurling-rs/probe-run/pull/200 279 | [#203]: https://github.com/knurling-rs/probe-run/pull/203 280 | [#204]: https://github.com/knurling-rs/probe-run/pull/204 281 | [#207]: https://github.com/knurling-rs/probe-run/pull/207 282 | [#190]: https://github.com/knurling-rs/probe-run/pull/190 283 | [#192]: https://github.com/knurling-rs/probe-run/pull/192 284 | [#194]: https://github.com/knurling-rs/probe-run/pull/194 285 | [#206]: https://github.com/knurling-rs/probe-run/pull/206 286 | [#209]: https://github.com/knurling-rs/probe-run/pull/209 287 | [#210]: https://github.com/knurling-rs/probe-run/pull/210 288 | [#197]: https://github.com/knurling-rs/probe-run/pull/197 289 | [#211]: https://github.com/knurling-rs/probe-run/pull/211 290 | 291 | ## [v0.2.2] - 2021-05-06 292 | 293 | ### Improvements 294 | 295 | - [#163] Report exit reason, make `backtrace` optional 296 | - [#171] Introduce even more verbose log level 297 | - [#174] Let developers skip defmt version check 298 | - [#179] Limit `backtrace` length, make limit configurable 299 | - [#184] Add some bounds checking to `unwinding` 300 | 301 | #### Docs 302 | 303 | - [#161] Remind the user that the `bench` profile should be also overridden 304 | - [#181] `README`: add copypasteable example how to run from repo 305 | - [#183] `README`: Add troubleshooting for use with RTIC 306 | 307 | ### Fixes 308 | 309 | - [#162] Remove `panic-probe` 310 | 311 | ### Internal Improvements 312 | 313 | - [#165] Various simplifications 314 | - [#175] Run `cargo fmt -- --check` in CI 315 | 316 | [#161]: https://github.com/knurling-rs/probe-run/pull/161 317 | [#162]: https://github.com/knurling-rs/probe-run/pull/162 318 | [#163]: https://github.com/knurling-rs/probe-run/pull/163 319 | [#165]: https://github.com/knurling-rs/probe-run/pull/165 320 | [#171]: https://github.com/knurling-rs/probe-run/pull/171 321 | [#174]: https://github.com/knurling-rs/probe-run/pull/174 322 | [#175]: https://github.com/knurling-rs/probe-run/pull/175 323 | [#179]: https://github.com/knurling-rs/probe-run/pull/179 324 | [#181]: https://github.com/knurling-rs/probe-run/pull/181 325 | [#184]: https://github.com/knurling-rs/probe-run/pull/184 326 | [#183]: https://github.com/knurling-rs/probe-run/pull/183 327 | 328 | ## [v0.2.1] - 2021-02-23 329 | 330 | - [#158] Fix Ctrl+C handling 331 | 332 | [#158]: https://github.com/knurling-rs/probe-run/pull/158 333 | 334 | ## [v0.2.0] - 2021-02-22 335 | 336 | ### New Features 337 | 338 | - [#153] Update to defmt 0.2.0 339 | - [#152] Allow selecting a probe by serial number 340 | - [#149] Update and deduplicate dependencies 341 | 342 | ### Fixes 343 | 344 | - [#141] Address Clippy lints 345 | 346 | [#141]: https://github.com/knurling-rs/probe-run/pull/141 347 | [#149]: https://github.com/knurling-rs/probe-run/pull/149 348 | [#152]: https://github.com/knurling-rs/probe-run/pull/152 349 | [#153]: https://github.com/knurling-rs/probe-run/pull/153 350 | 351 | ## [v0.1.9] - 2021-01-21 352 | 353 | ### Added 354 | 355 | - [#126] print a list of probes when multiple probes are found and none was selected 356 | - [#133] removes `supported defmt version: c4461eb1484...` from `-h` / ` --help` output 357 | 358 | [#126]: https://github.com/knurling-rs/probe-run/pull/126 359 | [#133]: https://github.com/knurling-rs/probe-run/pull/133 360 | 361 | ### Fixed 362 | 363 | - [#129] reject use of defmt logs and the `--no-flash` flag. 364 | - [#132] Make use of the new defmt-logger crate 365 | - [#134] updates `probe-run`'s `defmt` dependencies in order to make new features accessible 366 | 367 | [#129]: https://github.com/knurling-rs/probe-run/pull/129 368 | [#132]: https://github.com/knurling-rs/probe-run/pull/132 369 | [#134]: https://github.com/knurling-rs/probe-run/pull/134 370 | 371 | ## [v0.1.8] - 2020-12-11 372 | 373 | ### Added 374 | 375 | - [#119] `probe-run` has gained a `--connect-under-reset` command line flag. When used, the probe drives the NRST pin of the microcontroller to put it in reset state before establishing a SWD / JTAG connection with the device. 376 | 377 | [#119]: https://github.com/knurling-rs/probe-run/pull/119 378 | 379 | ### Fixed 380 | 381 | - [#117] wait for breakpoint before switching RTT from non-blocking mode to blocking mode. 382 | 383 | [#117]: https://github.com/knurling-rs/probe-run/pull/117 384 | 385 | ## [v0.1.7] - 2020-11-26 386 | 387 | ### Fixed 388 | 389 | - [#114] pin `hidapi` dependency to 1.2.3 to enable macOS builds 390 | - [#112] defmt decode errors are now reported to the user 391 | - [#110] colorize `assert_eq!` output 392 | 393 | [#114]: https://github.com/knurling-rs/probe-run/pull/114 394 | [#112]: https://github.com/knurling-rs/probe-run/pull/112 395 | [#110]: https://github.com/knurling-rs/probe-run/pull/110 396 | 397 | ## [v0.1.6] - 2020-11-23 398 | 399 | ### Fixed 400 | 401 | - [#109] `` is not printed twice in the backtrace when the firmware aborts. 402 | 403 | [#109]: https://github.com/knurling-rs/probe-run/pull/109 404 | 405 | ### Changed 406 | 407 | - [#108] `probe-rs` has been bumped to version 0.10. This should fix some ST-LINK bugs and expand device support. 408 | 409 | [#108]: https://github.com/knurling-rs/probe-run/pull/108 410 | 411 | ## [v0.1.5] - 2020-11-20 412 | 413 | - [#106] `probe-run` now reports the program size 414 | - [#105] `probe-run`'s `--defmt` flag is now optional. `probe-run` will auto-detect the use of the `defmt` crate so the flag is no longer needed. 415 | - [#259] building the crates.io version of `probe-run` no longer depends on the `git` command line tool (fixed [#256]) 416 | - [#264] `probe-run` doesn't panic if log message is not UTF-8 417 | 418 | [#106]: https://github.com/knurling-rs/probe-run/pull/106 419 | [#105]: https://github.com/knurling-rs/probe-run/pull/105 420 | [#259]: https://github.com/knurling-rs/defmt/pull/259 421 | [#264]: https://github.com/knurling-rs/defmt/pull/264 422 | 423 | ## [v0.1.4] - 2020-11-11 424 | 425 | ### Added 426 | 427 | - [#30] added a `--no-flash` flag to run a program without re-flashing it 428 | - [#40] added (`--help`) documentation to the many CLI flags 429 | - [#33] added canary-based stack overflow detection 430 | - [#38] added file location information to log messages 431 | - [#41] the `PROBE_RUN_CHIP` env variable can be used as an alternative to the `--chip` flag 432 | - [#49] `--list-probes` and `--probe` flags to list all probes and select a particular probe, respectively 433 | - [#55] added more precise stack overflow detection for apps linked with `flip-link` 434 | - [#57] added module location information to log messages 435 | - [#63] added file location information to the stack backtrace 436 | - [#83] added git info to the `--version` output 437 | - [#88] added `--speed` flag to set the frequency of the probe 438 | - [#98] the output of `--version` now includes the supported defmt version 439 | 440 | [#30]: https://github.com/knurling-rs/probe-run/pull/30 441 | [#33]: https://github.com/knurling-rs/probe-run/pull/33 442 | [#38]: https://github.com/knurling-rs/probe-run/pull/38 443 | [#40]: https://github.com/knurling-rs/probe-run/pull/40 444 | [#41]: https://github.com/knurling-rs/probe-run/pull/41 445 | [#49]: https://github.com/knurling-rs/probe-run/pull/49 446 | [#55]: https://github.com/knurling-rs/probe-run/pull/55 447 | [#57]: https://github.com/knurling-rs/probe-run/pull/57 448 | [#63]: https://github.com/knurling-rs/probe-run/pull/63 449 | [#83]: https://github.com/knurling-rs/probe-run/pull/83 450 | [#88]: https://github.com/knurling-rs/probe-run/pull/88 451 | [#98]: https://github.com/knurling-rs/probe-run/pull/98 452 | 453 | ### Fixed 454 | 455 | - [#28] notify the user ASAP that RTT logs were not found in the image 456 | - [#50] fixed a bug that was causing an infinite stack backtrace to be printed 457 | - [#51] fixed the handling of Ctrl-C 458 | - [#77] flush stdout after each write; fixes a bug where output was not printed until a newline was sent from the device using non-defmt-ed RTT 459 | 460 | [#28]: https://github.com/knurling-rs/probe-run/pull/28 461 | [#50]: https://github.com/knurling-rs/probe-run/pull/50 462 | [#51]: https://github.com/knurling-rs/probe-run/pull/51 463 | [#77]: https://github.com/knurling-rs/probe-run/pull/77 464 | 465 | ### Changed 466 | 467 | - [#25] increased RTT attach retries, which is sometimes needed for inter-operation with `rtt-target` 468 | - [#44] improve diagnostics when linker script is missing 469 | - [#53], [#60] the output format of logs 470 | - [#55], [#64] all hard faults make `probe-run` exit with non-zero exit code regardless of whether `panic-probe` was used or not 471 | - [#69] `probe-run` now changes the RTT mode to blocking at runtime, right after RAM initialization 472 | 473 | [#25]: https://github.com/knurling-rs/probe-run/pull/25 474 | [#44]: https://github.com/knurling-rs/probe-run/pull/44 475 | [#53]: https://github.com/knurling-rs/probe-run/pull/53 476 | [#55]: https://github.com/knurling-rs/probe-run/pull/55 477 | [#60]: https://github.com/knurling-rs/probe-run/pull/60 478 | [#64]: https://github.com/knurling-rs/probe-run/pull/64 479 | [#69]: https://github.com/knurling-rs/probe-run/pull/69 480 | 481 | ## [v0.1.3] - 2020-08-19 482 | 483 | ### Changed 484 | 485 | - Fixed outdated comment in readme 486 | 487 | ## [v0.1.2] - 2020-08-19 488 | 489 | ### Added 490 | 491 | - Support for the `thumbv7em-none-eabihf` target. 492 | 493 | ### Changed 494 | 495 | - Bumped the `probe-rs` dependency to 0.8.0 496 | - Cleaned up CLI documentation 497 | 498 | ## [v0.1.1] - 2020-08-17 499 | 500 | ### Added 501 | 502 | - Added setup instructions to check that there's enough debug info to make the unwinder worker 503 | 504 | ### Changed 505 | 506 | - Improved the error message produced when the unwinder fails 507 | 508 | ## v0.1.0 - 2020-08-14 509 | 510 | Initial release 511 | 512 | [Unreleased]: https://github.com/knurling-rs/probe-run/compare/v0.3.11...main 513 | [v0.3.11]: https://github.com/knurling-rs/probe-run/compare/v0.3.10...v0.3.11 514 | [v0.3.10]: https://github.com/knurling-rs/probe-run/compare/v0.3.9...v0.3.10 515 | [v0.3.9]: https://github.com/knurling-rs/probe-run/compare/v0.3.8...v0.3.9 516 | [v0.3.8]: https://github.com/knurling-rs/probe-run/compare/v0.3.7...v0.3.8 517 | [v0.3.7]: https://github.com/knurling-rs/probe-run/compare/v0.3.6...v0.3.7 518 | [v0.3.6]: https://github.com/knurling-rs/probe-run/compare/v0.3.5...v0.3.6 519 | [v0.3.5]: https://github.com/knurling-rs/probe-run/compare/v0.3.4...v0.3.5 520 | [v0.3.4]: https://github.com/knurling-rs/probe-run/compare/v0.3.3...v0.3.4 521 | [v0.3.3]: https://github.com/knurling-rs/probe-run/compare/v0.3.2...v0.3.3 522 | [v0.3.2]: https://github.com/knurling-rs/probe-run/compare/v0.3.1...v0.3.2 523 | [v0.3.1]: https://github.com/knurling-rs/probe-run/compare/v0.3.0...v0.3.1 524 | [v0.3.0]: https://github.com/knurling-rs/probe-run/compare/v0.2.5...v0.3.0 525 | [v0.2.5]: https://github.com/knurling-rs/probe-run/compare/v0.2.4...v0.2.5 526 | [v0.2.4]: https://github.com/knurling-rs/probe-run/compare/v0.2.3...v0.2.4 527 | [v0.2.3]: https://github.com/knurling-rs/probe-run/compare/v0.2.2...v0.2.3 528 | [v0.2.2]: https://github.com/knurling-rs/probe-run/compare/v0.2.1...v0.2.2 529 | [v0.2.1]: https://github.com/knurling-rs/probe-run/compare/v0.2.0...v0.2.1 530 | [v0.2.0]: https://github.com/knurling-rs/probe-run/compare/v0.1.9...v0.2.0 531 | [v0.1.9]: https://github.com/knurling-rs/probe-run/compare/v0.1.8...v0.1.9 532 | [v0.1.8]: https://github.com/knurling-rs/probe-run/compare/v0.1.7...v0.1.8 533 | [v0.1.7]: https://github.com/knurling-rs/probe-run/compare/v0.1.6...v0.1.7 534 | [v0.1.6]: https://github.com/knurling-rs/probe-run/compare/v0.1.5...v0.1.6 535 | [v0.1.5]: https://github.com/knurling-rs/probe-run/compare/v0.1.4...v0.1.5 536 | [v0.1.4]: https://github.com/knurling-rs/probe-run/compare/v0.1.3...v0.1.4 537 | [v0.1.3]: https://github.com/knurling-rs/probe-run/compare/v0.1.2...v0.1.3 538 | [v0.1.2]: https://github.com/knurling-rs/probe-run/compare/v0.1.1...v0.1.2 539 | [v0.1.1]: https://github.com/knurling-rs/probe-run/compare/v0.1.0...v0.1.1 540 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["The Knurling-rs developers"] 3 | categories = ["command-line-utilities", "embedded", "no-std"] 4 | description = "Runs embedded programs just like native ones" 5 | edition = "2021" 6 | keywords = ["knurling", "cargo-runner"] 7 | license = "MIT OR Apache-2.0" 8 | name = "probe-run" 9 | readme = "README.md" 10 | repository = "https://github.com/knurling-rs/probe-run" 11 | version = "0.3.11" 12 | 13 | [dependencies] 14 | addr2line = { version = "0.20", default-features = false, features = [ 15 | "fallible-iterator", 16 | "std-object", 17 | "rustc-demangle", 18 | "cpp_demangle", 19 | ] } 20 | anyhow = "1" 21 | clap = { version = "4.0", features = ["derive", "env"] } 22 | colored = "2" 23 | defmt-decoder = { version = "=0.3.8", features = ["unstable"] } 24 | gimli = { version = "0.27", default-features = false } 25 | git-version = "0.3" 26 | log = "0.4" 27 | object = { version = "0.31", default-features = false } 28 | probe-rs = "0.20" 29 | signal-hook = "0.3" 30 | 31 | [dev-dependencies] 32 | dirs = "5" 33 | # insta 1.12 introduces breaking changes to the snapshot tests. it's fixable, but takes time. 34 | insta = "~1.11" 35 | os_pipe = "1.0" 36 | pretty_assertions = "1" 37 | rstest = { version = "0.18", default-features = false } 38 | nix = "0.26" 39 | serial_test = { version = "2", default-features = false } 40 | 41 | [features] 42 | ftdi = ["probe-rs/ftdi"] 43 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > ⚠️ As of 11.10.2023 `probe-run` is in maintainance mode. We recommend everyone to switch to `probe-rs`. Read following article on the why and on how to migrate: https://ferrous-systems.com/blog/probe-run-deprecation/ 2 | 3 | # `probe-run` 4 | 5 | > Runs embedded programs just like native ones 6 | 7 | `probe-run` is a custom Cargo runner that transparently runs Rust firmware on an embedded device. 8 | 9 | `probe-run` is powered by [`probe-rs`] and thus supports all the devices and probes supported by 10 | `probe-rs`. 11 | 12 | [`probe-rs`]: https://probe.rs/ 13 | 14 | ## Features 15 | 16 | * Acts as a Cargo runner, integrating into `cargo run`. 17 | * Displays program output streamed from the device via RTT. 18 | * Exits the firmware and prints a stack backtrace on hardware breakpoints (e.g. `bkpt`), Rust panics and unrecoverable hardware exceptions (e.g. `HardFault`). 19 | 20 | ## Installation 21 | 22 | To install `probe-run`, use `cargo install probe-run`. 23 | 24 | On Linux, you might have to install `libudev` and `libusb` from your package 25 | manager before installing `probe-run`. 26 | 27 | ``` console 28 | # ubuntu 29 | $ sudo apt install -y libusb-1.0-0-dev libudev-dev 30 | 31 | # fedora 32 | $ sudo dnf install -y libusbx-devel systemd-devel 33 | ``` 34 | 35 | ### Using the latest `probe-rs` 36 | 37 | `probe-run` inherits device support from the `probe-rs` library. 38 | If your target chip does **not** appear in the output of `probe-run --list-chips` it could be that: 39 | 1. The latest *git* version `probe-rs` supports your chip but the latest version on crates.io does not. 40 | You could try building `probe-run` against the latest `probe-rs` version; see instructions below. 41 | 2. `probe-rs` does not yet support the device. 42 | You'll need to request support in [the `probe-rs` issue tracker](https://github.com/probe-rs/probe-rs/issues). 43 | 44 | To build `probe-run` against the latest git version `probe-rs` and install it follow these steps: 45 | 46 | ``` console 47 | $ # clone the latest version of probe-run; line below uses version v0.3.3 48 | $ git clone --depth 1 --branch v0.3.3 https://github.com/knurling-rs/probe-run 49 | 50 | $ cd probe-run 51 | 52 | $ # modify Cargo.toml to use the git version of probe-rs 53 | $ # append these lines to Cargo.toml; command below is UNIX-y 54 | $ cat >> Cargo.toml << STOP 55 | [patch.crates-io] 56 | probe-rs = { git = "https://github.com/probe-rs/probe-rs" } 57 | STOP 58 | 59 | $ # install this patched version 60 | $ cargo install --path . 61 | ``` 62 | 63 | Note that you may need to modify `probe-rs-rtt` and/or `probe-run` itself to get `probe-run` to compile. 64 | As we only support the crates.io version of `probe-rs` in the unmodified `Cargo.toml` we cannot provide further assistance in that case. 65 | 66 | ## Setup 67 | 68 | **NOTE** for *new* Cargo projects we recommend starting from [`app-template`](https://github.com/knurling-rs/app-template) 69 | 70 | ### 1. Set the Cargo runner 71 | 72 | The recommend way to use `probe-run` is to set as the Cargo runner of your application. 73 | 74 | Add these two lines to your Cargo configuration file (`.cargo/config.toml`) and set the particular `--chip` value for your target. In this case it is `nRF52840_xxAA` for the [nRF52840]: 75 | 76 | ``` toml 77 | [target.'cfg(all(target_arch = "arm", target_os = "none"))'] 78 | runner = "probe-run --chip nRF52840_xxAA" 79 | # ^^^^^^^^^^^^^ 80 | ``` 81 | 82 | To list all supported chips run `probe-run --list-chips`. 83 | 84 | #### **1.1 Env variable** 85 | 86 | To support multiple devices, or permit overriding default behavior, you may prefer to: 87 | 1. set the `${PROBE_RUN_CHIP}` environment variable, and 88 | 2. set `runner` (or `CARGO_TARGET_${TARGET_ARCH}_RUNNER`) to `probe-run`: 89 | 90 | ``` toml 91 | [target.'cfg(all(target_arch = "arm", target_os = "none"))'] 92 | runner = "probe-run" 93 | ``` 94 | 95 | #### **1.2 Multiple probes** 96 | 97 | If you have several probes connected, you can specify which one to use by adding the `--probe` option to the `runner` or setting the `${PROBE_RUN_PROBE}` environment variable with a value containing either `${VID}:${PID}` or `${VID}:${PID}:${SERIAL}`: 98 | 99 | ```console 100 | // --probe 101 | $ probe-run --probe '0483:3748' --chip ${PROBE_RUN_CHIP} 102 | 103 | // PROBE_RUN_PROBE 104 | $ PROBE_RUN_PROBE='1366:0101:123456' cargo run 105 | ``` 106 | 107 | To list all connected probes, run `probe-run --list-probes`. 108 | 109 | [nRF52840]: https://www.nordicsemi.com/Products/Low-power-short-range-wireless/nRF52840 110 | 111 | ### 2. Enable debug info 112 | 113 | Next check that debug info is enabled for all profiles. 114 | If you are using the `cortex-m-quickstart` template then this is already the case. 115 | If not check or add these lines to `Cargo.toml`. 116 | 117 | ``` toml 118 | [dependencies] 119 | ... 120 | panic-probe = { version = "0.2", features = ["print-rtt"] } 121 | 122 | # Cargo.toml 123 | [profile.dev] 124 | debug = 1 # default is `true`; not needed if not already overridden 125 | 126 | [profile.release] 127 | debug = 1 # default is `false`; using `true` is also OK as symbols reside on the host, not the target 128 | ``` 129 | 130 | ### 3. Look out for old dependencies 131 | 132 | The `cortex-m` dependency must be version 0.6.3 or newer. 133 | Older versions are not supported. 134 | Check your `Cargo.lock` for old versions. 135 | Run `cargo update` to update the `cortex-m` dependency if an older one appears in `Cargo.lock`. 136 | 137 | ### 4. Run 138 | 139 | You are all set. 140 | You can now run your firmware using `cargo run`. 141 | For example, 142 | 143 | ``` rust 144 | use cortex_m::asm; 145 | use cortex_m_rt::entry; 146 | use panic_probe as _; 147 | use rtt_target::rprintln; 148 | 149 | #[entry] 150 | fn main() -> ! { 151 | rtt_init_print!(); // You may prefer to initialize another way 152 | rprintln!("Hello, world!"); 153 | loop { asm::bkpt() } 154 | } 155 | ``` 156 | 157 | would output 158 | 159 | ``` console 160 | $ cargo run --bin hello 161 | Finished dev [unoptimized + debuginfo] target(s) in 0.07s 162 | Running `probe-run --chip nRF52840_xxAA target/thumbv7em-none-eabihf/debug/hello` 163 | (HOST) INFO flashing program (30.22 KiB) 164 | (HOST) INFO success! 165 | ──────────────────────────────────────────────────────────────────────────────── 166 | INFO:hello -- Hello, world! 167 | ──────────────────────────────────────────────────────────────────────────────── 168 | (HOST) INFO exiting because the device halted. 169 | To see the backtrace at the exit point repeat this run with 170 | `probe-run --chip nRF52840_xxAA target/thumbv7em-none-eabihf/debug/hello --force-backtrace` 171 | ``` 172 | 173 | ## Stack backtraces 174 | 175 | When the device raises a hard fault exception, indicating e.g. a panic or a stack overflow, `probe-run` will print a backtrace and exit with a non-zero exit code. 176 | 177 | This backtrace follows the format of the `std` backtraces you get from `std::panic!` but includes 178 | `` lines to indicate where an exception/interrupt occurred. 179 | 180 | ``` rust 181 | #![no_main] 182 | #![no_std] 183 | 184 | use cortex_m::asm; 185 | #[entry] 186 | fn main() -> ! { 187 | // trigger a hard fault exception with the UDF instruction. 188 | asm::udf() 189 | } 190 | ``` 191 | 192 | ``` console 193 | Finished dev [optimized + debuginfo] target(s) in 0.04s 194 | Running `probe-run --chip nRF52840_xxAA target/thumbv7em-none-eabihf/debug/hard-fault` 195 | (HOST) INFO flashing program (30.08 KiB) 196 | (HOST) INFO success! 197 | ──────────────────────────────────────────────────────────────────────────────── 198 | stack backtrace: 199 | 0: HardFaultTrampoline 200 | 201 | 1: __udf 202 | 2: cortex_m::asm::udf 203 | at /<...>/cortex-m-0.6.4/src/asm.rs:104 204 | 3: panic::__cortex_m_rt_main 205 | at src/bin/hard-fault.rs:12 206 | 4: main 207 | at src/bin/hard-fault.rs:8 208 | 5: ResetTrampoline 209 | at /<...>3/cortex-m-rt-0.6.13/src/lib.rs:547 210 | 6: Reset 211 | at /<...>/cortex-m-rt-0.6.13/src/lib.rs:550 212 | ``` 213 | 214 | If we look at the return code emitted by this `cargo run`, we'll see that it is non-0: 215 | 216 | ```console 217 | $ echo $? 218 | 134 219 | ``` 220 | 221 | ⚠️ **NOTE** when you run your application with `probe-run`, the `HardFault` handler (default or user-defined) will *NOT* be executed. 222 | 223 | ### Backtrace options 224 | #### --backtrace 225 | 226 | The `--backtrace` flag is optional and can get passed the following values: 227 | 228 | * `--backtrace=always` - forced backtrace (if you'd like to see a backtrace at the end of successful program run) 229 | * `--backtrace=never` - suppresed backtrace 230 | * `--backtrace=auto` - default, shows a backtrace if the program panics or the stack overflows 231 | 232 | Run it like this (example for a forced backtrace): 233 | 234 | ``` console 235 | $ cargo run --bin hello --backtrace=always 236 | ``` 237 | 238 | #### --backtrace-limit 239 | 240 | The `--backtrace-limit` flag is optional and defaults to 50. It is possible to set any number. 241 | 242 | `--backtrace-limit=0` is accepted and means "no limit". 243 | 244 | To show a shortened backtrace showing 5 frames, run: 245 | 246 | ``` console 247 | $ cargo run --bin panic --backtrace-limit=5 248 | ``` 249 | 250 | Note: if `--backtrace=never` is set, setting `--backtrace-limit` has no effect. 251 | 252 | ## Troubleshooting 253 | 254 | ### "Error: no probe was found." 255 | 256 | First, check your hardware: 257 | 258 | - make sure that your development board has an on-board *hardware* debugger. 259 | If it doesn't, you'll need a separate hardware debugger that works with the JTAG or SWD interface. 260 | Some boards have a USB micro-B or Type-C connector but only come with *bootloader* firmware that lets you load new program over USB Mass Storage, instead of having a dedicated on-board JTAG or SWD to USB chip; 261 | `probe-run` cannot be used with these boards. 262 | - make sure that it is connected to the right port on your development board 263 | - make sure that you are using a **data** cable– some cables are built for charging only! When in doubt, try using a different cable. 264 | - make sure you have the right drivers for the debugger installed (st-link or j-link) 265 | 266 | If this doesn't resolve the issue, try the following: 267 | 268 | #### [Linux only] udev rules haven't been set 269 | 270 | Check if your device shows up in `lsusb`: 271 | 272 | ```console 273 | $ lsusb 274 | Bus 001 Device 008: ID 1366:1015 SEGGER J-Link 275 | ``` 276 | 277 | If your device shows up like in the example, skip to the next troubleshooting section 278 | 279 | **If it doesn't show up**, you need to give your system permission to access the device as a non-root user so that `probe-run` can find your device. 280 | 281 | In order to grant these permissions, you'll need to add a new set of udev rules. 282 | 283 | To learn how to do this for the nRF52840 Development Kit, check out the [installation instructions](https://embedded-trainings.ferrous-systems.com/installation.html?highlight=udev#linux-only-usb) in our embedded training materials. 284 | 285 | afterwards, your device should show up in `probe-run --list-probes` similar to this: 286 | 287 | ```console 288 | $ probe-run --list-probes 289 | The following devices were found: 290 | [0]: J-Link (J-Link) (VID: 1366, PID: 1015, Serial: , JLink) 291 | ``` 292 | 293 | #### No external or on-board debugger present 294 | 295 | To use `probe-run` you need a "probe" (also known as "debugger") that sits between your PC and the microcontroller. 296 | 297 | Most development boards, especially the bigger ones, have a probe "on-board": If the product description of your board mentions something like a J-Link or ST-Link on-board debugger you're good to go. With these boards, all you need to do is connect your PC to the dev board using a USB cable you are all set to use `probe-run`! 298 | 299 | If this is *not* the case for your board, check in the datasheet if it exposes exposes SWD or JTAG pins. 300 | If they are exposed, you can connect a "stand alone" probe device to the microcontroller and then connect the probe to your PC via USB. Some examples of stand alone probes are: the ST-Link and the J-Link. 301 | 302 | Note that this may involve some soldering if your board does not come with a pre-attached header to plug your debugger into. 303 | 304 | ### Error: RTT up channel 0 not found 305 | 306 | This may instead present as `Error: RTT control block not found in target memory.` 307 | 308 | Your code, or a library you're using (e.g. RTIC) might be putting your CPU to 309 | sleep when idle. You can verify that this is the problem by busy looping instead 310 | of sleeping. When using RTIC, this can be achieved by adding an idle handler to 311 | your app: 312 | 313 | ```rust 314 | #[idle] 315 | fn idle(_ctx: idle::Context) -> ! { 316 | loop {} 317 | } 318 | ``` 319 | 320 | Assuming you'd like to still sleep in order to save power, you need to configure 321 | your microcontroller so that RTT can still be handled even when the CPU is 322 | sleeping. How to do this varies between microcontrollers. 323 | 324 | On an STM32G0 running RTIC it can be done by amending your init function to set 325 | the `dmaen` bit on `RCC.ahbenr`. e.g.: 326 | 327 | ```rust 328 | #[init] 329 | fn init(ctx: init::Context) -> init::LateResources { 330 | ctx.device.RCC.ahbenr.write(|w| w.dmaen().set_bit()); 331 | ... 332 | } 333 | ``` 334 | 335 | ### defmt version mismatch 336 | 337 | #### end-user 338 | Follow the instructions in the error message to resolve the mismatch. 339 | 340 | #### developer 341 | If you are hacking around with `probe-run`, you can disable the version check by setting the `PROBE_RUN_IGNORE_VERSION` environment variable to `true` or `1` at runtime. 342 | 343 | 344 | ## Developer Information 345 | 346 | ### running your locally modified `probe-run` 347 | 348 | For easier copy-paste-ability, here's an example how to try out your local `probe_run` modifications. 349 | 350 | ```console 351 | $ cd probe-run/ 352 | $ PROBE_RUN_IGNORE_VERSION=1 cargo run -- --chip nRF52840_xxAA --backtrace-limit=10 hello 353 | ˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆ ˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆˆ ˆˆˆˆˆ 354 | environment variables extra flags binary to be 355 | (optional) (optional) flashed & run 356 | ``` 357 | 358 | ### running snapshot tests 359 | 360 | To check whether your change has altered probe-run in unexpected ways, please run the snapshot tests in `tests` before opening a PR if at all possible. 361 | 362 | You can do so by connecting a nrf52840 Development Kit and running 363 | 364 | ```console 365 | $ cargo test -- --ignored 366 | ``` 367 | 368 | ## Support Us 369 | 370 | `probe-run` is part of the [Knurling] project, [Ferrous Systems]' effort at 371 | improving tooling used to develop for embedded systems. 372 | 373 | If you think that our work is useful, consider sponsoring it via [GitHub 374 | Sponsors]. 375 | 376 | ## License 377 | 378 | Licensed under either of 379 | 380 | - Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or 381 | http://www.apache.org/licenses/LICENSE-2.0) 382 | 383 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 384 | 385 | at your option. 386 | 387 | ### Contribution 388 | 389 | Unless you explicitly state otherwise, any contribution intentionally submitted 390 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 391 | licensed as above, without any additional terms or conditions. 392 | 393 | [Knurling]: https://knurling.ferrous-systems.com 394 | [Ferrous Systems]: https://ferrous-systems.com/ 395 | [GitHub Sponsors]: https://github.com/sponsors/knurling-rs 396 | -------------------------------------------------------------------------------- /src/backtrace/mod.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use probe_rs::Core; 4 | use signal_hook::consts::signal; 5 | 6 | use crate::{cli::Opts, elf::Elf, target_info::TargetInfo}; 7 | 8 | mod pp; 9 | mod symbolicate; 10 | mod unwind; 11 | 12 | #[derive(PartialEq, Eq)] 13 | pub enum BacktraceOptions { 14 | Auto, 15 | Never, 16 | Always, 17 | } 18 | 19 | impl From<&String> for BacktraceOptions { 20 | fn from(item: &String) -> Self { 21 | match item.as_str() { 22 | "auto" | "Auto" => BacktraceOptions::Auto, 23 | "never" | "Never" => BacktraceOptions::Never, 24 | "always" | "Always" => BacktraceOptions::Always, 25 | _ => panic!("options for `--backtrace` are `auto`, `never`, `always`."), 26 | } 27 | } 28 | } 29 | 30 | pub struct Settings { 31 | pub backtrace_limit: u32, 32 | pub backtrace: BacktraceOptions, 33 | pub current_dir: PathBuf, 34 | pub halted_due_to_signal: bool, 35 | pub include_addresses: bool, 36 | pub shorten_paths: bool, 37 | pub stack_overflow: bool, 38 | } 39 | 40 | impl Settings { 41 | pub fn new( 42 | current_dir: PathBuf, 43 | halted_due_to_signal: bool, 44 | opts: &Opts, 45 | stack_overflow: bool, 46 | ) -> Self { 47 | Self { 48 | backtrace_limit: opts.backtrace_limit, 49 | backtrace: (&opts.backtrace).into(), 50 | current_dir, 51 | halted_due_to_signal, 52 | include_addresses: opts.verbose > 0, 53 | shorten_paths: opts.shorten_paths, 54 | stack_overflow, 55 | } 56 | } 57 | 58 | fn panic_present(&self) -> bool { 59 | self.stack_overflow || self.halted_due_to_signal 60 | } 61 | } 62 | 63 | /// (virtually) unwinds the target's program and prints its backtrace 64 | pub fn print( 65 | core: &mut Core, 66 | elf: &Elf, 67 | target_info: &TargetInfo, 68 | settings: &mut Settings, 69 | ) -> anyhow::Result { 70 | let mut unwind = unwind::target(core, elf, target_info); 71 | let frames = symbolicate::frames(&unwind.raw_frames, &settings.current_dir, elf); 72 | 73 | let contains_exception = unwind 74 | .raw_frames 75 | .iter() 76 | .any(|raw_frame| raw_frame.is_exception()); 77 | 78 | let print_backtrace = match settings.backtrace { 79 | BacktraceOptions::Never => false, 80 | BacktraceOptions::Always => true, 81 | BacktraceOptions::Auto => { 82 | settings.panic_present() 83 | || unwind.outcome == Outcome::StackOverflow 84 | || unwind.corrupted 85 | || contains_exception 86 | } 87 | }; 88 | 89 | // `0` disables the limit and we want to show _all_ frames 90 | if settings.backtrace_limit == 0 { 91 | settings.backtrace_limit = frames.len() as u32; 92 | } 93 | 94 | if print_backtrace && settings.backtrace_limit > 0 { 95 | pp::backtrace(&frames, settings)?; 96 | 97 | if unwind.corrupted { 98 | log::warn!("call stack was corrupted; unwinding could not be completed"); 99 | } 100 | if let Some(err) = unwind.processing_error { 101 | log::error!( 102 | "error occurred during backtrace creation: {err:?}\n \ 103 | the backtrace may be incomplete.", 104 | ); 105 | } 106 | } 107 | 108 | // if general outcome was OK but the user ctrl-c'ed, that overrides our outcome 109 | if settings.halted_due_to_signal && unwind.outcome == Outcome::Ok { 110 | unwind.outcome = Outcome::CtrlC 111 | } 112 | 113 | Ok(unwind.outcome) 114 | } 115 | 116 | /// Target program outcome 117 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 118 | pub enum Outcome { 119 | HardFault, 120 | Ok, 121 | StackOverflow, 122 | /// Control-C was pressed 123 | CtrlC, 124 | } 125 | 126 | impl Outcome { 127 | pub fn log(&self) { 128 | match self { 129 | Outcome::StackOverflow => log::error!("the program has overflowed its stack"), 130 | Outcome::HardFault => log::error!("the program panicked"), 131 | Outcome::Ok => log::info!("device halted without error"), 132 | Outcome::CtrlC => log::info!("device halted by user"), 133 | } 134 | } 135 | } 136 | 137 | // Convert `Outcome` to an exit code. 138 | impl From for i32 { 139 | fn from(outcome: Outcome) -> i32 { 140 | match outcome { 141 | Outcome::HardFault | Outcome::StackOverflow => signal::SIGABRT, 142 | Outcome::CtrlC => signal::SIGINT, 143 | Outcome::Ok => 0, 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/backtrace/pp.rs: -------------------------------------------------------------------------------- 1 | //! Pretty printing the backtrace 2 | 3 | use std::{ 4 | borrow::Cow, 5 | fmt::Write, 6 | io::{self, Write as _}, 7 | }; 8 | 9 | use colored::Colorize as _; 10 | 11 | use crate::dep; 12 | 13 | use super::{symbolicate::Frame, Settings}; 14 | 15 | /// Pretty prints processed backtrace frames up to `backtrace_limit` 16 | pub fn backtrace(frames: &[Frame], settings: &Settings) -> io::Result<()> { 17 | let mut stderr = io::stderr().lock(); 18 | writeln!(stderr, "{}", "stack backtrace:".dimmed())?; 19 | 20 | let mut frame_index = 0; 21 | for frame in frames { 22 | match frame { 23 | Frame::Exception => writeln!(stderr, " ")?, 24 | Frame::Subroutine(subroutine) => { 25 | let is_local_function = subroutine 26 | .location 27 | .as_ref() 28 | .map(|location| location.path_is_relative) 29 | .unwrap_or(false); 30 | 31 | let mut line = format!("{frame_index:>4}:"); 32 | if settings.include_addresses || subroutine.name.is_none() { 33 | write!(line, " {:#010x} @", subroutine.pc).unwrap(); 34 | } 35 | write!( 36 | line, 37 | " {}", 38 | subroutine.name.as_deref().unwrap_or("") 39 | ) 40 | .unwrap(); 41 | 42 | let colorized_line = if is_local_function { 43 | line.bold() 44 | } else { 45 | line.normal() 46 | }; 47 | writeln!(stderr, "{colorized_line}")?; 48 | 49 | if let Some(location) = &subroutine.location { 50 | let dep_path = dep::Path::from_std_path(&location.path); 51 | 52 | let path = if settings.shorten_paths { 53 | dep_path.format_short() 54 | } else { 55 | dep_path.format_highlight() 56 | }; 57 | 58 | let line = location.line; 59 | let column = location 60 | .column 61 | .map(|column| Cow::Owned(format!(":{column}"))) 62 | .unwrap_or(Cow::Borrowed("")); 63 | 64 | writeln!(stderr, " at {path}:{line}{column}")?; 65 | } 66 | 67 | frame_index += 1; 68 | 69 | if frame_index >= settings.backtrace_limit { 70 | log::warn!( 71 | "maximum backtrace length of {} reached; cutting off the rest.", 72 | settings.backtrace_limit 73 | ); 74 | log::warn!("note: re-run with `--max-backtrace-len=` to extend this limit"); 75 | 76 | break; 77 | } 78 | } 79 | } 80 | } 81 | 82 | Ok(()) 83 | } 84 | -------------------------------------------------------------------------------- /src/backtrace/symbolicate.rs: -------------------------------------------------------------------------------- 1 | //! Turns PC addresses into function names and locations 2 | 3 | use std::{ 4 | collections::HashSet, 5 | path::{Path, PathBuf}, 6 | rc::Rc, 7 | }; 8 | 9 | use addr2line::fallible_iterator::FallibleIterator as _; 10 | use gimli::{EndianReader, RunTimeEndian}; 11 | use object::{Object as _, SymbolMap, SymbolMapName}; 12 | 13 | use crate::{cortexm, elf::Elf}; 14 | 15 | use super::unwind::RawFrame; 16 | 17 | pub fn frames(raw_frames: &[RawFrame], current_dir: &Path, elf: &Elf) -> Vec { 18 | let mut frames = vec![]; 19 | 20 | let symtab = elf.symbol_map(); 21 | let addr2line = addr2line::Context::new(&**elf).ok(); 22 | 23 | for raw_frame in raw_frames { 24 | match raw_frame { 25 | RawFrame::Exception => frames.push(Frame::Exception), 26 | 27 | RawFrame::Subroutine { pc } => { 28 | for subroutine in Subroutine::from_pc( 29 | *pc, 30 | addr2line.as_ref(), 31 | &elf.live_functions, 32 | current_dir, 33 | &symtab, 34 | ) { 35 | frames.push(Frame::Subroutine(subroutine)) 36 | } 37 | } 38 | } 39 | } 40 | 41 | frames 42 | } 43 | 44 | /// Processed frame 45 | #[derive(Debug)] 46 | pub enum Frame { 47 | Exception, 48 | Subroutine(Subroutine), 49 | } 50 | 51 | /// "Symbolicated" and de-inlined subroutine frame 52 | #[derive(Debug)] 53 | pub struct Subroutine { 54 | pub name: Option, 55 | pub pc: u32, 56 | pub location: Option, 57 | } 58 | 59 | type A2lContext = addr2line::Context>>; 60 | 61 | impl Subroutine { 62 | fn from_pc( 63 | pc: u32, 64 | addr2line: Option<&A2lContext>, 65 | live_functions: &HashSet<&str>, 66 | current_dir: &Path, 67 | symtab: &SymbolMap, 68 | ) -> Vec { 69 | addr2line 70 | .and_then(|addr2line| { 71 | Self::from_debuginfo(pc, addr2line, live_functions, current_dir, symtab) 72 | }) 73 | .unwrap_or_else(|| vec![Self::from_symtab(pc, symtab)]) 74 | } 75 | 76 | fn from_debuginfo( 77 | pc: u32, 78 | addr2line: &A2lContext, 79 | live_functions: &HashSet<&str>, 80 | current_dir: &Path, 81 | symtab: &SymbolMap, 82 | ) -> Option> { 83 | let frames = addr2line 84 | .find_frames(pc as u64) 85 | .skip_all_loads() 86 | .ok()? 87 | .collect::>() 88 | .ok()?; 89 | 90 | let top_subroutine = frames.last(); 91 | 92 | let has_valid_debuginfo = if let Some(function) = 93 | top_subroutine.and_then(|subroutine| subroutine.function.as_ref()) 94 | { 95 | live_functions.contains(&*function.raw_name().ok()?) 96 | } else { 97 | false 98 | }; 99 | 100 | if !has_valid_debuginfo { 101 | return None; 102 | } 103 | 104 | let mut subroutines = vec![]; 105 | 106 | for frame in &frames { 107 | let demangled_name = frame 108 | .function 109 | .as_ref() 110 | .and_then(|function| { 111 | let demangled = function.demangle(); 112 | log::trace!( 113 | "demangle {:?} (language={:X?}) -> {demangled:?}", 114 | function.raw_name(), 115 | function.language, 116 | ); 117 | demangled.ok() 118 | }) 119 | .map(|cow| cow.into_owned()); 120 | 121 | // XXX if there was inlining AND there's no function name info we'll report several 122 | // frames with the same PC 123 | let name = demangled_name.or_else(|| name_from_symtab(pc, symtab)); 124 | 125 | let location = if let Some((file, line, column)) = 126 | frame.location.as_ref().and_then(|loc| { 127 | loc.file 128 | .and_then(|file| loc.line.map(|line| (file, line, loc.column))) 129 | }) { 130 | let fullpath = Path::new(file); 131 | let (path, is_local) = if let Ok(relpath) = fullpath.strip_prefix(current_dir) { 132 | (relpath, true) 133 | } else { 134 | (fullpath, false) 135 | }; 136 | 137 | Some(Location { 138 | column, 139 | path_is_relative: is_local, 140 | line, 141 | path: path.to_owned(), 142 | }) 143 | } else { 144 | None 145 | }; 146 | 147 | subroutines.push(Subroutine { name, pc, location }) 148 | } 149 | 150 | Some(subroutines) 151 | } 152 | 153 | fn from_symtab(pc: u32, symtab: &SymbolMap) -> Subroutine { 154 | Subroutine { 155 | name: name_from_symtab(pc, symtab), 156 | pc, 157 | location: None, 158 | } 159 | } 160 | } 161 | 162 | fn name_from_symtab(pc: u32, symtab: &SymbolMap) -> Option { 163 | // the .symtab appears to use address ranges that have their thumb bits set (e.g. 164 | // `0x101..0x200`). Passing the `pc` with the thumb bit cleared (e.g. `0x100`) to the 165 | // lookup function sometimes returns the *previous* symbol. Work around the issue by 166 | // setting `pc`'s thumb bit before looking it up 167 | let address = cortexm::set_thumb_bit(pc) as u64; 168 | 169 | symtab 170 | .get(address) 171 | .map(|symbol| addr2line::demangle_auto(symbol.name().into(), None).into_owned()) 172 | } 173 | 174 | #[derive(Debug)] 175 | pub struct Location { 176 | pub column: Option, 177 | pub path_is_relative: bool, 178 | pub line: u32, 179 | pub path: PathBuf, 180 | } 181 | -------------------------------------------------------------------------------- /src/backtrace/unwind.rs: -------------------------------------------------------------------------------- 1 | //! unwind target's program 2 | 3 | use anyhow::{anyhow, Context as _}; 4 | use gimli::{ 5 | BaseAddresses, CieOrFde, DebugFrame, FrameDescriptionEntry, Reader, UnwindContext, 6 | UnwindSection as _, 7 | }; 8 | use probe_rs::{config::RamRegion, Core}; 9 | 10 | use crate::{ 11 | backtrace::Outcome, 12 | cortexm, 13 | elf::Elf, 14 | registers::{self, Registers}, 15 | stacked::Stacked, 16 | target_info::TargetInfo, 17 | }; 18 | 19 | fn missing_debug_info(pc: u32) -> String { 20 | format!("debug information for address {pc:#x} is missing. Likely fixes: 21 | 1. compile the Rust code with `debug = 1` or higher. This is configured in the `profile.{{release,bench}}` sections of Cargo.toml (`profile.{{dev,test}}` default to `debug = 2`) 22 | 2. use a recent version of the `cortex-m` crates (e.g. cortex-m 0.6.3 or newer). Check versions in Cargo.lock 23 | 3. if linking to C code, compile the C code with the `-g` flag") 24 | } 25 | 26 | /// Virtually* unwinds the target's program 27 | /// \* destructors are not run 28 | /// 29 | /// This returns as much info as could be collected, even if the collection is interrupted by an error. 30 | /// If an error occurred during processing, it is stored in `Output::processing_error`. 31 | pub fn target(core: &mut Core, elf: &Elf, target_info: &TargetInfo) -> Output { 32 | let mut output = Output { 33 | corrupted: true, 34 | outcome: Outcome::Ok, 35 | raw_frames: vec![], 36 | processing_error: None, 37 | }; 38 | 39 | /// Returns all info collected until the error occurred and puts the error into `processing_error` 40 | macro_rules! unwrap_or_return_output { 41 | ( $e:expr ) => { 42 | match $e { 43 | Ok(x) => x, 44 | Err(err) => { 45 | output.processing_error = Some(anyhow!(err)); 46 | return output; 47 | } 48 | } 49 | }; 50 | } 51 | 52 | let mut pc = unwrap_or_return_output!(core.read_core_reg(registers::PC)); 53 | let sp = unwrap_or_return_output!(core.read_core_reg(registers::SP)); 54 | let lr = unwrap_or_return_output!(core.read_core_reg(registers::LR)); 55 | let base_addresses = BaseAddresses::default(); 56 | let mut unwind_context = UnwindContext::new(); 57 | let mut registers = Registers::new(lr, sp, core); 58 | let active_ram_region = &target_info.active_ram_region; 59 | 60 | loop { 61 | if let Some(outcome) = 62 | check_hard_fault(pc, &elf.vector_table, &mut output, sp, active_ram_region) 63 | { 64 | output.outcome = outcome; 65 | } 66 | 67 | output.raw_frames.push(RawFrame::Subroutine { pc }); 68 | 69 | let fde = unwrap_or_return_output!(find_fde(&elf.debug_frame, &base_addresses, pc)); 70 | 71 | let uwt_row = unwrap_or_return_output!(fde 72 | .unwind_info_for_address( 73 | &elf.debug_frame, 74 | &base_addresses, 75 | &mut unwind_context, 76 | pc.into() 77 | ) 78 | .with_context(|| missing_debug_info(pc))); 79 | 80 | log::trace!("uwt row for pc {pc:#010x}: {uwt_row:?}"); 81 | 82 | let cfa_changed = unwrap_or_return_output!(registers.update_cfa(uwt_row.cfa())); 83 | 84 | for (reg, rule) in uwt_row.registers() { 85 | unwrap_or_return_output!(registers.update(reg, rule)); 86 | } 87 | 88 | let lr = unwrap_or_return_output!(registers.get(registers::LR)); 89 | 90 | log::debug!("LR={lr:#010X} PC={pc:#010X}"); 91 | 92 | // Link Register contains an EXC_RETURN value. This deliberately also includes 93 | // invalid combinations of final bits 0-4 to prevent futile backtrace re-generation attempts 94 | let exception_entry = lr >= cortexm::EXC_RETURN_MARKER; 95 | 96 | let program_counter_changed = !cortexm::subroutine_eq(lr, pc); 97 | 98 | // If the frame didn't move, and the program counter didn't change, bail out 99 | // (otherwise we might print the same frame over and over). 100 | if !cfa_changed && !program_counter_changed { 101 | // If we do not end up in the reset function the stack is corrupted. 102 | // If reset_fn_range is empty, we can't detect this and just assume that 103 | // the stack was not corrupted. 104 | let reset_fn_range = elf.reset_fn_range(); 105 | output.corrupted = !(reset_fn_range.contains(&pc) || reset_fn_range.is_empty()); 106 | break; 107 | } 108 | 109 | if exception_entry { 110 | output.raw_frames.push(RawFrame::Exception); 111 | 112 | // Read the `FType` field from the `EXC_RETURN` value. 113 | let fpu = lr & cortexm::EXC_RETURN_FTYPE_MASK == 0; 114 | 115 | let sp = unwrap_or_return_output!(registers.get(registers::SP)); 116 | let ram_bounds = active_ram_region 117 | .as_ref() 118 | .map(|ram_region| { 119 | ram_region.range.start.try_into().unwrap_or(u32::MAX) 120 | ..ram_region.range.end.try_into().unwrap_or(u32::MAX) 121 | }) 122 | .unwrap_or(cortexm::VALID_RAM_ADDRESS); 123 | let stacked = if let Some(stacked) = 124 | unwrap_or_return_output!(Stacked::read(registers.core, sp, fpu, ram_bounds)) 125 | { 126 | stacked 127 | } else { 128 | output.corrupted = true; 129 | break; 130 | }; 131 | 132 | registers.insert(registers::LR, stacked.lr); 133 | // adjust the stack pointer for stacked registers 134 | registers.insert(registers::SP, sp + stacked.size()); 135 | 136 | pc = stacked.pc; 137 | } else if cortexm::is_thumb_bit_set(lr) { 138 | pc = cortexm::clear_thumb_bit(lr); 139 | } else { 140 | output.processing_error = Some(anyhow!( 141 | "bug? LR ({lr:#010x}) didn't have the Thumb bit set", 142 | )); 143 | break; 144 | } 145 | 146 | // if the SP is above the start of the stack, the stack is corrupt 147 | match registers.get(registers::SP) { 148 | Ok(advanced_sp) if advanced_sp > target_info.stack_start => { 149 | output.corrupted = true; 150 | break; 151 | } 152 | _ => {} 153 | } 154 | } 155 | 156 | output 157 | } 158 | 159 | fn check_hard_fault( 160 | pc: u32, 161 | vector_table: &cortexm::VectorTable, 162 | output: &mut Output, 163 | sp: u32, 164 | sp_ram_region: &Option, 165 | ) -> Option { 166 | if cortexm::is_hard_fault(pc, vector_table) { 167 | assert!( 168 | output.raw_frames.is_empty(), 169 | "when present HardFault handler must be the first frame we unwind but wasn't" 170 | ); 171 | 172 | if overflowed_stack(sp, sp_ram_region) { 173 | return Some(Outcome::StackOverflow); 174 | } else { 175 | return Some(Outcome::HardFault); 176 | } 177 | } 178 | None 179 | } 180 | 181 | #[derive(Debug)] 182 | pub struct Output { 183 | pub corrupted: bool, 184 | pub outcome: Outcome, 185 | pub raw_frames: Vec, 186 | /// Will be `Some` if an error occured while putting together the output. 187 | /// `outcome` and `raw_frames` will contain all info collected until the error occurred. 188 | pub processing_error: Option, 189 | } 190 | 191 | /// Backtrace frame prior to 'symbolication' 192 | #[derive(Debug)] 193 | pub enum RawFrame { 194 | Subroutine { pc: u32 }, 195 | Exception, 196 | } 197 | 198 | impl RawFrame { 199 | /// Returns `true` if the raw_frame is [`Exception`]. 200 | pub fn is_exception(&self) -> bool { 201 | matches!(self, Self::Exception) 202 | } 203 | } 204 | 205 | fn overflowed_stack(sp: u32, active_ram_region: &Option) -> bool { 206 | if let Some(active_ram_region) = active_ram_region { 207 | // NOTE stack is full descending; meaning the stack pointer can be 208 | // `ORIGIN(RAM) + LENGTH(RAM)` 209 | let range = active_ram_region.range.start..=active_ram_region.range.end; 210 | !range.contains(&sp.into()) 211 | } else { 212 | log::warn!("no RAM region appears to contain the stack; probe-run can't determine if this was a stack overflow"); 213 | false 214 | } 215 | } 216 | 217 | /// FDEs can never overlap. Unfortunately, computers. It looks like FDEs for dead code might still 218 | /// end up in the final ELF, but get their offset reset to 0, so there can be overlapping FDEs at 219 | /// low addresses. 220 | /// 221 | /// This function finds the FDE that applies to `addr`, skipping any FDEs with a start address of 0. 222 | /// Since there's no code at address 0, this should never skip legitimate FDEs. 223 | fn find_fde( 224 | debug_frame: &DebugFrame, 225 | bases: &BaseAddresses, 226 | addr: u32, 227 | ) -> anyhow::Result> { 228 | let mut entries = debug_frame.entries(bases); 229 | let mut fdes = Vec::new(); 230 | while let Some(entry) = entries.next()? { 231 | match entry { 232 | CieOrFde::Cie(_) => {} 233 | CieOrFde::Fde(partial) => { 234 | let fde = partial.parse(DebugFrame::cie_from_offset)?; 235 | if fde.initial_address() == 0 { 236 | continue; 237 | } 238 | 239 | if fde.contains(addr.into()) { 240 | log::trace!( 241 | "{addr:#010x}: found FDE for {:#010x} .. {:#010x} at offset {:?}", 242 | fde.initial_address(), 243 | fde.initial_address() + fde.len(), 244 | fde.offset(), 245 | ); 246 | fdes.push(fde); 247 | } 248 | } 249 | } 250 | } 251 | 252 | match fdes.len() { 253 | 0 => Err(anyhow!(gimli::Error::NoUnwindInfoForAddress)) 254 | .with_context(|| missing_debug_info(addr)), 255 | 1 => Ok(fdes.pop().unwrap()), 256 | n => Err(anyhow!( 257 | "found {n} frame description entries for address {addr:#010x}, there should only be 1; \ 258 | this is likely a bug in your compiler toolchain; unwinding will stop here", 259 | )), 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /src/canary.rs: -------------------------------------------------------------------------------- 1 | use std::time::Instant; 2 | 3 | use probe_rs::{Core, MemoryInterface, RegisterId}; 4 | 5 | use crate::{ 6 | registers::PC, 7 | target_info::{StackInfo, TargetInfo}, 8 | Elf, TIMEOUT, 9 | }; 10 | 11 | /// Canary value 12 | const CANARY_U8: u8 = 0xAA; 13 | /// Canary value 14 | const CANARY_U32: u32 = u32::from_le_bytes([CANARY_U8, CANARY_U8, CANARY_U8, CANARY_U8]); 15 | 16 | /// (Location of) the stack canary 17 | /// 18 | /// The stack canary is used to detect *potential* stack overflows and report the 19 | /// amount of stack used. 20 | /// 21 | /// The whole stack is initialized to `CANARY_U8` before the target program is started. 22 | /// 23 | /// When the programs ends (due to panic or breakpoint) the size of the canary is checked. If more 24 | /// than 90% is "touched" (bytes != `CANARY_U8`) then that is considered to be a *potential* stack 25 | /// overflow. In any case the amount of used stack is reported. 26 | /// 27 | /// Before execution: 28 | /// ``` text 29 | /// +--------+ -> stack_range.end() (initial_stack_pointer) 30 | /// | | 31 | /// | | 32 | /// | canary | 33 | /// | | 34 | /// | | 35 | /// +--------+ -> stack_range.start() 36 | /// | static | (variables, fixed size) 37 | /// +--------+ -> lowest RAM address 38 | /// ``` 39 | /// 40 | /// After execution: 41 | /// ``` text 42 | /// +--------+ -> stack_range.end() (initial_stack_pointer) 43 | /// | | 44 | /// | stack | (grows downwards) 45 | /// | | 46 | /// +--------+ 47 | /// | canary | 48 | /// +--------+ -> stack_range.start() 49 | /// | static | (variables, fixed size) 50 | /// +--------+ -> lowest RAM address 51 | /// ``` 52 | #[derive(Clone, Copy)] 53 | pub struct Canary { 54 | addr: u32, 55 | data_below_stack: bool, 56 | size: u32, 57 | size_kb: f64, 58 | } 59 | 60 | impl Canary { 61 | /// Decide if and where to place the stack canary. 62 | /// 63 | /// Assumes that the target was reset-halted. 64 | pub fn install( 65 | core: &mut Core, 66 | elf: &Elf, 67 | target_info: &TargetInfo, 68 | ) -> anyhow::Result> { 69 | let canary = match Self::prepare(elf, &target_info.stack_info) { 70 | Some(canary) => canary, 71 | None => return Ok(None), 72 | }; 73 | 74 | let start = Instant::now(); 75 | 76 | // paint stack 77 | paint_subroutine::execute(core, canary.addr, canary.size)?; 78 | 79 | let seconds = start.elapsed().as_secs_f64(); 80 | canary.log_time("painting", seconds); 81 | 82 | Ok(Some(canary)) 83 | } 84 | 85 | /// Measure the stack usage. 86 | /// 87 | /// Returns `true` if a stack overflow is likely. 88 | pub fn measure(self, core: &mut Core, elf: &Elf) -> anyhow::Result { 89 | let start = Instant::now(); 90 | 91 | // measure stack usage 92 | let touched_address = measure_subroutine::execute(core, self.addr, self.size)?; 93 | 94 | let seconds = start.elapsed().as_secs_f64(); 95 | self.log_time("reading", seconds); 96 | 97 | let min_stack_usage = match touched_address { 98 | Some(touched_address) => { 99 | log::debug!("stack was touched at {touched_address:#010X}"); 100 | elf.vector_table.initial_stack_pointer - touched_address 101 | } 102 | None => { 103 | log::warn!("stack was not used at all"); 104 | 0 105 | } 106 | }; 107 | 108 | let used_kb = min_stack_usage as f64 / 1024.0; 109 | let pct = used_kb / self.size_kb * 100.0; 110 | let msg = format!( 111 | "program has used at least {used_kb:.2}/{:.2} KiB ({pct:.1}%) of stack space", 112 | self.size_kb 113 | ); 114 | 115 | // stack touched? 116 | // 117 | // We consider >90% stack usage a potential stack overflow 118 | if pct > 90.0 { 119 | log::warn!("{}", msg); 120 | if self.data_below_stack { 121 | log::warn!("data segments might be corrupted due to stack overflow"); 122 | } 123 | Ok(true) 124 | } else { 125 | log::info!("{}", msg); 126 | Ok(false) 127 | } 128 | } 129 | 130 | /// Prepare, but not place the canary. 131 | /// 132 | /// If this succeeds, we have all the information we need in order to place the canary. 133 | fn prepare(elf: &Elf, stack_info: &Option) -> Option { 134 | let stack_info = match stack_info { 135 | Some(stack_info) => stack_info, 136 | None => { 137 | log::debug!("couldn't find valid stack range, not placing stack canary"); 138 | return None; 139 | } 140 | }; 141 | 142 | if elf.program_uses_heap() { 143 | log::debug!("heap in use, not placing stack canary"); 144 | return None; 145 | } 146 | 147 | let stack_addr = *stack_info.range.start(); 148 | let stack_size = *stack_info.range.end() - stack_addr; 149 | 150 | log::debug!( 151 | "{stack_size} bytes of stack available ({stack_addr:#010X} ..= {:#010X})", 152 | stack_info.range.end(), 153 | ); 154 | 155 | Self::assert_subroutines(stack_addr, stack_size)?; 156 | 157 | Some(Canary { 158 | addr: stack_addr, 159 | data_below_stack: stack_info.data_below_stack, 160 | size: stack_size, 161 | size_kb: stack_size as f64 / 1024.0, 162 | }) 163 | } 164 | 165 | fn log_time(&self, action: &str, seconds: f64) { 166 | log::debug!( 167 | "{action} {:.2} KiB of RAM took {seconds:.3}s ({:.2} KiB/s)", 168 | self.size_kb, 169 | self.size_kb / seconds 170 | ) 171 | } 172 | 173 | /// Assert 4-byte-alignment and that subroutine fits inside stack. 174 | fn assert_subroutines(stack_addr: u32, stack_size: u32) -> Option<()> { 175 | assert_eq!(stack_addr % 4, 0, "low_addr needs to be 4-byte-aligned"); 176 | assert_eq!(stack_size % 4, 0, "stack_size needs to be 4-byte-aligned"); 177 | assert_eq!( 178 | paint_subroutine::size() % 4, 179 | 0, 180 | "paint subroutine needs to be 4-byte-aligned" 181 | ); 182 | assert_eq!( 183 | measure_subroutine::size() % 4, 184 | 0, 185 | "measure subroutine needs to be 4-byte-aligned" 186 | ); 187 | if (stack_size < paint_subroutine::size()) || (stack_size < measure_subroutine::size()) { 188 | log::warn!("subroutines do not fit in stack; not placing stack canary"); 189 | None 190 | } else { 191 | Some(()) 192 | } 193 | } 194 | } 195 | 196 | /// Paint-stack subroutine. 197 | /// 198 | /// # Rust 199 | /// 200 | /// Corresponds to following rust code: 201 | /// 202 | /// ```rust 203 | /// unsafe fn paint(low_addr: u32, high_addr: u32, pattern: u32) { 204 | /// while low_addr <= high_addr { 205 | /// (low_addr as *mut u32).write(pattern); 206 | /// low_addr += 4; 207 | /// } 208 | /// } 209 | /// ``` 210 | /// 211 | /// # Assembly 212 | /// 213 | /// The assembly is generated from the Rust function `fn paint()` above, using the 214 | /// jorge-hack. 215 | /// 216 | /// ```armasm 217 | /// 000200ec : 218 | /// 200ec: 4288 cmp r0, r1 219 | /// 200ee: d801 bhi.n #6 220 | /// 200f0: c004 stmia r0!, {r2} 221 | /// 200f2: e7fb b.n #-6 222 | /// 223 | /// 000200f4 : 224 | /// 200f4: be00 bkpt 0x0000 225 | /// ``` 226 | mod paint_subroutine { 227 | use super::*; 228 | 229 | /// Write the carnary value to the stack. 230 | /// 231 | /// # Safety 232 | /// 233 | /// - Expects the [`Core`] to be halted and will leave it halted when the function 234 | /// returns. 235 | /// - `low_addr` and `size` need to be 4-byte-aligned. 236 | /// 237 | /// # How? 238 | /// 239 | /// We place the subroutine inside the memory we want to paint. The subroutine 240 | /// paints the whole memory, except of itself. After the subroutine finishes 241 | /// executing we overwrite the subroutine using the probe. 242 | pub fn execute(core: &mut Core, low_addr: u32, stack_size: u32) -> Result<(), probe_rs::Error> { 243 | super::execute_subroutine(core, low_addr, stack_size, self::SUBROUTINE)?; 244 | self::overwrite_subroutine(core, low_addr)?; 245 | Ok(()) 246 | } 247 | 248 | /// Overwrite the subroutine with the canary value. 249 | /// 250 | /// Happens after the subroutine finishes. 251 | fn overwrite_subroutine(core: &mut Core, low_addr: u32) -> Result<(), probe_rs::Error> { 252 | core.write_8(low_addr as u64, &[CANARY_U8; self::SUBROUTINE.len()]) 253 | } 254 | 255 | const SUBROUTINE: [u8; 12] = [ 256 | 0x88, 0x42, // cmp r0, r1 257 | 0x01, 0xd8, // bhi.n #6 258 | 0x04, 0xc0, // stmia r0!, {r2} 259 | 0xfb, 0xe7, // b.n #-6 260 | 0x00, 0xbe, // bkpt 0x0000 261 | 0x00, 0xbe, // bkpt 0x0000 (padding instruction) 262 | ]; 263 | 264 | pub const fn size() -> u32 { 265 | self::SUBROUTINE.len() as _ 266 | } 267 | } 268 | 269 | /// Measure-stack subroutine. 270 | /// 271 | /// # Rust 272 | /// 273 | /// Corresponds to following rust code; 274 | /// 275 | /// ```rust 276 | /// #[export_name = "measure"] 277 | /// unsafe fn measure(mut low_addr: u32, high_addr: u32, pattern: u32) -> u32 { 278 | /// let mut result = 0; 279 | /// 280 | /// while low_addr < high_addr { 281 | /// if (low_addr as *const u32).read() != pattern { 282 | /// result = low_addr; 283 | /// break; 284 | /// } else { 285 | /// low_addr += 4; 286 | /// } 287 | /// } 288 | /// 289 | /// result 290 | /// } 291 | /// ``` 292 | /// 293 | /// # Assembly 294 | /// 295 | /// The assembly is generated from the Rust function `fn measure()` above, using the 296 | /// jorge-hack. 297 | /// 298 | /// ```armasm 299 | /// 000200ec : 300 | /// 200ec: 4288 cmp r0, r1 301 | /// 200ee: d204 bcs.n #0xc 302 | /// 200f0: 6803 ldr r3, [r0, #0] 303 | /// 200f2: 4293 cmp r3, r2 304 | /// 200f4: d102 bne.n #8 305 | /// 200f6: 1d00 adds r0, r0, #4 306 | /// 200f8: e7f8 b.n #-8 307 | /// 308 | /// 000200fa : 309 | /// 200fa: 2000 movs r0, #0 310 | /// 311 | /// 000200fc : 312 | /// 200fc: be00 bkpt 0x0000 313 | /// // ^^^^ this was `bx lr` 314 | /// ``` 315 | mod measure_subroutine { 316 | use super::*; 317 | 318 | /// Search for lowest touched byte in memory. 319 | /// 320 | /// The returned `Option` is `None`, if the memory is untouched. Otherwise it 321 | /// gives the position of the lowest byte which isn't equal to the pattern anymore. 322 | /// 323 | /// # Safety 324 | /// 325 | /// - Expects the [`Core`] to be halted and will leave it halted when the function 326 | /// returns. 327 | /// - `low_addr` and `size` need to be 4-byte-aligned. 328 | /// 329 | /// # How? 330 | /// 331 | /// Before we place the subroutine in the memory, we search through the memory we 332 | /// want to place the subroutine to check if the stack usage got that far. If we 333 | /// find a touched byte we return it. Otherwise we place the subroutine in this 334 | /// memory region and execute it. After the subroutine finishes we read out the 335 | /// address of the lowest touched 4-byte-word from the register r0. If r0 is `0` 336 | /// we return `None`. Otherwise we process it to get the address of the lowest 337 | /// byte, not only 4-byte-word. 338 | pub fn execute( 339 | core: &mut Core, 340 | low_addr: u32, 341 | stack_size: u32, 342 | ) -> Result, probe_rs::Error> { 343 | // use probe to search through the memory the subroutine will be written to 344 | if let Some(addr) = self::search_with_probe(core, low_addr)? { 345 | return Ok(Some(addr)); // return early, if we find a touched value 346 | } 347 | 348 | super::execute_subroutine(core, low_addr, stack_size, self::SUBROUTINE)?; 349 | self::get_result(core) 350 | } 351 | 352 | /// Searches though memory byte by byte using the SWD/JTAG probe. 353 | /// 354 | /// Happens before we place the subroutine in memory. 355 | fn search_with_probe(core: &mut Core, low_addr: u32) -> Result, probe_rs::Error> { 356 | let mut buf = [0; self::SUBROUTINE.len()]; 357 | core.read_8(low_addr as u64, &mut buf)?; 358 | match buf.into_iter().position(|b| b != CANARY_U8) { 359 | Some(pos) => Ok(Some(low_addr + pos as u32)), 360 | None => Ok(None), 361 | } 362 | } 363 | 364 | /// Read out result from register r0 and process it to get lowest touched byte. 365 | /// 366 | /// Happens after the subroutine finishes. 367 | fn get_result(core: &mut Core) -> Result, probe_rs::Error> { 368 | // get the address of the lowest touched 4-byte-word 369 | let word_addr = match core.read_core_reg(RegisterId(0))? { 370 | 0 => return Ok(None), 371 | n => n, 372 | }; 373 | 374 | // take a closer look at word, to get address of lowest touched byte 375 | let offset = core 376 | .read_word_32(word_addr as u64)? 377 | .to_le_bytes() 378 | .into_iter() 379 | .position(|b| b != CANARY_U8) 380 | .expect("some byte has to be touched, if `word_addr != 0`"); 381 | 382 | Ok(Some(word_addr + offset as u32)) 383 | } 384 | 385 | const SUBROUTINE: [u8; 20] = [ 386 | 0x88, 0x42, // cmp r0, r1 387 | 0x04, 0xd2, // bcs.n #0xc 388 | 0x03, 0x68, // ldr r3, [r0, #0] 389 | 0x93, 0x42, // cmp r3, r2 390 | 0x02, 0xd1, // bne.n #8 391 | 0x00, 0x1d, // adds r0, r0, #4 392 | 0xf8, 0xe7, // b.n #-8 393 | 0x00, 0x20, // movs r0, #0 394 | 0x00, 0xbe, // bkpt 0x0000 395 | 0x00, 0xbe, // bkpt 0x0000 (padding instruction) 396 | ]; 397 | 398 | pub const fn size() -> u32 { 399 | self::SUBROUTINE.len() as _ 400 | } 401 | } 402 | 403 | /// Prepare and run subroutine. Also clean up afterwards. 404 | /// 405 | /// # How? 406 | /// 407 | /// We place the parameters in the registers (see table below), place the subroutine 408 | /// in memory, set the program counter to the beginning of the subroutine, execute 409 | /// the subroutine and reset the program counter afterwards. 410 | /// 411 | /// ## Register-parameter-mapping 412 | /// 413 | /// | register | paramter | 414 | /// | :------: | :------------------------ | 415 | /// | `r0` | `low_addr` + return value | 416 | /// | `r1` | `high_addr` | 417 | /// | `r2` | `pattern` | 418 | fn execute_subroutine( 419 | core: &mut Core, 420 | low_addr: u32, 421 | stack_size: u32, 422 | subroutine: [u8; N], 423 | ) -> Result<(), probe_rs::Error> { 424 | let subroutine_size = N as u32; 425 | let high_addr = low_addr + stack_size; 426 | 427 | // set the registers 428 | // NOTE: add `subroutine_size` to `low_addr`, to avoid the subroutine overwriting itself 429 | core.write_core_reg(RegisterId(0), low_addr + subroutine_size)?; 430 | core.write_core_reg(RegisterId(1), high_addr)?; 431 | core.write_core_reg(RegisterId(2), CANARY_U32)?; 432 | 433 | // write subroutine to stack 434 | core.write_8(low_addr as u64, &subroutine)?; 435 | 436 | // store current PC and set PC to beginning of subroutine 437 | let previous_pc = core.read_core_reg(PC)?; 438 | core.write_core_reg(PC, low_addr)?; 439 | 440 | // execute the subroutine and wait for it to finish 441 | core.run()?; 442 | core.wait_for_core_halted(TIMEOUT)?; 443 | 444 | // reset PC to where it was before 445 | core.write_core_reg::(PC, previous_pc)?; 446 | 447 | Ok(()) 448 | } 449 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use clap::{ArgAction, Parser}; 4 | use defmt_decoder::DEFMT_VERSIONS; 5 | use git_version::git_version; 6 | use probe_rs::Probe; 7 | 8 | use crate::probe; 9 | 10 | /// Successfull termination of process. 11 | const EXIT_SUCCESS: i32 = 0; 12 | 13 | /// A Cargo runner for microcontrollers. 14 | #[derive(Parser)] 15 | #[command()] 16 | pub struct Opts { 17 | /// Disable or enable backtrace (auto in case of panic or stack overflow). 18 | #[arg(long, default_value = "auto")] 19 | pub backtrace: String, 20 | 21 | /// Configure the number of lines to print before a backtrace gets cut off. 22 | #[arg(long, default_value = "50")] 23 | pub backtrace_limit: u32, 24 | 25 | /// The chip to program. 26 | #[arg(long, required = true, conflicts_with_all = HELPER_CMDS, env = "PROBE_RUN_CHIP")] 27 | chip: Option, 28 | 29 | /// Path to chip description file, in YAML format. 30 | #[arg(long)] 31 | pub chip_description_path: Option, 32 | 33 | /// Connect to device when NRST is pressed. 34 | #[arg(long)] 35 | pub connect_under_reset: bool, 36 | 37 | /// Disable use of double buffering while downloading flash. 38 | #[arg(long)] 39 | pub disable_double_buffering: bool, 40 | 41 | /// Path to an ELF firmware file. 42 | #[arg(required = true, conflicts_with_all = HELPER_CMDS)] 43 | elf: Option, 44 | 45 | /// Mass-erase all nonvolatile memory before downloading flash. 46 | #[arg(long)] 47 | pub erase_all: bool, 48 | 49 | /// Output logs a structured json. 50 | #[arg(long)] 51 | pub json: bool, 52 | 53 | /// List supported chips and exit. 54 | #[arg(long)] 55 | list_chips: bool, 56 | 57 | /// Lists all the connected probes and exit. 58 | #[arg(long)] 59 | list_probes: bool, 60 | 61 | /// Applies the given format to the log output. 62 | /// 63 | /// The arguments between curly braces are placeholders for log metadata. 64 | /// The following arguments are supported: 65 | /// - {f} : file name (e.g. "main.rs") 66 | /// - {F} : file path (e.g. "src/bin/main.rs") 67 | /// - {l} : line number 68 | /// - {L} : log level (e.g. "INFO", "DEBUG", etc) 69 | /// - {m} : module path (e.g. "foo::bar::some_function") 70 | /// - {s} : the actual log 71 | /// - {t} : log timestamp 72 | /// 73 | /// For example, with the format "{t} [{L}] Location<{f}:{l}> {s}" 74 | /// a log would look like this: 75 | /// "23124 [INFO ] Location Hello, world!" 76 | #[arg(long, verbatim_doc_comment)] 77 | pub log_format: Option, 78 | 79 | /// Applies the given format to the host log output. (see --log-format) 80 | #[arg(long)] 81 | pub host_log_format: Option, 82 | 83 | /// Whether to measure the program's stack consumption. 84 | #[arg(long)] 85 | pub measure_stack: bool, 86 | 87 | /// Skip writing the application binary to flash. 88 | #[arg( 89 | long, 90 | conflicts_with = "disable_double_buffering", 91 | conflicts_with = "verify" 92 | )] 93 | pub no_flash: bool, 94 | 95 | /// The probe to use (eg. `VID:PID`, `VID:PID:Serial`, or just `Serial`). 96 | #[arg(long, env = "PROBE_RUN_PROBE")] 97 | pub probe: Option, 98 | 99 | /// Whether to shorten paths (e.g. to crates.io dependencies) in backtraces and defmt logs 100 | #[arg(long)] 101 | pub shorten_paths: bool, 102 | 103 | /// The probe clock frequency in kHz 104 | #[arg(long, env = "PROBE_RUN_SPEED")] 105 | pub speed: Option, 106 | 107 | /// Enable more verbose output. 108 | #[arg(short, long, action = ArgAction::Count)] 109 | pub verbose: u8, 110 | 111 | /// Verifies the written program. 112 | #[arg(long)] 113 | pub verify: bool, 114 | 115 | /// Prints version information 116 | #[arg(short = 'V', long)] 117 | version: bool, 118 | 119 | /// Arguments passed after the ELF file path are discarded 120 | #[arg(allow_hyphen_values = true, hide = true, trailing_var_arg = true)] 121 | _rest: Vec, 122 | } 123 | 124 | /// Helper commands, which will not execute probe-run normally. 125 | const HELPER_CMDS: [&str; 3] = ["list_chips", "list_probes", "version"]; 126 | 127 | pub fn handle_arguments() -> anyhow::Result { 128 | let opts = Opts::parse(); 129 | 130 | if opts.measure_stack { 131 | log::warn!("use of deprecated option `--measure-stack`: Has no effect and will vanish on next breaking release") 132 | } 133 | 134 | if opts.version { 135 | print_version(); 136 | Ok(EXIT_SUCCESS) 137 | } else if opts.list_probes { 138 | probe::print(&Probe::list_all()); 139 | Ok(EXIT_SUCCESS) 140 | } else if opts.list_chips { 141 | print_chips(); 142 | Ok(EXIT_SUCCESS) 143 | } else if let (Some(elf), Some(chip)) = (opts.elf.as_deref(), opts.chip.as_deref()) { 144 | crate::run_target_program(elf, chip, &opts) 145 | } else { 146 | unreachable!("due to `StructOpt` constraints") 147 | } 148 | } 149 | 150 | fn print_chips() { 151 | let registry = probe_rs::config::families().expect("Could not retrieve chip family registry"); 152 | for chip_family in registry { 153 | println!("{}\n Variants:", chip_family.name); 154 | for variant in chip_family.variants.iter() { 155 | println!(" {}", variant.name); 156 | } 157 | } 158 | } 159 | 160 | /// The string reported by the `--version` flag 161 | fn print_version() { 162 | /// Version from `Cargo.toml` e.g. `"0.1.4"` 163 | const VERSION: &str = env!("CARGO_PKG_VERSION"); 164 | 165 | /// `""` OR git hash e.g. `"34019f8"` 166 | /// 167 | /// `git describe`-docs: 168 | /// > The command finds the most recent tag that is reachable from a commit. (...) 169 | /// It suffixes the tag name with the number of additional commits on top of the tagged object 170 | /// and the abbreviated object name of the most recent commit. 171 | // 172 | // The `fallback` is `"--"`, cause this will result in "" after `fn extract_git_hash`. 173 | const GIT_DESCRIBE: &str = git_version!(fallback = "--", args = ["--long"]); 174 | // Extract the "abbreviated object name" 175 | let hash = extract_git_hash(GIT_DESCRIBE); 176 | 177 | println!( 178 | "{VERSION} {hash}\nsupported defmt versions: {}", 179 | DEFMT_VERSIONS.join(", ") 180 | ); 181 | } 182 | 183 | /// Extract git hash from a `git describe` statement 184 | fn extract_git_hash(git_describe: &str) -> &str { 185 | git_describe.split('-').nth(2).unwrap() 186 | } 187 | 188 | #[cfg(test)] 189 | mod tests { 190 | use rstest::rstest; 191 | 192 | use super::*; 193 | 194 | #[rstest] 195 | #[case::normal("v0.2.3-12-g25c50d2", "g25c50d2")] 196 | #[case::modified("v0.2.3-12-g25c50d2-modified", "g25c50d2")] 197 | #[case::fallback("--", "")] 198 | fn should_extract_hash_from_description(#[case] description: &str, #[case] expected: &str) { 199 | let hash = extract_git_hash(description); 200 | assert_eq!(hash, expected) 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/cortexm.rs: -------------------------------------------------------------------------------- 1 | //! ARM Cortex-M specific constants 2 | 3 | use std::{mem, ops::Range}; 4 | 5 | use gimli::LittleEndian; 6 | 7 | pub const ADDRESS_SIZE: u8 = mem::size_of::() as u8; 8 | 9 | /// According to Armv8-M Architecture Reference Manual, the most significant 8 bits are `0xFF` to 10 | /// indicate `EXC_RETURN`, the rest is either reserved or contains data. 11 | pub const EXC_RETURN_MARKER: u32 = 0xFF00_0000; 12 | 13 | pub const EXC_RETURN_FTYPE_MASK: u32 = 1 << 4; 14 | 15 | pub const ENDIANNESS: LittleEndian = LittleEndian; 16 | pub type Endianness = LittleEndian; 17 | 18 | const THUMB_BIT: u32 = 1; 19 | // According to the ARM Cortex-M Reference Manual RAM memory must be located in this address range 20 | // (vendors still place e.g. Core-Coupled RAM outside this address range) 21 | pub const VALID_RAM_ADDRESS: Range = 0x2000_0000..0x4000_0000; 22 | 23 | pub fn clear_thumb_bit(addr: u32) -> u32 { 24 | addr & !THUMB_BIT 25 | } 26 | 27 | /// Checks if PC is the HardFault handler 28 | // XXX may want to relax this to cover the whole PC range of the `HardFault` handler 29 | pub fn is_hard_fault(pc: u32, vector_table: &VectorTable) -> bool { 30 | subroutine_eq(pc, vector_table.hard_fault) 31 | } 32 | 33 | pub fn is_thumb_bit_set(addr: u32) -> bool { 34 | addr & THUMB_BIT == THUMB_BIT 35 | } 36 | 37 | pub fn set_thumb_bit(addr: u32) -> u32 { 38 | addr | THUMB_BIT 39 | } 40 | 41 | /// Checks if two subroutine addresses are equivalent by first clearing their `THUMB_BIT` 42 | pub fn subroutine_eq(addr1: u32, addr2: u32) -> bool { 43 | addr1 & !THUMB_BIT == addr2 & !THUMB_BIT 44 | } 45 | 46 | /// The contents of the vector table 47 | #[derive(Debug)] 48 | pub struct VectorTable { 49 | // entry 0 50 | pub initial_stack_pointer: u32, 51 | // entry 3: HardFault handler 52 | pub hard_fault: u32, 53 | } 54 | -------------------------------------------------------------------------------- /src/dep/cratesio.rs: -------------------------------------------------------------------------------- 1 | use std::path::{self, Component, Path as StdPath, PathBuf}; 2 | 3 | use colored::Colorize as _; 4 | 5 | #[derive(Debug, Eq, PartialEq)] 6 | pub struct Path<'p> { 7 | registry_prefix: PathBuf, 8 | crate_name_version: &'p str, 9 | path: &'p StdPath, 10 | } 11 | 12 | impl<'p> Path<'p> { 13 | pub fn from_std_path(path: &'p StdPath) -> Option { 14 | if !path.is_absolute() { 15 | return None; 16 | } 17 | 18 | let mut components = path.components(); 19 | 20 | let mut registry_prefix = PathBuf::new(); 21 | for component in &mut components { 22 | registry_prefix.push(component.as_os_str()); 23 | 24 | if let Component::Normal(component) = component { 25 | if component == "registry" { 26 | break; 27 | } 28 | } 29 | } 30 | 31 | let src = super::get_component_normal(components.next()?)?; 32 | if src != "src" { 33 | return None; 34 | } 35 | registry_prefix.push(src); 36 | 37 | let github = super::get_component_normal(components.next()?)?.to_str()?; 38 | if !github.starts_with("github.com-") { 39 | return None; 40 | } 41 | registry_prefix.push(github); 42 | 43 | let crate_name_version = super::get_component_normal(components.next()?)?.to_str()?; 44 | 45 | Some(Path { 46 | registry_prefix, 47 | crate_name_version, 48 | path: components.as_path(), 49 | }) 50 | } 51 | 52 | pub fn format_short(&self) -> String { 53 | format!( 54 | "[{}]{}{}", 55 | self.crate_name_version, 56 | path::MAIN_SEPARATOR, 57 | self.path.display() 58 | ) 59 | } 60 | 61 | pub fn format_highlight(&self) -> String { 62 | format!( 63 | "{}{sep}{}{sep}{}", 64 | self.registry_prefix.display().to_string().dimmed(), 65 | self.crate_name_version.bold(), 66 | self.path.display(), 67 | sep = path::MAIN_SEPARATOR, 68 | ) 69 | } 70 | } 71 | 72 | #[cfg(test)] 73 | mod tests { 74 | use super::*; 75 | 76 | #[test] 77 | fn end_to_end() { 78 | let home = dirs::home_dir().unwrap(); 79 | let home = home.to_str().unwrap(); 80 | 81 | let input = PathBuf::from(home) 82 | .join(".cargo") 83 | .join("registry") 84 | .join("src") 85 | .join("github.com-1ecc6299db9ec823") 86 | .join("cortex-m-rt-0.6.13") 87 | .join("src") 88 | .join("lib.rs"); 89 | let path = Path::from_std_path(&input).unwrap(); 90 | 91 | let expected = Path { 92 | registry_prefix: PathBuf::from(home) 93 | .join(".cargo") 94 | .join("registry") 95 | .join("src") 96 | .join("github.com-1ecc6299db9ec823"), 97 | crate_name_version: "cortex-m-rt-0.6.13", 98 | path: &PathBuf::from("src").join("lib.rs"), 99 | }; 100 | 101 | assert_eq!(expected, path); 102 | 103 | let expected = PathBuf::from("[cortex-m-rt-0.6.13]") 104 | .join("src") 105 | .join("lib.rs"); 106 | let formatted_str = path.format_short(); 107 | 108 | assert_eq!(expected.to_string_lossy(), formatted_str); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/dep/mod.rs: -------------------------------------------------------------------------------- 1 | //! Dependency path parsing 2 | 3 | use std::{ 4 | ffi::OsStr, 5 | path::{Component, Path as StdPath}, 6 | }; 7 | 8 | mod cratesio; 9 | mod rust_repo; 10 | mod rust_std; 11 | mod rustc; 12 | 13 | #[derive(Debug, Eq, PartialEq)] 14 | pub enum Path<'p> { 15 | Cratesio(cratesio::Path<'p>), 16 | /// Path into `rust-std` component 17 | RustStd(rust_std::Path<'p>), 18 | /// "Remapped" rust-lang/rust path AKA `/rustc` path 19 | Rustc(rustc::Path<'p>), 20 | Verbatim(&'p StdPath), 21 | } 22 | 23 | impl<'p> Path<'p> { 24 | pub fn from_std_path(path: &'p StdPath) -> Self { 25 | if let Some(rust_std) = rust_std::Path::from_std_path(path) { 26 | Self::RustStd(rust_std) 27 | } else if let Some(rustc) = rustc::Path::from_std_path(path) { 28 | Self::Rustc(rustc) 29 | } else if let Some(cratesio) = cratesio::Path::from_std_path(path) { 30 | Self::Cratesio(cratesio) 31 | } else { 32 | Self::Verbatim(path) 33 | } 34 | } 35 | 36 | pub fn format_short(&self) -> String { 37 | match self { 38 | Path::Cratesio(cratesio) => cratesio.format_short(), 39 | Path::RustStd(rust_std) => rust_std.format_short(), 40 | Path::Rustc(rustc) => rustc.format_short(), 41 | Path::Verbatim(path) => path.display().to_string(), 42 | } 43 | } 44 | 45 | pub fn format_highlight(&self) -> String { 46 | match self { 47 | Path::Cratesio(cratesio) => cratesio.format_highlight(), 48 | Path::RustStd(rust_std) => rust_std.format_highlight(), 49 | Path::Rustc(rustc) => rustc.format_highlight(), 50 | Path::Verbatim(path) => path.display().to_string(), 51 | } 52 | } 53 | } 54 | 55 | fn get_component_normal(component: Component) -> Option<&OsStr> { 56 | if let Component::Normal(string) = component { 57 | Some(string) 58 | } else { 59 | None 60 | } 61 | } 62 | 63 | #[cfg(test)] 64 | mod tests { 65 | use std::path::PathBuf; 66 | 67 | use super::*; 68 | 69 | #[test] 70 | fn from_std_path_returns_correct_variant() { 71 | let home = dirs::home_dir().unwrap(); 72 | let home = home.to_str().unwrap(); 73 | 74 | let cratesio = PathBuf::from(home) 75 | .join(".cargo") 76 | .join("registry") 77 | .join("src") 78 | .join("github.com-1ecc6299db9ec823") 79 | .join("cortex-m-rt-0.6.13") 80 | .join("src") 81 | .join("lib.rs"); 82 | assert!(matches!(Path::from_std_path(&cratesio), Path::Cratesio(_))); 83 | 84 | let rustc = PathBuf::from(home) 85 | .join("rustc") 86 | .join("9bc8c42bb2f19e745a63f3445f1ac248fb015e53") 87 | .join("library") 88 | .join("core") 89 | .join("src") 90 | .join("panicking.rs"); 91 | assert!(matches!(Path::from_std_path(&rustc), Path::Rustc(_))); 92 | 93 | let rust_std = PathBuf::from(home) 94 | .join(".rustup") 95 | .join("toolchains") 96 | .join("stable-x86_64-unknown-linux-gnu") 97 | .join("lib") 98 | .join("rustlib") 99 | .join("src") 100 | .join("rust") 101 | .join("library") 102 | .join("core") 103 | .join("src") 104 | .join("sync") 105 | .join("atomic.rs"); 106 | assert!(matches!(Path::from_std_path(&rust_std), Path::RustStd(_))); 107 | 108 | let local = PathBuf::from("src").join("lib.rs"); 109 | assert!(matches!(Path::from_std_path(&local), Path::Verbatim(_))); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/dep/rust_repo.rs: -------------------------------------------------------------------------------- 1 | use std::path::{self, Path as StdPath}; 2 | 3 | use colored::Colorize as _; 4 | 5 | /// Representation of a rust-lang/rust repo path 6 | #[derive(Debug, Eq, PartialEq)] 7 | pub enum Path<'p> { 8 | One52(One52Path<'p>), 9 | Verbatim(&'p StdPath), 10 | } 11 | 12 | impl<'p> Path<'p> { 13 | pub fn from_std_path(path: &'p StdPath) -> Self { 14 | if let Some(path) = One52Path::from_std_path(path) { 15 | Path::One52(path) 16 | } else { 17 | Path::Verbatim(path) 18 | } 19 | } 20 | 21 | pub fn format(&self) -> String { 22 | match self { 23 | Path::One52(path) => path.format(), 24 | Path::Verbatim(path) => path.display().to_string(), 25 | } 26 | } 27 | 28 | pub fn format_highlight(&self) -> String { 29 | match self { 30 | Path::One52(path) => path.format_highlight(), 31 | Path::Verbatim(path) => path.display().to_string(), 32 | } 33 | } 34 | } 35 | 36 | /// rust-lang/repo path format as of 1.52 e.g. "library/core/src/panic.rs" 37 | #[derive(Debug, Eq, PartialEq)] 38 | pub struct One52Path<'p> { 39 | pub library: &'p str, 40 | pub crate_name: &'p str, 41 | pub path: &'p StdPath, 42 | } 43 | 44 | impl<'p> One52Path<'p> { 45 | fn from_std_path(path: &'p StdPath) -> Option { 46 | let mut components = path.components(); 47 | 48 | let library = super::get_component_normal(components.next()?)?.to_str()?; 49 | if library != "library" { 50 | return None; 51 | } 52 | 53 | let crate_name = super::get_component_normal(components.next()?)?.to_str()?; 54 | 55 | Some(One52Path { 56 | library, 57 | crate_name, 58 | path: components.as_path(), 59 | }) 60 | } 61 | 62 | fn format_highlight(&self) -> String { 63 | format!( 64 | "{}{sep}{}{sep}{}", 65 | self.library, 66 | self.crate_name.bold(), 67 | self.path.display(), 68 | sep = path::MAIN_SEPARATOR 69 | ) 70 | } 71 | 72 | fn format(&self) -> String { 73 | format!( 74 | "{}{sep}{}{sep}{}", 75 | self.library, 76 | self.crate_name, 77 | self.path.display(), 78 | sep = path::MAIN_SEPARATOR 79 | ) 80 | } 81 | } 82 | 83 | #[cfg(test)] 84 | mod tests { 85 | use std::path::PathBuf; 86 | 87 | use super::*; 88 | 89 | #[test] 90 | fn v1_52_path() { 91 | let path = PathBuf::from("library") 92 | .join("core") 93 | .join("src") 94 | .join("sync") 95 | .join("atomic.rs"); 96 | 97 | let rust_repo_path = Path::from_std_path(&path); 98 | let expected = Path::One52(One52Path { 99 | library: "library", 100 | crate_name: "core", 101 | path: StdPath::new("src/sync/atomic.rs"), 102 | }); 103 | 104 | assert_eq!(expected, rust_repo_path); 105 | 106 | let expected = PathBuf::from("library") 107 | .join("core") 108 | .join("src") 109 | .join("sync") 110 | .join("atomic.rs"); 111 | let formatted_str = rust_repo_path.format(); 112 | 113 | assert_eq!(expected.to_string_lossy(), formatted_str); 114 | } 115 | 116 | #[test] 117 | fn v1_0_path() { 118 | let path = StdPath::new("src/libcore/atomic.rs"); 119 | 120 | let rust_repo_path = Path::from_std_path(path); 121 | let expected = Path::Verbatim(path); 122 | 123 | assert_eq!(expected, rust_repo_path); 124 | 125 | let expected_str = "src/libcore/atomic.rs"; 126 | let formatted_str = rust_repo_path.format(); 127 | 128 | assert_eq!(expected_str, formatted_str); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/dep/rust_std.rs: -------------------------------------------------------------------------------- 1 | use std::path::{self, Component, Path as StdPath, PathBuf}; 2 | 3 | use colored::Colorize; 4 | 5 | use self::toolchain::Toolchain; 6 | 7 | use super::rust_repo; 8 | 9 | mod toolchain; 10 | 11 | #[derive(Debug, Eq, PartialEq)] 12 | pub struct Path<'p> { 13 | rustup_prefix: PathBuf, 14 | toolchain: Toolchain<'p>, 15 | rust_std_prefix: PathBuf, 16 | rust_repo_path: rust_repo::Path<'p>, 17 | } 18 | 19 | impl<'p> Path<'p> { 20 | pub fn from_std_path(path: &'p StdPath) -> Option { 21 | if !path.is_absolute() { 22 | return None; 23 | } 24 | 25 | let mut components = path.components(); 26 | 27 | let mut rustup_prefix = PathBuf::new(); 28 | for component in &mut components { 29 | rustup_prefix.push(component); 30 | 31 | if let Component::Normal(component) = component { 32 | if component == "toolchains" { 33 | break; 34 | } 35 | } 36 | } 37 | 38 | let toolchain = 39 | Toolchain::from_str(super::get_component_normal(components.next()?)?.to_str()?); 40 | 41 | let mut rust_std_prefix = PathBuf::new(); 42 | for component in &mut components { 43 | rust_std_prefix.push(component); 44 | 45 | if let Component::Normal(component) = component { 46 | if component == "rust" { 47 | break; 48 | } 49 | } 50 | } 51 | 52 | let rust_repo_path = rust_repo::Path::from_std_path(components.as_path()); 53 | 54 | Some(Path { 55 | rustup_prefix, 56 | toolchain, 57 | rust_std_prefix, 58 | rust_repo_path, 59 | }) 60 | } 61 | 62 | pub fn format_short(&self) -> String { 63 | format!( 64 | "[{}]{}{}", 65 | self.toolchain.format_short(), 66 | path::MAIN_SEPARATOR, 67 | self.rust_repo_path.format() 68 | ) 69 | } 70 | 71 | pub fn format_highlight(&self) -> String { 72 | format!( 73 | "{}{sep}{}{sep}{}{sep}{}", 74 | self.rustup_prefix.display().to_string().dimmed(), 75 | self.toolchain.format_highlight(), 76 | self.rust_std_prefix.display().to_string().dimmed(), 77 | self.rust_repo_path.format_highlight(), 78 | sep = path::MAIN_SEPARATOR 79 | ) 80 | } 81 | } 82 | 83 | #[cfg(test)] 84 | mod tests { 85 | use pretty_assertions::assert_eq; 86 | 87 | use super::*; 88 | 89 | #[test] 90 | fn end_to_end() { 91 | let home = dirs::home_dir().unwrap(); 92 | let home = home.to_str().unwrap(); 93 | 94 | let input = PathBuf::from(home) 95 | .join(".rustup") 96 | .join("toolchains") 97 | .join("stable-x86_64-unknown-linux-gnu") 98 | .join("lib") 99 | .join("rustlib") 100 | .join("src") 101 | .join("rust") 102 | .join("library") 103 | .join("core") 104 | .join("src") 105 | .join("sync") 106 | .join("atomic.rs"); 107 | 108 | let path = Path::from_std_path(&input).unwrap(); 109 | 110 | let src_path = PathBuf::from("src").join("sync").join("atomic.rs"); 111 | 112 | let expected = Path { 113 | rustup_prefix: PathBuf::from(home).join(".rustup").join("toolchains"), 114 | toolchain: Toolchain::One52(toolchain::One52 { 115 | channel: toolchain::Channel::Stable, 116 | host: "x86_64-unknown-linux-gnu", 117 | }), 118 | rust_std_prefix: PathBuf::from("lib/rustlib/src/rust"), 119 | rust_repo_path: rust_repo::Path::One52(rust_repo::One52Path { 120 | library: "library", 121 | crate_name: "core", 122 | path: &src_path, 123 | }), 124 | }; 125 | 126 | assert_eq!(expected, path); 127 | 128 | let expected = PathBuf::from("[stable]") 129 | .join("library") 130 | .join("core") 131 | .join("src") 132 | .join("sync") 133 | .join("atomic.rs"); 134 | let formatted_str = path.format_short(); 135 | 136 | assert_eq!(expected.to_string_lossy(), formatted_str); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/dep/rust_std/toolchain.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | 3 | use colored::Colorize; 4 | 5 | #[derive(Debug, Eq, PartialEq)] 6 | pub enum Toolchain<'p> { 7 | One52(One52<'p>), 8 | Verbatim(&'p str), 9 | } 10 | 11 | impl<'p> Toolchain<'p> { 12 | pub fn from_str(input: &str) -> Toolchain { 13 | if let Some(toolchain) = One52::from_str(input) { 14 | Toolchain::One52(toolchain) 15 | } else { 16 | Toolchain::Verbatim(input) 17 | } 18 | } 19 | 20 | pub fn format_highlight(&self) -> Cow { 21 | match self { 22 | Toolchain::One52(toolchain) => toolchain.format_highlight().into(), 23 | Toolchain::Verbatim(toolchain) => Cow::Borrowed(toolchain), 24 | } 25 | } 26 | 27 | pub fn format_short(&self) -> Cow { 28 | match self { 29 | Toolchain::One52(toolchain) => toolchain.format_short(), 30 | Toolchain::Verbatim(toolchain) => Cow::Borrowed(toolchain), 31 | } 32 | } 33 | } 34 | 35 | #[derive(Debug, Eq, PartialEq)] 36 | pub struct One52<'p> { 37 | pub channel: Channel<'p>, 38 | pub host: &'p str, 39 | } 40 | 41 | impl<'p> One52<'p> { 42 | fn from_str(input: &'p str) -> Option { 43 | let (maybe_channel, rest) = input.split_once('-')?; 44 | 45 | Some(if maybe_channel == "stable" { 46 | One52 { 47 | channel: Channel::Stable, 48 | host: rest, 49 | } 50 | } else if maybe_channel == "beta" { 51 | One52 { 52 | channel: Channel::Beta, 53 | host: rest, 54 | } 55 | } else if maybe_channel == "nightly" { 56 | let maybe_year = rest.split('-').next()?; 57 | 58 | if maybe_year.starts_with("20") { 59 | let mut count = 0; 60 | let at_third_dash = |c: char| { 61 | c == '-' && { 62 | count += 1; 63 | count == 3 64 | } 65 | }; 66 | 67 | let mut parts = rest.split(at_third_dash); 68 | 69 | let date = parts.next()?; 70 | let host = parts.next()?; 71 | One52 { 72 | channel: Channel::Nightly { date: Some(date) }, 73 | host, 74 | } 75 | } else { 76 | One52 { 77 | channel: Channel::Nightly { date: None }, 78 | host: rest, 79 | } 80 | } 81 | } else if maybe_channel.contains('.') { 82 | One52 { 83 | channel: Channel::Version(maybe_channel), 84 | host: rest, 85 | } 86 | } else { 87 | return None; 88 | }) 89 | } 90 | 91 | fn format_highlight(&self) -> String { 92 | format!( 93 | "{}{}{}", 94 | self.format_short().bold(), 95 | "-".dimmed(), 96 | self.host.dimmed() 97 | ) 98 | } 99 | 100 | fn format_short(&self) -> Cow { 101 | match self.channel { 102 | Channel::Beta => "beta".into(), 103 | Channel::Nightly { date } => { 104 | if let Some(date) = date { 105 | format!("nightly-{date}").into() 106 | } else { 107 | "nightly".into() 108 | } 109 | } 110 | Channel::Stable => "stable".into(), 111 | Channel::Version(version) => version.into(), 112 | } 113 | } 114 | } 115 | 116 | #[derive(Debug, Eq, PartialEq)] 117 | pub enum Channel<'p> { 118 | Beta, 119 | Nightly { date: Option<&'p str> }, 120 | Stable, 121 | Version(&'p str), 122 | } 123 | 124 | #[cfg(test)] 125 | mod tests { 126 | use super::*; 127 | 128 | use rstest::rstest; 129 | 130 | #[rstest] 131 | #[case("stable-x86_64-unknown-linux-gnu", Channel::Stable, "stable")] 132 | #[case("beta-x86_64-unknown-linux-gnu", Channel::Beta, "beta")] 133 | #[case("nightly-x86_64-unknown-linux-gnu", Channel::Nightly { date: None }, "nightly")] 134 | #[case( 135 | "nightly-2021-05-01-x86_64-unknown-linux-gnu", 136 | Channel::Nightly { date: Some("2021-05-01") }, 137 | "nightly-2021-05-01", 138 | )] 139 | #[case( 140 | "1.52.1-x86_64-unknown-linux-gnu", 141 | Channel::Version("1.52.1"), 142 | "1.52.1" 143 | )] 144 | fn end_to_end( 145 | #[case] input: &str, 146 | #[case] channel: Channel, 147 | #[case] expected_short_format: &str, 148 | ) { 149 | let toolchain = Toolchain::from_str(input); 150 | let expected = Toolchain::One52(One52 { 151 | channel, 152 | host: "x86_64-unknown-linux-gnu", 153 | }); 154 | 155 | assert_eq!(expected, toolchain); 156 | 157 | let formatted_string = toolchain.format_short(); 158 | 159 | assert_eq!(expected_short_format, formatted_string); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/dep/rustc.rs: -------------------------------------------------------------------------------- 1 | use std::path::{self, Component, Path as StdPath, PathBuf}; 2 | 3 | use colored::Colorize; 4 | 5 | use super::rust_repo; 6 | 7 | #[derive(Debug, Eq, PartialEq)] 8 | pub struct Path<'p> { 9 | rustc_prefix: PathBuf, 10 | rust_repo_path: rust_repo::Path<'p>, 11 | } 12 | 13 | impl<'p> Path<'p> { 14 | pub fn from_std_path(path: &'p StdPath) -> Option { 15 | if !path.is_absolute() { 16 | return None; 17 | } 18 | 19 | let mut components = path.components(); 20 | 21 | let mut rustc_prefix = PathBuf::new(); 22 | for component in &mut components { 23 | rustc_prefix.push(component); 24 | 25 | if let Component::Normal(component) = component { 26 | if component == "rustc" { 27 | break; 28 | } 29 | } 30 | } 31 | 32 | let hash = super::get_component_normal(components.next()?)?.to_str()?; 33 | if !hash.chars().all(|c| char::is_ascii_hexdigit(&c)) { 34 | return None; 35 | } 36 | rustc_prefix.push(hash); 37 | 38 | let rust_repo_path = rust_repo::Path::from_std_path(components.as_path()); 39 | 40 | Some(Path { 41 | rustc_prefix, 42 | rust_repo_path, 43 | }) 44 | } 45 | 46 | pub fn format_short(&self) -> String { 47 | format!( 48 | "[rust]{}{}", 49 | path::MAIN_SEPARATOR, 50 | self.rust_repo_path.format() 51 | ) 52 | } 53 | 54 | pub fn format_highlight(&self) -> String { 55 | format!( 56 | "{}{}{}", 57 | self.rustc_prefix.display().to_string().dimmed(), 58 | path::MAIN_SEPARATOR, 59 | self.rust_repo_path.format_highlight() 60 | ) 61 | } 62 | } 63 | 64 | #[cfg(test)] 65 | mod tests { 66 | use super::*; 67 | 68 | #[test] 69 | fn end_to_end() { 70 | let home = dirs::home_dir().unwrap(); 71 | let home = home.to_str().unwrap(); 72 | 73 | let input = PathBuf::from(home) 74 | .join("rustc") 75 | .join("9bc8c42bb2f19e745a63f3445f1ac248fb015e53") 76 | .join("library") 77 | .join("core") 78 | .join("src") 79 | .join("panicking.rs"); 80 | 81 | let rustc_prefix = PathBuf::from(home) 82 | .join("rustc") 83 | .join("9bc8c42bb2f19e745a63f3445f1ac248fb015e53"); 84 | 85 | let path = Path::from_std_path(&input).unwrap(); 86 | let expected_path = PathBuf::from("src").join("panicking.rs"); 87 | let expected = Path { 88 | rustc_prefix, 89 | rust_repo_path: rust_repo::Path::One52(rust_repo::One52Path { 90 | library: "library", 91 | crate_name: "core", 92 | path: &expected_path, 93 | }), 94 | }; 95 | 96 | assert_eq!(expected, path); 97 | 98 | let expected = PathBuf::from("[rust]") 99 | .join("library") 100 | .join("core") 101 | .join("src") 102 | .join("panicking.rs"); 103 | let formatted_str = path.format_short(); 104 | 105 | assert_eq!(expected.to_string_lossy(), formatted_str); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/elf.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashSet, 3 | convert::TryInto, 4 | env, 5 | ops::{Deref, Range}, 6 | path::Path, 7 | }; 8 | 9 | use anyhow::{anyhow, bail}; 10 | use defmt_decoder::{Locations, Table}; 11 | use object::{ 12 | read::File as ObjectFile, Object as _, ObjectSection as _, ObjectSymbol as _, SymbolSection, 13 | }; 14 | 15 | use crate::cortexm; 16 | 17 | pub struct Elf<'file> { 18 | elf: ObjectFile<'file>, 19 | symbols: Symbols, 20 | 21 | pub debug_frame: DebugFrame<'file>, 22 | pub defmt_locations: Option, 23 | pub defmt_table: Option, 24 | pub elf_path: &'file Path, 25 | pub live_functions: HashSet<&'file str>, 26 | pub vector_table: cortexm::VectorTable, 27 | } 28 | 29 | impl<'file> Elf<'file> { 30 | pub fn parse( 31 | elf_bytes: &'file [u8], 32 | elf_path: &'file Path, 33 | reset_fn_address: u32, 34 | ) -> Result { 35 | let elf = ObjectFile::parse(elf_bytes)?; 36 | 37 | let live_functions = extract_live_functions(&elf)?; 38 | 39 | let (defmt_table, defmt_locations) = extract_defmt_info(elf_bytes)?; 40 | let vector_table = extract_vector_table(&elf)?; 41 | log::debug!("vector table: {:x?}", vector_table); 42 | 43 | let debug_frame = extract_debug_frame(&elf)?; 44 | 45 | let symbols = extract_symbols(&elf, reset_fn_address)?; 46 | 47 | Ok(Self { 48 | elf, 49 | symbols, 50 | debug_frame, 51 | defmt_locations, 52 | defmt_table, 53 | elf_path, 54 | live_functions, 55 | vector_table, 56 | }) 57 | } 58 | 59 | pub fn main_fn_address(&self) -> u32 { 60 | self.symbols.main_fn_address 61 | } 62 | 63 | pub fn program_uses_heap(&self) -> bool { 64 | self.symbols.program_uses_heap 65 | } 66 | 67 | pub fn reset_fn_range(&self) -> &Range { 68 | &self.symbols.reset_fn_range 69 | } 70 | 71 | pub fn rtt_buffer_address(&self) -> Option { 72 | self.symbols.rtt_buffer_address 73 | } 74 | } 75 | 76 | impl<'elf> Deref for Elf<'elf> { 77 | type Target = ObjectFile<'elf>; 78 | 79 | fn deref(&self) -> &ObjectFile<'elf> { 80 | &self.elf 81 | } 82 | } 83 | 84 | fn extract_live_functions<'file>(elf: &ObjectFile<'file>) -> anyhow::Result> { 85 | let text = elf 86 | .section_by_name(".text") 87 | .map(|section| section.index()) 88 | .ok_or_else(|| { 89 | anyhow!( 90 | "`.text` section is missing, please make sure that the linker script was passed \ 91 | to the linker (check `.cargo/config.toml` and the `RUSTFLAGS` variable)" 92 | ) 93 | })?; 94 | 95 | let live_functions = elf 96 | .symbols() 97 | .filter_map(|symbol| { 98 | if symbol.section() == SymbolSection::Section(text) { 99 | Some(symbol.name()) 100 | } else { 101 | None 102 | } 103 | }) 104 | .collect::, _>>()?; 105 | 106 | Ok(live_functions) 107 | } 108 | 109 | fn extract_defmt_info(elf_bytes: &[u8]) -> anyhow::Result<(Option
, Option)> { 110 | let defmt_table = match env::var("PROBE_RUN_IGNORE_VERSION").as_deref() { 111 | Ok("true") | Ok("1") => defmt_decoder::Table::parse_ignore_version(elf_bytes)?, 112 | _ => defmt_decoder::Table::parse(elf_bytes)?, 113 | }; 114 | 115 | let mut defmt_locations = None; 116 | 117 | if let Some(table) = defmt_table.as_ref() { 118 | let locations = table.get_locations(elf_bytes)?; 119 | 120 | if !table.is_empty() && locations.is_empty() { 121 | log::warn!("insufficient DWARF info; compile your program with `debug = 2` to enable location info"); 122 | } else if table 123 | .indices() 124 | .all(|idx| locations.contains_key(&(idx as u64))) 125 | { 126 | defmt_locations = Some(locations); 127 | } else { 128 | log::warn!("(BUG) location info is incomplete; it will be omitted from the output"); 129 | } 130 | } 131 | 132 | Ok((defmt_table, defmt_locations)) 133 | } 134 | 135 | fn extract_vector_table(elf: &ObjectFile) -> anyhow::Result { 136 | let section = elf 137 | .section_by_name(".vector_table") 138 | .ok_or_else(|| anyhow!("`.vector_table` section is missing"))?; 139 | 140 | let start = section.address(); 141 | let size = section.size(); 142 | 143 | if size % 4 != 0 || start % 4 != 0 { 144 | bail!("section `.vector_table` is not 4-byte aligned"); 145 | } 146 | 147 | let bytes = section.data()?; 148 | let mut words = bytes 149 | .chunks_exact(4) 150 | .map(|chunk| u32::from_le_bytes(chunk.try_into().unwrap())); 151 | 152 | if let (Some(initial_stack_pointer), Some(_reset), Some(_third), Some(hard_fault)) = 153 | (words.next(), words.next(), words.next(), words.next()) 154 | { 155 | Ok(cortexm::VectorTable { 156 | initial_stack_pointer, 157 | hard_fault, 158 | }) 159 | } else { 160 | Err(anyhow!( 161 | "vector table section is too short. (has length: {} - should be at least 16)", 162 | bytes.len() 163 | )) 164 | } 165 | } 166 | 167 | type DebugFrame<'file> = gimli::DebugFrame>; 168 | 169 | fn extract_debug_frame<'file>(elf: &ObjectFile<'file>) -> anyhow::Result> { 170 | let bytes = elf 171 | .section_by_name(".debug_frame") 172 | .map(|section| section.data()) 173 | .transpose()? 174 | .ok_or_else(|| anyhow!("`.debug_frame` section not found"))?; 175 | 176 | let mut debug_frame = gimli::DebugFrame::new(bytes, cortexm::ENDIANNESS); 177 | debug_frame.set_address_size(cortexm::ADDRESS_SIZE); 178 | Ok(debug_frame) 179 | } 180 | 181 | struct Symbols { 182 | main_fn_address: u32, 183 | program_uses_heap: bool, 184 | reset_fn_range: Range, 185 | rtt_buffer_address: Option, 186 | } 187 | 188 | fn extract_symbols(elf: &ObjectFile, reset_fn_address: u32) -> anyhow::Result { 189 | let mut main_fn_address = None; 190 | let mut program_uses_heap = false; 191 | let mut reset_symbols = Vec::new(); 192 | let mut rtt_buffer_address = None; 193 | 194 | for symbol in elf.symbols() { 195 | let name = match symbol.name() { 196 | Ok(name) => name, 197 | Err(_) => continue, 198 | }; 199 | 200 | let address = symbol.address().try_into().expect("expected 32-bit ELF"); 201 | match name { 202 | "main" => main_fn_address = Some(cortexm::clear_thumb_bit(address)), 203 | "_SEGGER_RTT" => rtt_buffer_address = Some(address), 204 | "__rust_alloc" | "__rg_alloc" | "__rdl_alloc" | "malloc" if !program_uses_heap => { 205 | log::debug!("symbol `{}` indicates heap is in use", name); 206 | program_uses_heap = true; 207 | } 208 | _ => {} 209 | } 210 | 211 | // find reset handler symbol based on address 212 | if address == reset_fn_address && symbol.size() != 0 { 213 | reset_symbols.push(symbol); 214 | } 215 | } 216 | 217 | let main_fn_address = main_fn_address.ok_or(anyhow!("`main` symbol not found"))?; 218 | let reset_fn_range = { 219 | if reset_symbols.len() == 1 { 220 | let reset = reset_symbols.remove(0); 221 | let addr = reset.address().try_into().expect("expected 32-bit ELF"); 222 | let size: u32 = reset.size().try_into().expect("expected 32-bit ELF"); 223 | addr..addr + size 224 | } else { 225 | log::debug!("unable to determine reset handler"); 226 | // The length of the reset handler is not known as it's not part of the ELF file 227 | reset_fn_address..reset_fn_address 228 | } 229 | }; 230 | 231 | Ok(Symbols { 232 | main_fn_address, 233 | program_uses_heap, 234 | reset_fn_range, 235 | rtt_buffer_address, 236 | }) 237 | } 238 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod backtrace; 2 | mod canary; 3 | mod cli; 4 | mod cortexm; 5 | mod dep; 6 | mod elf; 7 | mod probe; 8 | mod registers; 9 | mod stacked; 10 | mod target_info; 11 | 12 | use std::{ 13 | env, fs, 14 | io::{self, Write as _}, 15 | path::Path, 16 | process, 17 | sync::{ 18 | atomic::{AtomicBool, Ordering}, 19 | Arc, 20 | }, 21 | time::Duration, 22 | }; 23 | 24 | use anyhow::{anyhow, bail}; 25 | use colored::Colorize as _; 26 | use defmt_decoder::{DecodeError, Frame, Locations, StreamDecoder}; 27 | use log::Level; 28 | use probe_rs::{ 29 | config::MemoryRegion, 30 | flashing::{self, Format}, 31 | rtt::{Rtt, ScanRegion, UpChannel}, 32 | Core, 33 | DebugProbeError::ProbeSpecific, 34 | MemoryInterface as _, Permissions, Session, 35 | }; 36 | use signal_hook::consts::signal; 37 | 38 | use crate::{ 39 | canary::Canary, 40 | elf::Elf, 41 | registers::{PC, SP}, 42 | target_info::TargetInfo, 43 | }; 44 | 45 | const TIMEOUT: Duration = Duration::from_secs(1); 46 | 47 | const DEFAULT_LOG_FORMAT_WITH_TIMESTAMP: &str = "{t} {L} {s}\n└─ {m} @ {F}:{l}"; 48 | const DEFAULT_LOG_FORMAT_WITHOUT_TIMESTAMP: &str = "{L} {s}\n└─ {m} @ {F}:{l}"; 49 | const DEFAULT_HOST_LOG_FORMAT: &str = "(HOST) {L} {s}"; 50 | const DEFAULT_VERBOSE_HOST_LOG_FORMAT: &str = "(HOST) {L} {s}\n└─ {m} @ {F}:{l}"; 51 | 52 | fn main() -> anyhow::Result<()> { 53 | deprecated(); 54 | 55 | configure_terminal_colorization(); 56 | 57 | #[allow(clippy::redundant_closure)] 58 | cli::handle_arguments().map(|code| process::exit(code)) 59 | } 60 | 61 | #[deprecated = "⚠️ As of 11.10.2023 `probe-run` is in maintainance mode. We \ 62 | recommend everyone to switch to `probe-rs`. Read following article on the why \ 63 | and on how to migrate: https://ferrous-systems.com/blog/probe-run-deprecation/"] 64 | fn deprecated() { 65 | eprintln!( 66 | "⚠️ As of 11.10.2023 `probe-run` is in maintainance mode. We recommend \ 67 | everyone to switch to `probe-rs`. Read following article on the why and \ 68 | on how to migrate: https://ferrous-systems.com/blog/probe-run-deprecation/\n" 69 | ); 70 | } 71 | 72 | fn run_target_program(elf_path: &Path, chip_name: &str, opts: &cli::Opts) -> anyhow::Result { 73 | // connect to probe and flash firmware 74 | let probe_target = lookup_probe_target(elf_path, chip_name, opts)?; 75 | let mut sess = attach_to_probe(probe_target.clone(), opts)?; 76 | flash(&mut sess, elf_path, opts)?; 77 | 78 | // attack to core 79 | let memory_map = sess.target().memory_map.clone(); 80 | let core = &mut sess.core(0)?; 81 | 82 | // reset-halt the core; this is necessary for analyzing the vector table and 83 | // painting the stack 84 | core.reset_and_halt(TIMEOUT)?; 85 | 86 | // gather information 87 | let (stack_start, reset_fn_address) = analyze_vector_table(core)?; 88 | let elf_bytes = fs::read(elf_path)?; 89 | let elf = &Elf::parse(&elf_bytes, elf_path, reset_fn_address)?; 90 | let target_info = TargetInfo::new(elf, memory_map, probe_target, stack_start)?; 91 | 92 | let verbose = opts.verbose; 93 | let is_timestamping_available = if let Some(table) = &elf.defmt_table { 94 | table.has_timestamp() 95 | } else { 96 | false 97 | }; 98 | 99 | let mut log_format = opts.log_format.as_deref(); 100 | let mut host_log_format = opts.host_log_format.as_deref(); 101 | 102 | if log_format.is_none() { 103 | log_format = if is_timestamping_available { 104 | Some(DEFAULT_LOG_FORMAT_WITH_TIMESTAMP) 105 | } else { 106 | Some(DEFAULT_LOG_FORMAT_WITHOUT_TIMESTAMP) 107 | }; 108 | } 109 | 110 | if host_log_format.is_none() { 111 | if verbose == 0 { 112 | host_log_format = Some(DEFAULT_HOST_LOG_FORMAT); 113 | } else { 114 | host_log_format = Some(DEFAULT_VERBOSE_HOST_LOG_FORMAT); 115 | } 116 | } 117 | 118 | let logger_info = 119 | defmt_decoder::log::init_logger(log_format, host_log_format, opts.json, move |metadata| { 120 | if defmt_decoder::log::is_defmt_frame(metadata) { 121 | true // We want to display *all* defmt frames. 122 | } else { 123 | // Log depending on how often the `--verbose` (`-v`) cli-param is supplied: 124 | // * 0: log everything from probe-run, with level "info" or higher 125 | // * 1: log everything from probe-run 126 | // * 2 or more: log everything 127 | match verbose { 128 | 0 => { 129 | metadata.target().starts_with("probe_run") 130 | && metadata.level() <= Level::Info 131 | } 132 | 1 => metadata.target().starts_with("probe_run"), 133 | _ => true, 134 | } 135 | } 136 | }); 137 | 138 | if logger_info.has_timestamp() && !is_timestamping_available { 139 | log::warn!( 140 | "logger format contains timestamp but no timestamp implementation \ 141 | was provided; consider removing the timestamp `{{t}}` from the \ 142 | logger format or provide a `defmt::timestamp!` implementation" 143 | ); 144 | } else if !logger_info.has_timestamp() && is_timestamping_available { 145 | log::warn!( 146 | "`defmt::timestamp!` implementation was found, but timestamp is not \ 147 | part of the log format; consider adding the timestamp `{{t}}` \ 148 | argument to the log format" 149 | ); 150 | } 151 | 152 | // install stack canary 153 | let canary = Canary::install(core, elf, &target_info)?; 154 | if canary.is_none() { 155 | log::info!("stack measurement was not set up"); 156 | } 157 | 158 | // run program and print logs until there is an exception 159 | start_program(core, elf)?; 160 | let current_dir = env::current_dir()?; 161 | let halted_due_to_signal = print_logs(core, ¤t_dir, elf, &target_info.memory_map, opts)?; // blocks until exception 162 | print_separator()?; 163 | 164 | // analyze stack canary 165 | let stack_overflow = canary 166 | .map(|canary| canary.measure(core, elf)) 167 | .transpose()? 168 | .unwrap_or(false); 169 | 170 | // print the backtrace 171 | let mut backtrace_settings = 172 | backtrace::Settings::new(current_dir, halted_due_to_signal, opts, stack_overflow); 173 | let outcome = backtrace::print(core, elf, &target_info, &mut backtrace_settings)?; 174 | 175 | // reset the target 176 | core.reset_and_halt(TIMEOUT)?; 177 | 178 | outcome.log(); 179 | Ok(outcome.into()) 180 | } 181 | 182 | fn lookup_probe_target( 183 | elf_path: &Path, 184 | chip_name: &str, 185 | opts: &cli::Opts, 186 | ) -> anyhow::Result { 187 | if !elf_path.exists() { 188 | bail!( 189 | "can't find ELF file at `{}`; are you sure you got the right path?", 190 | elf_path.display() 191 | ); 192 | } 193 | 194 | // register chip description 195 | if let Some(cdp) = &opts.chip_description_path { 196 | probe_rs::config::add_target_from_yaml(fs::File::open(cdp)?)?; 197 | } 198 | 199 | // look up target and check combat 200 | let probe_target = probe_rs::config::get_target_by_name(chip_name)?; 201 | target_info::check_processor_target_compatability(&probe_target.cores[0], elf_path); 202 | 203 | Ok(probe_target) 204 | } 205 | 206 | fn attach_to_probe(probe_target: probe_rs::Target, opts: &cli::Opts) -> anyhow::Result { 207 | let permissions = match opts.erase_all { 208 | false => Permissions::new(), 209 | true => Permissions::new().allow_erase_all(), 210 | }; 211 | let probe = probe::open(opts)?; 212 | let sess = if opts.connect_under_reset { 213 | probe.attach_under_reset(probe_target, permissions) 214 | } else { 215 | let probe_attach = probe.attach(probe_target, permissions); 216 | if let Err(probe_rs::Error::Probe(ProbeSpecific(e))) = &probe_attach { 217 | // FIXME Using `to_string().contains(...)` is a workaround as the concrete type 218 | // of `e` is not public and therefore does not allow downcasting. 219 | if e.to_string().contains("JtagNoDeviceConnected") { 220 | eprintln!("Info: Jtag cannot find a connected device."); 221 | eprintln!("Help:"); 222 | eprintln!(" Check that the debugger is connected to the chip, if so"); 223 | eprintln!(" try using probe-run with option `--connect-under-reset`"); 224 | eprintln!(" or, if using cargo:"); 225 | eprintln!(" cargo run -- --connect-under-reset"); 226 | eprintln!(" If using this flag fixed your issue, this error might"); 227 | eprintln!(" come from the program currently in the chip and using"); 228 | eprintln!(" `--connect-under-reset` is only a workaround.\n"); 229 | } 230 | } 231 | probe_attach 232 | }?; 233 | log::debug!("started session"); 234 | Ok(sess) 235 | } 236 | 237 | fn flash(sess: &mut Session, elf_path: &Path, opts: &cli::Opts) -> anyhow::Result<()> { 238 | if opts.no_flash { 239 | log::info!("skipped flashing"); 240 | } else { 241 | let fp = Some(flashing_progress()); 242 | 243 | if opts.erase_all { 244 | flashing::erase_all(sess, fp.clone())?; 245 | } 246 | 247 | let mut options = flashing::DownloadOptions::default(); 248 | options.dry_run = false; 249 | options.progress = fp; 250 | options.disable_double_buffering = opts.disable_double_buffering; 251 | options.verify = opts.verify; 252 | 253 | flashing::download_file_with_options(sess, elf_path, Format::Elf, options)?; 254 | log::info!("success!"); 255 | } 256 | Ok(()) 257 | } 258 | 259 | fn flashing_progress() -> flashing::FlashProgress { 260 | flashing::FlashProgress::new(|evt| { 261 | match evt { 262 | // The flash layout has been built and the flashing procedure was initialized. 263 | flashing::ProgressEvent::Initialized { flash_layout, .. } => { 264 | let pages = flash_layout.pages(); 265 | let num_pages = pages.len(); 266 | let num_kb = pages.iter().map(|x| x.size() as f64).sum::() / 1024.0; 267 | log::info!("flashing program ({num_pages} pages / {num_kb:.02} KiB)",); 268 | } 269 | // A sector has been erased. Sectors (usually) contain multiple pages. 270 | flashing::ProgressEvent::SectorErased { size, time } => log::debug!( 271 | "Erased sector of size {size} bytes in {} ms", 272 | time.as_millis() 273 | ), 274 | // A page has been programmed. 275 | flashing::ProgressEvent::PageProgrammed { size, time } => log::debug!( 276 | "Programmed page of size {size} bytes in {} ms", 277 | time.as_millis() 278 | ), 279 | _ => { /* Ignore other events */ } 280 | } 281 | }) 282 | } 283 | 284 | /// Read stack-pointer and reset-handler-address from the vector table. 285 | /// 286 | /// Assumes that the target was reset-halted. 287 | /// 288 | /// Returns `(stack_start: u32, reset_fn_address: u32)` 289 | fn analyze_vector_table(core: &mut Core) -> anyhow::Result<(u32, u32)> { 290 | let stack_start = core.read_core_reg::(SP)?; 291 | let reset_address = cortexm::set_thumb_bit(core.read_core_reg::(PC)?); 292 | Ok((stack_start, reset_address)) 293 | } 294 | 295 | fn start_program(core: &mut Core, elf: &Elf) -> anyhow::Result<()> { 296 | log::debug!("starting device"); 297 | 298 | match (core.available_breakpoint_units()?, elf.rtt_buffer_address()) { 299 | (0, Some(_)) => bail!("RTT not supported on device without HW breakpoints"), 300 | (0, None) => log::warn!("device doesn't support HW breakpoints; HardFault will NOT make `probe-run` exit with an error code"), 301 | (_, Some(rtt_buffer_address)) => set_rtt_to_blocking(core, elf.main_fn_address(), rtt_buffer_address)?, 302 | (_, None) => {} 303 | } 304 | 305 | core.set_hw_breakpoint(cortexm::clear_thumb_bit(elf.vector_table.hard_fault).into())?; 306 | core.run()?; 307 | 308 | Ok(()) 309 | } 310 | 311 | /// Set rtt to blocking mode 312 | fn set_rtt_to_blocking( 313 | core: &mut Core, 314 | main_fn_address: u32, 315 | rtt_buffer_address: u32, 316 | ) -> anyhow::Result<()> { 317 | // set and wait for a hardware breakpoint at the beginning of `fn main()` 318 | core.set_hw_breakpoint(main_fn_address.into())?; 319 | core.run()?; 320 | core.wait_for_core_halted(Duration::from_secs(5))?; 321 | 322 | // calculate address of up-channel-flags inside the rtt control block 323 | const OFFSET: u32 = 44; 324 | let rtt_buffer_address = rtt_buffer_address + OFFSET; 325 | 326 | // read flags 327 | let channel_flags = &mut [0]; 328 | core.read_32(rtt_buffer_address.into(), channel_flags)?; 329 | // modify flags to blocking 330 | const MODE_MASK: u32 = 0b11; 331 | const MODE_BLOCK_IF_FULL: u32 = 0b10; 332 | let modified_channel_flags = (channel_flags[0] & !MODE_MASK) | MODE_BLOCK_IF_FULL; 333 | // write flags back 334 | core.write_word_32(rtt_buffer_address.into(), modified_channel_flags)?; 335 | 336 | // clear the breakpoint we set before 337 | core.clear_hw_breakpoint(main_fn_address.into())?; 338 | 339 | Ok(()) 340 | } 341 | 342 | fn print_logs( 343 | core: &mut Core, 344 | current_dir: &Path, 345 | elf: &Elf, 346 | memory_map: &[MemoryRegion], 347 | opts: &cli::Opts, 348 | ) -> anyhow::Result { 349 | let exit = Arc::new(AtomicBool::new(false)); 350 | let sig_id = signal_hook::flag::register(signal::SIGINT, exit.clone())?; 351 | 352 | let mut logging_channel = if let Some(address) = elf.rtt_buffer_address() { 353 | Some(setup_logging_channel(core, memory_map, address)?) 354 | } else { 355 | eprintln!("RTT logs not available; blocking until the device halts.."); 356 | None 357 | }; 358 | 359 | let use_defmt = logging_channel 360 | .as_ref() 361 | .map_or(false, |channel| channel.name() == Some("defmt")); 362 | 363 | if use_defmt && opts.no_flash { 364 | log::warn!( 365 | "You are using `--no-flash` and `defmt` logging -- this combination can lead to malformed defmt data!" 366 | ); 367 | } else if use_defmt && elf.defmt_table.is_none() { 368 | bail!("\"defmt\" RTT channel is in use, but the firmware binary contains no defmt data"); 369 | } 370 | 371 | let mut decoder_and_encoding = if use_defmt { 372 | elf.defmt_table 373 | .as_ref() 374 | .map(|table| (table.new_stream_decoder(), table.encoding())) 375 | } else { 376 | None 377 | }; 378 | 379 | print_separator()?; 380 | 381 | let mut stdout = io::stdout().lock(); 382 | let mut read_buf = [0; 1024]; 383 | let mut was_halted = false; 384 | while !exit.load(Ordering::Relaxed) { 385 | if let Some(logging_channel) = &mut logging_channel { 386 | let num_bytes_read = match logging_channel.read(core, &mut read_buf) { 387 | Ok(n) => n, 388 | Err(e) => { 389 | eprintln!("RTT error: {e}"); 390 | break; 391 | } 392 | }; 393 | 394 | if num_bytes_read != 0 { 395 | match decoder_and_encoding.as_mut() { 396 | Some((stream_decoder, encoding)) => { 397 | stream_decoder.received(&read_buf[..num_bytes_read]); 398 | 399 | decode_and_print_defmt_logs( 400 | &mut **stream_decoder, 401 | elf.defmt_locations.as_ref(), 402 | current_dir, 403 | opts.shorten_paths, 404 | encoding.can_recover(), 405 | )?; 406 | } 407 | 408 | _ => { 409 | stdout.write_all(&read_buf[..num_bytes_read])?; 410 | stdout.flush()?; 411 | } 412 | } 413 | } 414 | } 415 | 416 | let is_halted = core.core_halted()?; 417 | 418 | if is_halted && was_halted { 419 | break; 420 | } 421 | was_halted = is_halted; 422 | } 423 | 424 | drop(stdout); 425 | 426 | signal_hook::low_level::unregister(sig_id); 427 | signal_hook::flag::register_conditional_default(signal::SIGINT, exit.clone())?; 428 | 429 | // Ctrl-C was pressed; stop the microcontroller. 430 | // TODO refactor: a printing function shouldn't stop the MC as a side effect 431 | if exit.load(Ordering::Relaxed) { 432 | core.halt(TIMEOUT)?; 433 | } 434 | 435 | let halted_due_to_signal = exit.load(Ordering::Relaxed); 436 | 437 | Ok(halted_due_to_signal) 438 | } 439 | 440 | fn setup_logging_channel( 441 | core: &mut Core, 442 | memory_map: &[MemoryRegion], 443 | rtt_buffer_address: u32, 444 | ) -> anyhow::Result { 445 | const NUM_RETRIES: usize = 10; // picked at random, increase if necessary 446 | 447 | let scan_region = ScanRegion::Exact(rtt_buffer_address); 448 | for _ in 0..NUM_RETRIES { 449 | match Rtt::attach_region(core, memory_map, &scan_region) { 450 | Ok(mut rtt) => { 451 | log::debug!("Successfully attached RTT"); 452 | let channel = rtt 453 | .up_channels() 454 | .take(0) 455 | .ok_or_else(|| anyhow!("RTT up channel 0 not found"))?; 456 | return Ok(channel); 457 | } 458 | Err(probe_rs::rtt::Error::ControlBlockNotFound) => log::trace!( 459 | "Couldn't attach because the target's RTT control block isn't initialized (yet). retrying" 460 | ), 461 | Err(e) => return Err(anyhow!(e)), 462 | } 463 | } 464 | 465 | log::error!("Max number of RTT attach retries exceeded."); 466 | Err(anyhow!(probe_rs::rtt::Error::ControlBlockNotFound)) 467 | } 468 | 469 | fn decode_and_print_defmt_logs( 470 | stream_decoder: &mut dyn StreamDecoder, 471 | locations: Option<&Locations>, 472 | current_dir: &Path, 473 | shorten_paths: bool, 474 | encoding_can_recover: bool, 475 | ) -> anyhow::Result<()> { 476 | loop { 477 | match stream_decoder.decode() { 478 | Ok(frame) => forward_to_logger(&frame, locations, current_dir, shorten_paths), 479 | Err(DecodeError::UnexpectedEof) => break, 480 | Err(DecodeError::Malformed) => match encoding_can_recover { 481 | // if recovery is impossible, abort 482 | false => return Err(DecodeError::Malformed.into()), 483 | // if recovery is possible, skip the current frame and continue with new data 484 | true => continue, 485 | }, 486 | } 487 | } 488 | 489 | Ok(()) 490 | } 491 | 492 | fn forward_to_logger( 493 | frame: &Frame, 494 | locations: Option<&Locations>, 495 | current_dir: &Path, 496 | shorten_paths: bool, 497 | ) { 498 | let (file, line, mod_path) = location_info(frame, locations, current_dir, shorten_paths); 499 | defmt_decoder::log::log_defmt(frame, file.as_deref(), line, mod_path.as_deref()); 500 | } 501 | 502 | fn location_info( 503 | frame: &Frame, 504 | locations: Option<&Locations>, 505 | current_dir: &Path, 506 | shorten_paths: bool, 507 | ) -> (Option, Option, Option) { 508 | locations 509 | .and_then(|locations| locations.get(&frame.index())) 510 | .map(|location| { 511 | let path = if let Ok(relpath) = location.file.strip_prefix(current_dir) { 512 | relpath.display().to_string() 513 | } else { 514 | let dep_path = dep::Path::from_std_path(&location.file); 515 | match shorten_paths { 516 | true => dep_path.format_short(), 517 | false => dep_path.format_highlight(), 518 | } 519 | }; 520 | ( 521 | Some(path), 522 | Some(location.line as u32), 523 | Some(location.module.clone()), 524 | ) 525 | }) 526 | .unwrap_or((None, None, None)) 527 | } 528 | 529 | /// Print a line to separate different execution stages. 530 | fn print_separator() -> io::Result<()> { 531 | writeln!(io::stderr(), "{}", "─".repeat(80).dimmed()) 532 | } 533 | 534 | fn configure_terminal_colorization() { 535 | // ! This should be detected by `colored`, but currently is not. 536 | // See https://github.com/mackwic/colored/issues/108 and https://github.com/knurling-rs/probe-run/pull/318. 537 | 538 | if let Ok("dumb") = env::var("TERM").as_deref() { 539 | colored::control::set_override(false) 540 | } 541 | } 542 | -------------------------------------------------------------------------------- /src/probe.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use anyhow::{anyhow, bail}; 4 | use probe_rs::{DebugProbeInfo, Probe}; 5 | 6 | use crate::cli; 7 | 8 | const NO_PROBE_FOUND_ERR: &str = "no probe was found.\n 9 | Common reasons for this are faulty cables or missing permissions. 10 | For detailed instructions, visit: https://github.com/knurling-rs/probe-run#troubleshooting"; 11 | 12 | pub fn open(opts: &cli::Opts) -> Result { 13 | let all_probes = Probe::list_all(); 14 | let filtered_probes = if let Some(probe_opt) = opts.probe.as_deref() { 15 | let selector = probe_opt.parse()?; 16 | filter(&all_probes, &selector) 17 | } else { 18 | all_probes 19 | }; 20 | 21 | if filtered_probes.is_empty() { 22 | bail!("{}", NO_PROBE_FOUND_ERR) 23 | } 24 | 25 | log::debug!("found {} probes", filtered_probes.len()); 26 | 27 | if filtered_probes.len() > 1 { 28 | print(&filtered_probes); 29 | bail!("more than one probe found; use --probe to specify which one to use"); 30 | } 31 | 32 | let mut probe = filtered_probes[0].open()?; 33 | log::debug!("opened probe"); 34 | 35 | if let Some(speed) = opts.speed { 36 | probe.set_speed(speed)?; 37 | } 38 | 39 | Ok(probe) 40 | } 41 | 42 | pub fn print(probes: &[DebugProbeInfo]) { 43 | if !probes.is_empty() { 44 | println!("the following probes were found:"); 45 | probes 46 | .iter() 47 | .enumerate() 48 | .for_each(|(num, link)| println!("[{num}]: {link:?}")); 49 | } else { 50 | println!("Error: {NO_PROBE_FOUND_ERR}"); 51 | } 52 | } 53 | 54 | fn filter(probes: &[DebugProbeInfo], selector: &ProbeFilter) -> Vec { 55 | probes 56 | .iter() 57 | .filter(|probe| { 58 | if let Some((vid, pid)) = selector.vid_pid { 59 | if probe.vendor_id != vid || probe.product_id != pid { 60 | return false; 61 | } 62 | } 63 | 64 | if let Some(serial) = &selector.serial { 65 | if probe.serial_number.as_deref() != Some(serial) { 66 | return false; 67 | } 68 | } 69 | 70 | true 71 | }) 72 | .cloned() 73 | .collect() 74 | } 75 | 76 | struct ProbeFilter { 77 | vid_pid: Option<(u16, u16)>, 78 | serial: Option, 79 | } 80 | 81 | impl FromStr for ProbeFilter { 82 | type Err = anyhow::Error; 83 | 84 | fn from_str(s: &str) -> Result { 85 | let parts = s.split(':').collect::>(); 86 | match *parts { 87 | [serial] => Ok(Self { 88 | vid_pid: None, 89 | serial: Some(serial.to_string()), 90 | }), 91 | [vid, pid] => Ok(Self { 92 | vid_pid: Some((u16::from_str_radix(vid, 16)?, u16::from_str_radix(pid, 16)?)), 93 | serial: None, 94 | }), 95 | [vid, pid, serial] => Ok(Self { 96 | vid_pid: Some((u16::from_str_radix(vid, 16)?, u16::from_str_radix(pid, 16)?)), 97 | serial: Some(serial.to_string()), 98 | }), 99 | _ => Err(anyhow!("invalid probe filter")), 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/registers.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{btree_map, BTreeMap}; 2 | 3 | use gimli::{read::CfaRule, EndianSlice, LittleEndian, Register, RegisterRule}; 4 | use probe_rs::{Core, MemoryInterface, RegisterId}; 5 | 6 | pub const LR: RegisterId = RegisterId(14); 7 | pub const PC: RegisterId = RegisterId(15); 8 | pub const SP: RegisterId = RegisterId(13); 9 | 10 | /// Cache and track the state of CPU registers while the stack is being unwound. 11 | pub struct Registers<'c, 'probe> { 12 | cache: BTreeMap, 13 | pub core: &'c mut Core<'probe>, 14 | } 15 | 16 | impl<'c, 'probe> Registers<'c, 'probe> { 17 | pub fn new(lr: u32, sp: u32, core: &'c mut Core<'probe>) -> Self { 18 | let mut cache = BTreeMap::new(); 19 | cache.insert(LR.0, lr); 20 | cache.insert(SP.0, sp); 21 | Self { cache, core } 22 | } 23 | 24 | pub fn get(&mut self, reg: RegisterId) -> anyhow::Result { 25 | Ok(match self.cache.entry(reg.0) { 26 | btree_map::Entry::Occupied(entry) => *entry.get(), 27 | btree_map::Entry::Vacant(entry) => *entry.insert(self.core.read_core_reg(reg)?), 28 | }) 29 | } 30 | 31 | pub fn insert(&mut self, reg: RegisterId, val: u32) { 32 | self.cache.insert(reg.0, val); 33 | } 34 | 35 | /// Updates the Canonical Frame Address (CFA), e.g. 36 | /// the value of the Stack Pointer (SP) on function entry – the current frame we're looking at 37 | /// 38 | /// returns `true` if the CFA has changed 39 | pub fn update_cfa( 40 | &mut self, 41 | rule: &CfaRule>, 42 | ) -> anyhow::Result { 43 | match rule { 44 | CfaRule::RegisterAndOffset { register, offset } => { 45 | // Could be simplified when wrapping_add_signed becomes stable 46 | let cfa = (i64::from(self.get(gimli2probe(register))?) + offset) as u32; 47 | let old_cfa = self.cache.get(&SP.0); 48 | let changed = old_cfa != Some(&cfa); 49 | if changed { 50 | log::debug!("update_cfa: CFA changed {old_cfa:8x?} -> {cfa:8x}"); 51 | } 52 | self.cache.insert(SP.0, cfa); 53 | Ok(changed) 54 | } 55 | // NOTE not encountered in practice so far 56 | CfaRule::Expression(_) => todo!("CfaRule::Expression"), 57 | } 58 | } 59 | 60 | pub fn update( 61 | &mut self, 62 | reg: &Register, 63 | rule: &RegisterRule>, 64 | ) -> anyhow::Result<()> { 65 | match rule { 66 | RegisterRule::Offset(offset) => { 67 | let cfa = self.get(SP)?; 68 | let addr = (cfa as i64 + offset) as u32; 69 | let value = self.core.read_word_32(addr.into())?; 70 | log::trace!( 71 | "update reg={reg:?}, rule={rule:?}, abs={addr:#010x} -> value={value:#010x}" 72 | ); 73 | self.cache.insert(reg.0, value); 74 | } 75 | RegisterRule::Undefined => unreachable!(), 76 | _ => unimplemented!(), 77 | } 78 | Ok(()) 79 | } 80 | } 81 | 82 | fn gimli2probe(reg: &Register) -> RegisterId { 83 | RegisterId(reg.0) 84 | } 85 | -------------------------------------------------------------------------------- /src/stacked.rs: -------------------------------------------------------------------------------- 1 | use std::{mem, ops::Range}; 2 | 3 | use probe_rs::{Core, MemoryInterface}; 4 | 5 | /// Registers stacked on exception entry. 6 | #[derive(Debug)] 7 | pub struct Stacked { 8 | // also pushed onto the stack but we don't need to read them 9 | // r0: u32, 10 | // r1: u32, 11 | // r2: u32, 12 | // r3: u32, 13 | // r12: u32, 14 | pub lr: u32, 15 | pub pc: u32, 16 | contains_fpu_regs: bool, 17 | } 18 | 19 | fn bounds_check(bounds: Range, start: u32, len: u32) -> Result<(), ()> { 20 | let end = start + len; 21 | if bounds.contains(&start) && bounds.contains(&end) { 22 | Ok(()) 23 | } else { 24 | Err(()) 25 | } 26 | } 27 | 28 | impl Stacked { 29 | /// The size of one register / word in bytes 30 | const REGISTER_SIZE: usize = mem::size_of::(); 31 | 32 | /// Location (as an offset) of the stacked registers we need for unwinding 33 | const WORDS_OFFSET: usize = 5; 34 | 35 | /// Minimum number of stacked registers that we need to read to be able to unwind an exception 36 | const WORDS_MINIMUM: usize = 2; 37 | 38 | /// Number of 32-bit words stacked in a basic frame. 39 | const WORDS_BASIC: usize = 8; 40 | 41 | /// Number of 32-bit words stacked in an extended frame. 42 | const WORDS_EXTENDED: usize = Self::WORDS_BASIC + 17; // 16 FPU regs + 1 status word 43 | 44 | /// Reads stacked registers from RAM 45 | /// 46 | /// This performs bound checks and returns `None` if a invalid memory read is requested 47 | pub fn read( 48 | core: &mut Core<'_>, 49 | sp: u32, 50 | fpu: bool, 51 | ram_bounds: Range, 52 | ) -> anyhow::Result> { 53 | let mut storage = [0; Self::WORDS_MINIMUM]; 54 | let registers: &mut [_] = &mut storage; 55 | 56 | let start = sp + (Self::REGISTER_SIZE * Self::WORDS_OFFSET) as u32; 57 | if bounds_check( 58 | ram_bounds, 59 | start, 60 | (registers.len() * Self::REGISTER_SIZE) as u32, 61 | ) 62 | .is_err() 63 | { 64 | return Ok(None); 65 | } 66 | 67 | core.read_32(start.into(), registers)?; 68 | 69 | Ok(Some(Stacked { 70 | lr: registers[0], 71 | pc: registers[1], 72 | contains_fpu_regs: fpu, 73 | })) 74 | } 75 | 76 | /// Returns the in-memory size of these stacked registers, in Bytes. 77 | pub fn size(&self) -> u32 { 78 | let num_words = if self.contains_fpu_regs { 79 | Self::WORDS_EXTENDED 80 | } else { 81 | Self::WORDS_BASIC 82 | }; 83 | 84 | num_words as u32 * 4 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/target_info.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | convert::TryInto, 3 | ops::{Range, RangeInclusive}, 4 | path::Path, 5 | }; 6 | 7 | use object::{Object, ObjectSection as _}; 8 | use probe_rs::{ 9 | config::Core, 10 | config::{MemoryRegion, RamRegion}, 11 | CoreType, 12 | }; 13 | 14 | use crate::elf::Elf; 15 | 16 | pub struct TargetInfo { 17 | /// RAM region that contains the call stack 18 | pub active_ram_region: Option, 19 | pub memory_map: Vec, 20 | pub probe_target: probe_rs::Target, 21 | pub stack_info: Option, 22 | pub stack_start: u32, 23 | } 24 | 25 | pub struct StackInfo { 26 | /// Valid values of the stack pointer (that don't collide with other data). 27 | pub range: RangeInclusive, 28 | pub data_below_stack: bool, 29 | } 30 | 31 | impl TargetInfo { 32 | pub fn new( 33 | elf: &Elf, 34 | memory_map: Vec, 35 | probe_target: probe_rs::Target, 36 | stack_start: u32, 37 | ) -> anyhow::Result { 38 | let active_ram_region = 39 | extract_active_ram_region(&probe_target, elf.vector_table.initial_stack_pointer); 40 | let stack_info = active_ram_region 41 | .as_ref() 42 | .and_then(|ram_region| extract_stack_info(elf, &ram_region.range)); 43 | 44 | Ok(Self { 45 | active_ram_region, 46 | memory_map, 47 | probe_target, 48 | stack_info, 49 | stack_start, 50 | }) 51 | } 52 | } 53 | 54 | /// Check if the compilation target and processor fit and emit a warning if not. 55 | pub fn check_processor_target_compatability(core: &Core, elf_path: &Path) { 56 | let target = elf_path.iter().find_map(|a| { 57 | let b = a.to_string_lossy(); 58 | match b.starts_with("thumbv") { 59 | true => Some(b), 60 | false => None, 61 | } 62 | }); 63 | let target = match target { 64 | Some(target) => target, 65 | None => return, // NOTE(return) If probe-run is not called through `cargo run` the elf_path 66 | // might not contain the compilation target. In that case we return early. 67 | }; 68 | 69 | // NOTE(indexing): There *must* always be at least one core. 70 | let core_type = core.core_type; 71 | let matches = match core_type { 72 | CoreType::Armv6m => target == "thumbv6m-none-eabi", 73 | CoreType::Armv7m => target == "thumbv7m-none-eabi", 74 | CoreType::Armv7em => target == "thumbv7em-none-eabi" || target == "thumbv7em-none-eabihf", 75 | CoreType::Armv8m => { 76 | target == "thumbv8m.base-none-eabi" 77 | || target == "thumbv8m.main-none-eabi" 78 | || target == "thumbv8m.main-none-eabihf" 79 | } 80 | CoreType::Armv7a | CoreType::Armv8a => { 81 | log::warn!("Unsupported architecture ({core_type:?}"); 82 | return; 83 | } 84 | // NOTE(return) Since we do not get any info about instruction 85 | // set support from probe-rs we do not know which compilation 86 | // targets fit. 87 | CoreType::Riscv => return, 88 | }; 89 | 90 | if matches { 91 | return; 92 | } 93 | let recommendation = match core_type { 94 | CoreType::Armv6m => "must be 'thumbv6m-none-eabi'", 95 | CoreType::Armv7m => "should be 'thumbv7m-none-eabi'", 96 | CoreType::Armv7em => { 97 | "should be 'thumbv7em-none-eabi' (no FPU) or 'thumbv7em-none-eabihf' (with FPU)" 98 | } 99 | CoreType::Armv8m => { 100 | "should be 'thumbv8m.base-none-eabi' (M23), 'thumbv8m.main-none-eabi' (M33 no FPU), or 'thumbv8m.main-none-eabihf' (M33 with FPU)" 101 | } 102 | CoreType::Armv7a | CoreType::Armv8a => unreachable!(), 103 | CoreType::Riscv => unreachable!(), 104 | }; 105 | log::warn!("Compilation target ({target}) and core type ({core_type:?}) do not match. Your compilation target {recommendation}."); 106 | } 107 | 108 | fn extract_active_ram_region( 109 | target: &probe_rs::Target, 110 | initial_stack_pointer: u32, 111 | ) -> Option { 112 | target 113 | .memory_map 114 | .iter() 115 | .find_map(|region| match region { 116 | MemoryRegion::Ram(ram_region) => { 117 | // NOTE stack is full descending; meaning the stack pointer can be 118 | // `ORIGIN(RAM) + LENGTH(RAM)` 119 | let inclusive_range = ram_region.range.start..=ram_region.range.end; 120 | if inclusive_range.contains(&initial_stack_pointer.into()) { 121 | log::debug!( 122 | "RAM region: 0x{:08X}-0x{:08X}", 123 | ram_region.range.start, 124 | ram_region.range.end - 1 125 | ); 126 | Some(ram_region) 127 | } else { 128 | None 129 | } 130 | } 131 | _ => None, 132 | }) 133 | .cloned() 134 | } 135 | 136 | fn extract_stack_info(elf: &Elf, ram_range: &Range) -> Option { 137 | // How does it work? 138 | // - the upper end of the stack is the initial SP, minus one 139 | // - the lower end of the stack is the highest address any section in the elf file uses, plus one 140 | 141 | let initial_stack_pointer = elf.vector_table.initial_stack_pointer; 142 | 143 | // SP points one word (4-byte) past the end of the stack. 144 | let mut stack_range = 145 | ram_range.start.try_into().unwrap_or(u32::MAX)..=initial_stack_pointer - 4; 146 | 147 | for section in elf.sections() { 148 | let size: u32 = section.size().try_into().expect("expected 32-bit ELF"); 149 | if size == 0 { 150 | continue; 151 | } 152 | 153 | let lowest_address: u32 = section.address().try_into().expect("expected 32-bit ELF"); 154 | let highest_address = lowest_address + size - 1; 155 | let section_range = lowest_address..=highest_address; 156 | let name = section.name().unwrap_or(""); 157 | 158 | if ram_range.contains(&(*section_range.end() as u64)) { 159 | log::debug!("section `{name}` is in RAM at {section_range:#010X?}"); 160 | 161 | if section_range.contains(stack_range.end()) { 162 | log::debug!( 163 | "initial SP is in section `{name}`, cannot determine valid stack range", 164 | ); 165 | return None; 166 | } else if stack_range.contains(section_range.end()) { 167 | stack_range = section_range.end() + 1..=*stack_range.end(); 168 | } 169 | } 170 | } 171 | 172 | log::debug!("valid SP range: {stack_range:#010X?}"); 173 | Some(StackInfo { 174 | data_below_stack: *stack_range.start() as u64 > ram_range.start, 175 | range: stack_range, 176 | }) 177 | } 178 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # Snapshot Tests go 📸✨ 2 | 3 | All tests in this directory are snapshot tests, e.g. they compare `probe-run` output to a previous, known-good state. 4 | 5 | These tests need to be run *manually* because they require the target hardware to be present. 6 | 7 | To do this, 8 | 1. connect a nrf52840 DK to your computer via the J2 USB port on the *short* side of the DK 9 | 2. run `cargo test -- --ignored` 10 | 11 | ## adding a new snapshot test 12 | 13 | ### 1. compile a suitable ELF file 14 | By default, your elf file should be compiled to run on the `nRF52840_xxAA` chip. 15 | You can e.g. check that your `.cargo/config.toml` is set to cross-compile to the `thumbv7em-none-eabihf` target: 16 | 17 | ```toml 18 | [build] 19 | # cross-compile to this target 20 | target = "thumbv7em-none-eabihf" # = ARM Cortex-M4 21 | ``` 22 | 23 | 🔎 You can retrieve your ELF file from the `target/thumbv7em-none-eabihf/` folder of your app. 24 | 25 | ```console 26 | $ # get ELF `hello` that was compiled in debug mode 27 | $ cd target/thumbv7em-none-eabihf/debug/ 28 | $ # make sure that it's an elf file 29 | $ file `hello` 30 | hello: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, with debug_info, not stripped 31 | $ # copy it into `probe-run/tests/test_elfs` 32 | $ cp hello my/path/to/probe-run/tests/test_elfs 33 | ``` 34 | ❗️ if you'd rather not have full paths containing your name, folder structure etc. show up in your backtrace, extend your `.cargo/config.toml` like so: 35 | 36 | ```diff 37 | # either in your [target.xxx] or [build] settings 38 | rustflags = [ 39 | ... 40 | + "--remap-path-prefix", "/Users/top/secret/path/=test_elfs", 41 | ] 42 | ``` 43 | 44 | ### 2. write test and run it once 45 | 46 | Write your test that captures `probe-run`s output for your test ELF and check the result with `insta::assert_snapshot!(run_output);` 47 | 48 | ### 3. cargo insta review 49 | When you run `cargo test -- --ignored` for the first time after you've added your new test, it will fail. 50 | This first run creates a snapshot which you can then store as a "known good" 51 | 52 | run 53 | ```console 54 | $ cargo install cargo-insta 55 | $ cargo insta review 56 | ``` 57 | 58 | And review the snapshot that was created. Accept it if it looks right to you (adjust test, re-run and review again if it doesn't). 59 | 60 | Now, your test will fail in the future if the output doesn't match the snashot you created. 61 | 62 | For details, refer to the [insta](https://docs.rs/insta/1.7.1/insta/#writing-tests) docs. 63 | -------------------------------------------------------------------------------- /tests/snapshot.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | io::Read, 3 | process::{Child, Command, ExitStatus}, 4 | thread, 5 | time::Duration, 6 | }; 7 | 8 | use os_pipe::pipe; 9 | use rstest::rstest; 10 | use serial_test::serial; 11 | 12 | struct RunResult { 13 | exit_status: ExitStatus, 14 | output: String, 15 | } 16 | 17 | /// Run `probe-run` with `args` and truncate the output. 18 | /// 19 | /// If `terminate` is `true`, the command gets terminated after a short timeout. 20 | fn run(args: &[&str], terminate: bool) -> RunResult { 21 | let (mut reader, mut handle) = run_command(args); 22 | 23 | if terminate { 24 | #[cfg(target_family = "unix")] 25 | wait_and_terminate(&handle); 26 | } 27 | 28 | // retrieve output and clean up 29 | let mut probe_run_output = String::new(); 30 | reader.read_to_string(&mut probe_run_output).unwrap(); 31 | let exit_status = handle.wait().unwrap(); 32 | 33 | // remove the lines printed during flashing, as they contain timing info that's not always the same 34 | let output = truncate_output(probe_run_output); 35 | 36 | RunResult { 37 | exit_status, 38 | output, 39 | } 40 | } 41 | 42 | #[cfg(target_family = "unix")] 43 | fn wait_and_terminate(handle: &Child) { 44 | // sleep a bit so that child can process the input 45 | thread::sleep(Duration::from_secs(5)); 46 | 47 | // send SIGINT to the child 48 | nix::sys::signal::kill( 49 | nix::unistd::Pid::from_raw(handle.id() as i32), 50 | nix::sys::signal::Signal::SIGINT, 51 | ) 52 | .expect("cannot send ctrl-c"); 53 | } 54 | 55 | fn run_command(args: &[&str]) -> (os_pipe::PipeReader, Child) { 56 | let mut cmd = vec!["run", "--", "--chip", "nRF52840_xxAA", "--shorten-paths"]; 57 | cmd.extend(&args[1..]); 58 | 59 | let path = format!("tests/test_elfs/{}", args[0]); 60 | cmd.push(path.as_str()); 61 | 62 | // capture stderr and stdout while preserving line order 63 | let (reader, writer) = pipe().unwrap(); 64 | 65 | let handle = Command::new("cargo") 66 | .args(cmd) 67 | .stdout(writer.try_clone().unwrap()) 68 | .stderr(writer) 69 | .spawn() 70 | .unwrap(); 71 | (reader, handle) 72 | } 73 | 74 | // remove the lines printed during flashing, as they contain timing info that's not always the same 75 | fn truncate_output(probe_run_output: String) -> String { 76 | probe_run_output 77 | .lines() 78 | .filter(|line| { 79 | !line.starts_with(" Finished") 80 | && !line.starts_with(" Running `") 81 | && !line.starts_with(" Blocking waiting for file lock ") 82 | && !line.starts_with(" Compiling probe-run v") 83 | && !line.starts_with("└─ ") // remove after https://github.com/knurling-rs/probe-run/issues/217 is resolved 84 | }) 85 | .map(|line| format!("{line}\n")) 86 | .collect() 87 | } 88 | 89 | #[rstest] 90 | #[case::successful_run_has_no_backtrace("hello-rzcobs", true)] 91 | #[case::raw_encoding("hello-raw", true)] 92 | #[case::successful_run_can_enforce_backtrace("hello-rzcobs --backtrace=always", true)] 93 | #[case::stack_overflow_is_reported_as_such("overflow-rzcobs", false)] 94 | #[case::panic_is_reported_as_such("panic-rzcobs", false)] 95 | #[should_panic] // FIXME: see https://github.com/knurling-rs/probe-run/issues/336 96 | #[case::panic_verbose("panic-rzcobs --verbose", false)] 97 | #[case::unsuccessful_run_can_suppress_backtrace("panic-rzcobs --backtrace=never", false)] 98 | #[case::stack_overflow_can_suppress_backtrace("overflow-rzcobs --backtrace=never", false)] 99 | #[case::canary("overflow-no-flip-link", false)] 100 | #[serial] 101 | #[ignore = "requires the target hardware to be present"] 102 | fn snapshot_test(#[case] args: &str, #[case] success: bool) { 103 | // Arrange 104 | let args = args.split(' ').collect::>(); 105 | 106 | // Act 107 | let run_result = run(args.as_slice(), false); 108 | 109 | // Assert 110 | assert_eq!(success, run_result.exit_status.success()); 111 | insta::assert_snapshot!(run_result.output); 112 | } 113 | 114 | #[test] 115 | #[serial] 116 | #[ignore = "requires the target hardware to be present"] 117 | #[cfg(target_family = "unix")] 118 | fn ctrl_c_by_user_is_reported_as_such() { 119 | // Arrange 120 | let args = &["silent-loop-rzcobs"]; 121 | 122 | // Act 123 | let run_result = run(args, true); 124 | 125 | // Assert 126 | assert!(!run_result.exit_status.success()); 127 | insta::assert_snapshot!(run_result.output); 128 | } 129 | 130 | #[rstest] 131 | #[case::without_time(&["levels-rzcobs", "--log-format", "[{L}] Location<{f}:{l}> {s}"])] 132 | #[case::with_time(&["levels-with-timestamp", "--log-format", "{t} [{L}] Location<{f}:{l}> {s}"])] 133 | #[case::with_time_but_no_impl(&["levels-rzcobs", "--log-format", "{t} [{L}] Location<{f}:{l}> {s}"])] 134 | #[case::without_time_but_with_impl(&["levels-with-timestamp", "--log-format", "[{L}] Location<{f}:{l}> {s}"])] 135 | #[case::host_without_time(&["levels-rzcobs", "--host-log-format", "[{L}] Location<{f}:{l}> {s}"])] 136 | #[case::host_with_timestamp(&["levels-with-timestamp", "--host-log-format", "{t} [{L}] Location<{f}:{l}> {s}"])] 137 | #[serial] 138 | #[ignore = "requires the target hardware to be present"] 139 | fn log_format(#[case] args: &[&str]) { 140 | // Act 141 | let run_result = run(args, false); 142 | 143 | // Assert 144 | assert!(run_result.exit_status.success()); 145 | insta::assert_snapshot!(run_result.output); 146 | } 147 | -------------------------------------------------------------------------------- /tests/snapshots/snapshot__case_1_successful_run_has_no_backtrace.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/snapshot.rs 3 | assertion_line: 109 4 | expression: run_result.output 5 | 6 | --- 7 | (HOST) INFO flashing program (2 pages / 8.00 KiB) 8 | (HOST) INFO success! 9 | ──────────────────────────────────────────────────────────────────────────────── 10 | Hello, world! 11 | ──────────────────────────────────────────────────────────────────────────────── 12 | (HOST) INFO program has used at least 0.16/254.93 KiB (0.1%) of stack space 13 | (HOST) INFO device halted without error 14 | 15 | -------------------------------------------------------------------------------- /tests/snapshots/snapshot__case_1_without_time.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/snapshot.rs 3 | assertion_line: 143 4 | expression: run_result.output 5 | 6 | --- 7 | (HOST) INFO flashing program (2 pages / 8.00 KiB) 8 | (HOST) INFO success! 9 | ──────────────────────────────────────────────────────────────────────────────── 10 | [INFO ] Location info 11 | [TRACE] Location trace 12 | [WARN ] Location warn 13 | [DEBUG] Location debug 14 | [ERROR] Location error 15 | println 16 | ──────────────────────────────────────────────────────────────────────────────── 17 | (HOST) INFO program has used at least 0.23/254.93 KiB (0.1%) of stack space 18 | (HOST) INFO device halted without error 19 | 20 | -------------------------------------------------------------------------------- /tests/snapshots/snapshot__case_2_raw_encoding.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/snapshot.rs 3 | assertion_line: 109 4 | expression: run_result.output 5 | 6 | --- 7 | (HOST) INFO flashing program (2 pages / 8.00 KiB) 8 | (HOST) INFO success! 9 | ──────────────────────────────────────────────────────────────────────────────── 10 | Hello, world! 11 | ──────────────────────────────────────────────────────────────────────────────── 12 | (HOST) INFO program has used at least 0.12/254.93 KiB (0.0%) of stack space 13 | (HOST) INFO device halted without error 14 | 15 | -------------------------------------------------------------------------------- /tests/snapshots/snapshot__case_2_with_time.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/snapshot.rs 3 | assertion_line: 139 4 | expression: run_result.output 5 | 6 | --- 7 | (HOST) INFO flashing program (2 pages / 8.00 KiB) 8 | (HOST) INFO success! 9 | ──────────────────────────────────────────────────────────────────────────────── 10 | 0 [INFO ] Location info 11 | 1 [TRACE] Location trace 12 | 2 [WARN ] Location warn 13 | 3 [DEBUG] Location debug 14 | 4 [ERROR] Location error 15 | println 16 | ──────────────────────────────────────────────────────────────────────────────── 17 | (HOST) INFO program has used at least 0.23/254.93 KiB (0.1%) of stack space 18 | (HOST) INFO device halted without error 19 | 20 | -------------------------------------------------------------------------------- /tests/snapshots/snapshot__case_3_successful_run_can_enforce_backtrace.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/snapshot.rs 3 | assertion_line: 109 4 | expression: run_result.output 5 | 6 | --- 7 | (HOST) INFO flashing program (2 pages / 8.00 KiB) 8 | (HOST) INFO success! 9 | ──────────────────────────────────────────────────────────────────────────────── 10 | Hello, world! 11 | ──────────────────────────────────────────────────────────────────────────────── 12 | (HOST) INFO program has used at least 0.16/254.93 KiB (0.1%) of stack space 13 | stack backtrace: 14 | 0: lib::inline::__bkpt 15 | at ./asm/inline.rs:14:5 16 | 1: __bkpt 17 | at ./asm/lib.rs:51:17 18 | 2: app::exit 19 | at /tmp/app/src/lib.rs:17:5 20 | 3: hello::__cortex_m_rt_main 21 | at /tmp/app/src/bin/hello.rs:10:5 22 | 4: main 23 | at /tmp/app/src/bin/hello.rs:6:1 24 | 5: Reset 25 | (HOST) INFO device halted without error 26 | 27 | -------------------------------------------------------------------------------- /tests/snapshots/snapshot__case_3_with_time_but_no_impl.snap: -------------------------------------------------------------------------------- 1 | --- 2 | source: tests/snapshot.rs 3 | assertion_line: 143 4 | expression: run_result.output 5 | 6 | --- 7 | (HOST) INFO flashing program (2 pages / 8.00 KiB) 8 | (HOST) INFO success! 9 | (HOST) WARN logger format contains timestamp but no timestamp implementation was provided; consider removing the timestamp `{t}` from the logger format or provide a `defmt::timestamp!` implementation 10 | ──────────────────────────────────────────────────────────────────────────────── 11 |