Debugging with `probe-rs` and VS code: and nRF52840: can't set breakpoints

I'm trying to debug a program on a NordicSemi nRF52840 controller.

So far I followed the instructions at hackmd.io and from the probe-rs debugging docu.

I've adapted the configuration according to my environment (mostly paths). In the VS code file .vscode/launch.json I've additionally set "haltAfterReset": true.

When I invoke the debugging via VS code's probe-rs extension, the program is built, flashed to and executed on the uC. But if I want to add breakpoints in VS Code, the IDE shows the message:

Cannot set breakpoint here. [...] No valid breakpoint information found for file: [...]

Despite the haltAfterReset setting, the program is not halted but immediately executed after it is flashed. The probe-rs docu reads:

Supports halt-after-reset. This will allow you to set breakpoints in your main() function.

Also adding

use cortex_m::asm;
// ...
asm::bkpt();

doesn't do anything.

  • I'm working with Linux
  • gdb is installed
  • I didn't set the miDebuggerPath which should be correct
  • VS code: Allow Breakpoints Everywhere is set

The concerned Rust source file is in ./examples/demo_nrf52.rs, and the relevant VS code settings are

tasks.json:

{
  "label": "cargo build",
  "type": "shell",
  "command": "cargo",
  "args": [
    "build",
    "--target=thumbv7em-none-eabihf",
    "--example",
    "demo_nrf52"
  ],

launch.json:

"programBinary": "${workspaceFolder}/target/thumbv7em-none-eabihf/debug/examples/demo_nrf52"

Any ideas what's wrong with my setup?

please show the full content of your launch.json file. if the program is successfully flashed, the probe should work fine, I think maybe you have mis-configured some of the debugging related parameters.

Hm, is there a better way to provide a lenghty file content? Well, for now, my launch.json looks as follows:

{
  "version": "0.2.0",
  "configurations": [
    {
      // see https://probe.rs/docs/tools/debugger/ for more info
      "type": "probe-rs-debug",
      "request": "launch",
      "name": "probe_rs",
      "cwd": "${workspaceFolder}",
      "preLaunchTask": "cargo build",
      "runtimeExecutable": "/home/sn/.cargo/bin/probe-rs", //!MODIFY
      // "runtimeArgs": ["debug"],
      "runtimeArgs": ["dap-server"],
      "chip": "nRF52840_xxAA", //!MODIFY
      "flashingConfig": {
        "flashingEnabled": true,
        "resetAfterFlashing": true,
        "haltAfterReset": true,
        "formatOptions": {
          "format": "elf" //Valid values are: 'bin', 'hex', 'elf'(default), 'idf'
        }
      },
      "coreConfigs": [
        {
          "coreIndex": 0,
          "programBinary": "${workspaceFolder}/target/thumbv7em-none-eabihf/debug/examples/demo_nrf52", //!MODIFY
          "svdFile": "${workspaceFolder}/res/nrf52840.svd", //!MODIFY , required for register view
          "rttEnabled": true,
          "rttChannelFormats": [
            {
              "channelNumber": 0,
              // Format RTT data as String data
              "dataFormat": "String",
              // Include host-side timestamps for every line of data transferred from the target RTT output
              "showTimestamps": true
            },
            {
              "channelNumber": 1,
              // Treat data as raw binary data, and do not format in any way
              "dataFormat": "BinaryLE"
            }
          ]
        }
      ],
      "env": {
        //!MODIFY (or remove)
        "RUST_LOG": "info" // If you set this variable, check the VSCode console log window for the location of the log file.
      },
      "consoleLogLevel": "Console" //Info, Debug
    }
  ]
}

GDB server

I did have some more success with using a gdb server:

  • start session with probe-rs gdb --chip nRF52840_xxAA
  • use gdb-mulltiarch to connect, load the debug build binary and start debugging

Breakpoints generally work, however, while I can use next inside gdb, it won't do what it is supposed to do:

Step program, proceeding through subroutine calls.

The debugger will do the same as for step.

I don't see any obvious problem in the configuration, and since the gdb server works, the only reason I can guess is maybe the vscode extension and the probe-rs programn had mismatched version? I'm not sure, but you can try to update both the extension and probe-rs.

btw, although your launch.json looks fine, I would suggest you start with minimum configuration and leave the majority as default, just to make sure it's not really a configuration problem.

  • VS Code: 1.87.2
  • probe-rs extension: v0.23.0
  • probe-rs 0.22.0

I've updated probe-rs via cargo install probe-rs --locked --features cli.
And used the minimum config for launch.json with modifications for the items

  • "preLaunchTask": "cargo build"
  • programBinary: "${workspaceFolder}/target/thumbv7em-none-eabihf/debug/examples/demo_nrf52"
  • "chip": "nRF52840_xxAA"

but that didn't do much except ending here:

Screenshot_2024-03-24_19-06-57

so I switched back to my previous launch.json.

Now, if there's no active debugging session, then I can set breakpoints without the message "no valid breakpoint information found ...". If a debugging session is active, the same message appears when trying to set a breakpoint.

So for now I'll just go with the gdb + probe-rs approach.

Anyway, thanks for the support!

the callstack looks a bit odd. did you turn on optimization (and/or stripped debug info too) for the dev profile? it might explain why the breakpoints cannot be set.

For both debugging approaches (via VS Code and via gdb), the build is generated via:

cargo build --target=thumbv7em-none-eabihf --example demo_nrf52

and it does indeed say sth. about optimizations

Finished dev [optimized + debuginfo] target(s) in 0.17s
file target/thumbv7em-none-eabihf/debug/examples/demo_nrf52
target/thumbv7em-none-eabihf/debug/examples/demo_nrf52: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, with debug_info, not stripped

but I can use breakpoints with this build when using ?gdb-multiarch.

do you have something like the following in your Cargo.toml manifest?

[profile.dev]
opt-level = "s"
strip = true

if you enabled optimization or stripped off debug information, breakpoints will not work because it's not possible to map source code line numbers to addresses of the binary code.

it also affects single stepping for similar reasons.

It's not stripped, but the opt-level was set to "z", which worked for gdb using breakpoints but not for "stepping over" (next). I've now tried with opt-level = 0:

  • VS Code still doesn't work
  • gdb's command next still doesn't work

well, I'm out of ideas. probe-rs always works for me. there's must be some weird configuration problem somewhere for your project.

can you just try one of the minimal examples (e.g. blinky), just to make sure the breakpoint functionality can work?

I really should have thought of that...thanks.

I tried now to use the simple blinky program, with rust-exercises version v1.10.0 and original configuration.

I can set a breakpoint at main, that works as expected.

Any other breakpoint I'm trying to set soon after, e.g. b src/bin/blinky.rs:15, will be moved to line 22.

And as far as I can tell, the gdb's next command doesn't work as I'd expect:

(gdb) file target/thumbv7em-none-eabihf/debug/blinky
A program is being debugged already.
Are you sure you want to change the file? (y or n) y
Reading symbols from target/thumbv7em-none-eabihf/debug/blinky...
(gdb) mon reset halt
Resetting and halting target
Target halted
(gdb) b main
Breakpoint 1 at 0x15e: file src/bin/blinky.rs, line 10.
Note: automatically using hardware breakpoints for read-only addresses.
(gdb) c
Continuing.

Breakpoint 1, blinky::__cortex_m_rt_main_trampoline () at src/bin/blinky.rs:10
10	#[entry]
(gdb) c
Continuing.

Program received signal SIGINT, Interrupt.
blinky::__cortex_m_rt_main () at src/bin/blinky.rs:11
11	fn main() -> ! {
(gdb) n

Program received signal SIGINT, Interrupt.
0x00000166 in blinky::__cortex_m_rt_main () at src/bin/blinky.rs:11
11	fn main() -> ! {
(gdb) n

Program received signal SIGINT, Interrupt.
blinky::__cortex_m_rt_main () at src/bin/blinky.rs:14
14	    let board = dk::init().unwrap();
(gdb) n

Program received signal SIGINT, Interrupt.
lib::__primask_r () at asm/lib.rs:51
warning: 51	asm/lib.rs: No such file or directory
(gdb) 

The default Cargo.toml in the radio-app project uses opt-level = 'z'. I've also tried with opt-level = 0, but the result is pretty much the same.

Assuming that the project configuration is okay, and that debugging the blinky program with the nRF52840 development kit works for other people, what possible differences are left to look at?

  • a hardware problem?
  • thumbv7em-none-eabihf toolchain version
  • version of the rust-exercises repository
  • gdb-multiarch and gdb versions
  • probe-rs version
  • ...

ohhhhhhhh, seeing this symbol name, I think I remembered something.

the culprit must be the cortex_m_rt::entry macro, which changed the function name and also shifted line numbers.

the macro is really simple and expands to something like this:

#[export_name = "main"]
pub unsafe extern "C" fn __cortex_m_rt_entry_trampoline() {
	__cortex_m_rt_main()
}
fn __cortex_m_rt_main() -> ! {
	// your original code goes here
}

this explains why the breakpoint cannot be set or is shifted: your origianl main function is renamed to cortex_m_rt_main, while the main symbol in the binary actual points to the generated wrapper extern "C" fn __cortex_m_rt_entry_trampoline()

this also explains why the "step over" command didn't work: it tries to step over the original "real" main function!

you can try this work around:

#[entry]
fn name_of_this_function_desnt_matter() -> ! {
    main()
}
// the "real" main function
fn main() -> ! {
    //...
}

I should have thought of this earlier. in fact, I remember I was bitten by the same thing when I were starting to use rust for embedded system. yet I have not set breakpoints inside the "main" function for a long time, I almost completely forgot about this kind of quirks.

it's funny that, this problem only arise when the program is really simple (the entire app logic is in a single main function).

I hope this time the true cause should be identified. feel free to share more information if problem persists.

I've tried your suggested workaround with blinky:

It allows me to set breakpoints where I couldn't before, e.g. inside the for-loop, and then use continue to stop at each loop count. The next command still does not work exactly as expected (the debugger still enters the toggle() function), and gdb` once crashed.

I've also tested this workaround with our own code and it brings the same benefits.

So while this doesn't seem to fix the problems completely, it improves the debugging -- thanks!

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.