Create your own
Lesson illustration

Inspecting Stack Frames, Variables, and Control Flow with GDB

Hello. In the previous lesson, you used compiler diagnostics and sanitizers to identify a heap use-after-free. Sanitizers explain an error after an invalid operation is executed; GDB serves a complementary role. It lets you pause before or during a suspicious operation, inspect the currently active calls and variables, and follow execution one source-level statement at a time.

This lesson develops a compact, reusable GDB workflow in WSL. You will compile a small C++ program for debugging, stop inside a nested function call, inspect the stack frames and variables in both the current function and its callers, then control execution with stepping commands.


A debugger’s snapshot of a running program

A debugger runs your program under supervision. At a breakpoint, GDB pauses the process and gives you a prompt. The program’s state is preserved at that instant: the active function, the calls that led there, parameters, local variables, and the next source location to execute.

For C++ debugging, first build with debug information:

g++ -std=c++20 -Wall -Wextra -Wpedantic -g -O0 inspect.cpp -o inspect

Two flags matter especially here:

  • -g embeds debugging information. It lets GDB associate executable code with source files, function names, line numbers, and variables.
  • -O0 disables compiler optimization. This makes the relation between your source and the executed program much easier to inspect while learning.

Optimization does not make code “less correct,” but it can make debugging less literal. An optimizing compiler may inline a function, reuse a variable’s storage, eliminate a value, or rearrange operations while preserving observable program behavior. In GDB, this can produce messages such as optimized out or make stepping appear to skip around. For an initial investigation, use -g -O0.

If GDB is not installed in your WSL distribution, install it once:

sudo apt update
sudo apt install gdb

GDB’s fundamental cycle is:

  1. Set a breakpoint where useful state will exist.
  2. Run until execution stops.
  3. Inspect the current source location, variables, and call stack.
  4. Step, resume, or move out of a function based on what you need to learn.

Stack frames: why the call stack answers “how did we get here?”

Each active function call has a stack frame (also called an activation record). Conceptually, it preserves enough execution state for that invocation to run and later return to its caller. That state normally includes a way to return to the caller and may include parameters, local variables, saved registers, and temporary bookkeeping.

Suppose main calls summarize, which calls adjust. While adjust is running, all three calls are active. Their frames form the call stack:

GDB frame numberActive functionWhy it remains active
#0adjustIt is the currently executing function
#1summarizeIt called adjust and awaits its result
#2mainIt called summarize and awaits its result

GDB labels the innermost, currently executing frame as frame #0. Higher numbers are its callers. When adjust returns, its frame ceases to be active; summarize becomes the current frame and is relabeled #0.

A recursive factorial program has one active stack frame for each unfinished call. Each frame holds a distinct value of `n` and enough return information to resume its caller; the diagram’s physical stack-pointer details vary by platform, but the nesting of active calls is the key idea for GDB.

The image uses recursion, so several invocations of the same function are active simultaneously. GDB distinguishes them by frame number. This is why a backtrace is invaluable for recursive code, unexpected callback chains, and crashes deep in helper functions.

A useful caveat: “stack frame” is the debugging model you should use first. On a particular architecture and compiler configuration, some values may instead live in CPU registers, and optimized code can alter the physical layout substantially. GDB’s frame view remains useful because it reconstructs the source-level call context from debug information.

C/C++ Stack Frames and gdb

Watch “C/C++ Stack Frames and gdb” by John’s Basement for a visual model of calls creating frames, returning to callers, and reusing stack space.

Watch the call stack model. Focus on the distinction between a function definition and one particular active invocation of that function, and on why an unfinished caller’s frame remains available while a callee executes.


The small command set you need first

GDB has many commands, but a small group handles most early C++ investigations.

CS111 GDB Cheat Sheet

Read Stanford CS111’s compact command reference to consolidate the commands used in this lesson and see their standard abbreviations.

In the “Common Commands” section, read the command reference from the opening launch instruction through breakpoint deletion. Focus on break, run, backtrace, print, next, and step; treat the abbreviations as convenient, not mandatory.

Here is the practical command map:

