Mysterious Segmentation Fault (due to lack of libc ... again)

I tripped into a very mysterious segfault on Linux:

 λ ./zig-out/bin/test_sdl
Segmentation fault at address 0x0
???:?:?: 0x0 in ??? (???)
zsh: IOT instruction  ./zig-out/bin/test_sdl

from the following code:

const c = @import("c.zig");

pub fn main() !void {
    {
        const srv = c.sdl.SDL_Init(c.sdl.SDL_INIT_VIDEO);
        assert(srv == 0);
    }


    c.sdl.SDL_Quit();
}

The issue was that I hadn’t linked libc with:
exe.linkSystemLibrary2("c", .{});

Note that I didn’t get any linking errors from Zig whether I had that line or not.

To contrast: compiling a similar program directly in C:
clang main.c -o main -I/usr/include -L/usr/lib/x86_64-linux-gnu/ -lSDL2
gives an executable that works just fine without adding -lc

Is there a bug here or, at the very least, a way to create a better error message than just segfaulting?

I’m not the first to hit this either:

This was kind of annoying to track down as gdb isn’t very helpful:

  λ gdb ./zig-out/bin/test_sdl -c /tmp/core-test_sdl.203520.devel-x1.1729560859
GNU gdb (Ubuntu 15.0.50.20240403-0ubuntu1) 15.0.50.20240403-git
Copyright (C) 2024 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.

