Debugging Techniques

Using debugging tools and techniques to identify and fix errors in your C programs.


Examining Variables in C

Understanding and inspecting the values of variables during program execution is crucial for debugging and ensuring your C programs function correctly. This document outlines techniques for examining variables, considering their scope, and understanding memory addresses.

Variable Scope

The scope of a variable determines where in the code the variable is accessible. C has several types of scope:

  • Local Scope: Variables declared inside a function or block (delimited by curly braces {}) have local scope. They are only accessible within that function or block.
  • Global Scope: Variables declared outside of any function have global scope. They are accessible from anywhere in the program. Use global variables sparingly as they can make code harder to reason about and debug.
  • File Scope: Variables declared outside of any function, and declared static, have file scope. They are accessible from anywhere in the file in which they are declared but not from other files in the program. This is often preferred over global scope.

Understanding scope is essential because it dictates when and where you can inspect a variable's value. Attempting to access a variable outside its scope will result in a compilation error.

 #include <stdio.h>

int global_var = 10; // Global scope

void myFunction() {
  int local_var = 20; // Local scope
  printf("Inside myFunction: global_var = %d, local_var = %d\n", global_var, local_var);
}

int main() {
  int main_var = 30; // Local to main
  myFunction();
  printf("Inside main: global_var = %d, main_var = %d\n", global_var, main_var);
  // printf("Inside main: local_var = %d\n", local_var); // This would cause a compilation error because local_var is not in scope here.
  return 0;
} 

Memory Addresses

Every variable occupies a location in the computer's memory. This location is identified by its memory address. In C, you can obtain the memory address of a variable using the & (address-of) operator.

Understanding memory addresses is crucial when working with pointers and when debugging memory-related issues like segmentation faults.

 #include <stdio.h>

int main() {
  int my_variable = 42;
  printf("Value of my_variable: %d\n", my_variable);
  printf("Address of my_variable: %p\n", (void *)&my_variable); // Use %p to print memory addresses
  return 0;
} 

Techniques for Inspecting Variables

Here are some techniques for inspecting the values of variables during program execution:

1. Using printf

The simplest and most common method is to use the printf function to print the value of a variable to the console. Make sure to use the correct format specifier (e.g., %d for integers, %f for floats, %s for strings, %p for pointers/addresses).

 #include <stdio.h>

int main() {
  int age = 30;
  float salary = 50000.0;
  char name[] = "Alice";

  printf("Name: %s, Age: %d, Salary: %.2f\n", name, age, salary); // %.2f limits to 2 decimal places
  return 0;
} 

printf statements can be added throughout your code to track the values of variables at different points in the program's execution.

2. Using a Debugger (GDB)

A debugger, such as GDB (GNU Debugger), provides more sophisticated tools for examining variables and controlling program execution. GDB allows you to:

  • Set breakpoints to pause execution at specific lines of code.
  • Step through the code line by line.
  • Inspect the values of variables.

GDB Commands

  • print variable_name: Prints the value of the specified variable. You can also print more complex expressions, like print array[i], or print *pointer.
  • display variable_name: Displays the value of the variable automatically after each step of execution. This is particularly useful for tracking changes in a variable over time.
  • watch variable_name: Sets a watchpoint. The debugger will pause execution whenever the value of the specified variable changes. This is very helpful for tracking down when and where a variable's value is being unintentionally modified.
  • info locals: Displays the values of all local variables in the current function's scope.
  • p &variable_name: Prints the memory address of the variable.

Example GDB Session

Consider the following C code:

 #include <stdio.h>

int main() {
  int i;
  int sum = 0;

  for (i = 1; i <= 5; i++) {
    sum += i;
    printf("i = %d, sum = %d\n", i, sum); // for illustration, but gdb is better
  }

  printf("Final sum: %d\n", sum);
  return 0;
} 

To debug this code using GDB:

  1. Compile with debugging information: gcc -g myprogram.c -o myprogram The -g flag adds debugging information to the executable.
  2. Start GDB: gdb myprogram
  3. Set a breakpoint: break 8 (This sets a breakpoint at line 8).
  4. Run the program: run
  5. Inspect variables:
    • print i (Prints the current value of i).
    • display sum (Displays the value of sum after each step).
    • next (Executes the next line of code).
  6. Continue execution: continue

3. Using a Debugger with an IDE

Many Integrated Development Environments (IDEs) like Visual Studio Code, Eclipse, and CLion have built-in debuggers that provide a graphical interface to GDB. These IDEs often allow you to easily set breakpoints, step through code, inspect variables, and view memory addresses using mouse clicks and visual displays. This can be much easier than using the command-line interface of GDB directly.

Conclusion

Effectively examining variables is essential for debugging and understanding C programs. By mastering techniques like printf, and utilizing debuggers like GDB (either through the command line or via an IDE), you can gain valuable insights into your program's behavior and quickly identify and resolve issues.