rust-lang/rust

Linker error for no_std binary using musl

jgriebler opened this issue · 13 comments

I tried to build a simple no_std "Hello World!" program using the x86_64-unknown-linux-musl target. The source code (in src/main.rs) is here:

#![no_std]
#![no_main]

use libc::{c_char, c_int, exit, puts};

#[no_mangle]
pub extern "C" fn main(_: c_int, _: *const *const c_char) -> c_int {
    unsafe {
        puts(b"Hello World!\0" as *const u8 as *const i8);
    }

    0
}

#[panic_handler]
fn panic_handler(_: &core::panic::PanicInfo) -> ! {
    unsafe {
        exit(1)
    }
}

When using my default x86_64-unknown-linux-gnu target, the program compiles and runs like it should, but with x86_64-unknown-linux-musl, I get this linking error:

error: linking with `cc` failed: exit code: 1
  |
  = note: "cc" "-Wl,--as-needed" "-Wl,-z,noexecstack" "-Wl,--eh-frame-hdr" "-m64" "-nostdlib" "/home/johannes/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-musl/lib/crt1.o" "/home/johannes/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-musl/lib/crti.o" "-L" "/home/johannes/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-musl/lib" "/home/johannes/hello/target/x86_64-unknown-linux-musl/release/deps/hello-8e567d3b97d6bd11.hello.c72za6mx-cgu.0.rcgu.o" "-o" "/home/johannes/hello/target/x86_64-unknown-linux-musl/release/deps/hello-8e567d3b97d6bd11" "-Wl,--gc-sections" "-no-pie" "-Wl,-zrelro" "-Wl,-znow" "-Wl,-O1" "-nodefaultlibs" "-L" "/home/johannes/hello/target/x86_64-unknown-linux-musl/release/deps" "-L" "/home/johannes/hello/target/release/deps" "-L" "/home/johannes/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-musl/lib" "-Wl,-Bstatic" "/home/johannes/hello/target/x86_64-unknown-linux-musl/release/deps/liblibc-8c2b3cf08263e000.rlib" "/home/johannes/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-musl/lib/libcore-879310dc3b96af61.rlib" "/home/johannes/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-musl/lib/libcompiler_builtins-d0572f7a936161bf.rlib" "-static" "-Wl,-Bdynamic" "/home/johannes/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-musl/lib/crtn.o"
  = note: /usr/bin/ld: /home/johannes/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-musl/lib/crt1.o: in function `_start_c':
          /build/musl-1.1.20/crt/crt1.c:17: undefined reference to `__libc_start_main'
          /usr/bin/ld: /home/johannes/hello/target/x86_64-unknown-linux-musl/release/deps/hello-8e567d3b97d6bd11.hello.c72za6mx-cgu.0.rcgu.o: in function `main':
          hello.c72za6mx-cgu.0:(.text.main+0xa): undefined reference to `puts'
          collect2: error: ld returned 1 exit status

I can build a normal (std-using) "Hello World!" program for musl without problems, only this no_std version doesn't work.

I am also able to work around this problem by instead moving the source code to src/lib.rs and compiling it as a library with crate-type = ["staticlib"], then linking manually with musl-gcc. The error only occurs when trying to build a binary directly.

My Cargo.toml. Uncomment the two lines and move main.rs to lib.rs for the workaround.
[package]
name = "hello"
version = "0.1.0"
authors = ["me"]
edition = "2018"

#[lib]
#crate-type = ["staticlib"]

[dependencies]
libc = { version = "0.2", default-features = false }

[profile.dev]
panic = "abort"

[profile.release]
panic = "abort"

Which Rust version are you using rustc -vV?

On latest nightly your example fails with:

error[E0152]: duplicate lang item found: `panic_impl`.
  --> src/main.rs:16:1
   |
16 | / fn panic_handler(_: &core::panic::PanicInfo) -> ! {
17 | |     unsafe {
18 | |         exit(1)
19 | |     }
20 | | }
   | |_^
   |
   = note: first defined in crate `std`.

Without panic_handler(_) it builds for both gnu and musl.

Ah, I forgot to mention that libc needs default-features = false so that it doesn't link std implicitly. I'll attach my Cargo.toml to the issue description.

I tried it with both 1.32.0 and the latest nightly.

When using native no_std gnu target required symbols come from "-lc" "-lm" "-lrt" "-lpthread" "-lutil" "-lutil" linker arguments.
When cross compiling to musl Rust provides those symbols built in liblibc to make it easier but with no_std liblibc is not included anymore resulting in undefined symbols error.

Manually adding ~/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-musl/lib/liblibc-ad64cf40491cbcc1.rlib to the linker made it compile.

