Skip to content

Instantly share code, notes, and snippets.

@sidsenkumar11
Last active March 30, 2025 14:46
Show Gist options
  • Save sidsenkumar11/94d7a1495cca826489b56c3df73ecf71 to your computer and use it in GitHub Desktop.
Save sidsenkumar11/94d7a1495cca826489b56c3df73ecf71 to your computer and use it in GitHub Desktop.
GDB Tutorial

GDB for the Uninitiated

Overview

You're working on a C project for CS 2200. There's 2 days left before the assignment is due and for some reason, you're getting a segmentation fault after writing your last function. You've looked over your code 23 times but you just can't figure out where the bug is! Giving up and calling it a day, you decide to go to office hours and ask a TA for help.

If it sounds like this might be you in the near future, then allow me to run through a typical scenario for what happens when you come in and ask for help. You'll sit down, wait for 30 minutes while the TAs get through the queue of students asking questions, and finally notice that it's your turn next. The TA will walk over and ask you what's up. You'll explain that your code is seg-faulting and for the life of you, you can't figure out why. So the TA will take a quick peak at your code for any obvious errors, and then ask you the following question:

Did you run it through GDB?

You respond "No, I tried but I wasn't really sure..." (but we both know you didn't). So the TA sits down and tries to debug your code, and finds one small error - you accidentally malloc'd for a struct pointer instead of a struct. Finding the bug took you both about 5 minutes, but you spent the past hour waiting in office hours and the past day worrying whether you'd ever finish the project in time!

The truth is, we're not (always) just being lazy when we ask if you ran your code in GDB! Why do we emphasize GDB so much? Because GDB is your best friend! No seriously! Don't believe me?

GDB was literally made to help you figure out exactly where your bugs are. Think of it as your own little 2200-TA in a program that you have access to 24/7. Why are we so enamored with GDB?

  • Line by line code stepping
  • Easy/intuitive access to variables and data structures
  • Easily set break/watch points in your code to see what's being modified
  • Quickly examine contents of memory, stack, and registers
  • Allows de-referencing pointers and structs within the debugger itself
  • Cool extensions to make it even more usable

But I get it, GDB can be daunting - you Google for GDB commands and find things like "x/20i $eip" - who knows what that's supposed to mean? The syntax can seem a little esoteric, but with some practice, I guarantee that you will find it to be second nature! In fact, you probably won't even need most of those arcane GDB commands while you debug. So let's get started on some basics.

GDB Setup

When you use gcc to compile a C program, it produces a program binary that you can execute on the system. In addition, gcc includes a special compiler flag so that it not only compiles the basic binary but also includes special debugging symbols baked into the executable. These debugging symbols allow you to print variable names directly in GDB without knowing the memory address of those variables.

In order to enable those debugging symbols, we compile using the "-g" flag. Here's an example:

gcc helloworld.c -o hello -g

Doing this will let you run the binary in GDB with access to lots of conveniences. Our Makefiles should automatically be compiling your programs with the -g flag enabled when you type "make debug", so this is just for your own knowledge if you decide to compile your own programs yourself.

Now, you're good to go. To start GDB on a binary called "hello", type:

gdb hello

You will see some initialization text which you can largely ignore. Finally, you will be left inside the GDB shell prompt, where you can begin your debugging session.

When you want to run your program (maybe with arguments, if your program takes command line arguments), you would run it like this:

(gdb) run arg1 arg2 ... argn

Basic Debugging Toolkit

Let's say you're getting a segmentation fault, but you don't know what the issue is. How do you figure it out? First, open up GDB like in the previous section - but don't run the program yet.

Set Breakpoints

You can set breakpoints in your code to "pause" your code at certain lines. Let's say you have an idea for what function your code is messing up - maybe because it's your longest and most complicated function. You can set a breakpoint on the entry to that function by typing the following:

(gdb) b function_name

b is short for "breakpoint", and function_name tells GDB where to set the breakpoint. Now, when you run your code within GDB, GDB will pause the program every time the function gets called so that you can examine the state of the program. We can actually get even more fine-grained breakpoints and break on specific lines of code:

(gdb) b helloworld.c:23

This would set a breakpoint right before line 23 of helloworld.c executes. You can see how this might be valuable if you're suspicious of specific lines in your code being problematic!

Now let's say you've made quite a few breakpoints in your code and you think you've narrowed down the error-culprit. You can start reviewing and removing your old breakpoints so that they don't get in your way. To view your old breakpoints:

(gdb) info breakpoints

This command lists all the old breakpoints you've set in your code. To delete a breakpoint (let's say breakpoint 1):

(gdb) del 1

Printing Variables and Structs

So you've set your breakpoint on a suspicious function or line. Then, you type run in GDB and the program halts right before the breakpoint, waiting for you. What do you do? This would be a good place to start examining the variables in your program during run-time. Make sure that that struct pointer you declared actually points to what you think it does, and that the parameters of the function you're in are all correct. Back in the day, you might have done this by typing numerous "print" statements throughout your code, re-compiling, and re-running. Don't lie! I know you've done it, we've all done it! While this can work for some cases, it's not the most optimal way of debugging nor is it always the most accurate - sometimes the calls to print can actually cause more issues. So how do we see the contents of our variables? With GDB, this becomes extremely simple! Let's say we have an integer variable x and we want to know it's value at a given time.

