Skip to Content
Course content

131: Debugging with GDB

Click on the "Edit" button in the top corner of the screen to edit your slide content.

How do I actually get GDB to see my variables and line numbers?

If you just run gdb ./my_program, you're going to be disappointed. You'll see a bunch of hex addresses and "no debugging symbols found," which is basically GDB telling you it has no idea how the binary maps back to your source code. To fix this, you have to tell the compiler to include those symbols.

You need the -g flag. I usually just make it a habit to include it in my Makefile or build script during development. Here is how you'd compile a simple program for debugging:

g++ -g main.cpp -o my_program

Keep in mind that -g increases the size of your binary because it's storing a map of every variable and line number. You'll want to strip these out or switch to -O2 or -O3 optimization flags when you're ready for production, but for now, -g is your best friend.

My program just crashed with "Segmentation Fault"—how do I find the exact line?

This is where GDB really earns its keep. Instead of peppering your code with std::cout << "Got here" << std::endl; (which is tedious and honestly a bit primitive), you can let the debugger catch the crash in real-time.

Let's say you're working with a linked list and you forgot to check if a pointer was null before dereferencing it:

struct Node {
    int data;
    Node* next;
};

void printList(Node* head) {
    while (head != nullptr) {
        std::cout << head->data << " ";
        head = head->next;
    }
}

int main() {
    Node* head = nullptr; 
    printList(head); // This is fine
    
    Node* badNode = nullptr;
    std::cout << badNode->data; // CRASH!
}

To find the crash, load the program, type run, and wait for the segfault. Once it hits, type backtrace (or just bt). GDB will print the call stack, showing you the exact file and line number where the crash occurred. It’s an immediate "aha!" moment.

Can I move through the code line-by-line and see what's happening inside a loop?

Absolutely. You don't have to just wait for a crash; you can pause time. You do this using breakpoints. If you want the program to stop at the start of your printList function, you'd type break printList or break main.cpp:10.

Once the program hits that breakpoint, you have a few essential commands:

  • n (next): Executes the current line and moves to the next one. It treats function calls as a single step.
  • s (step): If the current line is a function call, this "steps into" that function so you can debug it internally.
  • p variable_name (print): This is the gold mine. You can print the current value of any variable in scope. p head will show you the address, and p head->data will show you the actual value.

I personally find display variable_name even more useful than print. While print is a one-time request, display tells GDB to show you the value of that variable every single time the program stops. It's great for watching a loop counter or a pointer move through a list.

What's the real difference between 'next' and 'step'?

This trips up a lot of people early on. Think of next as "skip over the details" and step as "dive into the details."

If you are on a line that calls std::sort() and you hit n, GDB will execute the entire sorting algorithm and stop at the very next line in your current function. If you hit s, GDB will jump you inside the std::sort implementation in the C++ Standard Library.

Fair warning: if you step into a standard library function, you're going to see a lot of complex, template-heavy code that is probably not what you're looking for. When that happens, just use finish. That command tells GDB to execute the rest of the current function and return you back to the caller.




📋 Practical Task

Hunting the Null Pointer in a Doubly Linked List

You have been given a snippet of a Doubly Linked List implementation where the removeNode function is causing a segmentation fault when trying to delete the tail of the list. Your goal is to use GDB to identify the exact line of the crash and the state of the pointers at that moment.

# save as debug_me.cpp
# Compile with: g++ -g debug_me.cpp -o debug_me

#include <iostream>

struct Node {
    int val;
    Node *prev, *next;
    Node(int v) : val(v), prev(nullptr), next(nullptr) {}
};

void removeNode(Node* node, Node*& head, Node*& tail) {
    if (node->prev) {
        node->prev->next = node->next;
    } else {
        head = node->next;
    }

    if (node->next) {
        node->next->prev = node->prev;
    } else {
        // The bug is lurking around here...
        tail = node->prev;
        std::cout << "Removing tail: " << node->next->val << std::endl; 
    }
    delete node;
}

int main() {
    Node* head = new Node(1);
    Node* tail = new Node(2);
    head->next = tail;
    tail->prev = head;

    removeNode(tail, head, tail);
    return 0;
}

Your task:

  1. Compile the code with debugging symbols.
  2. Run the program inside GDB.
  3. When the program crashes, use backtrace to find the line number.
  4. Use print node->next to verify why the crash is happening.
  5. Fix the code so that it can safely remove the tail node without crashing.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.