Debugging and Pointers

Brad Vander Zanden


Instructions

Work through the following tutorial and when I ask you to enter commands in gdb, please do so on your terminal--do not just read through the tutorial without executing the commands as I ask you to, or else you are not going to learn anything. Feel free to ask either the TA or a neighbor for assistance at any time.


Introduction

Pointers add an additional layer of complexity to your programs and significantly increase the challenge of debugging your programs. This lab activity is meant to give you some insight into how pointers work and also into how you might debug problems created by pointers.

In general there are two ways that you can debug problems created by pointers--either use gdb or use print statements. gdb is most helpful when your program seg faults because it can identify the line number where the seg fault occurred and this often gives you a good starting point for finding your bug. print statements are most helpful when a variable has an unexpected value and you are trying to determine how that variable was assigned that value. The reason that print statements are better in this situation is that you can use them to continuously divide the area of the program where the problem might lie in half. Every time you insert a print statement you will know whether the problem lies before the print statement or after the print statement. Hence you can eliminate the portion of the program where you know the problem does not lie. This search procedure is much like binary search and you can hopefully locate the problem with log2n probes, where n is the number of lines in your program. Of course you can find the problem even more quickly if you insert multiple print statements into your program because multiple print statements will divide the program into multiple parts, and only one of these parts can contain the problem. In contrast, if you try to use gdb to find where a variable changes value, you are effectively using linear search, because you generally execute the program one statement at a time, which is much like linear search. Linear search takes much longer to find something than binary search, which is why you are better off using print statements to try to find where in the code a variable has been assigned an unexpected value.

There are a number of problems that can arise with pointers, including:

  1. Failure to initialize a pointer before using it: This problem can either cause your program to seg fault if the pointer's value is 0, or can cause you to access an unexpected portion of memory if the pointer's value is some random address in memory. The former problem is easier to deal with because your program will immediately crash with a segmentation violation. The latter problem is harder to deal with because your program will keep running but will be using a bad value in its computations. Here is an example of an uninitialized pointer:

    int *ptr;
    
    cout << "*ptr = " << *ptr << endl;
    			     

    gdb is a good tool to use to locate the location of a bug when your program seg faults since it will give you the line number where the seg fault occurred. Hence gdb would be a good choice to use in the former case where the 0-valued pointer causes your program to seg fault. In contrast, you will probably need to insert print statements into your program to locate the latter problem.

  2. Dangling pointers: A dangling pointer occurs when you have a pointer to a piece of memory that gets de-allocated by the delete command. When you later try to access the value of this memory, you may get garbage because the memory allocator may have already given this piece of memory to another pointer and this other pointer may have already changed the value of the memory to an unexpected value. Here is an example of the dangling pointer problem:

    int *ptr1, *ptr2, *ptr3;
    
    ptr1 = new int;
    ptr2 = ptr1;
    *ptr2 = 30;
    
    delete ptr1;  // ptr2 still points to ptr1's memory
    ptr3 = new int;  // new might give ptr3 the memory that used to
                     // belong to ptr1
    *ptr3 = 50;
    
    // You would expect this print statement to print 30 because
    // that is the value you assigned to ptr2's memory. However,
    // when you deleted ptr1, you returned ptr2's memory to the
    // memory allocator and the memory allocator may have given
    // this memory to ptr3. Hence it is possible that the following
    // statement will print 50 rather than 30.
    cout << "*ptr2 = " << *ptr2 << endl;
    					   

    Dangling pointers are difficult to locate because your program will not crash when you try to use them. They simply will have an unexpected value.

    Dangling pointers can also cause unexpected issues when you assign to them. Continuing the previous example, suppose your program executes this statement much later in the program:

    *ptr2 = 100;
    	    

    If ptr3 was assigned ptr1's old memory, then this assignment statement will unexpectedly change the value of *ptr3 to 100. You may then perform some computation and be surprised to find that *ptr3 is 100. You can look endlessly in your code to try to find the place where you changed *ptr3 to 100 and you will never find it, because the culprit is the assignment of 100 to the dangling pointer in ptr2.

    Dangling pointers are among the toughest problems you will ever debug and it is tough to use gdb to debug them. Typically your best strategy is to insert print statements into your program to try to pinpoint the place in your code where *ptr3 becomes 100.

  3. Memory leaks caused by failing to de-allocate a memory location when no variable points to it any longer. Here is a common problem I see in students' code:

    int *ptr = new int;   // assign first memory block to ptr
    
    ptr = new int;        // assign second memory block to ptr
    *ptr = 10;
    	      

    Students think they have to initialize ptr when they declare it and so they assign it a block of memory. However, in their code they immediately assign it a second block of memory and never de-allocate the first piece of memory. This causes a memory leak because the first piece of memory is now inaccessable to the memory allocator and it will never be able to re-use this memory. In long running programs, your program may eventually run out of memory and terminate if you have too many memory leaks.

    The solution to the above problem is not to allocate the first memory block. There was no need to do so because you initialized ptr to a memory block before you tried to assign a value to it. Hence you should have written:

    int *ptr;   
    
    ptr = new int; 
    *ptr = 10;
    		

    Memory leaks are hard to locate because they do not cause a program to crash if the computation is correct, unless the program runs for a very long time. There are tools like valgrind that can help you detect memory leaks but we will not be using them in this course. In this course we are more concerned with other types of bugs.

  4. Making a pointer point to the wrong object. This bug often occurs once you start working with linked data structures and is something that gdb can help you with. We will illustrate that later in this lab activity.

