C
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Arrays and Strings
-
Section 5: Pointers
-
Section 6: Memory Management
-
Section 7: Structures and Unions
-
Section 8: The Preprocessor and Build Process
-
Section 9: Standard Library: stdio.h
-
Section 10: Standard Library: stdlib.h
-
Section 11: Standard Library: string.h
-
Section 12: Standard Library: ctype.h and wctype.h
-
Section 13: Standard Library: math.h, complex.h, fenv.h, tgmath.h
-
Section 14: Standard Library: Type and Limit Headers
-
Section 15: Standard Library: Error Handling and Debugging
-
Section 16: Standard Library: Localization and Encoding
-
Section 17: Standard Library: time.h
-
Section 18: Standard Library: Concurrency (C11)
-
Section 19: POSIX and System Programming (unistd.h)
-
Section 20: More Data Structures
-
Section 21: Algorithms in C
-
Section 22: Bitwise Operations
-
Section 23: Command-Line Programs
-
Section 24: Debugging and Best Practices
-
Section 25: Compiler and Language Internals
-
Section 26: Embedded and Cross-Platform Considerations
-
Section 27: Networking Basics
-
Section 28: Practical Projects
-
Section 29: Interview Practice
-
Section 30: C23 Modern Features
-
Section 31: More Practice and Review
193: Setting Breakpoints and Inspecting Variables in gdb
I can't tell you how many times I've seen a developer stare at a screen for three hours, adding printf statements every two lines, trying to figure out why their program is crashing. While "printf debugging" is a rite of passage, it's like trying to perform surgery with a flashlight and a butter knife. When your program hits a segmentation fault, you don't want to guess where it happened—you want to be standing right there when it happens.
Let's look at a piece of code that looks perfectly reasonable at first glance but will crash your program in a way that's frustratingly hard to spot without a debugger.
#include <stdio.h>
#include <string.h>
void reverse_string(char *str) {
if (!str || !*str) return;
char *end = str;
while (*end != '\0') {
end++;
}
end--; // Move back one to point at the last character
while (str < end) {
char temp = *str;
*str = *end;
*end = temp;
str++;
end--;
}
}
int main() {
char my_text[] = "Hello GDB";
reverse_string(my_text);
printf("Reversed: %s\n", my_text);
return 0;
}
The Mystery of the Occasional Segfault
In the example above, the logic for reversing the string seems sound. We find the end, then swap characters moving inward. However, imagine this was part of a larger project where reverse_string was being called on buffers that weren't always null-terminated correctly, or where the pointer arithmetic was slightly off. If end somehow started before str, or if the loop condition failed, we'd be in trouble.
Let's assume we've modified the code slightly, and now it's crashing. Instead of adding printf("Here 1\n"); printf("Here 2\n");, we're going to use gdb (the GNU Debugger). First, you must compile with the -g flag. If you don't, gdb won't have the symbol table, and you'll see memory addresses instead of your actual variable names. I've made this mistake more times than I'd like to admit.
gcc -g -o reverse_test main.c
gdb ./reverse_test
Pinpointing the Crash with Breakpoints
Once you're inside gdb, the first thing I usually do is set a breakpoint at the start of the suspect function. This tells the program: "Run normally until you hit this line, then freeze everything so I can look around."
(gdb) break reverse_string
Breakpoint 1 at 0x1149: file main.c, line 4.
(gdb) run
The program will run and then stop right at the entry of reverse_string. Now, we don't want to just run the whole thing; we want to step through it line by line. Use next (or just n) to execute the current line and move to the next one.
While stepping, you can inspect your variables. This is where the magic happens. Instead of guessing what end is, just ask gdb:
(gdb) print end
$1 = 0x7fffffffe110 "Hello GDB"
(gdb) print *end
$2 = 10 // This is the newline or null terminator
If the program crashes (a Segmentation Fault), gdb will stop exactly on the line that caused the crash. You can then use the backtrace (or bt) command to see the call stack—which is essentially a map of how you got to that specific line of code.
Watching Variables in Real Time
Sometimes a variable changes value when you don't expect it to. Rather than printing the variable after every single line, you can set a watchpoint. A watchpoint stops program execution the moment a specific variable's value changes.
(gdb) watch end
Hardware watchpoint 2: end
(gdb) continue
Now, gdb will let the program run, but it will interrupt you the second end is modified. This is an absolute lifesaver when you're dealing with complex loops or pointer arithmetic where a value is being overwritten by a rogue pointer elsewhere in your code.
To wrap up a session, you can use quit to exit. The core workflow I want you to remember is: Compile with -g → Break at the function → Step through with 'n' → Inspect with 'p' → Watch changes with 'watch'.
📋 Practical Task
Debugging the Binary Search Boundary Error
You are given a program that implements a binary search. However, the program crashes with a segmentation fault or returns the wrong index when searching for an element that doesn't exist in the array.
Your task:
- Copy the provided broken binary search code into a file named
search.c. - Compile it using
gcc -g search.c -o search. - Load it into
gdb. - Set a breakpoint at the
binary_searchfunction. - Use
printto inspect thelowandhighvariables during the loop iterations. - Identify the logic error in the
whileloop condition or the index calculation that leads to the bug. - Fix the code and verify the fix by running it through
gdbone last time to ensure the variables behave as expected.
# Broken Binary Search Code
#include <stdio.h>
int binary_search(int arr[], int size, int target) {
int low = 0;
int high = size; // Potential bug here
while (low <= high) {
int mid = (low + high) / 2;
if (arr[mid] == target) {
return mid;
} else if (arr[mid] < target) {
low = mid + 1;
} else {
high = mid - 1;
}
}
return -1;
}
int main() {
int data[] = {10, 20, 30, 40, 50};
int target = 60; // Searching for something not there
int result = binary_search(data, 5, target);
printf("Result: %d\n", result);
return 0;
}
There are no comments for now.