Skip to content

Using GDB and LLDB

GDB and LLDB are debuggers.

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.zig

Debug information is included in normal debug builds.

Then run the program with a debugger.

With LLDB:

lldb ./main

With GDB:

gdb ./main

On 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.zig

Start LLDB:

lldb ./main

Inside LLDB, run:

breakpoint set --name main
run

The program stops at main.

Common LLDB Commands

CommandMeaning
runstart the program
breakpoint set --name mainstop when main starts
breakpoint set --file main.zig --line 8stop at a specific line
nextrun the next line without entering function calls
stepenter the function call on this line
finishfinish the current function and return to the caller
frame variableshow local variables
btshow the call stack
continuecontinue running
quitexit the debugger

Example session:

(lldb) breakpoint set --name main
(lldb) run
(lldb) next
(lldb) frame variable
(lldb) continue

Common GDB Commands

CommandMeaning
runstart the program
break mainstop when main starts
break main.zig:8stop at a specific line
nextrun the next line without entering function calls
stepenter the function call on this line
finishfinish the current function and return to the caller
info localsshow local variables
backtraceshow the call stack
continuecontinue running
quitexit the debugger

Example session:

(gdb) break main
(gdb) run
(gdb) next
(gdb) info locals
(gdb) continue

Breakpoints

A breakpoint tells the debugger:

Stop here.

For example, in LLDB:

breakpoint set --file main.zig --line 5

In GDB:

break main.zig:5

When 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 variable

In GDB:

info locals

For a specific variable:

(lldb) frame variable x

or:

(gdb) print x

This 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:

bt

In GDB:

backtrace

Example:

main
parseConfig
parseNumber

Read 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.zig

It fails because division by zero is invalid.

To debug test code, you can build the test executable:

zig test main.zig -femit-bin=test-main

Then run it under LLDB:

lldb ./test-main

or GDB:

gdb ./test-main

Now 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.zig

Avoid starting with:

zig build-exe main.zig -O ReleaseFast

First 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:

  1. Reproduce the bug.
  2. Run the smallest failing example.
  3. Set a breakpoint near the failure.
  4. Inspect variables.
  5. Step backward mentally through the call stack.
  6. Fix the wrong assumption.
  7. 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.