Lab 2 Goals
By the end of this lab, you should feel comfortable using the GNU debugger, gdb, to diagnose both simple and subtle errors in C programs. You will also be able to use the debugger to step through assembly code and inspect the registers and memory locations of a running program.
Please perform this lab on a MacLab Linux machine. The programs used - gdb and gcc - have different behavior on different platforms. For example, if you run this lab on either a Mac OSX machine or an AMD64 linux box you will see substantially different outputs and behavior.
GDB
A debugger is a software tool used to inspect the execution of a program. Using a debugger, you can run your application in an environment that controls how the code runs and gives you tools to poke around the memory, stack, and even assembly translation of the program. GDB is the GNU debugger, which we will be using in this lab. All the features we'll cover are available in any other modern debugger you use later, though you might have to poke around to find them in the graphical user interfaces.
Walkthrough
These steps will work through the features of gdb that you will use most commonly to diagnose most problems in your code. Even if you're familiar with gdb from a previous course, please take the time to work through this walkthrough, as most lab walkthroughs don't go through several of the more advanced features.
Getting your program ready
The compiled binary output from gcc is just a pile of machine code. If you run gdb on a raw binary from gcc with no other information available, gdb will be able to turn the machine code into assembly but will not know the mapping from the assembly back to source code instructions. Follow the steps below to launch a program without compiler debugger support, just to see what it looks like:
$ cd ~/cs154/lab2/walkthrough $ make $ gdb walkthrough (gdb) start (gdb) where
gdb is usually either started with no arguments or just with the name of the program that you will execute. Once gdb is started, the prompt changes to (gdb). The start command will run the program specified and stop execution at the first instruction of the main function. When the program is stopped, the where command prints out the current stack. In this case, it will look like 0x00000000004004d0 in main (). Any time you see a hexadecimal address (code memory locations) in the output of the where command next to the method name instead of file names and line numbers, the source that you are looking at has not been compiled with debugger support enabled!
Use the kill command to stop running the program so that we can rebuild with debugging support. Leave gdb running in that terminal window and open up a new one. Since gdb can tell when a program has been rebuilt, you can have a separate terminal window in which to build and write code.
Open a second terminal window and edit the Makefile to add debugging information to the program.
$ emacs Makefile
In the CFLAGS variable definition, add the -g flag to turn on debugging information. Clean and remake the program. As a side note, when you change compiler flags in a Makefile you will frequently need to do a clean before you remake because make does not know that files depend on variables.
$ make clean $ make
Back in the terminal window that is running gdb, enter the following commands:
(gdb) start (gdb) where
Now, the where command will print out something more like main (argc=1, argv=0x7fffffffe568) at main.c:12, which is going to be far more useful, particularly when using more advanced features of the debugger. Additionally, after the start command, gdb will have printed output of the form: 'walkthrough' has changed; re-reading symbols. This message is your indication that gdb noticed that you re-compiled your code.
Breakpoints
A breakpoint is a way of telling the debugger to stop executing the statements of a program when a given location is reached. The locations can be source files with line numbers, function names, or literal code addresses. Perform the following steps to set a few breakpoints in the test program:
(gdb) break compute (gdb) break main.c:32 (gdb) break main.c:36
These commands will set three breakpoints: one at the entry point to the function compute, and one each on the lines that change the value of the local variable named result. Each breakpoint will be numbered by gdb so that you can issue commands to disable or temporarily skip a breakpoint by name. To see all the breakpoints you have set currently, type the following command:
(gdb) info breakpoints
Now continue execution of the program with the continue command:
(gdb) continue
The program should have stopped at the beginning of the compute function. GDB will also print out the reason for the stop event - in this case, gdb will print the number of the corresponding breakpoint. Use the where command again, noting that there are now two functions on the call stack. To no longer stop at this breakpoint, disable it with the following command, replacing "3" with the number of the breakpoint printed when gdb stopped execution:
(gdb) disable 3
To remove all breakpoints, issue the following command:
(gdb) delete breakpoints
While the where command shows you the current stack, the list command will show you the line of code currently being executed and five lines of context in each direction. You can provide different arguments to list to see different parts of source files (use "help list" for more examples).
(gdb) list
After hitting a breakpoint, you can continue the program again until another breakpoint is hit or the program terminates. Alternatively, there are commands for controlling execution on a finer granularity. The following are the three you will use most commonly:
- step - executes to the next line of source code (no matter how many expressions are in the current one!)
- next - executes to the next source line in this function or to the end of this function
- finish - execute to the end of this function
Step literally moves line by line through any part of your program. Next is used when you are debugging an individual function and don't care about any calls it makes to other functions. Finish is used when you're done looking at a function call and want to finish it off and return to the caller (or when you accidently "step" when you mean to "next"!).
Use the step and next commands a few times, then the finish command. Type continue when you are done to execute to the end of the program. Note that you will remain in the debugger.
Inspecting data
Of course, just executing and breaking in the program to see where control flows isn't very useful! Instead of relying on printf statements and recompilation to show values, the debugger has a powerful set of tools for inspecting variable, register, and memory values.
Start running the program again with the following statement:
(gdb) start
You should now be at the first line of main, just before the variable initialization occurs. Type the following command:
(gdb) info locals
This will list all local variables and their values. If this were a C++ function, it would include the this pointer as well. Note that if any of the variables have a value of "<value temporarily unavailable, due to optimizations>" then you are running in an optimized build of the program (with -O passed to gcc). This likely means that the value is probably going to be stored in a register but has not been initialized yet. In that case, inspection of the assembly is neceessary to understand what happened. More on that later!
Most interestingly, observe that the values of the result and i variables have not yet been initialized and contain trash data. This trash data may be zero or may be random, depending on the implementation of the C runtime and the state of memory. Use the following commands:
(gdb) step 2 (gdb) info locals
The step command can be given a number for the number of times to step, as in the example above. Now, execution is about to proceed into the call to the compute function and both of the local variables have been initialized.
To print out a specific value you're interested, use the print command. Use the following commands to step into the compute function and view the value of the new local variable, which is conveniently also named i.
(gdb) step (gdb) print i
If you'd like to see that value every time you step (so you don't have to keep typing "step" then "print i"), you can use display instead. Print and display also take a simple C expression as well, so you can perform some basic operations within the thing to show as below:
(gdb) display i*2+1 (gdb) step (gdb) step (gdb) step
The undisplay command turns off any display expression you have set:
(gdb) undisplay
Watchpoints
Sometimes, breakpoints are tedious. For example, you might want to break every time a value changes. The traditional way to see every time a value changes with breakpoints is to set a breakpoint on every line that changes the value. Instead, you can set a watchpoint on that specific value, and the debugger will stop any time that it is written.
(gdb) watch result (gdb) cont
The walkthrough will continue execution and run until the result value is updated. It will display both the previous and new values. These watchpoints are extremely helpful, as they also work on data in arrays and function appropriately in the presence of aliasing. So, if you have an array in memory, you can put a watchpoint on a value in that array and no matter which pointer the program uses to write that value, the debugger will still break on the watchpoint for that memory location.
You can get a list of all watchpoints and disable watchpoints through the same interface as breakpoints:
(gdb) info breakpoints (gdb) disable 7
Disassembly
Sometimes, you need to see the assembly representation of the machine code being executed. Usually, this situation occurs because either you don't have the source and debugging information for a program or the program was compiled with optimizations turned on (-O flag to gcc). To see the assembly representation of the machine code being run, use the disassemble command combined with a print of the program counter register:
(gdb) disassemble (gdb) print/x $pc (gdb) list
By default, disassemble will show you the function you are currently in. You can also provide it with the name of another function and it will show you a dump of that one. To understand which assembly instruction you're on, use output from the print command above (which has been modified to "show hex value" with the /x flag). The print command on the program counter is used instead of the where command because the gdb where command will only print the instruction code address if execution is stopped at the first single instruction associated with a line of C code. The register $eip will also show the current instruction address. Be careful with syntax - while assembly uses the % character to refer to registers, gdb uses the $ character.
Your output might look something like the following:
(gdb) disassemble Dump of assembler code for function compute: 0x000000000040050d <compute+0>: push %rbp 0x000000000040050e <compute+1>: mov %rsp,%rbp 0x0000000000400511 <compute+4>: mov %edi,-0x14(%rbp) 0x0000000000400514 <compute+7>: movl $0x0,-0x4(%rbp) 0x000000000040051b <compute+14>: jmp 0x40053d <compute+48> 0x000000000040051d <compute+16>: subl $0x1,-0x14(%rbp) 0x0000000000400521 <compute+20>: mov -0x14(%rbp),%eax 0x0000000000400524 <compute+23>: and $0x1,%eax 0x0000000000400527 <compute+26>: test %eax,%eax 0x0000000000400529 <compute+28>: jne 0x400537 <compute+42> 0x000000000040052b <compute+30>: mov -0x4(%rbp),%eax 0x000000000040052e <compute+33>: imul -0x14(%rbp),%eax 0x0000000000400532 <compute+37>: mov %eax,-0x4(%rbp) 0x0000000000400535 <compute+40>: jmp 0x40053d <compute+48> 0x0000000000400537 <compute+42>: mov -0x14(%rbp),%eax 0x000000000040053a <compute+45>: add %eax,-0x4(%rbp) 0x000000000040053d <compute+48>: cmpl $0x0,-0x14(%rbp) 0x0000000000400541 <compute+52>: jg 0x40051d <compute+16> 0x0000000000400543 <compute+54>: mov -0x4(%rbp),%eax 0x0000000000400546 <compute+57>: leaveq 0x0000000000400547 <compute+58>: retq End of assembler dump. (gdb) print/x $pc $3 = 0x40053d (gdb) list 21 22 23 int compute(int i) { 24 int result = 0; 25 26 while (i > 0) 27 { 28 i--; 29 30 if (i % 2 == 0) (gdb)
From the print command, you can see that the program counter is at 0x40053d, which in our disassembly corresponds to 0x000000000040053d <compute+48>: cmpl $0x0,-0x14(%rbp). Looking at the output from list and where we ended after the last watchpoint, the result variable has just been changed, so we're about to perform a the comparison for the while loop's conditional to decide whether to go through the loop body again or fall through to the return statement.
If you'd like to follow the individual assembly instructions, there are "i" versions of the step and next commands.
(gdb) stepi
Finally, if you need to see what values are in individual registers, use the info command:
(gdb) info registers
To quit gdb, kill (terminate) the application and then quit.
(gdb) kill (gdb) quit
You do not need to check in anything related to the walkthrough.
Exercises
First, please create and add a readme.txt file at the top of your lab2 directory with the names of all folks working together. Use this file to record your answers to questions in the exercises. If you work with several people, it's fine to just check in one file in one directory for this exercise.
$ cd ~/cs154/lab2 $ emacs readme.txt $ svn add readme.txt $ svn ci -m "Readme with member names"
Exercise 1
The files needed for this exercise are in the ex1 subdirectory. There are two .c files, test.c and library.c. Do not edit test.c - it is the driver program performing tests of the code in library.c.
There are two tests that will run when you execute the binary test - and both of them will initially fail. You should set appropriate breakpoints in the source to stop execution in the functions under test, inspect the arguments, and fix the functions to match the behavior expected by the test. Remember to re-run make after you make any changes to the source code!
Commit any changes you make to the source file, library.c. For each of the tests that you fix, make an entry in readme.txt describing any interesting gdb commands you used. Please try to use the debugger to find bugs instead of just staring at the code - it has been maliciously crafted to be trivial to find through the debugger and annoying to find via just staring at the code.
And don't try to just return the right answer (i.e. 42 or NULL) to kludge the tests to pass - fix the code!
Exercise 2
Make and run the program in the ex2 directory. Note that it's not printing out anything, even though stringToPrint has a value! Run it under the debugger - when and how is the array that gets printed getting corrupted? Specifically, on which line does it occur, and what are the values of the local variables at one point where a corrupted value is written? What debugger commands did you use? Commit answers to these questions into your readme.txt file. You do not need to fix this program.
DO LATER - Exercise 3
You do not need to do this exercise now, and it will not be graded for credit. After Prof. Foster has covered stack frames and assembly, you should attempt this lab on one of the MacLab Linux machines. The answers will be in a file called answers.txt. It will be useful preparation for the next large programming assignment.
There is a 32-bit x86 linux binary in the ex3 directory. There are no sources or debugging information available. Use gdb to load it, use the start command, and use the debugging commands from the walkthrough to answer the following questions. Commit your answers to the readme.txt file.
- 1) How much space is being reserved for locals on the stack frame? Ignore the first three lines of the assembly - those instructions are administrative setup related to stack frame alignment.
- 2) After the stack is set up but before a call is made to the function, a pointer value is stored into a local. What is the string at the address location being pointed to?
- 3) Set a breakpoint on the assembly instruction after returning from the function call. What debugger command did you use to do that?
- 4) What value was returned from the function call?
Bonus Problems
Do not attempt these problems until you have completed all of the normal exercises.
BONUS 1 (floating point fun)
Build and run the two versions of this program:
$ make floating-debug && ./floating-debug $ make floating && ./floating
Notice that the debug version prints the correct answer, but the version with optimizations turned on prints out something completely different. What's happening? How can you fix it? Check in your answers to the readme.txt file. HINT: your answer should involve observations about generated code, instruction ordering, and register selection.
This is based on a real bug I recently ran across porting an old C codebase to x86-amd64.