Lab 5: Debugging C with gdb

Goals

Your primary goal for this lab is to get familiar with using the GNU debugger (gdb) to explore and debug C programs.

Documentation and tutorials

You will find some potentially useful gdb resources posted on our Resources page. There is also a really handy summary sheet on page 280 of the textbook.

Getting started

Here are a few rough steps to guide you through some gdb basics. You’ll have questions, so write them down and ask on Slack!.

  • Grab a copy of the gdb_test.c sample.

  • Compile it like so:

    gcc -Wall -Werror -g -Og -o gdb_test gdb_test.c
    

The -g means “include debugging info in the executable. The -Og (that’s a capital O, not a zero) means “optmize this code for the best debugging experience”.

  • Try running the program to compute the factorial of 6:

    ./gdb_test 6
    
  • Open up gdb:

    gdb gdb_test
    ...lots of notices print out...
    (gdb) [this is the gdb prompt]
    

Note that I’ll use [ and ] to delineate comments to you in terminal interactions with gdb.

  • Run the program to compute 6! again:

    (gdb) run 6
    
  • Most gdb commands can be executed using abbreviations:

    (gdb) r 6
    
  • You can show 10 lines (the default count) of the source code:

    (gdb) list
    

The list command makes guesses about which 10 lines you want to see. For example, it selects the “current” source file and starting line (whatever that may mean in context).

  • Instead, show lines 10 through 25:

    (gdb) list 10,25
    
  • You can also explicitly name the source file if your executable comes from multiple sources (remember qtest from the queues assignment?):

    (gdb) list gdb_test.c:10,25
    
  • For all gdb commands, if you hit return at the next prompt, gdb tries to repeat the previous command. So, for example, try this:

    (gdb) list 1,20
    ...[listing of source code]...
    (gdb) [hit return]
    ...[what do you see?]
    

Working with breakpoints

Let’s set our first breakpoint and explore. Like in VS Code, a breakpoint is a place where gdb will pause execution of your code and let you look at the values in variables, registers, and memory at that moment.

  • Set a breakpoint at line 19 (this pauses just before executing the recursive call factorial(n-1), but you can put it anywhere):

    (gdb) br gdb_test.c:19
    [or]
    (gdb) br 19 [if the filename were ambiguous]
    
  • Run the program, which will pause at your breakpoint:

    (gdb) r 6    [recall that r=run]
    
  • Look at the code just before and after your breakpoint:

    (gdb) list
    
  • We can also look at the current contents of a parameter or local variable:

    (gdb) print n
    
  • We can also look at the current “backtrace” (i.e., the list of function calls that are currently active, which is especially handy for recursion):

    (gdb) bt
    
  • Let’s continue the program’s execution:

    (gdb) continue
    [or]
    (gdb) c
    
  • Again, this breaks (i.e., pauses) at line 19. Take a look at the backtrace (also known as a stack trace), to see that there’s a new call to factorial(5) on the stack:

    (gdb) bt
    

Looking at registers and memory

If you aren’t already debugging gdb_test.c, follow the steps in the previous section. We’ll continue where that left off.

  • Repeat the continue+backtrace sequence until the top of the backtrace is factorial(3).

  • Now, we can see the contents of the registers:

    (gdb) info reg
    [or]
    (gdb) i r
    

Take special note of the stack-pointer register %rsp. For me, as I write this, factorial(3) is the most recent function call in the backtrace, and the value stored in %rsp is 0x7fffffffe980.

  • Let’s look at the system stack, starting where %rsp points:
    (gdb) x/40wx 0x7fffffffe980    [change to whatever %rsp contains]
    

Here, the first x stands for “examine”, the w is “show me 4-byte words”, the 40 is how many words to see, and the second x means “show me the word values in hex”. You can find more formats in quick reference guides and documentation.

Study the memory contents. Can you see where the n variable for all of those successive calls to factorial() are stored? How many bytes seem to be in each of the function calls’ stack frames? What is stored in the rest of a given call’s stack frame?

Stepping through code beyond breakpoints

  • If you want to execute exactly one line of code, you can use next or ni (for “next instruction”):
    (gdb) ni
    

Note that if the upcoming line of code includes a function call and there’s a breakpoint inside of that function, execution will pause before your “next” operation gets a change to execute the entirety of line 19.

  • If you’re paused at line 19 (result = factorial(n-1)) and you want to “step into” the function call factorial(n-1), you can use si:
    (gdb) si
    

In this case, that should take you to the first line of factorial with a smaller value of n.

  • Keep playing around!

Next steps

Our next step is to do this same thing in assembly. In the meantime, you can start attempting to escape from the zoo. Good luck!