A Simple Pointer to a Stack Variable

We are first going to look at a couple programs that help show the difference between pointers to named "stack" variables and pointers to unnamed "heap" variables or objects. Stack variables are local variables and parameters in a function that are allocated in a stack frame. They always have a name, such as "int x;". By contrast unnamed "heap" variables are allocated from the heap using the new operator. For example "new int".

Take a look at pointer1.cpp:

#include <iostream>
using namespace std;

int main() {
    int number;
    int *ptr;

    number = 10;
    ptr = &number;
    cout << "number = " << number << endl;
    cout << "address of number = " << &number << endl;
    cout << "ptr = " << ptr << endl;
    cout << "*ptr = " << *ptr << endl;
}
  

This program makes ptr point to a stack variable named number. Try compiling and running this program. Note the difference between the value of ptr, which is a memory address and is printed in hexadecimal notation, and the value of *ptr, which is the value at the memory address to which ptr points. Also note that the memory address to which ptr points is number's memory address, and hence *ptr prints the value of number.

Your turn

Now let's see what happens when you try to reference *ptr before you assign ptr a legitimate memory address. Modify pointer1.cpp as follows. I have highlighted the lines you should add in blue and bold-faced them:

#include <iostream>
using namespace std;

int main() {
    int number;
    int *ptr;

 
    cout << "value of ptr and *ptr before initializing ptr\n";
    cout << "ptr = " << ptr << endl;
    cout << "*ptr = " << *ptr << endl << endl;


    number = 10;
    ptr = &number;
    cout << "number = " << number << endl;
    cout << "address of number = " << &number << endl;
    cout << "ptr = " << ptr << endl;
    cout << "*ptr = " << *ptr << endl;
}
  

Your program will do one of two things depending on the previous value stored in ptr:

  1. Seg fault: If ptr contains an invalid memory address, such as 0x0, then your program will seg fault.
  2. Print garbage: If ptr prints to a valid memory address in your program's memory space, then you will get random values printed, which we call "garbage".


A simple pointer to a heap variable

We are going to re-run our experiment except now we will be using a pointer to a heap variable. Take a look at pointerNew.cpp:

#include <iostream>
using namespace std;

int main() {
        int *ptr;

        ptr = new int;
        cout << "\nptr after assigning it a block of memory but before" << endl
                << "assigning the block of memory a value" << endl;
        cout << "ptr = " << ptr << endl;
        cout << "*ptr = " << *ptr << endl;

        *ptr = 30;
        cout << "\nptr after assigning it a block of memory and after" << endl
               << "assigning the block of memory a value" << endl;
        cout << "ptr = " << ptr << endl;
        cout << "*ptr = " << *ptr << endl;
}
    

This program makes ptr point to a heap-allocated block of memory. We will often refer to this block of memory as an unnamed variable or unnamed heap variable. We print out the value of *ptr before and after we assign the unnamed variable a value. Before we assign it a value, it has garbage (i.e., a random value). Afterwards it has the value 30.

Take note of the memory address of this unnamed variable. Note that it's memory address is quite different from the memory address of the stack variable in the previous section. This is because stack memory and heap memory come from very different locations in your program's memory space. Typically stack memory comes from the end of your program's memory space (hence the larger memory address) and heap memory comes from the beginning of your program's memory space (hence the smaller memory address).

