Lab 3: Debugging with GDB
In this lab, you will get hands-on experience using GDB to debug C programs. GDB will become an essential tool for your PA work as this course develops, so lock in!
Lab 3 learning objectives
- Recognize the need for debugging using a debugger
- Compile a C program for debugging
- Run a program in GDB while stopping at breakpoints
- Use various GDB commands to observe and control program execution
- Use GDB to troubleshoot a crashing program
Table of contents
- Practice Skill Demo
- Icebreaker
- What and why debugging?
- Debugging Activity
- TUI, Commands, Segfault, and BackTrace
- Next steps: Practice Skill Demo
Practice Skill Demo
We have no new Review Quiz this week. In the second half of this lab, we will instead conduct a practice Skill Demo on PrairieLearn.
Icebreaker
Pick a song as the soundtrack of your life! Why did you choose this song? Was this song part of the directory structure you created in Lab 1? What’s your favorite genre? Who’s your favorite artist?
Please write the answers of yourself and one of your group members in your lab report. No check-off is needed!
What and why debugging?
Debugging is the practice of finding and fixing errors in programs. There are no real bugs in your program, although this has actually happened at least once before.
Like programming, debugging is also an incremental process. Programmers often need to isolate and debug one error at a time, rather than all errors at once.
We could insert some print statements into our code to try and figure out what’s going on—for small programs, print statements are often enough! However, there are several situations where we would want to use a debugger instead:
- If your program reports a segmentation fault, it can be difficult to pin down exactly where the segfault took place using print statements. A debugger can pause your program when it crashes and enable you to inspect its final state.
- If you are searching for a bug in a loop with many iterations, searching through print statement output from that loop can be quite cumbersome.
- You might work on a program for which you can’t see the output of print statements (like your web browser or vim itself). In this case, the debugger is a convenient tool for inspecting program state.
GDB (GNU Project Debugger) is a command line program that offers a convenient way for us to pause execution at any point in the code, manually inspect the values of any variable in the scope, and walk through the program step-by-step.
Debugging Activity
Clone our starter code repo into your cse29
folder on ieng6
. This starter code contains two programs which we will debug with GDB: index_of.c
and charshift.c
. We’ll start with the index_of.c
program to introduce basic GDB commands, then look at charshift.c
to talk about segmentation faults and how to debug them with GDB.
Compile for GDB
The correct behavior of index_of
is to find the index of the first occurrence of the character f
in a string str
. Compile index_of.c
and try running it on your own input. Try a string without f
, then try a string with at least 10 characters whose first occurrence of f
is the last character. How is the actual output different from our expected output?
In order to use GDB, we have to compile our program with the -g
flag. This tells the compiler to add some extra information to your executable file, which GDB will use. Recompile index_of.c
with this flag.
$ gcc -o index_of -g index_of.c
In the future, please add -Wall
to your compilation command to ask the compiler to help you find potential bugs. We are leaving it off for this lab so that you can practice finding those bugs using GDB.
Running in GDB
To run the program in GDB, use the gdb
and pass the program as an argument:
$ gdb ./index_of
This will spit out several lines of text, with some software and legal information that we don’t need to worry about. In the last line, you should notice that the terminal command prompt $
(as well as the other stuff before $
) has been replaced by the GDB command prompt (gdb)
. This means that the GDB program is active and ready to receive commands.
Use the run
command to run the program in GDB. Since this program takes in a command line argument, you can provide it like so:
(gdb) run cse29_is_fantastic!
This is just like running the command ./index_of cse29_is_fantastic!
. For now, it runs through the program without stopping. GDB might say something here about “Missing separate debuginfos”, but don’t worry about this. GDB is just asking to install some extra packages to get more information for debugging, but these aren’t necessary for our purposes right now.
To quit out of GDB, you can use the quit
command.
(gdb) quit
Breakpoints
To pause execution at some point in the program, we have to set a breakpoint. Restart gdb ./index_of
. Use the break
command and give it a location in the source code that you want to pause execution at. At the moment, we’re not too sure exactly which part of the code is wrong, so we’ll pause at the beginning (the main function) and go step-by-step from there. You can use either
(gdb) break index_of.c:12
to set a breakpoint at line 12 in the file index_of.c
, which happens to be where the main function starts, or more simply
(gdb) break main
to automatically set a breakpoint wherever the main function is. You can set breakpoints at any line in a source file, but usually we find it helpful to set them at the start of functions, to investigate the behavior of a function from its beginning.
You can use the info breakpoints
command to list out which breakpoints have been set, as well as some information about where they are and what their associated number is. You can delete breakpoints with the command delete <number>
where <number>
is the number associated with the breakpoint.
After setting a breakpoint, you can run the program again (copy the run
command from earlier) and see that it pauses execution right where the breakpoint is.
Ask a tutor or TA to verify that your debugger is paused at the beginning of the main
function, and put a screenshot of the last few lines of your terminal into your lab report.
TUI, Commands, Segfault, and BackTrace
TUI in GDB
Notice that GDB printed out a single line of code from the source file. This is the line of code that is about to be executed next. Here, you can use the list
command to print out some of the surrounding code. This can be helpful to contextualize where the code is running.
The layout src
command may also be helpful to enable a TUI (Text User Interface) for GDB that automatically renders a portion of the source code in the top half of the screen. The highlighted line in the TUI shows which line of code is to be executed next. To disable the TUI, press Ctrl
+ X
, followed by A
.
(gdb) layout src
The TUI won’t render any source code unless there is a program that is active, i.e. in the middle of execution.
Sometimes the TUI can get messed up if the program prints something out (which index_of
does). When this happens, try using the refresh
command to refresh the TUI and hope that it will restore its correct format. If something seems really messed up, try disabling and re-enabling the TUI.
On the left hand side of the TUI, you can see that the breakpoint is marked with B+>
.
Basic GDB commands
After setting a breakpoint and running, execution is paused right after we enter the main function, before any other code is executed. You can verify this by using the print
command to print out the contents of the variable result
:
(gdb) print result
and see that it contains an uninitialized value. We see this because the line that initializes the variable has not been executed yet.
To run the next line of code, we use the next
command:
(gdb) next
The highlighted line has moved on, indicating that the previous line has been executed. You can check the values of argc
and argv
using the print
command, which accepts any valid expression in C:
(gdb) print argc
(gdb) print argv
(gdb) print argv[0]
(gdb) print argc < 2
Let the program finish by using the continue
command. Does its output match what you expect?
We now know that this input exposes a bug inside index_of
, so we want to look into this function with GDB. Let’s restart the execution and make line-by-line steps. Run the break main
command to have the program stop when the main
function begins, and then use the run
command to start the program again.
Use next
to execute the program up until, but not actually executing, the call to index_of()
. You’ll know that index_of()
is next to be executed, but not executed yet, when the line of code is highlighted. This time, instead of using next
to run the entire index_of()
function at once, use the step
command to step into the function and begin executing line-by-line from inside:
(gdb) step
GDB also provides shortcuts for some commonly used commands, which can be used in place of the full command name. Some of those include:
r
forrun
,q
forquit
,b
forbreak
,p
forn
fornext
, ands
forstep
.In addition to shortcuts, inputting no command and pressing
Enter
will automatically execute the most recently used command. This can be helpful when you need to usenext
repeatedly.
Then continue using next
to run through the loop and figure out why index_of()
is buggy. Try printing out the relevant variables as you go through each iteration. Once you figure out why index_of()
isn’t working, implement a fix for it.
Ask a tutor or TA to check your fix for index_of
, and put a screenshot of your fixed function’s code into your lab report.
This buggy program demonstrated an example of a logical error. A logical error is one in which the behavior of the program is different than what we expect it to be, without refusing to compile or crashing at runtime. However, before your fix, it was theoretically possible for this program to crash—why do you think it was? Feel free to verify your thoughts with your peers, tutors or TA.
When you tried to print
str
insideindex_of
, GDB printed the entire string, just as we expected. However, what if you want to print the contents of an integer array passed into a function?If you’d like, try this with
contains.c
from our previous labs (you canwget
our version if you don’t have one—visit Lab 2’s instructions for the command). When you try printing outarr
like you did while inmain()
, you’ll realize that it prints out some hexadecimal number, instead of its contents. This happens becausearr
is passed intocontains()
as a pointer to the start of the array. The hexadecimal number you see is the address of the start of the array. You can useprint *arr
(arr
with the dereference operator*
) to dereference the pointer and get the value at the start of the array, orprint *arr@NUM
to print out the values stored at addressarr
and the nextNUM
addresses. This means you can useprint *arr@6
to print out all of the contents of the size 6 array.
Segfault and Backtrace
Another common type of error is a segmentation fault (or just segfault). Segmentation faults can happen when we try to access someplace in memory illegally (yes, that’s the technical term). In shift_chars.c
, we’ll look at an example of that.
Compile charshift.c
for GDB and try running ./charshift
. It should immediately crash and give you a Segmentation fault (core dumped)
error, telling you that some kind of illegal memory access happened. Most unhelpfully, the error message does not care to tell you why or even where it happened. Somewhat helpfully, GDB can at least answer the “where”. From this lab onward, whenever you see your program segfaulting, you should consider debugging it with GDB.
Start GDB with the charshift
program. This time, instead of setting a breakpoint, we can let the program run through and give us a segmentation fault. GDB immediately tells us where the segfault happened and gives us a line number. From the output, we can see that the segfault occurred in shift_chars()
, where we attempt to dereference the pointer and see if it is in the range of capital letters. But main()
calls shift_chars()
, so which one is the one causing the error?
Here, we can use the backtrace
command (or the shorter bt
) to show a backtrace (also called a stack trace) of the functions that were called leading up to the segfault. The output shows that the segfault happened in shift_chars()
, when shift_chars()
was called from main()
at a certain line. This helps you discern which specific call to a function causes a segfault, when you have multiple calls to the same function. Feel free to run layout src
to help you find the line referenced from the backtrace.
Since we know that segfaults are caused by illegal memory access, and we know that line 6 in charshift.c
caused a segfault, we can infer that ch
probably contains an address that the program can’t access legally. Confirm this by running print ch
. Then, run print str
to see the address of the beginning of the message. What do you notice between these two addresses, and what does that imply about the loop in shift_chars
? This should help you pinpoint and fix the bug. Feel free to work with your peers and ask your tutors/TAs for a hint if you’re stuck!
Fix the bug in charshift.c
. Ask a tutor or TA to check your fix, and put a screenshot of the fixed program’s output into your lab report. You’re now ready to submit it to Gradescope!
Next steps: Practice Skill Demo
Unlike the Review Quizzes, the Practice Skill Demo is not graded. When we resume from the 5-minute break, you should decide whether to use a lab computer for the Practice Skill Demo. We highly recommend using a lab computer because its environment will be identical to that during the actual Skill Demo, especially if you use a Mac. But, if you can’t sign into PrairieLearn on your lab computer in 5 minutes or don’t have access to one, feel free to use your own computer. You may find PrairieLearn at us.prairietest.com.
We encourage you to work through the problem on your own, but you may ask for help from your tutors/TA if you’re stuck. Please do not collaborate with others in your group.
If you finish the Practice Skill Demo problem early, you’re free to leave the lab. Best wishes on your Skill Demo!