Debugging and the Malloc Lab

This lab commonly causes the infamous segmentation fault because of its heavy use of void pointers and pointer arithmetic. Routines that are provided must be understood in order to be used effectively. But effective debugging will most likely be needed at some point. Some of the following suggestions regarding the gdb debugger may help.

Reasons for using gdb

Inserting print statements in functions is not so effective for this lab. Typically you get far too much debugging output. This is partly because the mdriver main function reads the trace file of malloc, free requests several times. This explains why the mm_init function will be called several times.

It is probably more instructive to use gdb to be able to step through execution and then use the 'print' command to examine variables or better yet, use the 'call' command to call the mm_checkheap function to print the heap contents while stopped in the debugger.

Helpful modifications before using gdb

The mm_checkheap examines the prolog, each heap block, and the epilog. It checks each of these and also prints the address, the header and footer of each block by calling printblock. If you make changes to the prolog (e.g. adding free list sentinel) and to the free blocks (e.g. adding pred and succ), it would be important to modify mm_checkheap and printblock.

Modify mm_checkheap

The version provided "checks" that the prolog header size is 8 - header and footer and no payload. If you add a list sentinel to the payload, the size will change. You need to change mm_checkheap to check for the new size. Otherwise, it will erroneously print "Bad prolog block".

Modify void printblock(void *bp)

If bp is equal to heap_listp, bp is pointing to the prolog block. So check bp and print not only the header and footer, but also the sentinel (pred and succ addresses) that are in the payload. (Note: the %p conversion specification is used to print an address - that is, a pointer - value.) You can imitate the printf statement already in printblock. Remember that you printing bp prints the address of the block. To print the 4 byte contents of the location bp points to requires casting and dereferencing. For example, if bp points to a free block and the address of the previous free block is stored just after the header, then bp is also pointing to the location of this address. To get the 4 bytes at bp and then treat it as an address, you could use this expression:

	(void *) *(int *) (bp)
      

Using gdb

  1. At the linux prompt, $, set a value for the environment variable CFLAGS:

     $ export CFLAGS="-Wall -g"    
    	  

    Now CFLAGS is defined in the environment as "-Wall -g" and in the Makefile as "-Wall -O2". The -g option will include symbol table information about the program needed in order for gdb to display the source code (otherwise you only see machine code while in gdb). The -O2 option means "optimize" for speed and does not include the symbol table information.

  2. Type

     make clean
    	  

    and then

     make -e
    	  

    The -e option causes make to give precedence to the environment value of CFLAGS (which now has -g instead of -O2)

    This will allow you to use gdb. Once you have fixed any bugs, you can clean the project and rebuild it with optimization -O2 by typing

     make clean
     make
    	  
  3. To find where a program is causing a segmentation fault
    • Clean and Compile with debugging (make clean and then make -e)
    • Then start the debugger with mdriver and then run it (without breakpoints) with options specifying a trace file. E.g.,
       $ gdb mdriver
       (gdb) r -V -f traces/trace0.rep
      	
      	      

      If a segment fault occurs, it will show the line number and statement. You can't continue execution, but you can learn several important things. The next several gdb commands can still be executed.

  4. You can print the current values of any variables accessible at the point where the segment fault occurred as well as at any point where the debugger has stopped.

     (gdb) print bp      (or just p bp)		
    	  
  5. You can call a function, such as mm_checkheap.

     (gdb) call mm_checkheap(1)
    	  
  6. You can "see" how the program arrived at this point by printing the calling sequence (stack trace)

     (gdb) backtrace		
    	  
  7. You can list the source code

     (gdb) list   (or just l)
    
    or
    
     (gdb) list n  (where n is a line number)
    	  

    If you type list several times, it lists the next lines each time rather than repeating the same lines.

    At any time while debugging, you can see what line the debugger has reached with the 'where' command

     (gdb) where		
    	  

    This will display a line number and you can then use that with the list command to display the lines that would be executed next.

  8. Changing the number of lines listed

    The default number of lines listed is 10. You can change this. For example, to have 30 lines listed

     (gbd) set listsize 30	    
    	  
  9. r(un) runs the program again from the beginning with the same options and breakpoints (if any have been set)

  10. q(uit) stops the debugger

Executing gdb commands stored in a file

You can have gdb execute a common set of commands from a file each time gdb is started. This is convenient for setting the number of lines listed with the 'list' command, but can contain any set of commands including setting break points.

gdb will automatically read and execute commands in a file named ".gdbinit" in the current directory.

You can also specify a file of gdb commands with any name with the -x option. E.g., if the file name is 'cmds', run gdb on mdriver like this:

 $ gdb -x cmds mdriver
	  

How to check your mm_init function

Start gdb mdriver and then enter these gdb commands at the (gdb)

b mm_init set a breakpoint at start of mm_init
r -V -d 1 -f traces/trace0.rep start mdriver running with the specified options; the -d 1 option causes mdriver to only check the trace file once and doesn't repeat the trace for timing or memory utility.
s(tep) or n(ext) s(tep) means: Step one statement (if a function step into)
n(ext) means: step one statement (but step "over" if a function call)
call mm_checkheap(1) execute mm_checkheap which prints the heap, provided the argument is not 0.

In mm_init as you execute statements that change the heap with gdb commands s(tep) and/or n(ext), you could call mm_checkheap(1) to see if the change is what you expect.