5  Debugging Zig applications

Being able to debug your applications is essential for any programmer who wants to do serious programming in any language. That is why, in this chapter, we are going to talk about the available strategies and tools to debug applications written in Zig.

5.2 Debugging through debuggers

Although print debugging being a valid and very useful strategy, most programmers prefer to use a debugger to debug their programs. Since Zig is a low-level language, you can use either GDB (GNU Debugger), or LLDB (LLVM Project Debugger) as your debugger.

Both debuggers can work with Zig code, and it’s a matter of taste here. You choose the debugger of your preference, and you work with it. In this book, I will use LLDB as my debugger on the examples.

5.2.1 Compile your source code in debug mode

In order to debug your program through a debugger, you must compile your source code in Debug mode. Because when you compile your source code in other modes (such as Release), the compiler usually strips out some essential information that is used by the debugger to read and track your program, like PDB (Program Database) files.

By compiling your source code in Debug mode, you ensure that the debugger will find the necessary information in your program to debug it. By default, the compiler uses the Debug mode when compiling your code. Having this in mind, when you compile your program with the build-exe command (which was described at Section 1.2.4), if you don’t specify an explicit mode through the -O command-line 2 argument, then, the compiler will compile your code in Debug mode.

5.2.2 Let’s debug a program

As an example, let’s use LLDB to navigate and investigate the following piece of Zig code:

const std = @import("std");
const stdout = std.io.getStdOut().writer();

fn add_and_increment(a: u8, b: u8) u8 {
    const sum = a + b;
    const incremented = sum + 1;
    return incremented;
}

pub fn main() !void {
    var n = add_and_increment(2, 3);
    n = add_and_increment(n, n);
    _ = try stdout.print("Result: {d}!\n", .{n});
}
Result: 13!

There is nothing wrong with this program. But it is a good start for us. First, we need to compile this program with the zig build-exe command. For this example, suppose that I have compiled the above Zig code into a binary executable called add_program.

zig build-exe add_program.zig

Now, we can start LLDB with add_program, like this:

lldb add_program

From now on, LLDB is started, and you can know that I’m executing LLDB commands by looking at the prefix (lldb). If something is prefixed with (lldb), then you know that it is a LLDB command.

The first thing I will do, is to set a breakpoint at the main() function, by executing b main. After that, I just start the execution of the program with run. You can see in the output below, that the execution stopped at the first line in the function main(), as we expected.

(lldb) b main
Breakpoint 1: where = debugging`debug1.main + 22
    at debug1.zig:11:30, address = 0x00000000010341a6
(lldb) run
Process 8654 launched: 'add_program' (x86_64)
Process 8654 stopped
* thread #1, name = 'add_program',
    stop reason = breakpoint 1.1 frame #0: 0x10341a6
    add_program`debug1.main at add_program.zig:11:30
   8    }
   9    
   10   pub fn main() !void {
-> 11       var n = add_and_increment(2, 3);
   12       n = add_and_increment(n, n);
   13       try stdout.print("Result: {d}!\n", .{n});
   14   }

I can start navigating through the code, and checking the objects that are being generated. If you are not familiar with the commands available in LLDB, I recommend you to read the official documentation of the project3. You can also look for cheat sheets, which quickly describes all commands available for you4.

Currently, we are in the first line at the main() function. In this line, we create the n object, by executing the add_and_increment() function. To execute the current line of code, and go to the next line, we can run the n LLDB command. Let’s execute this command.

After we executed this line, we can also look at the value stored inside this n object by using the p LLDB command. The syntax for this command is p <name-of-object>.

If we take a look at the value stored in the n object (p n), notice that it stores the hexadecimal value 0x06, which is the number 6 in decimal. We can also see that, this value have a type unsigned char, which is an unsigned 8-bit integer. We have talked already about this at Section 1.8, that u8 integers in Zig are equivalent to the C data type unsigned char.

(lldb) n
Process 4798 stopped
* thread #1, name = 'debugging',
    stop reason = step over frame #0: 0x10341ae
    debugging`debug1.main at debug1.zig:12:26
   9    
   10   pub fn main() !void {
   11       var n = add_and_increment(2, 3);
-> 12       n = add_and_increment(n, n);
   13       try stdout.print("Result: {d}!\n", .{n});
   14   }
(lldb) p n
(unsigned char) $1 = '\x06'

Now, on the next line of code, we are executing the add_and_increment() function once again. Why not step inside this function? Shall we? We can do that, by executing the s LLDB command. Notice in the example below that, after executing this command, we have entered into the context of the add_and_increment() function.

Also notice in the example below that, I have walked two more lines in the function’s body, then, I execute the frame variable LLDB command, to see at once, the value stored in each of the variables that were created inside the current scope.

You can see in the output below that, the object sum stores the value \f, which represents the form feed character. This character in the ASCII table, corresponds to the hexadecimal value 0x0C, or, in decimal, the number 12. So, this means that the result of the expression a + b executed at line 5, resulted in the number 12.

(lldb) s
Process 4798 stopped
* thread #1, name = 'debugging',
    stop reason = step in frame #0: 0x10342de
    debugging`debug1.add_and_increment(a='\x02', b='\x03')
    at debug1.zig:4:39
-> 4    fn add_and_increment(a: u8, b: u8) u8 {
   5        const sum = a + b;
   6        const incremented = sum + 1;
   7        return incremented;
(lldb) n
(lldb) n
(lldb) frame variable
(unsigned char) a = '\x06'
(unsigned char) b = '\x06'
(unsigned char) sum = '\f'
(unsigned char) incremented = '\x06'

5.3 How to investigate the data type of your objects

Since Zig is a strongly-typed language, the data types associated with your objects are very important for your program. So, debugging the data types associated with your objects might be important to understand bugs and errors in your program.

When you walk through your program with a debugger, you can inspect the types of your objects by simply printing them to the console, with the LLDB p command. But you also have alternatives embedded in the language itself to access the data types of your objects.

In Zig, you can retrieve the data type of an object, by using the built-in function @TypeOf(). Just apply this function over the object, and you get access to the data type of the object.

const std = @import("std");
const stdout = std.io.getStdOut().writer();
const expect = std.testing.expect;

pub fn main() !void {
    const number: i32 = 5;
    try expect(@TypeOf(number) == i32);
    try stdout.print("{any}\n", .{@TypeOf(number)});
}
i32

This function is similar to the type() built-in function from Python, or, the typeof operator in Javascript.


  1. https://ziglang.org/documentation/master/std/#std.fs.File.↩︎

  2. See https://ziglang.org/documentation/master/#Debug.↩︎

  3. https://lldb.llvm.org/↩︎

  4. https://gist.github.com/ryanchang/a2f738f0c3cc6fbd71fa.↩︎