Your turn

Now we are going to introduce a memory leak into your program and see what it looks like. Modify programNew.cpp as follows. I have highlighted the new code in blue and bold-faced it:

#include <iostream>
using namespace std;

int main() {
        
        int *ptr = new int;

        cout << "ptr before assigning it a block of memory\n";
        cout << "ptr = " << ptr << endl << endl;
        

        ptr = new int;
        cout << "\nptr after assigning it a block of memory but before" << endl
                << "assigning the block of memory a value" << endl;
        cout << "ptr = " << ptr << endl;
        cout << "*ptr = " << *ptr << endl;

        *ptr = 30;
        cout << "\nptr after assigning it a block of memory and after" << endl
               << "assigning the block of memory a value" << endl;
        cout << "ptr = " << ptr << endl;
        cout << "*ptr = " << *ptr << endl;
}
    

Compile and run this program. Note that the initial unnamed variable referenced by ptr is different than the second unnamed variable referenced by ptr--their memory addresses are different. This program has a memory leak because this program never returns the first unnamed variable to the memory allocator using delete.

The bacefook server

Next we are going to reprise Dr. Plank's bacefook server. Take a couple minutes to reacquaint yourself with his bacefook_server write-up.

What we are going to do is use gdb to examine some of the bacefook server's data structures as we execute some of the commands in Dr. Plank's notes. Start by compiling bacefook_server.cpp using the -g option. If you forget how to do this, ask the TA.

Let's run the bacefook server with a hash table size of 5:

UNIX> gdb bacefook_server
(gdb) r 5   // you give the command line args to the r command, not gdb
    

Add a couple people to the server:

NEW-PERSON
Scarlett O'Hara
SUCCESSFUL
NEW-PERSON
Rhett Butler
SUCCESSFUL
    

Now press Ctrl-C. This will send a break signal to gdb and will cause the program to pause at its current instruction, which is an input statement.

We would like to examine the hash table, which is named Hash_Table. Try typing:

(gdb) p Hash_Table
    

You should get the message:

No symbol "Hash_Table" in current context.
    

What's the problem? The problem is that we are not in the function that contains the variable Hash_Table. Do a "backtrace" command (bt):

(gdb) bt
    

Note that you get a bunch of stack frames. The stack frame we want is the very last one that says "at bacefook_server.cpp:97". On my computer it is stack frame 8 so I type the select-frame command followed by my print statement:

(gdb) select-frame 8
(gdb) p Hash_Table
$1 = std::vector of length 5, capacity 5 = {
std::vector of length 0, capacity 0, 
std::vector of length 0, capacity 0, 
std::vector of length 2, capacity 2 = {0x6081a0, 0x608200}, 
std::vector of length 0, capacity 0, std::vector of length 0, capacity 0}
    

Success! We have a hash table that consists of 5 entries and each entry is a vector of Person * (or a PVec). We can tell that both Scarlett and Rhett appear to have ended up at index 2 because the vector at that index has length 2 and we have inserted 2 people.

Let's see if we can verify that the program has correctly created the two entries for these two people.

(gdb) p Hash_Table[2][0]    // print the first entry at index location 2
$3 = (Person *&) @0x6082a0: 0x6081a0
    

Hmm...that's not quite what we wanted. We got the address of the entry, which is correct, because the hash table buckets store pointers to Person entries, not the Person entries themselves. Do you remember how to get the value pointed to by a pointer?

(gdb) p *Hash_Table[2][0]  // use the * de-referencing operator
$4 = {Name = "Scarlett O'Hara", Mood = "Neutral", InRelationship = 0x0, 
Friends = std::vector of length 0, capacity 0}
    

This looks good. The Person constructor initializes the entry to the person's name and sets the Mood to the default value of "Neutral". InRelationship is initialized to NULL, which shows up as 0x0 (memory address 0), and Scarlett does not yet have any friends, so her Friends vector correctly has length 0.

Now try to look at Rhett's information. If you have trouble, ask the TA for help.

Let's continue the program by typing "c" for continue and then set Scarlett's Mood to "Impetuous":

(gdb) c
MOOD
Scarlett O'Hara
Impetuous
SUCCESSFUL
    

