If you main use of GDB is for debugging embedded devices you can't really go too long without encountering the GDB remote protocol. This is the protocol used to communicate between the GDB application and the debugger, usually over either TCP or serial.
Although this protocol is documented, it is not always clear exactly when which packets are actually used and when. Not knowing which packets to expect makes implementing the debugger side of things a little tricky. Thankfully there is a straight forward way to see what is going on:
$ set debug remote 1
This post is basically a crash course in using gdb to run and debug
low-level code. Learning to us a debugger effectively can
be much more powerful than ghetto printf() and putc()
debugging. I should point out that I am far from a power-gdb user, and
am usually much more comfortable with printf() and putc(),
so this is very much a beginners guide, written by a newbie. With those
caveats in mind, lets get started.
So the first thing to do is to get our target up and running. For
this our target will be a virtual device running with Skyeye. When you start up
Skyeye and pass it the -d flag, e.g: $ skeye -c
config.cfg -d. This will halt the virtual processor and provide
an opportunity to attach the debugger. The debugger will be available
on a UNIX socket. It defaults to port 12345. Of course
a decent JTAG adapter should be able to give you the same type of
thing with real hardware.
Now, you run GDB: $ arm-elf-gdb. Once gdb is running
you need to attach to the target. To do this we use:
(gdb) target remote :12345. Now you can start the
code running with (gdb) continue.
Now, just running the code isn’t very useful, you can do that already.
If you are debugging you probably want to step through the code.
You do this with the step command. You can step through
code line at-a-time, or instruction at-a-time. At the earliest stages
you probably want to use the si command to step through
instruction at-a-time.
To see what you code is doing you probably want to be able to display
information. For low-level start up code, being able to inspect the register
and memory state is import. You can look at the register using the
info registers command, which prints out all the general-purpose
registers as well as the program counter and status registers.
For examing memory the x command is invaluable. The
examine command takes a memory address as an argument (actually, it
can be a general expression that quates to a memory address). The
command has some optional arguments. You can choose the number of
units to display, the format to display memory in (hex (x), decimal
(d), binary (t), character (c), instruction (i), string(s), etc), and
also the unit size (byte (b), halfword (h), word (w)). So, for
example to display the first five words in memory as hex we can do:
(gdb) x /5x 0x0. If we want to see the values of
individual bytes as decimal we could do: (gdb) x /20bd 0x0.
Another common example is to display the next 5 instructions, which can
be done with (gdb) x /5i $pc. The $pc expression
returns the value in the pc register.
Poking at bits and bytes and stepping instruction at a time is
great for low-level code, but gdb can end up being a lot more useful
if it knows a little bit more about the source code you are
debugging. If you have compiled the source code with the
-g option, your ELF file should have the debugging
information you need embedded in it. You can let gdb know about this
file by using the (gdb) symbol program.elf. Now
that you actually have symbols and debugging information, you can
do things like normal then step command, and it will
step through lines of source code (rather than instructions).
The other nice thing you have is that you can easily set
breakpoint and watchpoints. (You don’t have to have
source debugging enabled for this, but it makes things a lot easier!).
Seting a breakpoint is easy, you can set it on a line e.g: (gdb) break file.c:37,
or on a particular function e.g: (gdb) break schedule.
Breakpoints are neat, but watchpoints are even cooler, since you can
test for a specific conditions e.g: (gdb) watch mask < 2000.
Now that you have these nice watchpoints and breakpoints, you
probably find that most of the time, you just end up printing out some
variables each time you hit the point. To avoid this repetitive typing
you can use the display command. Each expression you
install with the display command will be printed each time program execution
stops (e.g: you hit a break-point or watch-point). This avoids a lot
of tedious typing!
So, this is of course just scratching the surface. One final thing
to consider that will likely make your time using gdb more useful and
less painful (i.e: less repetitive typing), is the define
command which lets you create simple little command scripts. The other
is that when you start gdb you can pass a command script with the
-x. So, you might want to consider, instead of littering
your code with printf() statements everywhere you might want to write
some gdb commands that enable a breakpoint and display some the relevant
data.
Good luck, and happy debugging!