Debug it
Let's figure out how to debug our little program. It doesn't really have any interesting bugs yet, but that's the best kind of program to learn debugging on.
How does this even work?
Before we debug our program let's take a moment to quickly understand what is actually happening here. In the previous chapter we already discussed the purpose of the second chip on the board, as well as how it talks to our computer, but how can we actually use it?
The little option default.gdb.enabled = true
in Embed.toml
made cargo embed
open a so-called
"GDB stub" after flashing. This is a server that our GDB can connect to and send commands like "set
a breakpoint at address X". The server can then decide on its own how to handle this command. In the
case of the cargo embed
GDB stub it will forward the command via USB to the "debugging probe" on
the second chip. This chip does the job of talking to the MCU for us.
Let's debug!
cargo-embed
is running in our current shell. We can open a new shell and go back into our project
directory. Once we are there we first have to open the binary in gdb like this:
$ gdb ../../../target/thumbv7em-none-eabihf/debug/examples/init
NOTE: Depending on which GDB you installed you will have to use a different command to launch it. Check out chapter 3 if you forgot which one it was.
The ../../..
in this command is needed, since each example project is in a "workspace" that
contains the entire book. Workspaces have a single shared target
directory. Check out Workspaces
chapter in Rust Book for more.
NOTE If
cargo-embed
prints a lot of warnings here don't worry about it. As of now it does not fully implement the GDB protocol, and thus might not recognize all the commands your GDB is sending to it. As long as GDB does not crash, you are fine.
Next we will have to connect to the GDB stub. It runs on localhost:1337
by default so in order to
connect to it run the following:
(gdb) target remote :1337
Remote debugging using :1337
0x00000116 in nrf52833_pac::{{impl}}::fmt (self=0xd472e165, f=0x3c195ff7) at /home/nix/.cargo/registry/src/github.com-1ecc6299db9ec823/nrf52833-pac-0.9.0/src/lib.rs:157
157 #[derive(Copy, Clone, Debug)]
Next what we want to do is get to the main
function of our program. We will do this by first
setting a breakpoint there and then continuing program execution until we hit the breakpoint:
(gdb) break main
Breakpoint 1 at 0x104: file src/05-meet-your-software/examples/init.rs, line 9.
Note: automatically using hardware breakpoints for read-only addresses.
(gdb) continue
Continuing.
Breakpoint 1, init::__cortex_m_rt_main_trampoline () at src/05-meet-your-software/examples/init.rs:9
9 #[entry]
Breakpoints can be used to stop the normal flow of a program. The continue
command will let the
program run freely until it reaches a breakpoint. In this case, until it reaches the main
function because there's a breakpoint there.
Note that GDB output says "Breakpoint 1". Remember that our processor can only use a limited amount
of these breakpoints, so it's a good idea to pay attention to these messages. If you happen to run
out of breakpoints, you can list all the current ones with info break
and delete desired ones with
delete <breakpoint-num>
.
For a nicer debugging experience, we'll be using GDB's Text User Interface (TUI). To enter into that mode, on the GDB shell enter the following command:
(gdb) layout src
NOTE: Apologies Windows users. The GDB shipped with the GNU ARM Embedded Toolchain doesn't support this TUI mode
:-(
.
GDB's break command does works for more than just function names: it can also break at certain line numbers. If we want to break in line 13 we can simply do:
(gdb) break 13
Breakpoint 2 at 0x110: file src/05-meet-your-software/examples/init.rs, line 13.
(gdb) continue
Continuing.
Breakpoint 2, init::__cortex_m_rt_main () at src/05-meet-your-software/examples/init.rs:13
(gdb)
At any point you can leave the TUI mode using the following command:
(gdb) tui disable
We are now "on" the _y = x
statement; that statement hasn't been executed yet. This means that x
is initialized but _y
could contain anything. Let's inspect x
using the print
command:
(gdb) print x
$1 = 42
(gdb) print &x
$2 = (*mut i32) 0x20003fe8
(gdb)
As expected, x
contains the value 42
. The command print &x
prints the address of the variable
x
. The interesting bit here is that GDB output shows the type of the reference: *mut i32
, a
pointer to a mutable i32
value.
If we want to continue the program execution line by line, we can do that using the next
command.
Let's proceed to the loop {}
statement:
(gdb) next
16 loop {}
And _y
should now be initialized.
(gdb) print _y
$5 = 42
Instead of printing the local variables one by one you can also use the info locals
command:
(gdb) info locals
x = 42
_y = 42
(gdb)
If we use next
again on top of the loop {}
statement, we'll get stuck because the program will
never pass that statement. Instead, we'll switch to the disassemble view with the layout asm
command and advance one instruction at a time using stepi
. You can always switch back into Rust
source code view later by issuing the layout src
command again.
NOTE: If you used the
next
orcontinue
command by mistake and GDB got stuck, you can get unstuck by hittingCtrl+C
.
(gdb) layout asm
If you are not using the TUI mode, you can use the disassemble /m
command to disassemble the
program around the line you are currently at.
(gdb) disassemble /m
Dump of assembler code for function _ZN12init18__cortex_m_rt_main17h3e25e3afbec4e196E:
10 fn main() -> ! {
0x0000010a <+0>: sub sp, #8
0x0000010c <+2>: movs r0, #42 ; 0x2a
11 let _y;
12 let x = 42;
0x0000010e <+4>: str r0, [sp, #0]
13 _y = x;
0x00000110 <+6>: str r0, [sp, #4]
14
15 // infinite loop; just so we don't leave this stack frame
16 loop {}
=> 0x00000112 <+8>: b.n 0x114 <_ZN12init18__cortex_m_rt_main17h3e25e3afbec4e196E+10>
0x00000114 <+10>: b.n 0x114 <_ZN12init18__cortex_m_rt_main17h3e25e3afbec4e196E+10>
End of assembler dump.
See the fat arrow =>
on the left side? It shows the instruction the processor will execute next.
If not inside the TUI mode on each stepi
command GDB will print the statement and the line number
of the instruction the processor will execute next.
(gdb) stepi
16 loop {}
(gdb) stepi
16 loop {}
One last trick before we move to something more interesting. Enter the following commands into GDB:
(gdb) monitor reset
(gdb) c
Continuing.
Breakpoint 1, init::__cortex_m_rt_main_trampoline () at src/05-meet-your-software/src/main.rs:9
9 #[entry]
(gdb)
We are now back at the beginning of main
!
monitor reset
will reset the microcontroller and stop it right at the program entry point.
The following continue
command will let the program run freely until it reaches the main
function that has a breakpoint on it.
This combo is handy when you, by mistake, skipped over a part of the program that you were interested in inspecting. You can easily roll back the state of your program back to its very beginning.
The fine print: This
reset
command doesn't clear or touch RAM. That memory will retain its values from the previous run. That shouldn't be a problem though, unless your program behavior depends on the value of uninitialized variables — but that's the definition of Undefined Behavior (UB).
We are done with this debug session. You can end it with the quit
command.
(gdb) quit
A debugging session is active.
Inferior 1 [Remote target] will be detached.
Quit anyway? (y or n) y
Detaching from program: $PWD/target/thumbv7em-none-eabihf/debug/meet-your-software, Remote target
Ending remote debugging.
[Inferior 1 (Remote target) detached]
NOTE: If the default GDB CLI is not to your liking check out gdb-dashboard. It uses Python to turn the default GDB CLI into a dashboard that shows registers, the source view, the assembly view and other things.
If you want to learn more about what GDB can do, check out the section How to use GDB.
What's next? The high level API I promised.