Debugging
Setup
Before we start, let's add some code to debug:
// -- snip -- entry!(main); fn main() -> ! { let _y; let x = 42; _y = x; loop {} }
GDB session
We are already inside a debugging session so let's debug our program.
After the load
command, our program is stopped at its entry point. This is indicated by the
"Start address 0x8000XXX" part of GDB's output. The entry point is the part of a program that a
processor / CPU will execute first.
The starter project I've provided to you has some extra code that runs before the main
function.
At this time, we are not interested in that "pre-main" part so let's skip right to the beginning of
the main
function. We'll do that using a breakpoint:
(gdb) break rustled::main
Breakpoint 1 at 0x8000218: file src/main.rs, line 8.
(gdb) continue
Continuing.
Note: automatically using hardware breakpoints for read-only addresses.
Breakpoint 1, rustled::main () at src/rustled/src/main.rs:13
13 let x = 42;
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 four of these breakpoints so it's a good idea to pay attention to these messages.
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
:(
.
At any point you can leave the TUI mode using the following command:
(gdb) tui disable
OK. We are now at the beginning of main
.
We can advance the program statement by statement using the step
command.
So let's use that twice to reach the y = x
statement.
Once you've typed step
once you can just hit enter to run it again.
(gdb) step
14 _y = x;
If you are not using the TUI mode,
on each step
call GDB will print back the current statement along with its line number.
We are now "on" the y = x
statement; that statement hasn't been executed yet. This means that x
is initialized but y
is not. Let's inspect those stack/local variables using the print
command:
(gdb) print x
$1 = 42
(gdb) print &x
$2 = (i32 *) 0x10001fdc
(gdb) print _y
$3 = 134219052
(gdb) print &_y
$4 = (i32 *) 0x10001fd8
As expected, x
contains the value 42
.
_y
however, contains the value 134219052
(?).
Because _y
has not been initialized yet, it contains some garbage value.
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:
i32*
, a pointer to an i32
value.
Another interesting thing is that the addresses of x
and _y
are very close to each other:
their addresses are just 4
bytes apart.
Instead of printing the local variables one by one, you can also use the info locals
command:
(gdb) info locals
x = 42
_y = 134219052
OK. With another step
, we'll be on top of the loop {}
statement:
(gdb) step
17 loop {}
And _y
should now be initialized.
(gdb) print _y
$5 = 42
If we use step
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
.
NOTE If you used the
step
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 led_roulette::main:
11 fn main() -> ! {
0x08000188 <+0>: sub sp, #8
12 let _y;
13 let x = 42;
0x0800018a <+2>: movs r0, #42 ; 0x2a
0x0800018c <+4>: str r0, [sp, #4]
14 _y = x;
0x0800018e <+6>: ldr r0, [sp, #4]
0x08000190 <+8>: str r0, [sp, #0]
15
16 // infinite loop; just so we don't leave this stack frame
17 loop {}
=> 0x08000192 <+10>: b.n 0x8000194 <led_roulette::main+12>
0x08000194 <+12>: b.n 0x8000194 <led_roulette::main+12>
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,
the line number and the address of the instruction the processor will execute next.
(gdb) stepi
0x08000194 17 loop {}
(gdb) stepi
0x08000194 17 loop {}
One last trick before we move to something more interesting. Enter the following commands into GDB:
(gdb) monitor reset halt
Unable to match requested speed 1000 kHz, using 950 kHz
Unable to match requested speed 1000 kHz, using 950 kHz
adapter speed: 950 kHz
target halted due to debug-request, current mode: Thread
xPSR: 0x01000000 pc: 0x08000188 msp: 0x10002000
(gdb) continue
Continuing.
Breakpoint 1, led_roulette::main () at src/main.rs:8
8 let x = 42;
We are now back at the beginning of main
!
monitor reset halt
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 of 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/led-roulette, Remote target
Ending remote debugging.
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.
Don't close OpenOCD though! We'll use it again and again later on. It's better just to leave it running.
What next?
In the next chapter we will learn how to send messages from the micro:bit to your computer, as well as howt to control the HAL GPIO.