UMBC CMSC 202 Computer Science II

Lab7: Debugging with gdb


Objective

In this lab you will use practice using the gdb debugger.

Introduction: Why you should use a debugger

When you run your program within a debugger, you can stop the program at critical points and examine the values of variables and objects. The debugger provides you with much more capability and flexibility than debugging your program using print statements. For example: In this lab, we will use gdb, a debugger with a command line interface. Although the user interface is a bit clunky, you will find that gdb has many useful features. It "understands" C and C++ types and syntax. It works well with source code that is distributed across multiple files.

You might ask: If gdb is so great, why don't more people use it?

The answer is that most people don't take the time to learn gdb before they have a bad bug. Trying to learn gdb when you have a bad bug and a project due date hanging over your head does not leave you with warm fuzzies about the debugger.


Assignment

We will take a tour of gdb's many features in this lab using two simple examples of buggy programs. You should compile and run these programs on a Linux system on an Intel platform (e.g., the UMBC GL system). The bug in the first example might not manifest itself in the same way on other Unix systems (e.g., Mac OS X).


Step 1: Get the files

Here are the files you need: You will not need to modify any of these files.


Step 2: A Simple Debugging Session

We will use the debugger to check out the buggy code in buggy.cpp.

Follow these steps:

  1. Compile buggy.cpp using the -g option. This option tells the compiler and the loader to keep the symbol table around for the debugger: g++ -g buggy.cpp

  2. Run the program by typing "./a.out". It should give you a segmentation fault and dump core. This creates a file with file name like "core.1234".

  3. Start the gdb debugger: gdb a.out core.1234 Using gdb, you can look at the memory contents of your program just after the segmentation fault. This is convenient if running your program requires several steps involving user I/O.

    Note: if you start gdb without the core file, you can still run the program using "run".

  4. After gdb starts up, it prints out lots of information. Look at the last two lines: #0 0x080486d4 in main () at buggy.cpp:12 12 A[i] = i*i ; This says the program crashed while executing main() on line 12 of the file buggy.cpp. It also helpfully prints out line 12 for you.

    At any time while running gdb, you can type the command "where" to find out where you are in the program.

    If all you remember about gdb is the "where" command, you still have a very useful tool in debugging pointer errors.

  5. Of course, we don't remember our programs. The "list" command shows you the source code. Try these variations of the "list" command: list 12 list main list 1,18 If you type "list" right after another "list" command, it shows you another 10 lines below the previously listed lines.

  6. The command "help list" will tell you more about list. In general, "help" followed by a topic or a command will give you documentation on that topic. Typing just "help" will give you a list of topics.

  7. To see the value of variables and objects in the program, use the "print" command. Try these different print commands (you can also use "p" as a shortcut for print): print i print A[i] print A print &A print &i print &A[2] Conclusion: somehow the value of i has grown so large that A[i] causes a segmentation fault.

  8. Now we will re-run the program to see what happened. In gdb we can set "breakpoints" which stop the program at critical places. Type: break 12 This stops the execution of the program before the code on line 12 is executed. Type "run" to start the program running. You should see something like: Breakpoint 1, main () at buggy.cpp:12 12 A[i] = i*i ; Type "print A" and "print i" to see that the values of A and i are as expected.

    To execute the code in line 12, type:

    step The "step" command executes 1 line of source code and shows you the next line. Print out A and i again to see the effects of line 12.

  9. We can continue to step through the program this way, but it is simpler to issue the "continue" command (or just "c" for short). continue This runs the program until the next breakpoint.

  10. We can keep typing "continue" and "print" commands this way, but all those prints get a bit tedious. Type: display A display i This tells the compiler to print out A and i whenever it stops at a breakpoint.

  11. Type "continue" again. Now you see the next iteration of the while loop. To continue again, you just have to hit the return key. Notice that the value of i eventually grows quite large. How did that happen?

  12. If you missed the iteration where the value of i ballooned, type "kill" to stop running the program and "run" to restart it. Your breakpoints and display items are still intact.

  13. To make sure you don't blow past the critical iteration of the while loop again, stop at the iteration when i is 4. Then use "step". You don't have to type "step" every time. After the first step, hitting the return key is equivalent to typing "step".

  14. Now type "p &A[5]" and "p &i". Notice that these are the same hexadecimal (base 16) values (or should be on GL).

  15. Now you can quit, using "quit".
Summary: In general you can have many breakpoints and display items. Sometimes you forget where and what they are. To see a list of your breakpoints and display items, use: info breakpoints info display You can remove all breakpoints using "delete" or individual breakpoints using "delete 1", "delete 2", ... (Use the breakpoint number from the "info" list.) Sometimes you just want to temporary disable a breakpoint. You can do that with "disable 1", "disable 2", ... To enable the breakpoint again, use "enable 1", "enable 2", ...