Again hit Ctrl-C and see if you can follow the previous instructions for printing Scarlett's Person entry. You will have to again use the select-frame command to get to the appropriate frame. If you have difficulty, ask the TA for help. When you print Scarlett's entry, you should see:

$6 = {Name = "Scarlett O'Hara", Mood = "Impetuous", InRelationship = 0x0, 
Friends = std::vector of length 0, capacity 0}
    

That look's correct--her Mood variable has been set to "Impetuous".

Setting the InRelationship Field

Now let's try putting Rhett into a relationship with Scarlett. This command should set Rhett's InRelationship variable to point to Scarlett's record. Let's see if that is the case. First execute the command to put Rhett into a relationship with Scarlett and make sure that it appears to work:

IN-RELATIONSHIP      
Rhett Butler
Scarlett O'Hara
SUCCESSFUL
QUERY
Rhett Butler
SUCCESSFUL
NAME Rhett Butler
MOOD Neutral
IN-RELATIONSHIP Scarlett O'Hara : Impetuous
END
    

It looks good--when we query Rhett Butler the editor says that he is in a relationship with Scarlett O'Hara. Let's make sure that Rhett's record has been modified correctly before we proceed. Hit Ctrl-C to break into the gdb editor, "select-frame 8" (or whatever frame it is on your computer that is the bottommost frame on the stack), and take a look at Rhett's record, which is entry 2 (index entry 1) in bucket 2 in the Hash_Table. gdb should output something like:

$1 = {Name = "Rhett Butler", Mood = "Neutral", InRelationship = 0x6081a0, 
  Friends = std::vector of length 0, capacity 0}
    
If you forget how to print an entry in the Hash_Table, ask either the TA or a neighbor for help.

You should also print the address of Scarlett's record to verify that we have assigned the correct address to Rhett's InRelationship variable. If you cannot figure out how to do this, ask the TA.

Adding Friends

The final thing we are going to do is add some friends. In order to do this we must first add a couple more people to bacefook and then execute several friend commands:

NEW-PERSON
Ashley Wilkes
NEW-PERSON
Melanie Hamilton
ADD-FRIEND
Scarlett O'Hara
Melanie Hamilton
ADD-FRIEND
Scarlett O'Hara
Ashley Wilkes
ADD-FRIEND
Melanie Hamilton
Scarlett O'Hara
    

Now check to make sure that these three friend commands were properly executed. I'm going to make you figure out the gdb commands I used but here is what I did to check that the bacefook server correctly updated the friends fields:

  1. selected the bottommost frame of the stack using select-frame.
  2. printed the Hash_Table variable so that I could see where the new people were added. They both should have been added to bucket 3.
  3. printed Scarlett O'Hara's record and verified that her Friends vector contains two entries, one which points to Melanie Hamilton and one which points to Ashley Wilkes. When you print Scarlett's record, gdb will print her Friends vector, which should contain 2 pointer addresses. You should verify that these two pointer addresses are the addresses of the records for Ashley Wilkes and Melanie Hamilton.
  4. printed Melanie Hamilton's record and verified that her Friends vector contains one entry, which points to Scarlett O'Hara.

If you are not sure of the gdb commands to perform these checks, ask a TA or a neighbor for help.


Debugging the bacefook_server

The last thing you are going to do is to debug the program named bacefook_server_bug.cpp. I have intentionally introduced a bug into the program and you need to find it. The bug is in the IN-RELATIONSHIP command as shown by the following sequence of commands:

NEW-PERSON
Scarlett O'Hara
SUCCESSFUL
NEW-PERSON
Rhett Butler
SUCCESSFUL
IN-RELATIONSHIP
Rhett Butler
Scarlett O'Hara
SUCCESSFUL
QUERY
Rhett Butler
SUCCESSFUL
NAME Rhett Butler
MOOD Neutral
END
	

Note that after the IN-RELATIONSHIP command is executed, Rhett Butler is not shown as being in a relationship with Scarlett O'Hara (when someone is not in a relationship, nothing is printed out for their relationship status). Use gdb to examine the records for Rhett Butler and Scarlett O'Hara. This examination should give you a clue as to what went wrong. Then look at the code in the bacefook_server_bug.cpp that starts at line 131 and which reads:

} else if (s == "IN-RELATIONSHIP") {
	

Fix the bug, re-compile the program, and verify that the IN-RELATIONSHIP command now works properly.