GoalCommandShort formImportant meaning
Launch GDB with an executablegdb ./inspectStarts GDB; does not run the program yet
Set a function breakpointbreak adjustb adjustPause when execution reaches adjust
Run the programrunrStart or restart the program
Show nearby sourcelistlDisplays source around the selected location
Show current positionframefReports the selected stack frame and source line
Show all active callsbacktracebtLists frames from current function through callers
Select a caller frameframe 1f 1Changes inspection context, not execution
Inspect argumentsinfo argsShows parameters of the selected frame
Inspect localsinfo localsShows local variables in the selected frame
Evaluate a C++ expressionprint exprp exprEvaluates in the selected frame’s context
Step over a callnextnExecutes the current source statement without entering called functions
Step into a callstepsEnters a called function when source is available
Finish current functionfinishRuns until the current function returns
Resume freelycontinuecRuns to the next breakpoint, signal, or program end
List breakpointsinfo breakpointsi bShows number, location, and hit count
Remove breakpoint 1delete 1d 1Deletes a breakpoint by number

For ordinary variable inspection, print is usually safe. Be cautious with expressions that call functions: calling a nontrivial function from a debugger can allocate memory, mutate state, acquire locks, or otherwise change the very behavior you are trying to understand.


Lab: inspect frames, variables, and control flow

Create inspect.cpp:

#include <iostream>

int adjust(int value) {
    const int doubled = value * 2;
    const int result = doubled + 3;
    return result;
}

int summarize(int start) {
    const int offset = 1;
    const int adjusted = adjust(start);
    return adjusted - offset;
}

int main() {
    const int input = 7;
    const int answer = summarize(input);

    std::cout << answer << '\n';
}

Before debugging, reason about the intended result:

  • main passes 7 to summarize.
  • summarize passes 7 to adjust.
  • adjust computes , which is 17.
  • summarize subtracts 1, leaving 16.

Build and launch the debugger:

g++ -std=c++20 -Wall -Wextra -Wpedantic -g -O0 inspect.cpp -o inspect
gdb ./inspect

At the (gdb) prompt, set a breakpoint at the entry to adjust, then run:

break adjust
run

GDB should report that it stopped in adjust, with value=7. The highlighted line is generally the next source statement GDB associates with the current instruction location.

Start by viewing the local source context:

list

Then execute the two initialization statements one at a time:

next
next

You should now be stopped at or near:

return result;

At this point, both local values have been assigned. Inspect the current frame:

info args
info locals
print value
print doubled + result

The important observations are:

value = 7
doubled = 14
result = 17

and the expression doubled + result evaluates to 31. GDB evaluates names relative to the currently selected frame, which is currently adjust.

Now ask the central control-flow question: which calls led to this point?

backtrace

Your addresses and exact formatting will differ, but the logical structure should resemble:

#0  adjust (value=7) at inspect.cpp:...
#1  summarize (start=7) at inspect.cpp:...
#2  main () at inspect.cpp:...

Read this from top to bottom:

  • Frame #0: execution is currently paused inside adjust.
  • Frame #1: summarize called adjust and has not received its result yet.
  • Frame #2: main called summarize and has not received its result yet.

This is a dynamic execution history, not merely a list of functions that happen to exist in the source file.


Inspecting a caller without rewinding time

While stopped in adjust, select summarize’s frame:

frame 1
frame
info args
info locals
print start
print offset

The first frame command changes GDB’s inspection context. It does not make summarize execute again and does not move program control backward. It simply lets you inspect the caller’s currently suspended state.

You should find:

start = 7
offset = 1

Notice the subtle status of adjusted. The caller is paused in the middle of this initialization:

const int adjusted = adjust(start);

Since adjust has not returned yet, adjusted does not yet hold its final meaningful value. In a less controlled program, inspecting a variable before initialization is a common source of misleading debugger output. A debugger can show raw program state; it cannot turn an uninitialized value into valid information.

Return to the active frame before resuming execution:

frame 0

Now use finish:

finish

finish runs the remainder of the current function, adjust, and stops after control has returned to its caller. GDB now considers summarize the active frame, so it becomes frame #0.

Inspect its completed local state:

info locals
print adjusted

Now adjusted should be 17. Step once:

next

This executes the return expression in summarize, returning 16 to main. You should now be at or near the output statement in main.

At this point, inspect main:

info locals
print answer

The answer should be 16.