For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./zig-out/bin/test_sdl...
[New LWP 203520]
Core was generated by `./zig-out/bin/test_sdl'.
Program terminated with signal SIGABRT, Aborted.
#0  0x0000000001038542 in os.linux.x86_64.syscall4 (number=rt_sigprocmask, arg1=2, arg2=140736924885552, arg3=0, arg4=8)
    at /home/devel/local/zig-linux-x86_64-0.14.0-dev.1410+13da34955/lib/std/os/linux/x86_64.zig:57
57          return asm volatile ("syscall"
(gdb) bt
#0  0x0000000001038542 in os.linux.x86_64.syscall4 (number=rt_sigprocmask, arg1=2, arg2=140736924885552, arg3=0, arg4=8)
    at /home/devel/local/zig-linux-x86_64-0.14.0-dev.1410+13da34955/lib/std/os/linux/x86_64.zig:57
#1  0x0000000001055f2d in os.linux.sigprocmask (flags=2, set=0x7fffde6a2230, oldset=0x0)
    at /home/devel/local/zig-linux-x86_64-0.14.0-dev.1410+13da34955/lib/std/os/linux.zig:1634
#2  0x00000000010389d8 in posix.sigprocmask (flags=2, set=0x7fffde6a2230, oldset=0x0)
    at /home/devel/local/zig-linux-x86_64-0.14.0-dev.1410+13da34955/lib/std/posix.zig:5703
#3  0x0000000001038953 in posix.raise (sig=6 '\006') at /home/devel/local/zig-linux-x86_64-0.14.0-dev.1410+13da34955/lib/std/posix.zig:729
#4  0x00000000010368df in posix.abort () at /home/devel/local/zig-linux-x86_64-0.14.0-dev.1410+13da34955/lib/std/posix.zig:673
#5  0x000000000103619b in debug.handleSegfaultPosix (sig=11, info=0x7fffde6a25f0, ctx_ptr=0x7fffde6a24c0)
    at /home/devel/local/zig-linux-x86_64-0.14.0-dev.1410+13da34955/lib/std/debug.zig:1246
#6  0x00000000010384f0 in ?? () at /home/devel/local/zig-linux-x86_64-0.14.0-dev.1410+13da34955/lib/std/debug.zig:1275
#7  0x0000000000000007 in ?? ()
#8  0x0000000000000000 in ?? ()

I tried

const sdl = @cImport({
    @cInclude("SDL/SDL.h");
});

pub fn main() void {
    _ = sdl.SDL_Init(sdl.SDL_INIT_VIDEO);
    sdl.SDL_Quit();
}

Building without linking anything:

$ zig build-exe sdl.zig 
sdl.zig:2:13: error: C import failed
const sdl = @cImport({
            ^~~~~~~~
sdl.zig:2:13: note: libc headers not available; compilation does not link against libc

Building with -lc only:

$ zig build-exe sdl.zig -lc
error: ld.lld: undefined symbol: SDL_Init
    note: referenced by sdl.zig:7
    note:               sdl.o:(sdl.main)
error: ld.lld: undefined symbol: SDL_Quit
    note: referenced by sdl.zig:8
    note:               sdl.o:(sdl.main)

Building with -lc and with -lSDL - success.
Running program - ok, no segfault.

$ zig version
0.14.0-dev.872+a60810b5a

Interesting. I wonder what the difference is?

$ zig version
0.14.0-dev.1410+13da34955

I’m on Ubuntu 24.04 LTS.

I get the same results as you when using “build-exe”, but not when using a build.zig file.

λ cat build.zig
const std = @import("std");

// Although this function looks imperative, note that its job is to
// declaratively construct a build graph that will be executed by an external
// runner.
pub fn build(bb: *std.Build) void {
    // Standard target options allows the person running `zig build` to choose
    // what target to build for. Here we do not override the defaults, which
    // means any target is allowed, and the default is native. Other options
    // for restricting supported target set are available.
    const target = bb.standardTargetOptions(.{});

    // Standard optimization options allow the person running `zig build` to select
    // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
    // set a preferred release mode, allowing the user to decide how to optimize.
    const optimize = bb.standardOptimizeOption(.{});

    const exe = bb.addExecutable(.{
        .name = "test_sdl",
        .root_source_file = bb.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

    exe.addIncludePath(std.Build.LazyPath{ .cwd_relative = "/usr/include/X11" });
    exe.addIncludePath(std.Build.LazyPath{ .cwd_relative = "/usr/include" });
    exe.addIncludePath(std.Build.LazyPath{ .cwd_relative = "/usr/include/x86_64-linux-gnu" });

    exe.addLibraryPath(std.Build.LazyPath{ .cwd_relative = "/usr/lib/x86_64-linux-gnu/" });
    exe.linkSystemLibrary2("SDL2", .{});
    exe.linkSystemLibrary2("vulkan", .{});
    //    exe.linkLibC();

    // This declares intent for the executable to be installed into the
    // standard location when the user invokes the "install" step (the default
    // step when running `zig build`).
    bb.installArtifact(exe);

    // This *creates* a Run step in the build graph, to be executed when another
    // step is evaluated that depends on it. The next line below will establish
    // such a dependency.
    const run_cmd = bb.addRunArtifact(exe);

    // By making the run step depend on the install step, it will be run from the
    // installation directory rather than directly from within the cache directory.
    // This is not necessary, however, if the application depends on other installed
    // files, this ensures they will be present and in the expected location.
    run_cmd.step.dependOn(bb.getInstallStep());

    // This allows the user to pass arguments to the application in the build
    // command itself, like this: `zig build run -- arg1 arg2 etc`
    if (bb.args) |args| {
        run_cmd.addArgs(args);
    }

    // This creates a build step. It will be visible in the `zig build --help` menu,
    // and can be selected like this: `zig build run`
    // This will evaluate the `run` step rather than the default, which is "install".
    const run_step = bb.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);
}

I’d call it a linker bug. Once we fully move from LLD I’ll make sure this has test coverage. Existing linkers are crap in terms of usefulness of diagnostics.

1 Like

Ok, doing it with build.zig.

program

const std = @import("std");
const sdl = @cImport({
    @cInclude("SDL/SDL.h");
});

pub fn main() void {
    const s = sdl.SDL_Init(sdl.SDL_INIT_VIDEO);
    std.debug.print("{}\n", .{s});
    sdl.SDL_Quit();
}

build.zig

const std = @import("std");
pub fn build(b: *std.Build) void {

    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const exe = b.addExecutable(.{
        .name = "sdl-test",
        .root_source_file = b.path("sdl.zig"),
        .target = target,
        .optimize = optimize,
        .single_threaded = true,
        .strip = true,
        .link_libc = true, // <<<<<<<<<<<<<<<<<<<<
    });
    exe.linkSystemLibrary("SDL");
    b.installArtifact(exe);
}

Everything ok:

$ ./zig-out/bin/sdl-test 
0

without .link_libc = true and exe.linkSystemLibrary("SDL");

$ zig build
install
└─ install sdl-test
   └─ zig build-exe sdl-test Debug native 2 errors
sdl.zig:4:13: error: C import failed
const sdl = @cImport({
            ^~~~~~~~
sdl.zig:4:13: note: libc headers not available; compilation does not link against libc

same as with build-exe.

with .link_libc = true, but without exe.linkSystemLibrary("SDL");:

$ zig build
install
└─ install sdl-test
   └─ zig build-exe sdl-test Debug native 2 errors
error: ld.lld: undefined symbol: SDL_Init
error: ld.lld: undefined symbol: SDL_Quit

Also same errors as with build-exe.

I think it’s not, everything as expected for me.

Aha… the difference is that I used SDL, not SDL2.
I tried SDL2 and got that segfault.
Without .link_libc = true and with exe.linkSystemLibrary("SDL2") building produces no errors and then Segmentation fault at address 0x0.

Very strange.

OK, the summary and I am trailing off :slight_smile:

  • SDL - everything as expected both with build-exe and with build script.
  • SDL2 - ok with build-exe, but something strange is happening when using build script