How to use Compiler Explorer with Zig

TLDR

What is Compiler Explorer?

To quote from Compiler Explorers Readme.md: “Compiler Explorer is an interactive compiler exploration website. Edit code in […] 30+ supported languages […], and see how that code looks after being compiled in real time.”

Start with Zig on Compiler Explorer

For a quick start, head over to https://zig.godbolt.org. By default you will see two windows. On the left the default Zig example code

// Type your code here, or load an example.
export fn square(num: i32) i32 {
    return num * num;
}

and on the right the resulting assembly (shortened for brevity):

square:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     dword ptr [rbp - 4], edi
        imul    edi, edi
        mov     dword ptr [rbp - 8], edi
        seto    al
        jo      .LBB0_1
        jmp     .LBB0_2
.LBB0_1:
        movabs  rdi, offset __anon_1381
        mov     esi, 16
        xor     eax, eax
        mov     edx, eax
        movabs  rcx, offset .L__unnamed_1
        call    example.panic
.LBB0_2:
        mov     eax, dword ptr [rbp - 8]
        add     rsp, 16
        pop     rbp
        ret

example.panic:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 32
[...]

In case the default example changed since writing this, try this short URL.

Note the function name square appearing as the label square: right at the beginning of its assembly code.

Looking at the source you might wonder why the function is introduced with the export keyword. Try to remove it! You will notice the assembly window becomes empty. Why is that? Our example code does not contain any use of the function square, so it is omitted in the assembly.

Ways to make the compiler emit assembly

The export keyword states the intent to make the function part of a C Library. The compiler will then generate assembly for it, even if it can not see any use of the function.

There is a drwaback to use the export keyword: The function is required to have a signature compatible with C. So no slices, optionals, error unions and other Zig features may appear in exported signatures. In easy cases it is possible to use small adapter functions with C compatible signatures which in turn call the function of interest. Slice parameters can be adapted to a C Pointer and a length, like in this example:

export fn sumArrayC(array: [*]const u32, len: usize) u32 {
    return sumArrayZig(array[0..len]);
}

pub fn sumArrayZig(slice: []const u32) u32 {
    var sum: u32 = 0;
    for (slice) |item| {
        sum += item;
    }
    return sum;
}

If this approach becomes impractical, the next thing to do is to include a main function. This way we can analyze whole Zig programs and not only single functions. Doing this for the previous example shows why we might want to stick to the export approach as long as that is feasible.

pub fn main() void {
    const sum = sumArrayZig(&.{1, 2, 3, 4, 5});
    _ = sum;
}

pub fn sumArrayZig(slice: []const u32) u32 {
    var sum: u32 = 0;
    for (slice) |item| {
        sum += item;
    }
    return sum;
}

The assembly window now contains well over 100.000 lines, including loads of infrastructure code! Luckily the usual search key combo <Ctrl> + f comes to the rescue. Enter the name of the function of interest, here sumArrayZig, and hit the Enter key a few times:

[...]
767        mov     esi, 5
768        call    example.sumArrayZig
769        mov     dword ptr [rbp - 4], eax
[...]
2470  
2471  example.sumArrayZig:
2472          push    rbp
2473          mov     rbp, rsp
[...]

On line 768 we find the call of sumArrayZig in main and further down on line 2471 the function we are looking for begins.

Choose different build modes

By default the Zig toolchain uses the build mode Debug. If we want to see what assembly gets generated for other build modes, we have to explicitly tell the compiler on the command line. On Compiler Explorer we do this by entering the build mode into the text input Compiler options....

Let us return to our initial square example with the default build mode set explicitly: -O Debug. What does our square function do again?

export fn square(num: i32) i32 {
    return num * num;
}

For one, the obvious thing: one multiplication. But remember, Zig has safety checks for arithmetic overflow. Can we find both things in the assembly?

 1 square:
 2         push    rbp
 3         mov     rbp, rsp
 4         sub     rsp, 16
 5         mov     dword ptr [rbp - 4], edi
 6         imul    edi, edi
 7         mov     dword ptr [rbp - 8], edi
 8         seto    al
 9         jo      .LBB0_1
10         jmp     .LBB0_2
11 .LBB0_1:
12         movabs  rdi, offset __anon_1381
13         mov     esi, 16
14         xor     eax, eax
15         mov     edx, eax
16         movabs  rcx, offset .L__unnamed_1
17         call    example.panic
18 .LBB0_2:
19         mov     eax, dword ptr [rbp - 8]
20         add     rsp, 16
21         pop     rbp
22         ret
23 
24 example.panic:
25         push    rbp
26         mov     rbp, rsp
27         sub     rsp, 32
28         mov     qword ptr [rbp - 16], rsi
29         mov     qword ptr [rbp - 24], rdi
30         mov     qword ptr [rbp - 8], rdx
31         call    zig_panic@PLT
[...]
38 
39 __anon_1381:
40         .asciz  "integer overflow"
[...]

The multiplication (imul) is located on line 6. Then on line 9 we find a conditional jump on overflow (jo) to .LBB0_1 which starts just below on line 12 loading something called __anon_1381 which we can find on line 39 marking the text “integer overflow”. Continuing after line 12 leads to calls to example.panic (line 17) and ultimately zig_panic@PLT (line 31).

Also note, that the whole assembly code is sprinkled with stack operations (use of registers rbp and rsp) which is common in Debug modes.

How does this change if we change the build mode to -O ReleaseSafe enabling optimizations while keeping safety checks?

square:
        imul    edi, edi
        jo      .LBB0_2
        mov     eax, edi
        ret
.LBB0_2:
        push    rax
        call    zig_panic@PLT

Not much is left. But the multiplication (imul) is there, followed by the overflow check (jo) which now quite directly leads to the call of zig_panic@PLT. All those stack operations are gone, as well as the descriptive message why the call to zig_panic@PLT was made.

Finally, let us look what remains if we even disable safety checks by using the build mode -O ReleaseFast:

square:
        mov     eax, edi
        imul    eax, edi
        ret

The multiplication is still there. As expected, overflows are no longer checked for and consequently no calls to zig_panic@PLT are present anymore.

:warning: The Zig toolchain uses two different conventions to specify build modes depending on whether you use the buildsystem (zig build -Doptimize=<MODE>) or other subcommands like zig build-exe -O <MODE>. On Compiler Explorer we need to use the latter. Be aware though, that the use of -Doptimize=<MODE> on Compiler Explorer will seemingly work, but have no influence on the build mode. This will instead define a C macro named optimize and set its value to the mode.

Other platforms

Compiler Explorer runs Zig in x86_64 Linux containers. The Zig toolchain therefore autodetects the target as x86_64-linux-gnu. If you are developing for a different platform use the -target compiler option. See the square example with explicit -target x86_64-linux-gnu.

If you want to know what Zig autodetects for your system, an easy way is to run zig env. Look for the key target in the resulting JSON output. The computer I am writing this on gets detected as "target": "x86_64-linux.6.5...6.5-gnu.2.35",. If you do not need it that specific, consider removing the version numbers and version ranges.

Ask questions about generated Assembly

You played around with code on Compiler Explorer and now want to ask questions or discuss your findings?
Then consider to include a short link to the code and compiler settings you used. This gives readers all details in case they need them. According to Compiler Explorer’s FAQ short links are designed to stay valid forever.

To emphasize points you want to make, repeat parts of the source code and generated assembly in your post. Aim to write posts that are understandable without following links.

7 Likes

Let’s move this to a Doc instead of an explain topic - great work @Eisenhauer

1 Like