Skip to content

zig cc: detect -static and treat -l options as static library names #4986

Open

Description

So I'm not sure if I'm doing something incorrectly, but I can't really figure out, so I came with this reproducible example.
Consider we have a C file:

#include <stdio.h>
int main()
{
    printf("Hello World!\n");
}

We want to build it for arm-linux-musleabi as a static binary and link to libressl, so we do this:

wget https://ftp.openbsd.org/pub/OpenBSD/LibreSSL/libressl-3.1.0.tar.gz
tar xvf libressl-3.1.0.tar.gz
cd libressl-3.1.0

export CC="zig cc -target arm-linux-musleabi -static"

./configure --host=arm-linux-musleabi --disable-shared --prefix=/some/dir

make -j16 -C crypto
make -j16 -C ssl
make -j16 -C crypto install
make -j16 -C ssl install

After that we build our C file:

zig cc -target arm-linux-musleabi -I/path/to/libressl-3.1.0/include/openssl \
-L/some/dir/lib -lssl -lcrypto -static hello.c

ldd tells that it's in fact not a dynamic binary:

$ ldd hello
        not a dynamic executable

But if we try to run file on it:

$ file hello
hello: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-musl-arm.so.1, not stripped

Or even if we do strings hello, /lib/ld-musl-arm.so.1 will be there too.
And we won't be able to run it on any ARM-based machine which doesn't have that file:

$ qemu-arm ./hello
/lib/ld-musl-arm.so.1: No such file or directory

