Skip to content
Courses

Stop a Program and Look Inside

Debugging FoundationsLesson 1 of 575 minutesLessonBuilds on: Programs Asking Linux for HelpNo lab next

Our earlier classify function prints 14 for an input of 12. That’s useful, but the output doesn’t show what value the function received, where it made its decision, or when the addition happened. Running it again gives us another 14, which is consistent but not especially revealing.

We need a way to pause the process before it finishes. A debugger is a separate program that lets us control another program’s execution and examine its state: the values it holds and the point it has reached in its work. GDB, the GNU Debugger, is the debugger we’ll use here.

You can ask GDB to stop when execution reaches a chosen location, let the program advance a little, or resume running until the next stop. While it’s paused, the process hasn’t finished or disappeared. Its memory and registers are still there to inspect. This lets us ask questions that the program’s own output doesn’t answer, without adding a new printf for every value we want to see.

The same stopped process gives us several views of its work:

  • Where execution is: the current function and machine instruction, and the corresponding source line when debugging information is available. We can see which part of the calculation we’re about to follow.
  • The values in use: function arguments and local variables. In our example, we can check whether classify actually received 12 before reasoning about the result.
  • How execution got here: the active chain of function calls. If main called classify, we can inspect those calls and the values belonging to each one.
  • The underlying machine state: CPU registers and bytes in accessible memory. These connect the source-level explanation to the instructions and addresses we studied in How Programs Work.

Those views describe the program at the stop we chose. They don’t automatically explain which value is wrong, or reconstruct every change that happened before we paused. We still need an expectation to compare against the evidence. If classify received 12, that supports one part of our explanation; it doesn’t yet show which branch ran or what the function returned. To investigate those questions, we’ll let execution move forward and observe again.

We’ll start with the argument value in one function we already understand. Later lessons follow calls and connect the source view to memory and registers. Learning a new interface is quite enough work without also hiding the program from you.

Finish How Programs Work first. This lesson uses C compilation, shell arguments, and the difference between a file and a running process. Work only with this example inside the course VM.

In the Ubuntu terminal, install the debugger:

Terminal window
sudo apt update
sudo apt install gdb
gdb --version
gcc --version

The lesson examples were checked with GCC 13.3 and GDB 15.1 on Ubuntu 24.04 x86-64. Package suffixes and debugger addresses may differ; the small programs’ integer results should not. If your guest isn’t the course’s Ubuntu x86-64 environment, return to Prepare Your Course Workspace before continuing.

Create a separate directory so we don’t overwrite the earlier decision.c:

Terminal window
mkdir -p ~/underdunn-course/exercises/debugging
cd ~/underdunn-course/exercises/debugging

Save this complete file as decision.c there:

#include <stdio.h>
#include <string.h>
int classify(int value) {
if (value > 10) {
return value + 2;
}
return value - 2;
}
int main(int argc, char **argv) {
int value = 12;
if (argc == 2 && strcmp(argv[1], "low") == 0) {
value = 10;
}
if (argc == 2 && strcmp(argv[1], "lower") == 0) {
value = 8;
}
printf("%d\n", classify(value));
return 0;
}

The function is unchanged from the instruction lesson. The small selector in main compares an argument with low or lower and chooses a known integer. strcmp returns zero when the strings match. No argument selects 12; low selects 10; lower selects 8. This is a teaching selector, not a general integer parser.

Predict the default output, then build and run in the shell:

Terminal window
gcc -std=c11 -Wall -Wextra -Wpedantic -O0 -g decision.c -o decision
file decision
sha256sum decision.c decision
./decision

-g includes debugging information, which connects machine locations to source lines and variables. -O0 keeps optimization from rearranging this first example. Neither makes incorrect C correct. file should identify an x86-64 ELF executable with debug information, and the ordinary run should print 14. Save the checksums as build identity, not as universal answers.

Each new GDB session starts with its default settings unless we tell it otherwise. We want the same behavior throughout these lessons, and typing the setup commands every time would get old quickly. GDB can read them from an initialization file when it starts.