To list your display items, "info display". Use "undisplay" to remove a display item. Use "disable display 1", "disable display 2",... to temporarily disable a display item.


Step 3: A more complicated example

Now, let's try something more complicated, involving programs with different source files and code that uses objects and pointers. We'll see that gdb actually understands C++.

  1. Remove the a.out and core file from the previous exercise.

  2. Compile a buggy implementation of the List class from Lab 6. Use the files linked here, not the files from Lab 6: g++ -g -Wall -ansi List7.cpp List7test.cpp Run the program, you should see something like: Testing Insert... Should Print: -9 -7 -5 -4 -4 -3 -1 -1 -1 0 1 1 1 2 2 3 5 5 6 7 8 9 Actual Print: -9 -7 -5 -4 -4 -3 -1 -1 -1 0 1 1 1 2 2 3 5 5 6 7 8 9 Testing Remove... Remove 5... Should Print: -9 -7 -5 -4 -4 -3 -1 -1 -1 0 1 1 1 2 2 3 5 6 7 8 9 Actual Print: -9 -7 -5 -4 -4 -3 -1 -1 -1 0 1 1 1 2 2 3 5 6 7 8 9 Remove -1... Should Print: -9 -7 -5 -4 -4 -3 -1 -1 0 1 1 1 2 2 3 5 6 7 8 9 Actual Print: -9 -7 -5 -4 -4 -3 -1 -1 0 1 1 1 2 2 3 5 6 7 8 9 Remove 9... Segmentation fault (core dumped) Apparently, the first two removes worked correctly, but the third one did not.

  3. Run gdb with the name of the core dump file in your directory: gdb a.out core.5678 Make sure you are using the core dump from this program and not the previous one. The last two lines of gdb's output says something like: #0 0x08048949 in List::remove (this=0xbffffb0c, data=9) at List7.cpp:96 96 current->next = ptr->next ; The remove() function is apparently the culprit. Not surprising.

  4. We can list the source code from different files this way: list List7.cpp:1,20 list List7test.cpp:1,20 list main list remove list List::remove When the source code is spread out over multiple files, listing by function name is much more convenient.

  5. To set a break point at a line number in a particular file: break List7.cpp:96 We can also break after entering a function: break List::remove

  6. Run the program using "run". It should break on line 88 after entering the remove function. Type "where". You should see something like: #0 List::remove (this=0xbffffa9c, data=5) at List7.cpp:88 #1 0x08048e25 in main () at List7test.cpp:40 This says that the execution of the program is stopped in function List::remove() at line number 88 of List7.cpp. Also, often quite important, it says remove() was called from main() at line 40 of List7test.cpp. This is called the "backtrace of the stack frames".

  7. Where we stopped the program, the variable current is not yet initialized. Type: step to initialize it.

  8. We can now print out various local variables in remove. Unfortunately, everything other than data is a pointer, so printing them just gives obscure hexadecimal values. Fortunately, gdb understands C/C++ operators *, & and ->. Try these print statements: print data print m_head print current print *m_head print *current print current->next print *(current->next) We can also print out values of local variables in main(). Try: print main::List1 print &main::List1 Sometimes we forget the type of the variables we are using: ptype current ptype *current ptype current->m_data ptype main::List1 Note the * at the end of the reported type of current. This says current is a Node pointer rather than a Node.

    We can even use the "this" pointer:

    print this print &main::List1 The output confirms that the host/calling object is indeed List1 from main().

  9. Type "where" and "info breakpoints" to review where we are. There should be a breakpoint on line 96. This is the part of the while loop that is executed when the node to be removed is found. Type: list List7.cpp:96 continue

  10. Now execution has terminated in the portion of the code that is buggy. See if you can figure out what happened... These print commands should help: print *current print *current->next print ptr print *ptr print *ptr->next

  11. Type "continue" (or use the return key) 4 times. The program should trigger a segmentation fault. Type in these commands: list print data print ptr print current print *current You should see why this program crashes.

  12. Use "quit" to get out of gdb.

One command we didn't go over here is the "next" command. This command is very much like the "step" command. The difference is when the next line of code is a function call. In this case, "step" will stop at the first line of the function being called, whereas "next" will wait for the function call to finish.


Step 4: Try it in your own program

If you have time remaining, practice debugging a piece of your own code, say from Project 1 or Project 2. Remember to use the -g option when you compile. Try stepping through your code using "next" versus "step" to see the difference.

With some practice, the next time you have a buggy piece of code and a deadline looming, you can pull out gdb and know how to use it.