(Maybe I just used some compiler options incorrectly, but I'm not really experienced with C compiler arguments)

Activity

  1. added
    zig ccZig as a drop-in C compiler feature
    on Apr 9, 2020
  2. andrewrk commented on Apr 9, 2020

    @andrewrk
    Member

    Also, on an unrelated note - is it possible to do "--strip" (like in zig build-exe) with zig cc?

    --strip (like in zig build-exe) is enabled by default for zig cc, and turns off if you pass -g or other similar debug options.

    For the other problem-

    I looked through the code a bit and I believe the problem is -lssl and -lcrypto making zig think you are dynamic linking against those libraries. So this is a missing feature of zig cc, to detect -static and make it look for .a files instead.

    If you are looking for a workaround in the meantime, it is to pass the .a files as positional arguments instead of -l parameters.

  3. added
    enhancementSolving this issue will likely involve adding new logic or components to the codebase.
    on Apr 9, 2020
  4. added this to the 0.7.0 milestone on Apr 9, 2020
  5. changed the title [-]zig cc makes a dynamic library even when told not to[/-] [+]zig cc: detect -static and treat -l options as static library names[/+] on Apr 9, 2020
  6. andrewrk commented on Apr 10, 2020

    @andrewrk
    Member

    Is that different than zig build-exe though? The strip functionality is not fully implemented; currently it only omits debug info.

  7. modified the milestones: 0.7.0, 0.8.0 on Oct 14, 2020
  8. modified the milestones: 0.8.0, 0.9.0 on Nov 6, 2020
  9. modified the milestones: 0.9.0, 0.10.0 on May 19, 2021
  10. makew0rld commented on Sep 14, 2021

    @makew0rld

    Not sure if this is the same issue, but zig cc has the same problem with -Wl,-Bstatic which sets all the -l libs after it as statically linked. They end up being dynamically linked anyway. This happens without -static being specified. Running the same command with clang doesn't have this issue.

    Edit: Also, Zig doesn't even seem to know the options:

    warning: unsupported linker arg: -Bstatic
    warning: unsupported linker arg: -Bdynamic
    
  11. SoftwareApe commented on Oct 13, 2021

    @SoftwareApe

    Facing the same issue as @makeworld-the-better-one, also doesn't work when using lld as a linker.

  12. nuald commented on Oct 23, 2021

    @nuald
    Contributor

    As @andrewrk suggested, there is a workaround by using object files (addObjectFile method), i.e.

    const exe = b.addExecutable("exe", null);
    exe.addObjectFile("/path/to/a-file");
  13. YugoCode commented on Jul 2, 2023

    @YugoCode

    Could someone please provide a workaround for using zig cc instead of zig build?

    I tried following command, but still get error: undefined symbol for every libcurl function:

    zig cc main.c web/https.c helper/helper.c utils/json/cJSON.c -I. -L/usr/local/lib /usr/local/lib/libcurl.a /usr/local/lib/libmbedcrypto.a /usr/local/lib/libmbedtls.a /usr/local/lib/libmbedx509.a -static
    

    This command builds without a problem:

    gcc main.c web/https.c helper/helper.c utils/json/cJSON.c -I. -L/usr/local/lib -lcurl -lmbedcrypto -lmbedtls -lmbedx509
    
  14. polarathene commented on Mar 7, 2024

    @polarathene

    UPDATE: I misunderstood this issue:

    • -static is not recognized by zig cc, it doesn't complain about it when provided like some other options do with error: Unknown Clang option. This issue is asking for zig cc to detect and handle -static properly.
    • As shown below when providing the static *.a libraries explicitly as a suggested workaround above, Zig is still configuring a dynamic linker / interpreter which is causing the segfault?

    I looked through the code a bit and I believe the problem is -lssl and -lcrypto making zig think you are dynamic linking against those libraries. So this is a missing feature of zig cc, to detect -static and make it look for .a files instead.

    If you are looking for a workaround in the meantime, it is to pass the .a files as positional arguments instead of -l parameters.

    Just building the hello world example without any -l parameters still ignores -static for a glibc (-gnu) target:

    hello.c:

    #include <stdio.h>
    int main()
    {
        printf("Hello World!\n");
    }
    # zig 0.11.0 (Aug 2023) via Fedora 40 Docker image:
    
    $ zig cc -static -o hello hello.c
    $ file hello
    hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, with debug_info, not stripped
    $ ldd hello
            linux-vdso.so.1 (0x00007fff22ad0000)
            libc.so.6 => /lib64/libc.so.6 (0x00007f59ddc91000)
            /lib64/ld-linux-x86-64.so.2 (0x00007f59dde83000)
    
    # Explicit target, same output except for `GNU/Linux 2.0.0`:
    $ zig cc -static -target x86_64-linux-gnu -o hello hello.c
    $ file hello
    hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.0.0, with debug_info, not stripped
    
    # Musl target is static, unlike glibc/gnu:
    $ zig cc -static -target x86_64-linux-musl -o hello hello.c
    $ file hello
    hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, with debug_info, not stripped
    $ ldd hello
            not a dynamic executable

    I imagine that's the actual issue here?


    UPDATE: Nevermind, I understand the advice/discussion now 😅

    To successfully static link the hello world on a -gnu target, you need to reference the absolute path for libc.a, which then fails with:

    LLD Link... ld.lld: error: undefined symbol: _Unwind_Resume
    >>> referenced by iofputs.o:(_IO_fputs.cold) in archive /usr/lib64/libc.a
    >> referenced by fileops.o:(_IO_new_file_underflow.cold) in archive /usr/lib64/libc.a
    >> referenced by wfileops.o:(_IO_wfile_underflow.cold) in archive /usr/lib64/libc.a
    >> referenced 4 more times
    
    ld.lld: error: undefined symbol: __gcc_personality_v0
    >>> referenced by iofputs.o:(DW.ref.__gcc_personality_v0) in archive /usr/lib64/libc.a
    

    So you need to also reference libgcc_eh.a explicitly too.

    On fedora these can be found:

    # Find a package that provides this file, in this case it's `gcc`:
    $ dnf provides *gcc_eh.a
    gcc-14.0.1-0.7.fc40.x86_64 : Various compilers (C, C++, Objective-C, ...)
    Repo        : @System
    Matched from:
    Other       : *gcc_eh.a
    
    # Query the package for the actual install path:
    $ dnf repoquery -l gcc | grep gcc_eh.a
    /usr/lib/gcc/x86_64-redhat-linux/14/32/libgcc_eh.a
    /usr/lib/gcc/x86_64-redhat-linux/14/libgcc_eh.a
    
    # Same process for `libc.a`:
    $ dnf provides *libc.a
    glibc-static-2.39-2.fc40.x86_64 : C library static libraries for -static linking.
    Repo        : @System
    Matched from:
    Other       : *libc.a
    
    $ dnf repoquery -l glibc-static | grep libc.a
    /usr/lib/libc.a
    /usr/lib64/libc.a

    So until this issue is fixed, it'll vary based on build host:

    # Split to multi-line for readability:
    $ zig cc -static -target x86_64-linux-gnu -o hello \
      hello.c \
      /usr/lib64/libc.a \
      /usr/lib/gcc/x86_64-redhat-linux/14/libgcc_eh.a
    
    $ ldd hello
            statically linked
    
    # Despite that, seems to still be dynamically linked?
    $ file hello
    hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.0.0, with debug_info, not stripped
    
    $ nm -an hello | grep dl_open
    0000000000290e60 t dl_open_worker
    0000000000291190 t _dl_open
    00000000002914c0 t dl_open_worker_begin

    Perhaps it needs something else for file to not treated it as dynamically linked?

    • I assume that's due to the _dl_open. I'm aware that glibc can be statically linked and still have _dl_open, so it may be due to that.
    • I'm more familiar with Rust where the equivalent glibc static link build does not have the extra dynamic output from file / nm commands.
    • EDIT: I probably should have run it first, the above approach segfaults 😂 (despite being run on the same build host) so I'm definitely missing something else, or doing something wrong?

    Related issues:

  15. polarathene commented on May 2, 2025

    @polarathene

    Follow-up to my previous comment.

    Reproduction

    Use the Dockerfile below to build a container with a version of Zig for testing. It embeds the main.c hello world program and a test-zig script for convenience, you can just run the container to build, or shell in to inspect.

    # Build some containers:
    docker build --tag localhost/example:master .
    docker build --tag localhost/example:0.14.0 --build-arg ZIG_VERSION=0.14.0 .
    docker build --tag localhost/example:0.13.0 --build-arg ZIG_VERSION=0.13.0 .
    
    # See outputs below

    Dockerfile:

    FROM fedora:42
    ARG ZIG_VERSION=master # 0.14.0
    RUN <<"HEREDOC"
      dnf --setopt=install_weak_deps=0 install -yq file jq gcc glibc-static
    
      # Install Zig:
      ZIG_RELEASE=$(curl -fsSL https://ziglang.org/download/index.json | jq -rc ".[\"${ZIG_VERSION}\"].[\"x86_64-linux\"].tarball")
      ZIG_DIR="/opt/zig-${ZIG_VERSION}"
    
      # Prefer mkdir with `--strip-components=1` otherwise archive top-level directory is the longer archive filename from URL
      # Use `--no-same-owner` because ownership is otherwise 1000:1000
      mkdir "${ZIG_DIR}"
      curl -fsSL "${ZIG_RELEASE}" | tar --extract --xz --directory "${ZIG_DIR}" --strip-components=1 --no-same-owner
      ln -s "${ZIG_DIR}/zig" /usr/local/bin/zig
    HEREDOC
    # Defaults for convenience:
    WORKDIR /example
    CMD ["test-zig-static"]
    
    # Remainder of this file embeds content:
    # Example program to test with:
    COPY <<HEREDOC /example/main.c
    #include <stdio.h>
    int main()
    {
        printf("Hello World!\n");
    }
    HEREDOC
    
    COPY --chmod=755 <<HEREDOC /usr/local/bin/test-zig-static
    #!/bin/sh
    zig cc main.c -static -target x86_64-linux-gnu -o example /usr/lib64/libc.a /usr/lib/gcc/x86_64-redhat-linux/15/libgcc_eh.a
    HEREDOC
    
    # Same as prior `test-zig-static` file, except no `-static` flag:
    COPY --chmod=755 <<HEREDOC /usr/local/bin/test-zig
    #!/bin/sh
    zig cc main.c -target x86_64-linux-gnu -o example /usr/lib64/libc.a /usr/lib/gcc/x86_64-redhat-linux/15/libgcc_eh.a
    HEREDOC

    Here's the failure scenario from my last comment:

    docker run --rm -it localhost/example:0.13.0 bash
    # Build the example:
    $ test-zig
    
    # Notice it segfaults:
    $ ./example
    Segmentation fault (core dumped)
    
    $ ldd example
            statically linked
    
    $ file example
    example: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.0.0, with debug_info, not stripped, too many notes (256)

    Newer versions of Zig since fail to build it, master (to become 0.15.0? Tested with this PR) additionally blocks the usage of -static with a new error message:

    # Only from zig-master:
    $ docker run --rm -it localhost/example:master
    error: libc of the specified target requires dynamic linking
    
    
    # Both Zig 0.14.0 (with or without `-static`) and master (without `-static`):
    $ docker run --rm -it localhost/example:0.14.0
    
    ld.lld: error: undefined hidden symbol: _init
    >>> referenced by libc-start.o:(__libc_start_main_impl) in archive /usr/lib64/libc.a
    
    ld.lld: error: undefined hidden symbol: _fini
    >>> referenced by libc-start.o:(call_fini) in archive /usr/lib64/libc.a

    The _init / _fini errors seems to be due to crt0.o + crtend.o https://stackoverflow.com/questions/8410383/linker-error-undefined-reference-to-fini/8410815#8410815

    Those specific files do not appear to be available on Fedora or Alpine, nor does Zig or Rust toolchains seem to provide them.

    Here's the Rust toolchain where only the musl target (due to that targets -C link-self-contained=yes default) has these files for static linking:

    $ ls /root/.rustup/toolchains/*/lib/rustlib/x86_64-unknown-linux-musl/lib/self-contained
    
    Scrt1.o  crt1.o  crtbegin.o  crtbeginS.o  crtend.o  crtendS.o  crti.o  crtn.o  libc.a  libunwind.a  rcrt1.o
  16. polarathene commented on May 5, 2025

    @polarathene

    Another update. While the original issue may not be resolved, the related one for static glibc will be for 0.15.0, except only when you build without specifying -target (even if it matches the same build host, but -target native should be valid).

    Full reproduction (Dockerfile): #23572 (comment)


    #include <stdio.h>
    int main()
    {
        printf("Hello World!\n");
    }
    # Does not support cross-compilation target (which apparently includes a `-target` that matches the host)
    # https://github.com/ziglang/zig/pull/23752#issue-3037284792
    $ zig cc main.c -o example -static
    
    # ✅ Nothing linked:
    $ ldd ./example
            not a dynamic executable
    
    # ✅ No interpreter:
    $ file ./example
    ./example: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, for GNU/Linux 3.2.0, with debug_info, not stripped
    
    # ✅ No segfault:
    ./example
    Hello World!
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementSolving this issue will likely involve adding new logic or components to the codebase.zig ccZig as a drop-in C compiler feature

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions