Debugging Zig Programs
Comprehensive guide to debugging Zig programs using various tools and techniques.
Debug Symbols
Strip Control
-fstrip # Remove debug symbols
-fno-strip # Keep debug symbols
Debug symbols are kept by default in Debug mode and removed in release modes.
Debug Build
Release with Symbols
Release Stripped
zig build-exe main.zig
# Debug symbols included by default
zig build-exe -O ReleaseSafe -fno-strip main.zig
zig build-exe -O ReleaseFast -fstrip main.zig
-gdwarf32 # Use 32-bit DWARF format
-gdwarf64 # Use 64-bit DWARF format
DWARF is the standard debugging data format used by GDB and LLDB.
Built-in Debugging Features
Stack Traces
Zig provides automatic stack traces on panics in Debug and ReleaseSafe modes:
Program
Output (Debug mode)
const std = @import ( "std" );
fn foo () void {
bar ();
}
fn bar () void {
baz ();
}
fn baz () void {
@panic ( "Something went wrong!" );
}
pub fn main () void {
foo ();
}
Error Return Traces
-ferror-tracing # Enable error return traces
-fno-error-tracing # Disable error traces
const std = @import ( "std" );
fn readFile () ! [] const u8 {
return error . FileNotFound ;
}
fn processFile () ! void {
_ = try readFile ();
}
pub fn main () ! void {
try processFile ();
}
Reference Traces
-freference-trace[ =N] # Show N lines of reference trace
-fno-reference-trace # Disable reference traces
Shows how compile errors propagate through the code:
zig build-exe -freference-trace=10 main.zig
Print Debugging
Debug Print
const std = @import ( "std" );
pub fn main () void {
const x : i32 = 42 ;
std . debug . print ( "Value of x: {d} \n " , .{ x });
const name = "Alice" ;
std . debug . print ( "Hello, {s}! \n " , .{ name });
const items = [ _ ] i32 { 1 , 2 , 3 , 4 , 5 };
std . debug . print ( "Items: {any} \n " , .{ items });
}
Assert
const std = @import ( "std" );
pub fn divide ( a : i32 , b : i32 ) i32 {
std . debug . assert ( b != 0 );
return @divExact ( a , b );
}
pub fn main () void {
const result = divide ( 10 , 2 );
std . debug . print ( "Result: {d} \n " , .{ result });
}
Assertions are only active in Debug and ReleaseSafe builds. They are compiled out in ReleaseFast and ReleaseSmall.
Using GDB
Basic GDB Workflow
Build
Start GDB
Common Commands
zig build-exe -fno-strip main.zig
GDB Example Session
const std = @import ( "std" );
fn add ( a : i32 , b : i32 ) i32 {
return a + b ;
}
pub fn main () void {
const x : i32 = 10 ;
const y : i32 = 20 ;
const result = add ( x , y );
std . debug . print ( "Result: {d} \n " , .{ result });
}
$ zig build-exe -fno-strip debug_gdb.zig
$ gdb ./debug_gdb
( gdb ) break main
Breakpoint 1 at 0x...: file debug_gdb.zig, line 8.
( gdb ) run
Starting program: ./debug_gdb
Breakpoint 1, main () at debug_gdb.zig:8
8 const x: i32 = 10 ;
( gdb ) next
9 const y: i32 = 20 ;
( gdb ) print x
$1 = 10
( gdb ) next
10 const result = add ( x, y );
( gdb ) step
add (a=10, b= 20 ) at debug_gdb.zig:4
4 return a + b ;
( gdb ) continue
Continuing.
Result: 30
[Inferior 1 ( process ...) exited normally]
Advanced GDB Features
Conditional Breakpoints
Watchpoints
Pretty Printing
(gdb) break main.zig:42 if x > 10
Using LLDB
Basic LLDB Workflow
Build
Start LLDB
Common Commands
zig build-exe -fno-strip main.zig
LLDB Example
$ zig build-exe -fno-strip debug_lldb.zig
$ lldb ./debug_lldb
( lldb ) target create "./debug_lldb"
Current executable set to './debug_lldb'
( lldb ) b main
Breakpoint 1: where = debug_lldb` main + ..., address = 0x...
( lldb ) run
Process ... launched: './debug_lldb'
Process ... stopped
* thread #1, name = 'debug_lldb', stop reason = breakpoint 1.1
frame #0: 0x... debug_lldb`main at debug_lldb.zig:8
( lldb ) frame variable
( i32 ) x = 10
( i32 ) y = 20
( lldb ) continue
Process ... resuming
Result: 30
Process ... exited with status = 0
LLDB vs GDB Commands
Task GDB LLDB Set breakpoint break mainbreakpoint set --name mainRun runprocess launchStep over nextthread step-overStep in stepthread step-inContinue continueprocess continuePrint variable print varframe variable varBacktrace backtracethread backtrace
Compiler Debug Options
These options are for debugging the Zig compiler itself, not your programs.
Verbose Output
--verbose-link # Display linker invocations
--verbose-cc # Display C compiler invocations
--verbose-air # Show Zig AIR (Abstract IR)
--verbose-intern-pool # Show InternPool debug output
--verbose-llvm-ir[ =path] # Show unoptimized LLVM IR
--verbose-llvm-bc = [path ] # Show unoptimized LLVM BC
--verbose-cimport # Show C import debug output
--verbose-llvm-cpu-features # Show LLVM CPU features
Debug Compilation
--debug-compile-errors # Crash with diagnostics on first compile error
--debug-incremental # Enable incremental compilation debug features
--debug-rt # Debug compiler runtime libraries
--debug-log [scope] # Enable debug logging for scope
These flags require Zig to be built with -Denable-debug-extensions.
Memory Debugging
Valgrind Integration
-fvalgrind # Include Valgrind client requests
-fno-valgrind # Omit Valgrind requests
zig build-exe -O ReleaseSafe -fvalgrind memory_debug.zig
valgrind --leak-check=full ./memory_debug
AddressSanitizer (for C code)
-fsanitize-c # Enable UBSan for C code
ThreadSanitizer
-fsanitize-thread # Detect data races
zig build-exe -fsanitize-thread concurrent.zig
Debug Logging
Custom Logging
const std = @import ( "std" );
const log = std . log . scoped (. my_module );
pub fn main () void {
log . debug ( "This is a debug message" , .{});
log . info ( "This is an info message" , .{});
log . warn ( "This is a warning" , .{});
log . err ( "This is an error" , .{});
}
Log Level Control
Set Log Level
Build with Logging
pub const std_options = struct {
pub const log_level = . debug ;
};
Debugging Tests
zig test --test-filter "test name" test.zig
Run Specific Test
Debug Test
zig test --test-filter "add" math.zig
zig test --test-no-exec math.zig
gdb ./zig-cache/.../math
Debugging in Different Modes
Debug Mode
Best for debugging:
Full debug symbols
All safety checks enabled
No optimizations
Clear stack traces
zig build-exe -O Debug main.zig
gdb ./main
ReleaseSafe Mode
Good for finding bugs in optimized code:
Safety checks still enabled
Can add debug symbols with -fno-strip
Optimizations may make debugging harder
zig build-exe -O ReleaseSafe -fno-strip main.zig
gdb ./main
ReleaseFast/Small Modes
Difficult to debug:
Safety checks disabled
Heavy optimizations
Need -fno-strip for symbols
Stack traces may be incomplete
zig build-exe -O ReleaseFast -fno-strip -ferror-tracing main.zig
gdb ./main
Common Debugging Scenarios
Segmentation Fault
Find Location
Common Causes
gdb ./program
( gdb ) run
... Program received signal SIGSEGV ...
( gdb ) backtrace
( gdb ) frame 0
( gdb ) print variable
Memory Leak
valgrind --leak-check=full --show-leak-kinds=all ./program
Infinite Loop
# In GDB/LLDB, press Ctrl+C to interrupt
( gdb ) interrupt
( gdb ) backtrace
var iterations : usize = 0 ;
while ( condition ) {
iterations += 1 ;
std . debug . assert ( iterations < 1000000 );
// loop body
}
Debugging Best Practices
Always start with Debug mode - Use -O Debug during development
Keep debug symbols - Use -fno-strip for release builds you need to debug
Enable error tracing - Use -ferror-tracing in release modes
Use assertions liberally - They catch bugs early in Debug/ReleaseSafe
Test in ReleaseSafe - Catch optimization-exposed bugs before ReleaseFast
Profile before optimizing - Don’t sacrifice debuggability unnecessarily
Use logging - Better than removing debug prints
Write tests - Catch bugs before debugging is needed
VSCode Integration
{
"version" : "0.2.0" ,
"configurations" : [
{
"name" : "Debug Zig" ,
"type" : "lldb" ,
"request" : "launch" ,
"program" : "${workspaceFolder}/zig-out/bin/myprogram" ,
"args" : [],
"cwd" : "${workspaceFolder}" ,
"preLaunchTask" : "zig build"
}
]
}
{
"version" : "2.0.0" ,
"tasks" : [
{
"label" : "zig build" ,
"type" : "shell" ,
"command" : "zig build" ,
"group" : {
"kind" : "build" ,
"isDefault" : true
}
}
]
}
Next Steps
Compilation Modes Understand how different modes affect debugging
Optimization Learn when to use which optimization level