The Debugger

The previous section examined the transformations that take C source through preprocessing, compilation, assembly, and linking to produce an executable file. That process explains how the executable is constructed, but reveals little about what occurs once the machine begins executing the instructions it contains.

A debugger makes that execution observable. It enables a programmer to stop a running program at selected locations, inspect its state, and control how execution proceeds.

This primer uses LLDB, the debugger in the LLVM project. Comprehensive LLDB instruction is beyond the scope of this page; the objective here is to introduce the small collection of operations needed to inspect the programs and system behavior examined throughout the remaining pages.

For additional information about LLDB and its command language, see the LLDB Tutorial.

Preparing a Program for Debugging

A debugger provides limited insight into an ordinary executable. However, compiling with debug information1 provides substantially more contextual detail about the program. This debug information consists of metadata that relates executing machine code to source-level constructs, allowing the debugger to present meaningful information about the program’s state to the programmer.

Native compilation, such as compiling C to machine code, creates a particularly visible separation between source code and the instructions that execute, making explicit debug metadata necessary for reconstructing much of the source-level context.2

To demonstrate, create a file named debug.c containing:

#include <stdio.h>

int add(int a, int b)
{
  int result = a + b;
  return result;
}

int main(void)
{
  int x = 2;
  int y = 3;
  int sum = add(x, y);

  printf("%d\n", sum);

  return 0;
}

First, compile the program normally:

clang debug.c -o nodebug

Next, compile the same source with debugging information:

clang -g debug.c -o debug

Compare the sizes of the resulting executables:

ls -lh nodebug debug

The exact sizes vary by system, but debug will normally be larger 3 because it contains additional debugging metadata. The -g option instructs Clang to include that metadata in the generated executable. The following sections use LLDB to inspect debug; before the end of this page, the same program is examined without debugging information to make the practical effect directly observable.

Debug and Release Builds: Development builds commonly include debugging information so tools such as LLDB can relate machine code to source-level constructs. Production builds often omit or strip that information to reduce binary size and avoid distributing unnecessary implementation metadata. Build systems frequently make other changes between development and production configurations as well, but those differences are beyond the scope of this page.

Starting and Leaving LLDB

The first step in debugging is to invoke the debugger and identify the executable to inspect. LLDB does not automatically execute the supplied file. Instead, it loads information about the executable and establishes it as the debugging target. The general form is:

lldb <executable>

For this example, run:

lldb ./debug

In this case, ./debug identifies the executable created in the previous section. LLDB loads the executable and produces output similar to:

(lldb) target create "./debug"
Current executable set to '/home/-------/src/c-course/debugger/debug' (x86_64).
(lldb)

The exact path and target information vary by system. The final (lldb) is the interactive command prompt, indicating that LLDB is ready to accept debugger commands.

At this point, LLDB has loaded the executable, but no process has been created and no instructions have executed. An executable is a file; a process is an executing instance of a program. This distinction becomes clearer in later pages.

To leave LLDB, use:

quit

LLDB terminates the debugging session and returns control to the shell. If a process is still running or suspended, LLDB may request confirmation before exiting.4

Breakpoints

Allowing a program to execute without interruption provides little opportunity for inspection. A breakpoint instructs the debugger to stop execution at a specified location. The general form for setting a breakpoint by function name is:

breakpoint set --name <function>

For this example, restart LLDB with the executable compiled with debug information, then set a breakpoint at add using the following command:5

breakpoint set --name add

LLDB produces output similar to:

