Advanced C Programming

Spring 2024 ECE 26400 :: Purdue University

This is a PAST SEMESTER (Spring 2024).
Due 1/26

Debugging: GDB

Goals

  1. Learn strategies for debugging code
  2. Practice using the gdb debugging tool
  3. 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).

  1. gdb Frequently Asked Questions (FAQ) (only #1, #2, #3, and #4; skip the rest)
  2. How do I use gdb? (all)
  3. How do I watch the execution of my program? (all)
  4. How do I use the call stack? (all)
  5. How do I use breakpoints? (all except 4.3)
  6. How do I use watchpoints? (all)
  7. Advanced gdb Features (only 6.1 and 6.3; skip the rest)
  8. Example Debugging Session: Infinite Loop Example (all)
  9. 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:

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_factor
Notice 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:

SAMPLE OUTPUT
(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.

SAMPLE OUTPUT
(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.

SAMPLE OUTPUT
… 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.)

SAMPLE OUTPUT
(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.)

SAMPLE OUTPUT
(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.

SAMPLE OUTPUT
(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.

SAMPLE OUTPUT
(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 seeing n=0 even though the function was originally called as print_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.)

SAMPLE OUTPUT
(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(…).

SAMPLE OUTPUT
(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.

SAMPLE OUTPUT
(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.)

SAMPLE OUTPUT
(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.)

SAMPLE OUTPUT
(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).

SAMPLE OUTPUT
(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.

SAMPLE OUTPUT
(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.

SAMPLE OUTPUT
(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.

SAMPLE OUTPUT
(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.

SAMPLE OUTPUT
(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.

SAMPLE OUTPUT
(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 is 2 because 5 divided by 5 is 1 with a remainder of 2.

The %= and /= operators are just shortcuts. x %= y; is the same as x = x % y;. Likewise x /= y; is the same as x = x / y;

Quit gdb. When it warns that your debugging session is active and asks if you really want to quit, answer "y".

SAMPLE OUTPUT
(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.

These filenames have “.2.” in them. If you accidentally type the old filenames (with “.1.”), they may be overwritten. Beware.
SAMPLE OUTPUT
(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(…).

SAMPLE OUTPUT
(gdb) ████████████
Breakpoint 1 at 0x400653: file prime_factor.c, line 17.
(gdb)

Run your program (inside GDB, of course).

SAMPLE OUTPUT
(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.

SAMPLE OUTPUT
(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.

SAMPLE OUTPUT
(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.

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

  1. 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.
  2. 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.
  3. 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.
  4. 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.