zig
zig copied to clipboard
zig cc: detect -static and treat -l options as static library names
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)
Also, on an unrelated note - is it possible to do "--strip" (like in zig build-exe) with zig cc?
Also, on an unrelated note - is it possible to do "--strip" (like in
zig build-exe) withzig 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.
@andrewrk I'm not sure if I'm doing something wrong but it doesn't seem like strip is enabled by default, with the same C file:
#include <stdio.h>
int main()
{
printf("Hello World!\n");
}
$ zig cc -target x86_64-linux-musl -static -O3 hello.c
$ file file hello
hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
$ du -sh hello
12K hello
$ strip hello
$ du -sh hello
8.0K hello
$ file hello
hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped
Is that different than zig build-exe though? The strip functionality is not fully implemented; currently it only omits debug info.
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
Facing the same issue as @makeworld-the-better-one, also doesn't work when using lld as a linker.
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");
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
UPDATE: I misunderstood this issue:
-staticis not recognized byzig cc, it doesn't complain about it when provided like some other options do witherror: Unknown Clang option. This issue is asking forzig ccto detect and handle-staticproperly.- As shown below when providing the static
*.alibraries 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
-lssland-lcryptomaking zig think you are dynamic linking against those libraries. So this is a missing feature ofzig cc, to detect-staticand make it look for.afiles instead.If you are looking for a workaround in the meantime, it is to pass the .a files as positional arguments instead of
-lparameters.
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/nmcommands. - 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:
- https://github.com/ziglang/zig/issues/17268#issuecomment-1982295303
- https://github.com/rust-cross/cargo-zigbuild/issues/231#issuecomment-1983434802
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
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): https://github.com/ziglang/zig/pull/23572#issuecomment-2846055154
#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!