Today, we are going to investigate some of the more sophisticated features of the gnu debugger, gdb. Hopefully, this will be a fun exercise, but introduce some of the very sophisticated features of gdb that you can make use of in this class (for instance, while doing project 3) as well as in future classes, and eventually the work force.
I will include hyperlinks to specific sections of the on-line gdb manual (the top level is linked here, as well as in the supplemental material of the class web page.
The code we will be working on in this lab is a variant of an old computer game, first invented by a student from Dartmouth in the 1970's called "Hunt the Wumpus". See Hunt the Wumpus from Wikipedia for some background on the original game.
Download lab8.tar.gz and untar it to make a Lab_08 directory. cd to that directory, and type ./wumpus to run the game. Try it out. Did you die? Unless you are VERY lucky, you probably didn't make it all the way to the end of the game without getting killed. We are going to try to introduce some gdb techniques that help you get through the wumpus game.
You have probably noticed that when you are debugging gdb, that you are re-starting gdb over and over again, and that there is a series of commands that you type each time you restart gdb. One trick to make this process easier is to put all of the commands you want to execute each time you start gdb in a file. Then, you use the -x <filename> flag on the command line of gdb to run your commands when it starts gdb. See gdb manual file options under -x for specifics on the flag, and gdb manual command files for a description of what you can put in your text file.
There is such a file included in the lab8 tar file (and directory) called dbg.txt. Open up that file in an editor, and take a look at it. Then try running gdb -x dbg.txt wumpus
While the above command is fairly simple, you still have to remember the name of the gdb command, the name of your gdb command file, the name of the flag (-x) required to invoke your gdb command file, and the name of the executable. I often find it easier to put all of this in a Makefile with a gdb target. I've supplied an example makefile in the Lab_07 tar file called Makefile. Take a look at what is there, and then try running make gdb.
Notice that the gdb target in the makefile depends on the wumpus executable. If you were debugging wumpus.c, making changes, and then debugging again, the dependency of the gdb target on wumpus would cause wumpus to get rebuilt when it is out of date, so that you can be sure you are debugging the version of the wumpus binary executable that matches your wumpus.c. That's a very useful feature that we won't be taking advantage of in this lab.
Notice that even though wumpus is compiled with no debug information, function names of all the functions in wumpus can still be printed with the info functions gdb subcommand. (The debug command file, dbg.txt, already does this for you.) In this list of functions, do you see anything that looks like it will be called before the wumpus player is killed?
Usually, when we set a breakpoint in gdb, we specify the line number in a C file to stop at. However, it is also possible to specify a function name as a breakpoint. This causes gdb to break before the first instruction in that function is executed. Since wumpus was compiled with no debug (and therefore no line-number information), and you don't have access to the source code for the wumpus executable, this can be very useful.
Edit the dbg.txt file by commenting out the info functions command, and replacing it with a break <kill function name> command. Save your changes, and run make gdb. Then play the game until you get to the breakpoint.
Use the where command to try to figure out what function is calling the kill function. You can use the up command to get into the context of the caller, but since wumpus has no debug information that probably won't help much. However, it would be kind of nice to make sure that whenever you get to the kill function, you run the where command automatically to see who is calling the kill function. You can do this by attaching commands to breakpoints. See gdb manual breakpoint command lists for a description of how to do this.
Edit your dbg.txt file again, and after the break <kill function> line, enter the lines
commands where end
... save and then run make gdb again. Now, whenever you are about to get killed, you can see who is calling you.
Under GDB, registers are treated the same way as C variables. You can print register values, just like you print the value of a variable. The only trick is that GDB needs some way to differentiate between, for instance, a variable named ebx and a the %ebx register. In GDB, you can reference a register by prefixing the register name with a dollar sign. Thus, the GDB command p $ebx prints the value of the %ebx register. (Yeah... it's confusing... $ prefixes are reserved for constants in x86, but GDB is just different, and we just have to deal with this confusion.)
I'll give you a big hint here... The getMove function takes a pointer to the current "position" of a player as its first argument. The position consists of two integers... the current row, and the current column in a matrix of locations. Wouldn't it be nice to know the row/col passed in to the getMove function! We haven't learned the details about how to find parameters, but we are going to learn soon that the first paramater is always 8 bytes above the value in the %ebp register when the function starts. That means that we can get the pointer to the X and Y parameters passed to getMove by looking at whatever is in memory at the value of %ebp + 8
Confirm this by setting a breakpoint at getMove before you "run" (which is now in dbg.txt). Then, when you get to getMove, you can p $ebp to print the current value of the ebp register. You could also use the info registers command, but it's good to know about the $<regname> , and capability... we'll use that again real soon. See gdb manual registers for all the details.
When we learned about GDB, we mentioned that you could actually look at memory by using the GDB X command, but since we never really needed to look at memory, we never actually used that command. Today, we need to be able to look at memory... specifically, the pointer that is at ebx+8, and the values that pointer is pointing at.
The GDB x /<format> <address> sub-command displays the value in memory at address <address> using the format specified in <format>. The format specification is optional, but if it is specified, consists of three parts... /<nfu> where <n> is the number of data elements to display, <f> is the format to display, and <u> is the width or precision of each data element. See the gdb manual examining memory page for more details.
In this case, it would be interesting to see the first four (or so) addresses pointed to by the base pointer. We can do this by typing x /4aw $ebp on the gdb command line. Notice that when gdb knows about the context of an address, it will give you hints about that context.
Notice that you can specify an expression as an argument to the x gdb sub-command. For instance, to see the value at an offset of 8 from the base pointer, you can enter x /a $ebp+8. Try it. You get back the address of the row/column position argument back when you do this!
Next, we want to de-reference this pointer - in other words, find out what it's pointing at. In order to do that, maybe we can use the C "*" construct to get back what $ebp+8 is pointing at. Try it... x /2db *($ebp+8).
That didn't work. The problem is that gdb knows that $ebp+8 is a pointer, but it's got no clue what it's pointing at. Even though you told gdb to format the result as two decimal values, gdb is still confused.
It turns out that the way to make things clear to gdb is to cast $ebp+8 as a pointer to an integer in the expression you pass to the x subcommand. Try x /2db *(int *)($ebp+8)
It's a pain to have to remember all that detail each time you get to getMove. One alternative is to use "convenience variables". See gdb manual convenience variables for all the details. The bottom line is that we can set variables that we define for ourselves in gdb, and use them later. These convenience variables have a $ prefix to distinguish them from normal C variables. (See... gdb has already created convenience variables for the registers for you, like $ebp.) So, in our case, when we are stopped at the start of the getMove function, we can set $pos = *(int *)($ebp + 8). Then, from then on, we can type x /2db $pos to print out the two decimal integers that make up the current position.
This is getting easier, but let's put it all together in one more step
Now that we've figured out how to print the parameter to the getMove function, wouldn't it be nice to print the current parameter value each time we start the getMove function, and to keep running. To do that, change dbg.txt ... after the b getMove line, enter the following:
commands set $pos = *(int *)($ebp+8) p "Row/Col" x /2db $pos continue end
Now, run make gdb and play the game. Every time you reach the breakpoint before the kill player function, you can see the row and column location of the specific danger. Write down on paper or in a file the locations where you run into problems so you can avoid them in the future. See how the various commands affect the location. Now play the game until you are successful. If you reach the end still alive, you are done with the lab.
No lab report this week. Have a great spring break!