So the question to Rust (libs?) team is whether liblibc should be given to the linker for no_std builds or something else would be preferred.

Gist with linker args for different builds: https://gist.github.com/mati865/210dbf7f27b15ffa96bf9e0b79f5d0b5

the program compiles and runs like it should, but with x86_64-unknown-linux-musl, I get this linking error:

This is expected behavior. This program is not linking musl at all, to do that, one currently needs to enable the private rustc-dep-of-std libc cargo feature which requires other hacks like linking the rustc-hack crates from https://github.com/rust-lang/rust/tree/master/src/tools (not from crates.io) . It is probably easier to just manually patch libc to always link musl (EDIT: see below for a possibly easier workaround).

If you want to statically link musl to your binary, the easiest way is to not make it a #![no_std] binary, such that libstd links it properly for you.

Sadly, solving this issue isn't trivial. I suppose we could expose a cargo feature from libc that allows users to force the linking of the C library such that you could use that here, but there are other things to consider here, like how this interacts with -C target-feature=-crt-static.

An alternative workaround that might be easier is to try adding this to your binary:

#[link(name = "c", kind = "static", cfg(target_feature = "crt-static"))]
#[link(name = "c", cfg(not(target_feature = "crt-static")))]
extern {}

I'm not sure if this is the right place to ask this, but is that available on stable? For me this only works when using #![feature(link_cfg)], even though the tracking issue linked in the error has been closed for a long time.

But even with that, the workaround isn't working for me. On the gnu target it works fine (as before), but on musl I now get this error:

