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:
- pause the program
- show assembly and source code
- show and change variable values
- step through the program line by line
Some debuggers can do more advanced things like:
- Pausing the program when certain values are changed
- Run the program backwards to see what caused the current state
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:
-
Control
The next line to be executed, or where the current control flow is located. This corresponds to the instruction pointer or the program counter. -
Breakpoint
A breakpoint is a place in the program where the debugger should pause execution and allow the program state to be inspected. The user can set breakpoints at the desired places in the program. -
Watch
Like a breakpoint, but set on a variable or expression. The debugger will pause the program when the variable or expression changes. Useful for checking when -
Step over
Run the next apparent line of source code. Does not go into any functions called by the current line. -
Step into
Run the next line of source code, going into functions called by the current line if needed. -
Step out
Run the current function until it returns to its caller. -
Target
The program being run by the 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:
list
: show the next few lines of source codedisassem
: show the next few lines of assembly
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:
p b
prints6
p a+b
print11
p sizeof(a)
prints4
on x86p &a
prints0xbffffb78
on my machine
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:
rs
: reverse steprn
: reverse nextrc
: reverse continue
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