Checkpoint: before leaving this lab, your GDB session should have demonstrated all of the following:

  • backtrace showed adjust, summarize, and main.
  • Frame #0 was the active function at the instant of the breakpoint.
  • frame 1 let you inspect summarize’s parameter and initialized local variable.
  • finish returned from adjust without manually stepping through every remaining statement.
  • The final computed value was 16.

End the run cleanly:

continue
quit

Choosing next, step, finish, and continue

A debugger becomes efficient when your command matches your question.

If you want to…PreferWhy
Verify how a local value changes across nearby statementsnextMoves through your current function predictably
Understand what a helper function does internallystepEnters the called function
Escape a helper or library call you enteredfinishCompletes the active function and returns to its caller
Reach a later breakpoint without watching intervening codecontinueRuns normally until GDB has another reason to stop

Use next as the default when debugging your own high-level logic. A statement such as:

std::cout << answer << '\n';

can involve many standard-library function calls. step may take you deep into implementation details that are irrelevant to the suspected defect. Use next to execute that statement as one source-level unit.

Use step deliberately when a function’s internal behavior is exactly what you need to inspect.

C/C++ Stack Frames and gdb

Continue with John’s Basement’s “C/C++ Stack Frames and gdb” for a concise demonstration of setting up a debugging session, entering a function, and interpreting the resulting call stack.

Watch breakpoints and stepping to see start, list, and step used at source level. Then watch the frame view for the relationship between the active frame and its caller in a where/backtrace display.


Breakpoints as diagnostic hypotheses

A breakpoint should be placed where it can test an idea about the program.

For example:

break adjust

tests the hypothesis, “Does adjust receive the value I expect?”

A source-location breakpoint can be more precise:

break inspect.cpp:5

This says, “Pause when execution reaches the source location around line 5.” Line numbers naturally change as you edit; function breakpoints are often more durable in a small program.

Manage breakpoints with:

info breakpoints
delete 1

A breakpoint may trigger many times in a loop or frequently called helper. In those cases, do not blindly step from the start each time. First inspect the breakpoint list, then either move the breakpoint closer to the suspected condition or use a condition when you have a relevant variable:

condition 1 value > 100

This tells GDB to stop at breakpoint 1 only when value > 100 is true. Conditional breakpoints are powerful because they narrow a long execution to a state that matters.


A compact workflow for real defects

When a program crashes or produces the wrong result, use this disciplined loop:

  1. Rebuild with -g -O0 and your warning baseline.
  2. Choose a location where the relevant values should exist: the suspicious function, a branch, or a line before a failing operation.
  3. Set a breakpoint and run.
  4. Use list and frame to confirm exactly where execution paused.
  5. Use info args, info locals, and targeted print expressions to test your assumptions.
  6. Use backtrace to identify the call chain that produced the state.
  7. Switch to caller frames when the source of a bad argument may lie outside the active function.
  8. Use next, step, finish, or continue according to the control-flow question.
  9. Repair the actual source defect, rebuild, and rerun.

If a program instead crashes before you know where to set a breakpoint, launch it under GDB and let it fail:

gdb ./app
run
backtrace

When GDB stops on a signal such as SIGSEGV, frame #0 identifies the immediate fault location. The caller frames tell you how execution arrived there. This complements the sanitizer workflow from the previous lesson: GDB lets you examine live state, while sanitizers often provide stronger diagnosis for memory-safety and undefined-behavior violations.


Takeaways

  • Build debug targets with -g; use -O0 initially to make source-level debugging clearer.
  • A stack frame represents one active function invocation. GDB’s frame #0 is the currently executing function; higher-numbered frames are its callers.
  • backtrace reveals the active call chain, and frame N changes the variable-inspection context without changing execution.
  • Use info args, info locals, and print to inspect state at a breakpoint, while remembering that variables not yet initialized do not have meaningful values.
  • Use next to step over calls, step to enter them, finish to return from the current function, and continue to run to the next stopping point.

Next, you will distinguish automatic, dynamic, and static storage duration by tracing object lifetimes in a C++ program. The stack-frame view from this lesson will provide a useful starting point, but object lifetime is a language-level rule that cannot be inferred solely from an address or a frame.

Can't find a good explanation? Sign up and we'll make it for you

Sign up