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.
-
Determine the memory addresses of the
aandbvariables while execution is stopped insideadd. -
Determine the value of
resultimmediately before and immediately after the assignment inaddexecutes. -
Stop execution in
mainand examine the backtrace. Determine what calledmainon 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. -
Stop execution in
mainbefore the call toadd. Determine the difference between stepping over the call and stepping into it. -
Stop execution inside
add, determine the address ofa, and use LLDB to read the memory beginning at that address. Verify that the memory read starts at the same address reported fora. -
Exit LLDB and repeat the inspection of
addusing thenodebugexecutable created earlier. Determine what information LLDB can still provide and what source-level information is no longer available.
-
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. ↩
-
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. ↩
-
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 -ginvocation used here stores the debugging information with the generated binary. ↩ -
LLDB also accepts the abbreviated form
q. ↩ -
LLDB also accepts the abbreviated form
b add. ↩ -
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. ↩
-
LLDB also accepts the abbreviated form
p a + b. ↩ -
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. ↩
-
LLDB accepts abbreviated forms for several common execution-control commands:
nfornext,sforstep, andcforcontinue. ↩ -
LLDB provides
btas shorthand forthread backtrace. ↩ -
The amount of memory displayed by
memory readcan be changed explicitly using options such as--countand--size. The default 32-byte read is an LLDB behavior rather than a property of the target architecture. ↩