What is a debugger?

You should have a gdb_example directory in your repo with code to follow along with.

A debugger allows you to monitor your program's behavior while it runs. Debuggers can often:

Some debuggers can do more advanced things like:

To do any of these things, debuggers need special CPU instructions and OS access. We won't be reviewing the details of these topics in this course, but taking a compilers course is a good way to get started on these ideas.

Some definitions when using a debugger:

GDB

GDB is the GNU project's debugger. It has a command-line interface and can be intimidating to learn. However, many debuggers provide a similar interface, so it can be helpful to learn.

Using GDB

Start the program with

gdb name_of_program

So, if you want to debug a.out in the current directory:

gdb ./a.out

GDBTUI

gdbtui is a text-user-interface for gdb. It is much easier to understand than the regular gdb interface. By default, gdb only shows context when issuing commands, so it can be hard to understand what is happening. TUI shows the source code context around the line of execution.

To use gdbtui, instead of debugging with gdb ./a.out, use gdbtui ./a.out. If you do use regular gdb, you will want to know these commands:

No matter the mode used, after the debugger starts, you're ready to start issuing commands. The most useful debugging commands are:

r : run : run the program

Run the program until something happens. The program could finish and exit, it could crash, or execution encounters a breakpoint.

A simple example:

r # run the program

An example with arguments:

r arg1 arg2 arg3

b : breakpoint : set a breakpoint

Indicates a place to pause the program execution. When the debugger runs the program, it will stop at any breakpoints it encounters. You can then inspect the state of the program.

b list_node.c:110 #set a breakpoint at line 110 in list_node.c

n : next : run to next apparent line

Execute the current line and move to the next line in the file. If the current line is a function call, the function is executed, returned from, and control stops at next line in the function.

In the code below, using the next command will move to line 3.

   1: int main(int argc, char** argv) {
-> 2:   printArgs(argc, argv);
   3:   return 0;
   4: }

s : step : step to next runnable line

Steps into a function or moves to the next line. If the current line is a function call, control stops at first line in the function.

In the code below, using the step command will move into printArgs(). Before:

   1: int main(int argc, char** argv) {
-> 2:   printArgs(argc, argv);
   3:   return 0;
   4: }

After step:

-> 1: void printArgs(int count, char** str) {
   2:   for(int i=0; i<count; i++)
   3:      printf("%s", str[i]);
   4: }

c : continue : continue the program

Starts running the program again. Use this after hitting a breakpoint. The program will run until it ends, it hits a breakpoint, or it crashes.

p : print : print expression result

Print a variable or expression value.

   1: int main(int argc, char** argv) {
   2:   int a = 5;
   3:   int b = 6;
-> 4:   return 0;
   5: }

In the above example, execution is stopped at line 4. The command p a will print the value 5.

More examples:

Super secret commands

bt : backtrace : show program stack

Shows the stack trace. This is the sequence of function calls that resulted the current position in code. This can be helpful when the debugger stops at a crash to know what chain of events caused the issue.

start : run program, breakpoint at main()

This is a short cut to this:

b main
r

fin : finish : step out of current function

Run the current function till it returns. For example, the debugger is paused inside print:

void print(int count, char** strings) {
   for(int i=0; i<count; i++)
      printf("%s\n", strings[i]);
}

int main(int argc, char** argv) {
   print(argc, argv);
   return 0;
}

Issuing the finish command would cause the debugger to finish all execution of print and pause back in main.

watch : break when item changes

Set a breakpoint for when a value changes. If you are debugging a program and know that a variable has an incorrect value, you can rerun the program and use a watchpoint to find out when the value becomes incorrect.

Here's a small example:

int main(int argc, char** argv) {
    int c[10];
    for(int i=0; i<10; i++) c[i] = i;
    for(int i=0; i<10; i++) c[i] = 0;

    return 0;
}

Debugging this program with watch c[5] will cause it to pause two times: once when c[5] is set to 4 and once when it is set to 0.

b if : break if : break on condition

Only break when some condition is met. This can be useful when a line is going be run many times, but you are only interested in checking program state on certain cases. For example, if a loop is going to execute many times and you want to observe program state in the middle of the loop.

For example:

  1: int main(int argc, char** argv) {
  2:    int i;
  3:    int c[10];
  4:    for(i=0; i<10; i++)
  5:       c[i] = i;
  6:
  7:    return 0;
}

When debugged with b 5 if i>7, execution will pause execution on the last two iterations of the loop. Conditionals set on loop keyword behave oddly, so set them on the inner lines of loops instead.

x : examine : show raw memory

This command is a bit complicated, but allows you to explore memory. The format is x/nfs a, where f is a format (x for hex, d for decimal, f for float, etc.) and s is a size (b for 1 byte, w for 4 bytes), n is the number of items to show, and a is the address to show.

For example:

  1: int main(int argc, char** argv) {
  2:    int i;
  3:    int c[10];
  4:    for(i=0; i<10; i++)
  5:       c[i] = i;
  6:
  7:    return 0;
}

When execution is on line 2, x/10dw &c might print something like

0xbffffb88: -1073742884    0    -1881141100    0
0xbffffb98: 0              0    -1073742852    -1880975602
0xbffffba8: 15             0

If run again when execution is at line 7, the output would be:

0xbffffb88: 0    1    2    3
0xbffffb98: 4    5    6    7
0xbffffba8: 8    9

You can also do things like look at the stack frame. This command will show the 8 things above the stack pointer:

x/8xw $sp-32

i : info : get info about state

This command shows you info about the debugger and program state. Useful things to see:

i locals   #show local variable values
i reg      #show cpu register values
i b        #show current breakpoints and watchpoints
i frame    #show stack frame

d : delete : remove a breakpoint

Delete a breakpoint or watchpoint. Use i b to get a list of breakpoints numbers, then use the number with delete. For example, to delete the second breakpoint:

d 2

There are other breakpoint management commands: clear and enable/disable.

record : turn on reverse debugging

This command allows the program to be debugged forwards (as is normal) and backwards. The debugger records a lot of data to do this, so it might be a slow. This can be very helpful for many types of bugs. These commands allow control to go backwards:

For example, if the program crashes, you can debug with and recording. When the debugger stops at the crash, you can reverse step to find out what caused the problem.

GDB reverse debugging requires special CPU instructions and is not supported on very many architectures. The rr project allows more flexible reverse debugging on some CPUs.

commands/end : complex breakpoints

This allows you to issue commands when breakpoints are hit. Set a normal breakpoint, then issue the commands command. You'll enter a special mode where you can enter more commands, then finish with end. For example, a breakpoint that promptly continues (thus doing nothing):

b 10
commands
c
end