(gdb) p x

"p" is short for print, and x is just the variable name. So this prints the value of x! This works for char[] strings as well. In another use case, often you might only have a variable that represents a pointer to the variable you care about, and not the variable itself. Take for example the following C code:

struct list_node {
    int value;
    list_node *next;
} node_t;
...
struct list_node *node = malloc(sizeof(struct list_node));

The line of code on the bottom allocates some memory from the heap and returns a pointer to a struct list_node. Let's say you're suspicious of the integer value inside the list_node struct and don't think it's accurate. You can't just p node in GDB to view the contents of the struct because node is a pointer to the struct, not the struct itself. So how do you view value? Just do the following!

(gdb) p node->value

That's right, GDB supports in-line dereferencing pointers! This works for any pointer in your program, not just struct pointers. Let's say you had the following:

int *x = malloc(sizeof(int));

...
// Some calculations involving x
...

You can view the value of x in GDB by typing:

(gdb) p *x

The integer pointer x gets dereferenced before being printed, just as it would in C! GDB has tons of useful, handy shortcuts like this. Now, you can see the dynamic values of your program as your program runs. This can be super helpful when you're trying to find out why certain values aren't appearing as they should. If you think your variable values look fine, you can continue running your program by entering continue or c in your GDB prompt.

Setting Watchpoints

Let's go through another common debugging scenario, one of my favorites! Let's say you're debugging and you're getting a segfault, but you have no idea why. Your code seems completely fine to you, so you run it in GDB and set some breakpoints. You find the line causing the segfault:

some_struct_pointer->value = 23;

What? Why is this seg-faulting? You print out the pointer value and realize that it's NULL! You see that you definitely properly malloc'd memory for some_struct_pointer and don't think you're doing anything wrong with it. So what gives?

In a scenario such as this, GDB watchpoints will become your best friend. Think of a watchpoint as a breakpoint on a variable instead of a breakpoint on a specific line of code or a function. You don't know where your some_struct_pointer variable is getting set to null, but you do know that it's happening. You can set a watchpoint on some_struct_pointer so that whenever its value gets changed, the program will automatically break for you. Then, you can figure out exactly where in the code some_struct_pointer gets set to NULL and work backwards to stop that from happening! Watchpoints are very handy when you want to know exactly when specific variables are getting changed. The syntax is almost identical to that of a breakpoint.

(gdb) watch x

This sets a watchpoint on the variable x. You can view and delete watchpoints by typing info watchpoints and delete them same as before.

Backtrace

Let's say you're suspicious that the program is running a line of code that it shouldn't be. You set a breakpoint on that line and run your program. The program stops, and you see that you're on that line of code that shouldn't have been run! How did your program end up getting here? GDB has this awesome command called backtrace that can tell you exactly this.

(gdb) backtrace 
#0 level0 () at recursion.c:5
#1 0x08048462 in test (level=0) at recursion.c:17
#2 0x0804845b in test (level=1) at recursion.c:14
#3 0x0804845b in test (level=2) at recursion.c:14
#4 0x0804845b in test (level=3) at recursion.c:14
#5 0x0804845b in test (level=4) at recursion.c:14
#6 0x0804845b in test (level=5) at recursion.c:14
#7 0x08048479 in main () at recursion.c:22

In this example, we ran a program that had recursive function calls, so backtrace printed out the function calls that led up to the current line of code being executed. Finally, it shows that the function main was the original function that called the recursive function. With backtrace, we can even see what lines of code in each function made the function calls (5, 17, 14, 22). This is super useful for figuring out how we got to certain places in the code. If instead of recursive function calls, you had several different functions in your program, then backtrace would have printed those out.

Some More Tips and Tricks

Here's a few tips and tricks to make your debugging sessions go more smoothly.

Restarting the program within GDB

Many people think that if they want to re-run the program from the beginning, they have to quit GDB and then type gdb program_name again to re-enter it. It turns out that you don't have to do this! Any time you're in the GDB command prompt, just type run arg1 arg2 ... argn as you did when you first started the program, and the program will restart. This way, you don't have to keep resetting your breakpoints and watchpoints!

Find Line of Code Currently Executing

You can find out what line of code you're on with info line. You can also view some of the surrounding lines of code by typing list. This is helpful so you don't have to keep going back to your source C file to find out where you are in the code in GDB!

GDB Extensions

GDB is already an amazing tool, but some people like extending GDB with their own tools to make it more convenient. There are tons of tools for this that make GDB look and feel more like a debugger from a full-fledged IDE. For example, there is a popular GDB front-end called Voltron that helps lay out different windows to display the contents of the memory, stack, and program (check it out here if interested: https://github.com/snare/voltron). Personally, as someone interested in binary reverse engineering, I use https://github.com/hugsy/gef. You can choose to use any or none of these GDB extensions to aid you in your debugging process. Don't feel as if these are required for debugging - GDB has all the tools you need already built in!

Summary

This tutorial doesn't even scratch the surface of what GDB can do - GDB is that powerful. But, I believe that these basic commands will give you enough basic knowledge to know how use GDB as you debug your projects in CS 2200 and beyond. Hopefully, with this knowledge equipped, there won't be any reason for you or anyone to fear C programming. Good luck on your projects and have fun!

-Sid

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment