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: FWIW, it looks like LLVM's LLD linker automatically adds a terminator to the end of .ehframe, even if one isn't present in the linker inputs, so that could be a workaround: I think libunwind should stop scanning .ehframe when it finds unwind info from .ehframehdr (i.e. the GNUEH_FRAME segment).
Rustc's musl-targeting link includes: I don't exactly know where the files come from (they're shipped with the compiler toolchain, e.g. mine are in ) but looking at the musl codebase, there's no terminator in : (Intriguingly there's also no in the musl codebase, suggesting Rustc gets these objects from somewhere else?)_ I wouldn't expect to see a or since the rustc musl target doesn't include a C++ runtime and musl libc doesn't define either of these objects for the C runtime either.
It looks like musl's crt1 is a C file, not assembly. crt/{S,r,}crt1.c When crtbegin and crtend were to LLVM (), there was some discussion about which project is responsible for which aspects of the CRT begin/end files. Apparently the status quo on Linux is that: the compiler (e.g. libgcc) provides crtbegin/crtend the libc provides crti/crte (and maybe crt1, too?) That is the case on my gLinux (i.e. Debian) system: /usr/lib/x8664-linux-gnu/crt[1ie].o all come from the libc6-dev:amd64 package /usr/lib/gcc/x8664-linux-gnu/9/{crtbegin.o,crtend.o} come from libgcc-9-dev:amd64 The .ehframe terminator is usually in crtend, which is part of libgcc (not glibc or musl). LLVM's compiler-rt also provides crtbegin/crtend files. The gcc and clang drivers link a crtend object even for C programs. On Alpine, the gcc driver links a libgcc crtend object. It looks like rustc typically invokes the driver, but when it's targeting musl, it enters a mode where it provides the start files explicitly, and in this mode, it doesn't include crtbegin/crtend. For Alpine, I see , , , and .