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 session

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 or continue command by mistake and GDB got stuck, you can get unstuck by hitting Ctrl+C.

(gdb) layout asm

GDB session

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.