Code change:
fs::write(&self.path, self.hash.as_deref().unwrap_or(b"")) } } <ins> #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] pub struct CrtBeginEnd { pub target: TargetSelection, } impl Step for CrtBeginEnd { type Output = PathBuf; fn should_run(run: ShouldRun<'_>) -> ShouldRun<'_> { run.path("src/llvm-project/compiler-rt/lib/crt") } fn make_run(run: RunConfig<'_>) { run.builder.ensure(CrtBeginEnd { target: run.target }); } /// Build crtbegin.o/crtend.o for musl target. fn run(self, builder: &Builder<'_>) -> Self::Output { let out_dir = builder.native_dir(self.target).join("crt"); if builder.config.dry_run { return out_dir; } let crtbegin_src = builder.src.join("src/llvm-project/compiler-rt/lib/crt/crtbegin.c"); let crtend_src = builder.src.join("src/llvm-project/compiler-rt/lib/crt/crtend.c"); if up_to_date(&crtbegin_src, &out_dir.join("crtbegin.o")) && up_to_date(&crtend_src, &out_dir.join("crtendS.o")) { return out_dir; } builder.info("Building crtbegin.o and crtend.o"); t!(fs::create_dir_all(&out_dir)); let mut cfg = cc::Build::new(); if let Some(ar) = builder.ar(self.target) { cfg.archiver(ar); } cfg.compiler(builder.cc(self.target)); cfg.cargo_metadata(false) .out_dir(&out_dir) .target(&self.target.triple) .host(&builder.config.build.triple) .warnings(false) .debug(false) .opt_level(3) .file(crtbegin_src) .file(crtend_src); // Those flags are defined in src/llvm-project/compiler-rt/lib/crt/CMakeLists.txt // Currently only consumer of those objects is musl, which use .init_array/.fini_array // instead of .ctors/.dtors cfg.flag("-std=c11") .define("CRT_HAS_INITFINI_ARRAY", None) .define("EH_USE_FRAME_REGISTRY", None); cfg.compile("crt"); t!(fs::copy(out_dir.join("crtbegin.o"), out_dir.join("crtbeginS.o"))); t!(fs::copy(out_dir.join("crtend.o"), out_dir.join("crtendS.o"))); out_dir } } </ins>

Related discussion:
text: For rustc/glibc, I don't see any of those flags, so the cc driver uses the default CRT begin/end/i/n/1 files. I see that rustc documents a lot of this at The unwinder needs the .ehframe terminator, because the .ehframehdr search table is optional, and because .ehframehdr has the start address of .ehframe, but not its size. ( / ). I don't think it's reasonable to open the ELF file indicated by dlpiname -- in principle, that duplicates the work done by the loader, and I can think of many specific things that wouldn't work. (e.g. the vdso, the executable w/glibc, Android's androiddlopen_ext and ability to map DSOs from zip files, unlinked files, etc).
Using self-contained mode is not generally recommended for serious work. If the compiler heuristics enable it by default, then it can be explicitly disabled with , then gcc will be used for linking without and will find all the necessary CRT objects by itself. Then it's important to make sure that gcc for the right target (musl in this case) is used by rustc.
If the compiler heuristics enable it by default Here's a FIXME for improving the heuristic for musl targets:
Thanks for that, very interesting. I think there are two separate use cases for musl-targeting builds: an environment where there's a full "native musl" toolchain (like on Alpine etc) where I think you're correct that we shouldn't use and should let the toolchain sort it out for us though I can't see an easy way to tell if a real toolchain is available a generic linux environment where the goal is fully static binaries (for easy of redistribution/embedding in thin container images/etc.)