Debugging with gdb – CMPSC 311 Lecture

Debugging: Purpose & Context

  • Debugging = locating, understanding, and removing faults in software.

    • Frequently the most complicated and time-consuming activity in development.

    • Goal: discover where real program behaviour diverges from intended behaviour and correct it.

    • Continuous process: confirm the program is actually "doing what you think it is doing".

First-Line Techniques

  • Printing / Logging

    • Insert temporary output statements to reveal values/state.

    • Example (C): printf("My variable value is %d", myvar);

    • Pros: quick, works everywhere.

    • Cons: intrusive, must be removed or disabled later, produces large output, limited visibility.

  • ## Assertions (Logic / Program Guards)

    • Provided by C via #include <assert.h>.

    • Syntax: assert(expression); – program aborts if expression is false.

    • Enforces contractual assumptions about inputs, return values, pointer validity, etc.

    • Typical uses:

    • assert(i >= 0); – non-negative loop/index.

    • assert(ptr != NULL); – pointer validity.

    • assert((ptr = malloc(100)) != NULL); – allocation succeeded.

    • assert(func(10,20)); – boolean function returned true/non-zero.

    • Active only when compiled without -DNDEBUG; they disappear in release builds, so never place side-effects inside.

Demonstration Code (Factorial)

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>

int factorial(int i) {
    assert(i >= 0);
    if (i <= 1) return i;
    return factorial(i-1) * i;
}

int main(int argc, char **argv) {
    int i = 5;
    if (argc > 1) i = atoi(argv[1]);
    printf("Factorial of %d: %d\n", i, factorial(i));
    return 0;
}
  • Compile with symbols: gcc -g debugging.c -o debugging.

  • Sample runs:

    • ./debuggingFactorial of 5: 120

    • ./debugging 4Factorial of 4: 24

    • ./debugging -1 → assertion failure (stops at runtime).

  • Mathematical definition enforced by the code: n! = n \times (n-1)! with base case 0! = 1 (here coded as \le 1).

The Debugger Environment

  • A debugger executes the target program in a controlled sandbox where you can:

    • Start/attach, halt, resume, step through code/instructions.

    • Inspect or modify state (registers, memory, variables, environment).

    • Automate via conditions & scripts.

  • Linux/Unix mainstream: GNU gdb. (Apple & LLVM world: lldb).

Launching gdb

  • Terminal: gdb <program> (does not start the program yet).

  • Can also be integrated into editors/IDEs, e.g., Emacs → Tools → Debugger (GDB).

  • Basic prompt features:

    • help or help <command> – built-in manual.

    • Tab-completion, command abbreviations (e.g., l for list).

Running the Program

  • run (alias r) actually begins execution.

    • Pass arguments exactly as on shell: r 12.

  • After normal termination gdb prints [Inferior … exited normally] and drops back to prompt.

Inspecting Source

  • list (alias l) shows 10 source lines surrounding a locus.

    • list 4 – centered around line 4 of current file.

    • list main – shows function main.

Breakpoints

  • Pause execution at specified locations.

    • Set: break <function> or break <line> — alias b.

    • Delete: delete <num> or d.

    • Each breakpoint auto-numbered; visible with info breakpoints.

  • Conditional Breakpoints:

    • Add condition later: cond <bp#> <expr>.

    • Inline: b <loc> if <expr>.

    • Example:

    1. b 7 → Breakpoint 1 at line 7.

    2. cond 1 i<=1 — only stops when i <= 1.

  • Saving / Restoring:

    • save breakpoints <file> – writes gdb commands.

    • source <file> – re-load their definitions.

Watchpoints (Data Breakpoints)

  • Trigger when a variable’s value changes, independent of code location.

    • watch <variable>.

    • Hardware vs software implementation; may be limited in number.

    • Example flow:
      (gdb) watch i (gdb) continue Hardware watchpoint 2: i Old value = 5 New value = 4

Stack & Frames

  • where (alias backtrace/bt) – full call stack with line numbers.

  • up, down – navigate frames.

    • p i prints local i in current frame.

    • Use to inspect callers/callees during recursion.

Examining Data

  • Printing Variables

    • print <var> or p shows value in default format.

    • Format override: p/<fmt> <var> where fmt is one of:

    • o octal, x hex, d signed dec, u unsigned dec, t binary, f float, a address, i instruction, s string.

    • Example outputs:

    • p/x values{0x1, 0x2, 0x3, 0x4}

    • p/f val22.45678.

  • ## Examining Raw Memory

    • Command: x/[num][fmt][size] <address>.

    • [num] = repeat count, [fmt] = print code as above, [size] = b(1), h(2), w(4), g(8 bytes).

    • Examples:

    • x buf (default word/hex): 0xefefefef.

    • x/8xb buf – 8 bytes, hex byte-wise: ef ef ef ef ef ef ef ef.

    • x/xg buf – one 8-byte giant word.

    • x &buf – show pointer stored in stack variable buf.

Stepping Through Execution

Four primary flow-control commands (all can be abbreviated):

  1. next (n) – executes the current source line; does not enter called functions.

  2. step (s) – executes current line and does enter called functions.

  3. continue (c) – resumes until next breakpoint, watchpoint, or program exit.

  4. finish (fin) – continues until current function returns, then stops in caller.

Integrated Example Walk-Through

Workflow summary using factorial program:

  1. Set breakpoints before run:

    • (gdb) b factorial

    • (gdb) b 20 (line in main)

  2. (gdb) run 3 – program stops at main’s breakpoint.

  3. (gdb) continue – runs to factorial breakpoint.

  4. Use n, s, c to step/descend/continue through recursive calls.

  5. Observe where, p i, up/down to watch changing argument values.

  6. Confirm expected output Factorial of 3: 6, program exits normally.

Compiler & Build Considerations

  • Always compile with debug symbols for full gdb power: gcc -g ....

  • Optimisation (-O2, -O3) may reorder/remove code → stepping looks odd; turn it off during heavy debugging or accept discrepancies.

  • Assertion removal with -DNDEBUG may shift line numbers; keep assertions enabled while debugging.

Connections & Practical Implications

  • Printing/logging & assertions provide front-line defense and lightweight sanity checks even after release.

  • Symbolic debuggers (gdb/lldb) deliver:

    • Fine-grained control → faster root-cause identification vs printf-spamming.

    • Reproducibility: breakpoints/watchpoints scripts create deterministic sessions.

  • Ethical & professional obligation: deliver robust, well-tested code; knowledge of debugging tools is essential.

  • Real-world relevance: techniques apply to low-level OS dev, embedded systems, performance tuning, security analysis (reverse engineering uses same stack/backtrace methods).

Quick gdb Command Reference (Cheat Sheet)

  • Startup & Help

    • gdb prog, help, quit.

  • Execution

    • run [args], attach <pid>, kill.

  • Breakpoints

    • b <loc>, cond <bp#> <expr>, d <bp#>, info b, save breakpoints.

  • Watchpoints

    • watch <expr>, rwatch (read), awatch (access).

  • Flow

    • n, s, c, fin, until <loc>.

  • Stack

    • where, bt, frame, up, down.

  • Data

    • p[/fmt] expr, set var <expr> = <value>, x/[nfmt][size] addr.

  • Source

    • list, disassemble.

  • Misc

    • info <topic>, set pagination off, display expr (auto-print each stop).


These bullet-style notes compile every key detail from the lecture slides and transcripts, offering a stand-alone study reference for CMPSC 311’s unit on debugging and gdb usage.