GDB and LLDB are debuggers.
A debugger lets you run a program under inspection. You can stop the program, move through it line by line, inspect variables, and see the call stack.
For Zig beginners, debuggers are useful when std.debug.print is no longer enough.
Build with Debug Information
First, compile normally:
zig build-exe main.zigDebug information is included in normal debug builds.
Then run the program with a debugger.
With LLDB:
lldb ./mainWith GDB:
gdb ./mainOn macOS, LLDB is usually the better default. On Linux, both GDB and LLDB are common.
A Small Program to Debug
Save this as main.zig:
const std = @import("std");
fn add(a: i32, b: i32) i32 {
const result = a + b;
return result;
}
pub fn main() void {
const x = add(10, 20);
std.debug.print("x = {}\n", .{x});
}Build it:
zig build-exe main.zigStart LLDB:
lldb ./mainInside LLDB, run:
breakpoint set --name main
runThe program stops at main.
Common LLDB Commands
| Command | Meaning |
|---|---|
run | start the program |
breakpoint set --name main | stop when main starts |
breakpoint set --file main.zig --line 8 | stop at a specific line |
next | run the next line without entering function calls |
step | enter the function call on this line |
finish | finish the current function and return to the caller |
frame variable | show local variables |
bt | show the call stack |
continue | continue running |
quit | exit the debugger |
Example session:
(lldb) breakpoint set --name main
(lldb) run
(lldb) next
(lldb) frame variable
(lldb) continueCommon GDB Commands
| Command | Meaning |
|---|---|
run | start the program |
break main | stop when main starts |
break main.zig:8 | stop at a specific line |
next | run the next line without entering function calls |
step | enter the function call on this line |
finish | finish the current function and return to the caller |
info locals | show local variables |
backtrace | show the call stack |
continue | continue running |
quit | exit the debugger |
Example session:
(gdb) break main
(gdb) run
(gdb) next
(gdb) info locals
(gdb) continueBreakpoints
A breakpoint tells the debugger:
Stop here.
For example, in LLDB:
breakpoint set --file main.zig --line 5In GDB:
break main.zig:5When the program reaches that line, it pauses.
This lets you inspect the program before it continues.
Stepping Through Code
After the program stops, you can move through it one line at a time.
next runs the next line but does not enter function calls.
step enters a function call.
Example:
const x = add(10, 20);If you use next, the debugger runs the whole call to add.
If you use step, the debugger enters add so you can inspect it.
Use next when the function is not interesting.
Use step when the function may contain the bug.
Inspecting Variables
Inside a stopped program, inspect local variables.
In LLDB:
frame variableIn GDB:
info localsFor a specific variable:
(lldb) frame variable xor:
(gdb) print xThis is useful when a value is wrong but you do not know where it first became wrong.
Reading the Call Stack
The call stack shows which functions are active.
In LLDB:
btIn GDB:
backtraceExample:
main
parseConfig
parseNumberRead this as:
main called parseConfig.
parseConfig called parseNumber.
The program is currently inside parseNumber.
The call stack helps you find the route to the bug.
Debugging a Failing Test
You can also debug tests.
Suppose this file is main.zig:
const std = @import("std");
fn divide(a: i32, b: i32) i32 {
return @divTrunc(a, b);
}
test "divide works" {
try std.testing.expectEqual(@as(i32, 5), divide(10, 0));
}Build and run the test normally first:
zig test main.zigIt fails because division by zero is invalid.
To debug test code, you can build the test executable:
zig test main.zig -femit-bin=test-mainThen run it under LLDB:
lldb ./test-mainor GDB:
gdb ./test-mainNow you can set breakpoints and inspect variables like a normal executable.
Debuggers and Optimized Builds
Debug optimized code only when you have to.
In optimized builds, the compiler may inline functions, remove variables, reorder instructions, or combine operations. That can make debugging confusing.
For normal debugging, use a debug build:
zig build-exe main.zigAvoid starting with:
zig build-exe main.zig -O ReleaseFastFirst reproduce the bug in debug mode. Then use optimized builds later for performance testing.
Debugging Memory Problems
For memory problems, start with tests and the testing allocator.
Example:
const std = @import("std");
test "allocate buffer" {
const allocator = std.testing.allocator;
const buf = try allocator.alloc(u8, 100);
defer allocator.free(buf);
buf[0] = 42;
try std.testing.expectEqual(@as(u8, 42), buf[0]);
}If the program crashes, use the debugger to inspect:
the pointer
the length
the index
the caller
Most beginner memory bugs are caused by one of these:
using the wrong length
using an invalid index
freeing memory too early
forgetting who owns the memory
returning a pointer to temporary data
Debugger or Print?
Use std.debug.print when the question is simple:
std.debug.print("len = {}\n", .{items.len});Use a debugger when the question needs repeated inspection:
Where did this value first change?
Which branch did the program take?
What is the full call stack?
What are several variables at this point?
Print debugging is faster to start. A debugger gives you deeper control.
A Practical Debugging Loop
A good workflow is:
- Reproduce the bug.
- Run the smallest failing example.
- Set a breakpoint near the failure.
- Inspect variables.
- Step backward mentally through the call stack.
- Fix the wrong assumption.
- Add a test for the bug.
The last step matters. After you fix a bug, keep a test that would have caught it.
The Main Idea
GDB and LLDB let you pause a Zig program and inspect it while it runs.
You do not need them for every bug. Many bugs can be solved with tests, stack traces, and debug prints. But when a value changes in a surprising way, or when a crash happens deep inside several function calls, a debugger gives you a controlled way to look inside the program.