Breakpoint 1: where = debug`add + 10 at debug.c:3:16, address = 0x000000000000113a

The exact address and source location vary by system. LLDB identifies the location associated with add and creates a breakpoint there. LLDB has not yet created the process that will execute the program. The structure and resources of that process are examined in later pages.

Create the process and begin execution with:

run

LLDB creates the process and begins executing the program. The breakpoint at add suspends the process and returns control to the LLDB command prompt.

Inspecting Program State

Suspending execution gives LLDB an opportunity to inspect the current state of the process. The general form for displaying variables in the current stack frame6 is:

frame variable [<variable>]

Without specifying a variable, LLDB displays all variables visible in the current frame:

frame variable

LLDB produces output similar to:

(int) a = 2
(int) b = 3

LLDB reads the variables associated with the current stack frame and displays their current values. The exact formatting may vary by debugger version and system.

To inspect a single variable, provide its name:

frame variable a

Observe the result and compare it with the variable values displayed above.

LLDB can also evaluate expressions using the current program state. The general form is:

expression <expression>

To demonstrate, evaluate the sum of a and b:

expression a + b

Observe the result and verify that it reflects the current values of a and b.7

The salient idea is that the debugger exposes state belonging to the executing process and allows the programmer to interrogate that state directly.

Try It Without Debug Information: Exit LLDB and start a new session using nodebug. Try setting a breakpoint at add and running the program. LLDB may still recognize the function name, but compare the source locations, variable information, and other source-level context with the debug build. Without the debugging metadata added by -g, LLDB has substantially less information connecting the executing machine code to the original C source.

Inspecting Addresses

Variables occupy storage during program execution, and their stored values reside at addresses within the process’s address space.8 LLDB reveals those addresses by evaluating ordinary C expressions. In C, the unary & operator yields the address of its operand, so &a and &b are valid C expressions that refer to the addresses of those variables.

To demonstrate, evaluate both expressions:

expression &a
expression &b

The resulting hexadecimal values are virtual addresses within the process. The meaning of “virtual” is deliberately deferred. For now, notice only that familiar C variables correspond to specific locations within the process’s address space; those addresses become central to topics covered in later pages.

Controlling Execution

Suspending a process allows the programmer to inspect its state, but LLDB also provides precise control over how execution resumes.

The next command advances execution one source line at a time. When the current line contains a function call, LLDB executes that function without entering its source. After the function returns, LLDB stops at the next source line in the current function.

next

The step command behaves differently when the current line calls another function. Instead of completing the call and stopping afterward, LLDB enters the called function and stops at its first executable source line:

step

To observe the distinction, restart LLDB with the executable compiled with debug information, then set a breakpoint at main and run the program:

breakpoint set --name main
run

Advance execution until LLDB reaches:

int sum = add(x, y);

At this point, try both commands. First use next and observe that LLDB executes the call to add and stops at the following source line in main. Restart the program and return to the same breakpoint, then use step. This time, LLDB enters add and stops within that function.

To resume execution without stopping at each source line, use:

continue

LLDB resumes the process until another event stops execution, such as reaching a breakpoint or terminating.9

The salient idea is that next steps over a function call, step steps into it, and continue resumes normal execution until another stopping condition is reached.

Stack Frames

The preceding examples inspected variables in the current stack frame. LLDB can also display the other frames associated with the active sequence of function calls.

For this example, restart LLDB with the debug build, set a breakpoint at add, and run the program. Once LLDB stops at the breakpoint, display the current backtrace:

thread backtrace

The command above displays the active stack frames. Frame 0 is the frame in which execution is currently stopped. In this example, it should correspond to add. Frame 1 represents the function that called add, which should be main. Subsequent frame numbers represent progressively earlier calls in the active call sequence.10

The backtrace may also contain frames associated with the C runtime or other startup code. These functions participate in program startup and termination outside the code written in this example. LLDB may display less information for these frames when corresponding debug information is unavailable.

Use the following command to display information about the currently selected frame:

frame info

LLDB selects frame 0 by default. To inspect the caller, select frame 1 using the command below.

frame select 1

Returning to a command learned earlier, inspect the variables associated with the selected frame:

frame variable

Compare these variables with those displayed for frame 0. Although the program remains stopped at the breakpoint in add, selecting another frame allows LLDB to inspect the state associated with an earlier function call.

Any frame present in the backtrace can be selected by its frame number using frame select <number>. This includes frames associated with the C runtime or other startup code. These frames may provide less source-level context when corresponding debug information is unavailable, but LLDB can still select and inspect them. Experiment with selecting different frames and inspecting their variables.

The precise mechanics underlying stack frames are deliberately deferred. For now, it is sufficient to observe that active function calls have distinct execution contexts, that those contexts form an ordered sequence, and that LLDB can inspect each of them individually.

Examining Memory

LLDB can also examine memory directly. While stopped in a frame where a is visible, read the memory beginning at the address of a (recall the address-of operator introduced earlier) using the command below:

memory read &a

The & operator obtains the address of a, and memory read displays the contents of memory beginning at that location. By default, LLDB displays 32 bytes of memory starting at the specified address.11 The resulting output represents bytes from the process’s address space and is typically displayed in hexadecimal form.

Compare this output with the value reported by:

frame variable a

The bytes stored in memory do not, by themselves, specify what they mean. Meaning arises from how the program interprets those bytes. A particular bit pattern might represent an integer, part of a floating-point value, a character, or something else depending on the type information and instructions acting upon it. LLDB can therefore present the same underlying memory either as a C object or as its raw byte representation.

The relationship among C objects, addresses, memory, and the operating system is substantially more complicated than this brief example suggests. For now, the important observation is that LLDB provides a means of examining these relationships directly rather than reasoning about them only in the abstract.

LLDB’s GUI

LLDB also provides a terminal-based graphical interface that presents several views of the current debugging state simultaneously. The interface does not replace the commands introduced above; it provides another way to interact with the same debugger state.

Launch the interface from the LLDB command prompt with:

gui

Detailed use of the interface is beyond the scope of this page. For a practical walkthrough, see The LLDB TUI (text user interface).

The Debugger as an Instrument

Programmers often use debuggers to find defects in software. That is certainly one of their purposes, but it is not the reason LLDB appears here.

Throughout this primer, LLDB serves primarily as an instrument of observation.

Source code describes a program at one level of abstraction. LLDB allows the programmer to descend beneath that abstraction and examine the state of the program while it executes.

The objective is not to memorize debugger commands. It is to acquire enough facility with LLDB to ask questions of a running program, observe its state, and evaluate the evidence directly.

Where Do We Go From Here?

The compiler transformed source code into an executable. LLDB provided a means of observing that executable during execution.

Several questions remain unanswered. What does the operating system create when the executable begins running? What distinguishes an executable file from the running entity that LLDB controls?

The next section begins answering those questions by distinguishing a program from a process and examining what happens when a program begins execution.

Next: Programs and Processes

Exercises

Use the debug.c program developed throughout this page for the following exercises. Each exercise states an objective; determine which LLDB commands are necessary to accomplish it.

  1. Determine the memory addresses of the a and b variables while execution is stopped inside add.

  2. Determine the value of result immediately before and immediately after the assignment in add executes.

  3. Stop execution in main and examine the backtrace. Determine what called main on your system and identify any runtime or library frames that appear before it.

    Answer The exact call stack varies by platform and runtime implementation. On a typical Linux system using glibc, frames associated with C runtime startup code may appear above main, such as \_\_libc_start_main or related startup functions. The important observation is that main does not represent the beginning of process execution; runtime startup code executes before control reaches it.
  4. Stop execution in main before the call to add. Determine the difference between stepping over the call and stepping into it.

  5. Stop execution inside add, determine the address of a, and use LLDB to read the memory beginning at that address. Verify that the memory read starts at the same address reported for a.

  6. Exit LLDB and repeat the inspection of add using the nodebug executable created earlier. Determine what information LLDB can still provide and what source-level information is no longer available.

  1. On Unix-like systems, compilers commonly encode debug information using DWARF, a standardized debugging-data format. DWARF can describe source locations, variables, types, functions, stack information, and other relationships between source code and generated machine code. The debugger reads this metadata from the executable or associated debug files when reconstructing source-level context. Detailed examination of DWARF is beyond the scope of this primer. 

  2. Interpreted and managed languages also require mechanisms for relating runtime state to source-level constructs, but the relationship is often maintained by the interpreter or runtime rather than reconstructed from native machine code and separate debugging metadata. 

  3. Some toolchains can store debugging information separately from the main executable, so the executable itself is not necessarily larger in every configuration. The normal clang -g invocation used here stores the debugging information with the generated binary. 

  4. LLDB also accepts the abbreviated form q

  5. LLDB also accepts the abbreviated form b add

  6. A stack frame represents the execution context associated with a particular function call, including information needed to manage that call and access its local state. Stack frames and their relationship to the call stack are examined in greater detail in later pages. 

  7. LLDB also accepts the abbreviated form p a + b

  8. The addresses displayed here are virtual addresses rather than direct physical-memory locations. Processes, virtual address spaces, memory mappings, and address translation are examined in detail in later pages. 

  9. LLDB accepts abbreviated forms for several common execution-control commands: n for next, s for step, and c for continue

  10. LLDB provides bt as shorthand for thread backtrace

  11. The amount of memory displayed by memory read can be changed explicitly using options such as --count and --size. The default 32-byte read is an LLDB behavior rather than a property of the target architecture.