Development tools: GDB
Goals
- Learn strategies for debugging code
- Practice using the gdb debugging tool
- Reinforce your understanding of memory
Overview
In this assignment, you will learn how to debug C programs using gdb, a very widely used command-line debugger. You still start by reading a tutorial. Then, you will use gdb to diagnose some problems in a small program. You will not turn in any C code. Instead, you will turn in your gdb sessions in the form of log files that capture your commands and gdb's output.
1. Setup
In this course, you will work on the ecegrid server. That's where you will get your starter files (if any), pre-test your submission (when possible), and submit your code. This gives everyone the same environment, with the same versions of GCC and GDB and the same platform for testing code.
Before you proceed, please complete the setup.
2. Read
Before you do anything else, READ the following sections of Richard M. Stallman's excellent gdb tutorial. Do not skim. We think you will find this tutorial easy to understand. If not, post questions to Piazza. The rest of this assignment will assume you have read and understood every word of this (except the parts that it says to skip).
- gdb Frequently Asked Questions (FAQ) (only #1, #2, #3, and #4; skip the rest)
- How do I use gdb? (all)
- How do I watch the execution of my program? (all)
- How do I use the call stack? (all)
- How do I use breakpoints? (all except 4.3)
- How do I use watchpoints? (all)
- Advanced gdb Features (only 6.1 and 6.3; skip the rest)
- Example Debugging Session: Infinite Loop Example (all)
- Example Debugging Session: Segmentation Fault Example (all)
When asking questions to course staff, refer to the relevant section of the reading.
That is so we can clarify the issue in that context. Everyone needs to come away from this assignment with a big picture understanding of how you can use gdb to solve future problems—not just the specific commands you need for this homework.
3. Get the homework files
To fetch the files for this assignment, enter
264get hw03
from bash. That should create a new directory called
hw03. To see the directory enter
ls
. Next, enter
cd hw03
to enter the directory and start working.
4. Try the code
Get the code for hw03 using 264get. You will find four files:
-
prime_factor.c - the implementation file, containing the
print_prime_factors(…)
function, as well as some helper functions -
prime_factor.h - the header file, containing the declaration of the
print_prime_factors(…)
function -
test_prime_factor.c - a test file that calls
print_prime_factors(…)
in several ways to make sure it is working - test_prime_factor.txt - the intended output when the above files are compiled and run
This code has bugs. You will learn how to find them. Finding the bugs is not the purpose of the assignment, so you are welcome to ask course staff or classmates for help finding them. Your main purpose here is to understand the functionality of gdb, and demonstrate that you are able to use it.
Compile prime_factor.c and to create an executable called prime_factor.
gcc -o prime_factor prime_factor.c test_prime_factor.c
Try running it.
./prime_factorNotice that it prints junk indefinitely. There must be an infinite loop. Press Ctrl-C to stop it.
5. Start gdb and turn on logging
Run the prime_factor program using the debugger (gdb). The command to start gdb was in the required reading for this assignment. (Hint: See section 1.2 “How do I run programs with the debugger?”.) If you have not read all 9 sections yet, please stop and do so now.
For this assignment, you save the commands you type and gdb's output in files, which you will turn in. You will have two debugging sessions in gdb. For simplicity, you will save the log files for the two sessions separately. Logging must be turned on manually when you start gdb. For the first gdb session, enter the following four commands:
(gdb) set logging file gdb.1.log (gdb) set logging on (gdb) set history filename gdb.1.history (gdb) set history save (gdb)
6. Diagnose the infinite loop
Start prime_factor from within gdb using the run command. It will go into an infinite loop.
(gdb) run
Starting program: …/prime_factor
2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2…
Press Ctrl-C to stop your program. You will now be in gdb, ready to diagnose the problem.
… 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
Program received signal SIGINT, Interrupt.
0x0000003f1eadb7d0 in __write_nocancel () from /lib64/libc.so.6
(gdb)
View the backtrace to see what function you are in, and what function called it. (Your output may differ slightly from what you see below.)
(gdb) ████████████ #0 0x0000003f1eadb7d0 in __write_nocancel () from /lib64/libc.so.6 #1 0x0000003f1ea71943 in _IO_new_file_write () from /lib64/libc.so.6 #2 0x0000003f1ea72ef5 in _IO_new_do_write () from /lib64/libc.so.6 #3 0x0000003f1ea715bd in _IO_new_file_xsputn () from /lib64/libc.so.6 #4 0x0000003f1ea45018 in vfprintf () from /lib64/libc.so.6 #5 0x0000003f1ea4effa in printf () from /lib64/libc.so.6 #6 0x0000000000400658 in print_prime_factors (n=0) at prime_factor.c:29 #7 0x000000000040070b in main (argc=1, argv=0x7fffffffda48) at test_prime_factor.c:9 (gdb)
As you can see, your function, print_prime_factors(…)
, has called printf(…)
,
which called some other functions. Use the frame command to tell gdb you want to
focus on what is happening in the print_prime_factors(…)
function. (The frame number you use may
vary from what you see below.)
(gdb) ████████████ #6 0x0000000000400658 in print_prime_factors (n=0) at prime_factor.c:29 29 printf(" %d", 2); (gdb)
We are at line 29. (Your output may vary slightly.) View the code near where you are.
(gdb) ████████████ 24 printf(" (none)"); 25 } 26 else { 27 // Print all occurrences of 2 in the prime factorization 28 while(n % 2 == 0) { 29 printf(" %d", 2); 30 n %= 2; // Divide n by 2 (integer division) 31 } 32 33 // Try all odd integers, from 3 to sqrt(n) (gdb)
We are in a while loop. Apparently, the while loop is never exited. Have gdb print the local variables and arguments to the current function.
(gdb) ████████████ No locals. (gdb) ████████████ n = 0 (gdb)
Why is n=0?
In the above sample output, the first command prints the local variables. The second command prints the arguments that were passed to the current function. Note that when gdb prints the value of an argument, it prints the current value, which may be different from what was initially passed in. In this case, you are seeingn=0
even though the function was originally called asprint_prime_factors(6)
(with n=6).
Let's restart the program, but this time, we will trace through to find out
what is happening in the while loop. Before you restart the program, set
a breakpoint to stop when we enter main(…)
. (Use the name
of the function, not the line number.)
(gdb) ████████████ main Breakpoint 1 at 0x4006ed: file test_prime_factor.c, line 7. (gdb)
Now, restart the program using the run (or r) command. GDB will stop at the beginning of
main(…)
.
(gdb) run Starting program: prime_factor Breakpoint 1, main (argc=1, argv=0x7fffffffda48) at test_prime_factor.c:7 7 print_prime_factors(6); (gdb)
It's okay if it says Start it from the beginning? (y or n) y, too.
Step through the code for a while. There are two commands in gdb for
stepping through code. One will step over function calls, while the other
will step into them. To get into print_prime_factors(…)
you will need to first use the
command that steps into a function call.
(gdb) ████████████ print_prime_factors (n=6) at prime_factor.c:17 17 if(n <= 0) { (gdb)
Now you are in print_prime_factors(…)
. Find the line number of the beginning of the
while loop, from within gdb, so that we can set a breakpoint there. Once
you have entered the command, you can simply press enter to repeat it.
That will cause gdb to print more lines. (This trick works for many gdb
commands.)
(gdb) ████████████ 12 |* This is about finding bugs. *| 13 |* *| 14 \*****************************************************/ 15 16 void print_prime_factors(int n) { 17 if(n <= 0) { 18 printf("Only positive numbers are supported.\n"); 19 } 20 else { 21 printf("Prime factors of %d:", n); (gdb) 22 23 if(n == 1) { 24 printf(" (none)"); 25 } 26 else { 27 // Print all occurrences of 2 in the prime factorization 28 while(n % 2 == 0) { 29 printf(" %d", 2); 30 n %= 2; // Divide n by 2 (integer division) 31 } (gdb)
The line number is 28. Set a breakpoint at that line number. (You may need to specify the filename.)
(gdb) ████████████ prime_factor.c:28 Breakpoint 2 at 0x400669: file prime_factor.c, line 28. (gdb)
Now, tell gdb to continue running your code until it hits your breakpoint (or exits).
(gdb) ████████████ Continuing. Breakpoint 2, print_prime_factors (n=6) at prime_factor.c:28 28 while(n % 2 == 0) { (gdb)
Great. The breakpoint worked. Notice that gdb prints the line of code that is about to be executed (line 28, in this case).
It is important to note that gdb breaks before executing the line. In this particular case, it doesn't make any difference, but if this line modified a variable, you would be looking at the state of variables before that variable was modified.
Print the value of n using the print (or p) command.
(gdb) ████████████ $1 = 6 (gdb)
The value of n is 6, which is expected.
Step through two more lines of code. We are trying to figure out why
n was being set to 0. This time, we will need to use the command
that steps over function calls, since you probably don't want to see
what happens inside printf(…)
. Again, remember
that to repeat the command, you can just press enter.
(gdb) ████████████ 29 printf(" %d", 2); (gdb) 30 n %= 2; // Divide n by 2 (integer division) (gdb)
Let's check the value of n again, using the same command you used above.
(gdb) ████████████ $2 = 6 (gdb)
The value of n is still 6. But remember: You are looking at the value before this line of code is executed.
Step one more time and check the value of n again, using the same commands you used above.
(gdb) ████████████ Breakpoint 2, print_prime_factors (n=0) at prime_factor.c:28 28 while(n % 2 == 0) { (gdb)
You saw the breakpoint message because we looped around to the top of the loop, which is where we had set the breakpoint. Breakpoints remain as long as gdb is running, unless you explicitly delete them.
Print the value of n again.
(gdb) ████████████ $3 = 0 (gdb)
We found the bug! It was the last line that executed before this one (line 30).
The comment on that line says it is supposed to divide, but it is using the
modulo assignment operator (%=
) instead of the
division-assignment operator (/=
).
The%
operator is called “modulo” (or “mod”) and gives you the remainder from integer division. For example,7 % 5
is2
because 5 divided by 5 is 1 with a remainder of 2.
The%=
and/=
operators are just shortcuts.x %= y;
is the same asx = x % y;
. Likewisex /= y;
is the same asx = x / y;
Quit gdb. When it warns that your debugging session is active and asks if you really want to quit, answer "y".
(gdb) ████████████ A debugging session is active. Inferior 1 [process 18729] will be killed. Quit anyway? (y or n) y aq@ecegrid-thin1 ~/264/hw03 $
7. Fix the infinite loop in the code
Edit prime_factor.c (using vim or your preferred code editor) and fix the bug.
Simply change the %=
operator to /=
at line 30.
8. Test the fix
Compile and run the code again, just like before.
gcc -o prime_factor prime_factor.c test_prime_factor.c ./prime_factor
This time the results are more reasonable.
Prime factors of 6: 2 3 Prime factors of 1: (none) Only positive numbers are supported. Prime factors of 48: 2 2 2 2 3 Prime factors of 49: 49 Prime factors of 74: 2 37
Let's check these results more carefully. Before creating the test code (test_prime_factor.c), we wrote the expected output in a text file (test_prime_factor.txt). We will compare the output of the program with the contents of that file.
diff -w <(./prime_factor) test_prime_factor.txt 5c5 < Prime factors of 49: 49 --- > Prime factors of 49: 7 7
The diff command is normally used to compare the contents of two text files. It tells you which
lines are different. In this case, we are using it in a slightly different way, comparing
the output of the ./prime_factor
command with the contents of the text file
test_prime_factor.txt.
We see that the output of the command is different from the expected output. The prime factorization of 49 is 7*7, not 49!
9. Diagnose the incorrect result
Start gdb like before and turn on loging. This time, you will use different filenames for the logs, to avoid overwriting the old ones.
(gdb) set logging file gdb.2.log (gdb) set logging on (gdb) set history filename gdb.2.history (gdb) set history save (gdb)
For print_prime_factors(49)
, it should have printed 7 7
instead of 49. Your job is to find out why.
This time, instead of setting a breakpoint at main(…)
,
set the breakpoint for the top of print_prime_factors(…)
.
(gdb) ████████████ Breakpoint 1 at 0x400653: file prime_factor.c, line 17. (gdb)
Run your program (inside GDB, of course).
(gdb) ████████████ Starting program: prime_factor Breakpoint 1, print_prime_factors (n=6) at prime_factor.c:17 17 if(n <= 0) { (gdb)
The first call to print_prime_factors(…)
was with n=6. Use the continue (or c) command
several times until it is entering print_prime_factors(…)
with n=49.
(gdb) ████████████ Continuing. Prime factors of 6: 2 3 Breakpoint 1, print_prime_factors (n=1) at prime_factor.c:17 17 if(n <= 0) { (gdb) Continuing. Prime factors of 1: (none) Breakpoint 1, print_prime_factors (n=0) at prime_factor.c:17 17 if(n <= 0) { (gdb) Continuing. Only positive numbers are supported. Breakpoint 1, print_prime_factors (n=48) at prime_factor.c:17 17 if(n <= 0) { (gdb) Continuing. Prime factors of 48: 2 2 2 2 3 Breakpoint 1, print_prime_factors (n=49) at prime_factor.c:17 17 if(n <= 0) { (gdb)
This is the first line in print_prime_factors(…)
.
Use the until command to have GDB run until the top of the for loop.
(gdb) ████████████ print_prime_factors (n=49) at prime_factor.c:34 34 for(int i = 3; i * i < n; i += 2) { (gdb)
Hint: To learn about the until command, try help until
.
Now step through to find out what is causing the incorrect output.
OPTIONAL: Find and fix the bug that is causing the incorrect output. We won't check whether you fixed the last bug, but you are already pretty close. (Hint: The problem is on line 34.)
10. Submit
First, check that you have all four log/history files.
ls -l
The ls -l
command lists all files in the current directory,
including the file size and date.
Make sure you see all of the following files, and their sizes are not 0.
- gdb.1.history - command history from the first gdb session (diagnosing infinite loop)
- gdb.1.log - output from the first gdb session (diagnosing infinite loop)
- gdb.2.history - command history from the second gdb session (diagnosing memory problem)
- gdb.2.log - output from the second gdb session (diagnosing memory problem)
You will submit those four files using the 264submit command on ecegrid. In general, the command is used like this:
264submit ASSIGNMENT FILES…
Here's how you submit the files for this assignment:
264submit hw03 gdb.1.history gdb.1.log gdb.2.history gdb.2.log
If that seems like too much typing, here's a shortcut that does the same thing:264submit hw03 gdb.*It submits all files that start with “gdb.”. (Any extra files you submit will simply be ignored.)
Requirements
Look at your files and make sure they contains all of the steps above. If anything goes wrong, you might need to redo this. It is okay if you experimented with commands or started, stopped, made mistakes, etc. in the middle, as long as those steps are in there. Differences in memory addresses will be ignored.
Pre-tester
Throughout this course, it is your responsibility to ensure that your submission meets the criteria. For some assignments, we may offer a pre-tester to help you avoid big surprises. The feedback it gives is limited, to ensure that everyone learns to test their own code. (Obviously, this assignment is atypical.)
To use the pre-tester, you must first submit your code. Then, type the following command. Do this only after you have submitted, and only after you believe your submission is perfect. The pre-tester is not a substitute for your own checking.
264test hw03
Do not ask TAs or instructors which tests you failed.
Keep in mind:
- Pre-testing is intended only for those who believe they are done and believe their submission is perfect.
- You are responsible for ensuring that your submission meets the requirements.
- The pre-tester is not part of the requirements of any assignment.
- If we discover that we have not checked some significant part of the assignment requirements, we may add additional tests at any time up to the point when scores are released.
- The pre-tester will only be enabled after much of the class has submitted the assignment, and at least a few people have submitted perfect submissions. This is to allow us to test the pre-tester.
- The pre-tester checks your most recent submission. You must submit first.
- In the future, there will be limits on the number of times you can run the tester. You will be able to run it no more than 24 times in a 24-hour period. (This is not implemented yet, but will be added soon.)
Your score will be posted to the Scores page after the deadline for each assignment.
Q&A
-
How will this be scored?
We will look through your gdb.log file for the commands needed to follow the steps listed above. Make sure you have done all of the steps listed. It's okay if you have some junk in between, or if you have to repeat things now and then. -
Will there be partial credit?
Yes, but you shouldn't need it. As long as you read the tutorial carefully, and get clarification on anything you misunderstood, the rest of this should be pretty easy. -
Can I start over?
Yes, of course. One way is to just delete your hw03 directory (rm -rf hw03) and then 264get hw03 again. If you want to redo just one session or the other, you can delete the gdb.#.log and gdb.#.history files. -
Can I exit a session and resume later?
No. The history file is apparently overwritten (not appended) each time you start gdb. However, you definitely can do the two sessions (infinite loop and memory problem) separately and redo either of those without redoing the other.
Updates
… | May be posted later. |