zig icon indicating copy to clipboard operation
zig copied to clipboard

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

Open ghost opened this issue 5 years ago • 11 comments

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)

ghost avatar Apr 09 '20 11:04 ghost

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

ghost avatar Apr 09 '20 14:04 ghost

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.

andrewrk avatar Apr 09 '20 21:04 andrewrk

@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

ghost avatar Apr 10 '20 07:04 ghost

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

andrewrk avatar Apr 10 '20 20:04 andrewrk

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

makew0rld avatar Sep 14 '21 15:09 makew0rld

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

SoftwareApe avatar Oct 13 '21 14:10 SoftwareApe

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");

nuald avatar Oct 23 '21 17:10 nuald

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

YugoCode avatar Jul 02 '23 21:07 YugoCode

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:

  • https://github.com/ziglang/zig/issues/17268#issuecomment-1982295303
  • https://github.com/rust-cross/cargo-zigbuild/issues/231#issuecomment-1983434802

polarathene avatar Mar 07 '24 05:03 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

polarathene avatar May 02 '25 10:05 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): 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!

polarathene avatar May 05 '25 05:05 polarathene