error: linking with `cc` failed: exit code: 1
  |
  = note: "cc" "-Wl,--as-needed" "-Wl,-z,noexecstack" "-Wl,--eh-frame-hdr" "-m64" "-nostdlib" "/home/johannes/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-musl/lib/crt1.o" "/home/johannes/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-musl/lib/crti.o" "-L" "/home/johannes/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-musl/lib" "/home/johannes/hello/target/x86_64-unknown-linux-musl/release/deps/hello-88b5f1c27ffd5796.hello.81h291ix-cgu.0.rcgu.o" "-o" "/home/johannes/hello/target/x86_64-unknown-linux-musl/release/deps/hello-88b5f1c27ffd5796" "-Wl,--gc-sections" "-no-pie" "-Wl,-zrelro" "-Wl,-znow" "-Wl,-O1" "-nodefaultlibs" "-L" "/home/johannes/hello/target/x86_64-unknown-linux-musl/release/deps" "-L" "/home/johannes/hello/target/release/deps" "-L" "/home/johannes/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-musl/lib" "-Wl,-Bstatic" "-Wl,--whole-archive" "-lc" "-Wl,--no-whole-archive" "/home/johannes/hello/target/x86_64-unknown-linux-musl/release/deps/liblibc-9b25b07157410626.rlib" "/home/johannes/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-musl/lib/librustc_std_workspace_core-58a61c6fb52da4c6.rlib" "/home/johannes/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-musl/lib/libcore-8f8f2fe3f7b7398f.rlib" "/home/johannes/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-musl/lib/libcompiler_builtins-6db28108a6c10236.rlib" "-static" "-Wl,-Bdynamic" "/home/johannes/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-musl/lib/crtn.o"
  = note: /usr/bin/ld: /usr/lib/gcc/x86_64-pc-linux-gnu/9.1.0/../../../../lib/libc.a(rcmd.o): in function `__validuser2_sa':
          (.text+0x5a9): warning: Using 'getaddrinfo' in statically linked applications requires at runtime the shared libraries from the glibc version used for linking
          /usr/bin/ld: /home/johannes/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-unknown-linux-musl/lib/crt1.o: in function `_start':
          crt1.c:(.text+0x9): undefined reference to `_DYNAMIC'
          /usr/bin/ld: /usr/lib/gcc/x86_64-pc-linux-gnu/9.1.0/../../../../lib/libc.a(printf_fp.o): in function `__printf_fp_l':
          (.text+0x52a): undefined reference to `__unordtf2'
          /usr/bin/ld: (.text+0x562): undefined reference to `__unordtf2'
          /usr/bin/ld: (.text+0x588): undefined reference to `__letf2'
          /usr/bin/ld: /usr/lib/gcc/x86_64-pc-linux-gnu/9.1.0/../../../../lib/libc.a(printf_fphex.o): in function `__printf_fphex':
          (.text+0xac): undefined reference to `__unordtf2'
          /usr/bin/ld: (.text+0xe2): undefined reference to `__unordtf2'
          /usr/bin/ld: (.text+0xfa): undefined reference to `__letf2'
          /usr/bin/ld: /usr/lib/gcc/x86_64-pc-linux-gnu/9.1.0/../../../../lib/libc.a(iofclose.o): in function `_IO_new_fclose.cold':
          (.text.unlikely+0x4c): undefined reference to `_Unwind_Resume'
          /usr/bin/ld: /usr/lib/gcc/x86_64-pc-linux-gnu/9.1.0/../../../../lib/libc.a(iofclose.o):(.data.rel.local.DW.ref.__gcc_personality_v0[DW.ref.__gcc_personality_v0]+0x0): undefined reference to `__gcc_personality_v0'
          /usr/bin/ld: /usr/lib/gcc/x86_64-pc-linux-gnu/9.1.0/../../../../lib/libc.a(iofflush.o): in function `_IO_fflush.cold':
          (.text.unlikely+0x4b): undefined reference to `_Unwind_Resume'
          /usr/bin/ld: /usr/lib/gcc/x86_64-pc-linux-gnu/9.1.0/../../../../lib/libc.a(iofputs.o): in function `_IO_fputs.cold':
          (.text.unlikely+0x4b): undefined reference to `_Unwind_Resume'
          /usr/bin/ld: /usr/lib/gcc/x86_64-pc-linux-gnu/9.1.0/../../../../lib/libc.a(iofwrite.o): in function `_IO_fwrite.cold':
          (.text.unlikely+0x4b): undefined reference to `_Unwind_Resume'
          /usr/bin/ld: /usr/lib/gcc/x86_64-pc-linux-gnu/9.1.0/../../../../lib/libc.a(iogetdelim.o): in function `_IO_getdelim.cold':
          (.text.unlikely+0x4b): undefined reference to `_Unwind_Resume'
          /usr/bin/ld: /usr/lib/gcc/x86_64-pc-linux-gnu/9.1.0/../../../../lib/libc.a(ioputs.o): in function `_IO_puts.cold':
          (.text.unlikely+0x4c): undefined reference to `_Unwind_Resume'
          /usr/bin/ld: /usr/lib/gcc/x86_64-pc-linux-gnu/9.1.0/../../../../lib/libc.a(wfileops.o):(.text.unlikely+0x4c): more undefined references to `_Unwind_Resume' follow
          /usr/bin/ld: /usr/lib/gcc/x86_64-pc-linux-gnu/9.1.0/../../../../lib/libc.a(dl-reloc-static-pie.o): in function `_dl_relocate_static_pie':
          (.text+0x1a): undefined reference to `_DYNAMIC'
          /usr/bin/ld: (.text+0x39): undefined reference to `_DYNAMIC'
          /usr/bin/ld: /home/johannes/hello/target/x86_64-unknown-linux-musl/release/deps/hello-88b5f1c27ffd5796: hidden symbol `_DYNAMIC' isn't defined
          /usr/bin/ld: final link failed: bad value
          collect2: error: ld returned 1 exit status

I don't know enough about how linking is done to try to get this to work, but maybe someone else knows what to do with this error.

(Also, just to clarify: I don't actually need this for anything, I just stumbled across this behaviour when toying around with #[panic_handler] when it was stabilised.)

I'm not sure if this is the right place to ask this, but is that available on stable?

Nope, but neither are #![no_std] binaries IIRC, right ? (that is, you'd need nightly anyways to be able to reach this issue)

On the gnu target it works fine (as before), but on musl I now get this error:

I see x86_64-pc-linux-gnu in those error messages. Is that normal? How are you compiling the #![no_std] binary ? E.g. if you want to compile with cargo build --target=x86_64-unknown-linux-musl I think you need to make sure that the proper C toolchain is used.

(Also, just to clarify: I don't actually need this for anything, I just stumbled across this behaviour when toying around with #[panic_handler] when it was stabilised.)

Makes sense - this is kind of a known issue, but is good to have a proper bug report for it, so thank you for trying so many things here!

Nope, but neither are #![no_std] binaries IIRC, right ? (that is, you'd need nightly anyways to be able to reach this issue)

It's available on stable since 1.30, when #[panic_handler] was stabilised, though afaik only with panic=abort.

I see x86_64-pc-linux-gnu in those error messages. Is that normal? How are you compiling the #![no_std] binary ? E.g. if you want to compile with cargo build --target=x86_64-unknown-linux-musl I think you need to make sure that the proper C toolchain is used.

Ah, that's interesting. I tried it again with -C linker=musl-gcc and now your workaround works (though my original example still doesn't). Does this mean that #[link(...)] will always use the system cc even on the musl target?

The problem is that we can't statically link musl to a binary twice, and libstd for musl always links musl statically.

The only thing that the std feature of libc controls is whether libc itself has libstd as a dependency. If it doesn't, that does not mean that libstd won't be linked to the final binary (e.g. if the final binary isn't #![no_std], or if it is #![no_std] but contains an extern crate std, then libstd will be linked). There is no way for libc to know whether libstd will be linked, so it has to assume it will, and never link musl, since if libstd is actually linked, and libc links musl, musl will be statically linked twice, and the build will fail.

So when you have a #![no_std] binary that links libc, and you disable the libc std feature, that does not pull in musl, because you could write extern crate std; and that would break that use case. If you enable the std feature, libc should pull libstd for you, so that you get a valid musl, but this is probably not what you want if you are building a #![no_std] binary. So the only thing you can do is link musl yourself, which is what that the workaround here does.

This is bad, super hard to discover, etc and there should be a better way to do this.

The simplest thing to do would be adding a new cargo feature to the libc crate, that forces linking the C library no matter what. That way, you could add libc with a force_link_c_libs feature as a dependency, and your example would "just work". If you then, by accident, pull in libstd, e.g., by doing extern crate std;, or adding a dependency that does that (e.g. by not using #![no_std]), then your binary would fail to link, and the error message would be an obscure linking error, and there is no easy fix for that.

So I think we probably want a more elaborated way of solving all these issues, that has nice error messages, and always works in the obvious way.

I think, that if you add:

#![feature(rustc_private)]
extern crate libc;

to your example, and remove the libc dependency from your Cargo.toml, your code will also work, because it will use the libc version that the standard library uses, but without pulling in libstd. That libc version is special and different from the one in crates.io, and it will statically link musl for you.

Exploring a solution in this direction might be worth it. That is, you want a #![no_std] binary with a libc that does link the C standard library. It shouldn't be necessary for you to have to pull in libc from crates.io to do that, although it should be possible.

@mati865 maybe we could move the parts of the libc crate that actually link stuff into a separate crate, e.g., libc_linkage, that always links the stuff. We can then make that crate "unique", by using the Cargo links = "..." key, preventing it from being linked twice into a dependency graph.

We could then make libc via libstd always link it, and just allow configuring for libc from crates.io whether it should link it or not. If a user messes this up, cargo will error saying that there are two libc_link crates in the dependency graph., which is nice. A problem is that cargo will error even if libc_link dynamically links stuff, but I think this might be fine ?

This allows you to pull in different versions of the bindings, by pulling different versions of the libc crate, into a dependency graph, but ensuring that stuff is linked only once.

Thanks for the explanation, that does make some sense. I've tried your suggestion with rustc_private, but I couldn't get it to work right now. Maybe I still did something wrong.

I've found an even simpler alternative to your previous workaround though: Just passing -lc to rustc (along with the right linker) seems to be enough. That one even works on stable, which probably makes this whole thing mostly a non-issue. I guess it's still warranted to leave this issue open, since having to do that seems rather unintuitive.

@gnzlbg I never played with links field but from what I've read that could work.

To make sure I understand libc_linkage would be native library that links to libc, libm, libpthread and whatever else either as static or dynamic libs. Then libc crate would use links = "libc_link".
There are targets that do not support creating dylibs, could that be issue?

Thanks for the explanation, that does make some sense. I've tried your suggestion with rustc_private, but I couldn't get it to work right now. Maybe I still did something wrong.

No, i think this just doesn't work. The libc used by rustc is not the same as the one used by the std library :(

To make sure I understand libc_linkage would be native library that links to libc, libm, libpthread and whatever else either as static or dynamic libs.

I think it can be an rlib, it just needs to contain the #[link ...] extern { ... } blocks to force rustc to pass the appropriate -lc etc. flags.

Then libc crate would use links = "libc_link".

This isn't what I meant. The libc_link crate would have a links = "...unique..." key. libc would have libc_link as a dependency.

I haven't worked out the rest. For example, if libc_link is optional, we could build libstd with it enabled, and if a user tries to enable it while compiling libc from crates.io that will only work if libstd is not linked. However, if a user does not include libstd, and forgets to enable it, the error message will be obscure. This is because this dependency isn't really optional for libc.

So maybe we could publish libc_link along with libcore, liballoc, etc. and have it as a required dependency of libc. Crates can use whatever libc version they want, but they all link to the exact same libc_links. libstd would link against it as well.

That means that just including libc as a dependency would work for #![no_std] binaries, even if libstd is not available. The "con" of this approach, is that changes to libc_links have to go through the release process. Maybe we could offer a way to override the libc_link crate used, e.g., via a -C ... argument or something. I've wanted to be able to select a different implementation of libm in the past, so this might be a step towards that direction.