In the Ubuntu shell, open your user configuration:

Terminal window
nano ~/.gdbinit

The ~ puts this file in your home directory, so it applies when you launch GDB from any exercise directory. If the file already contains settings, preserve them and update any matching lines instead of adding conflicting copies. Add these GDB commands to the file, without a (gdb) prompt:

set debuginfod enabled off
set disable-randomization off
set disassembly-flavor intel

set debuginfod enabled off prevents optional downloads of debugging information and source. set disable-randomization off leaves address randomization enabled for the program GDB launches; the double negative means we’re telling GDB not to disable it. Neither changes the machine-wide configuration. set disassembly-flavor intel selects the Intel instruction notation used earlier in How Programs Work, ready for the machine-state lesson.

Save with Ctrl+O, press Enter to confirm the filename, then leave nano with Ctrl+X. Start the executable from the debugging directory:

Terminal window
gdb -q ./decision

-q suppresses GDB’s greeting. The terminal now shows a (gdb) prompt. It belongs to GDB, not your shell. Commands in the next blocks go there, without typing the prompt itself.

Before running the program, check what GDB loaded. GDB input:

show debuginfod enabled
show disable-randomization
show disassembly-flavor

The reported settings should be off, off, and intel, respectively. Now enter quit, launch gdb -q ./decision again in the shell, and repeat those checks. The settings should survive without retyping any set commands. Leave this second session open for the breakpoint below.

Keep program-specific commands such as break classify and run low out of this shared file: the next executable may have different functions and arguments. If you edit the file while GDB is open, source ~/.gdbinit at the GDB prompt applies it to that session; future launches read it automatically.

GDB’s initialization-file reference gives the search order. On Ubuntu, an existing $XDG_CONFIG_HOME/gdb/gdbinit or ~/.config/gdb/gdbinit takes precedence over ~/.gdbinit; update the first existing file in that order if you already use one, and use its path with source. A fresh course account can use ~/.gdbinit as shown here.

We want to pause when execution reaches classify. A breakpoint records that stopping location. Loading an executable into GDB doesn’t run it; run starts the process.

GDB input:

break classify
run

GDB should report a breakpoint in classify, with value=12, and show the comparison line. That line is the next source statement at this stop, not evidence that the comparison has already completed. A source breakpoint can also be placed after a function’s entry instructions, so don’t infer the original argument register from the line display.

Before inspecting it, predict the argument from main. Then enter:

print value
list
continue

print evaluates the expression in the stopped program’s current context. Expect a result such as $1 = 12; $1 is GDB’s value-history label, not your program’s variable. list shows nearby source. continue resumes execution, producing 14 and a normal exit. The source file still exists, but that process’s local variables no longer do.

The GDB breakpoint reference is useful when you need a stopping location other than a named function. For now, keep this one.

Predict the argument and result for low. In the same GDB session:

run low
print value
continue

The breakpoint remains installed, but run low creates a new execution with low as its argument. You should observe 10 at the stop and 8 at the end. In particular, equality with 10 takes the lower branch: the comparison is strictly greater than, not greater than or equal to.

  • Saved settings did not load: check the filename and initialization-file search order above. The -nx option skips all initialization files, so omit it from normal launches. If a configuration error prevents normal startup, use gdb -q -nx ./decision to bypass it, then quit, fix the reported line in the file, and relaunch normally. Repeat the three show checks before continuing.
  • gdb: command not found: return to the installation step in the guest. Preserve any package error before retrying.
  • No executable or source found: leave GDB with quit, check pwd and ls in the shell, and rebuild the file in this lesson’s directory.
  • No variable information: check that you loaded the newly built executable with -g, not an older copy. Rebuild and start a fresh session.
  • gcc is an undefined GDB command: you’re at the wrong prompt. quit returns to the shell. If GDB asks to end its running example, answer y.
  • The program is still paused: continue resumes it. run starts it again and asks before replacing an active run. These operations are not interchangeable.

Knowledge check

Question 1 of 1

GDB stops on the comparison line in classify. What does that establish?