Learning GDB
I’m starting to learn C after primarily working in Ruby and one intimidating things about C so far is the lack of visibility into programs. The ability in Ruby to run code directly from a REPL — and in the middle of your program with binding.pry
— gives me confidence that I understand how a program is behaving. So far coding in C feels more like working on a black box, but I’m hoping learning GDB will help with that.
Inspired by Julia Evans, I thought it would be useful to document what I’ve learned so far in the form of a blog post. I had trouble finding a good resource that gave a basic overview of GDB and its capabilities, so I want to try to do that here. Two resources that did help a lot were this post by Julia Evans, and this part of the GDB docs. Using the help
command from within gdb
was also useful to learn more about the commands it supports. I have only just started using GDB so definitely let me know if there are any mistakes or if I am misunderstanding anything!
Here’s the program we’re going to walk through running gdb
on:
#include <stdio.h>
int add(int a, int b) { return (a + b); }
void hello_world() {
int i = add(1, 2);
printf("Hello World %i\n", i);
}
int main(int argc, char **argv) { hello_world(); }
Compile your program with symbols
The first step is to compile your program for GDB using the -g
flag:
gcc -g src/main.c -o bin/main
I’m sure there is more to it than this, but what I’ve observed is that the -g
flag compiles your program in such a way that gdb
is able to create a mapping between lines in the source code and the binary.
Set breakpoints
In Ruby (at least using Pry) I debug by setting breakpoints in the source and then running the program as normal. With GDB we instead run the gdb
program, specify breakpoints, and then trigger our own program from within gdb
. I think of debugging in Ruby as working from inside the target program, while gdb
instead lets us inspect a program from the outside.
First we call gdb
with the filename of our program’s binary:
gdb bin/main
Then we use the break
command from within gdb
to set breakpoints:
(gdb) break hello_world
Breakpoint 1 at 0x6cc: file src/main.c, line 6.
This sets a breakpoint at the start of the hello_world
function. We can then run the program, and execution will pause at the breakpoint:
(gdb) run
Starting program: .../bin/main
Breakpoint 1, hello_world () at src/main.c:6
6 int i = add(1, 2);
We can also set a breakpoint at any line in the source code:
(gdb) break src/main.c:7
Breakpoint 1 at 0x6de: file src/main.c, line 7.
Step through program execution
Once the program is paused we can step through execution using next
and step
. next
(or n
) resumes execution until the next source line of the current function — skipping over calls to other functions. next
(or s
) also resumes execution until the next line, but “steps into” called functions.
(gdb) run
Starting program: .../bin/main
Breakpoint 1, hello_world () at src/main.c:6
6 int i = add(1, 2);
(gdb) next
7 printf("Hello World %i\n", i);
To resume execution of the program until the next breakpoint, or until it completes, use continue
(or c
).
Inspect program state
While execution is paused GDB has many tools that let us inspect the current state, and can function almost like a REPL. Here are some of the tools that seem most essential to me.
print
displays the value of a variable:
(gdb) print i
$1 = 3
I’m not exactly sure what the $1
means, but the value here is 2
.
print
actually supports full expressions, not just variables, and we can even call functions:
(gdb) p add(4, 5)
$1 = 9
whatis
prints the type of a variable:
(gdb) whatis i
type = int
disassemble
shows the assembly code for the current function:
(gdb) disassemble
Dump of assembler code for function hello_world:
0x00005555555546c4 <+0>: push %rbp
0x00005555555546c5 <+1>: mov %rsp,%rbp
0x00005555555546c8 <+4>: sub $0x10,%rsp
0x00005555555546cc <+8>: mov $0x2,%esi
0x00005555555546d1 <+13>: mov $0x1,%edi
0x00005555555546d6 <+18>: callq 0x5555555546b0 <add>
0x00005555555546db <+23>: mov %eax,-0x4(%rbp)
=> 0x00005555555546de <+26>: mov -0x4(%rbp),%eax
0x00005555555546e1 <+29>: mov %eax,%esi
0x00005555555546e3 <+31>: lea 0xba(%rip),%rdi # 0x5555555547a4
0x00005555555546ea <+38>: mov $0x0,%eax
0x00005555555546ef <+43>: callq 0x555555554560 <printf@plt>
0x00005555555546f4 <+48>: nop
0x00005555555546f5 <+49>: leaveq
0x00005555555546f6 <+50>: retq
End of assembler dump.
The =>
shows either the last or the next instruction, I’m not sure which.
I haven’t worked with assembly before so this isn’t very helpful to me right now, but I can see how it would useful to see how the compiler has translated your source into assembly. Perhaps there is a bug because your expectation of how the source should be compiled does not match the actual behavior of the compiler.
x
lets us inspect a memory region by address. For example, we can use it to examine one of the addresses from the above assembly:
(gdb) x/s 0x5555555547a4
0x5555555547a4: "Hello World %i\n"
The /s
part specifies the format to display the data in — here I used s
for a string.
Quiting
I often have trouble figuring out or remembering the incantation to quit a command line application — is it quit()
, exit;
, or :q
? CTRL+D or quit
works for gdb.
I haven’t used gdb
to debug something I’m actually working on yet, but these are the things that seem like they will be most useful.