Skip to main content

rucc_sysroot/
link.rs

1//! The start files, the libraries and the loader for one target's link.
2//!
3//! Design: `spec/cross-compile/08-sysroots.md` section 8.2 and `spec/cross-compile/11-linking.md`.
4//!
5//! What is here is what has to be linked, in what order, and which loader will start the result.
6//! How that is spelled for a particular linker is [`crate::argv`], which is the division
7//! `spec/cross-compile/11-linking.md` draws: the files are a fact about the target and the flags are
8//! a fact about the linker.
9//!
10//! # Why musl is first
11//!
12//! `spec/cross-compile/09-libc-stubs.md` section 9.3 is the argument. musl exercises the header
13//! tree, the search paths, the start files, the compiler runtime and the link line, and it does
14//! that without symbol versioning and without stub generation, which are the two hardest pieces of
15//! the glibc path. If a musl cross link works end to end then the pipeline is right and what is
16//! left for M9.5 is the glibc specific parts rather than the shape of the thing.
17//!
18//! # Why the line has three parts and not one
19//!
20//! `crtn.o` goes after the libraries and `crti.o` goes before them, because between them they open
21//! and close the `.init` and `.fini` sections and anything contributing to those has to land in the
22//! middle. A link line that is one list gets this wrong in a way that produces a binary which links,
23//! runs, and does not run its static constructors, so the three parts are three fields here rather
24//! than a comment on an ordering somebody has to preserve.
25
26use std::path::{Path, PathBuf};
27
28use rucc_tuple::{Abi, Arch, DataModel, Endian, Env, ObjectFormat, Os, TargetTuple};
29
30use crate::layout::Sysroot;
31
32/// How the program is linked, which decides the first start file and the flags.
33///
34/// Five cases rather than two booleans for static and position independent, because the two are not
35/// independent and the start file is a different file in four of the five. A pair of flags would
36/// admit a sixth combination, a shared object that is not position independent, which is not a thing
37/// any of these linkers will produce.
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
39pub enum LinkMode {
40    /// Everything in the binary, no interpreter, no relocation at load. The default for musl, and
41    /// the mode `spec/cross-compile/02-the-goal.md`'s exit criterion names.
42    #[default]
43    Static,
44    /// Static, and position independent, so the loader may place it anywhere. A different first
45    /// start file, because the program has to relocate itself before `main` and `rcrt1.o` is what
46    /// does that.
47    StaticPie,
48    /// Against the shared libc, position independent, with the libc's loader named in the program
49    /// header. What every distribution builds today and what `-pie` asks for.
50    Dynamic,
51    /// Against the shared libc, at a fixed address, which is `-no-pie`.
52    ///
53    /// The same link as [`LinkMode::Dynamic`] with a different start file, because the reference to
54    /// `main` in `crt1.o` is an absolute one and the reference in `Scrt1.o` is not. Build systems
55    /// that pass `-no-pie` are usually doing it because something in them takes the address of a
56    /// function and compares it, and they get the file that matches.
57    DynamicNoPie,
58    /// A shared object rather than a program, which is `-shared`.
59    ///
60    /// No start file at all, since nothing starts a shared object and it has no `main` to be
61    /// started at, and no loader named either: the program that loads this one carries that.
62    Shared,
63}
64
65impl LinkMode {
66    /// Whether the result is linked against a shared libc, which decides whether a loader is named.
67    #[must_use]
68    pub const fn is_dynamic(self) -> bool {
69        matches!(self, LinkMode::Dynamic | LinkMode::DynamicNoPie | LinkMode::Shared)
70    }
71
72    /// Whether the result may be placed anywhere in memory.
73    #[must_use]
74    pub const fn is_pie(self) -> bool {
75        matches!(self, LinkMode::StaticPie | LinkMode::Dynamic | LinkMode::Shared)
76    }
77}
78
79/// What a produced sysroot holds for a target's C library.
80///
81/// Four cases, from `spec/cross-compile/08-sysroots.md` section 8.2's table, and the line differs
82/// between them in what goes on it rather than in how it is spelled. The table has seven rows and two
83/// of those are legal walls rather than technical ones, so what is left is these four.
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum Libc {
86    /// Nothing, which is the freestanding row: the nine compiler headers and no link inputs at all.
87    /// Our runtime is still there, because an architecture without a division instruction needs it
88    /// whether there is a libc or not.
89    None,
90    /// A real static archive, which today means musl built from source. The only row where the code
91    /// behind the names is present, so the only row a static link can use.
92    Archive,
93    /// A generated stub shared object: the names the platform's libc exports and none of the code.
94    /// glibc is the row this was written for, and bionic, the BSDs and illumos take the same shape
95    /// for the same reason, which is that their libc is a shared object on the target machine and a
96    /// list of names is enough to link against one.
97    Stub,
98    /// A set of import libraries, which is what the same idea is called in COFF.
99    ///
100    /// Windows is the row this is, and it is a separate case from [`Libc::Stub`] rather than a
101    /// spelling of it, for two reasons that both show up on the line. The container is different: a
102    /// Windows program links against an archive of tiny objects per DLL rather than against one
103    /// shared object, which is `spec/cross-compile/09-libc-stubs.md` section 9.4 and what
104    /// `rucc_stub::coff` writes. And the C library is not one file: the msvcrt import library,
105    /// mingw-w64's own `libmingwex.a` and `libmoldname.a`, and the Win32 libraries a CRT calls into
106    /// are all on the line, where a glibc line has one `libc.so` on it.
107    ///
108    /// A static link against this is not refused, which is the other difference. On Windows the C
109    /// library is a DLL on every machine and always has been, so `-static` there is a statement
110    /// about our libraries and mingw-w64's rather than about the CRT, and a program linked that way
111    /// runs. That is why the refusal in [`crate::argv::argv`] is about [`Libc::Stub`] by name.
112    Import,
113}
114
115/// Which of the four cases this target is.
116///
117/// Asked in two places, which is why it is a function rather than a `match` in each: [`LinkLine`]
118/// uses it to pick the files and [`crate::argv::argv`] uses it to refuse a static link against a
119/// stub. Two copies of this rule would be two rules.
120///
121/// The format is asked before the environment, because what holds a libc's names is a property of
122/// the object format and `Env::Gnu` means mingw-w64 on a Windows target and glibc on a Linux one.
123#[must_use]
124pub fn libc(target: TargetTuple) -> Libc {
125    match (target.os(), target.env()) {
126        (Os::None, _) => Libc::None,
127        _ if target.object_format() == ObjectFormat::Coff => Libc::Import,
128        (_, Env::Musl) => Libc::Archive,
129        _ => Libc::Stub,
130    }
131}
132
133/// Our own runtime library, which every one of the three lines below carries.
134///
135/// Named once because two callers ask about it by name: the line that puts it on, and
136/// [`crate::argv::argv`] when `-fno-builtins-lib` asks for it to be left off. A second spelling of
137/// the name in the second place is a flag that stops working the day the first one is renamed.
138pub const BUILTINS: &str = "librucc_builtins.a";
139
140/// The inputs to a link, in the three groups a linker needs them in.
141///
142/// Paths rather than strings, and no flags at all, because
143/// `spec/cross-compile/11-linking.md` owns which linker is invoked and how its arguments are
144/// spelled and [`crate::argv`] is where that happens. What is here is what has to be linked and in
145/// what order, which is a target fact and the same fact whichever linker reads it.
146#[derive(Debug, Clone, PartialEq, Eq)]
147pub struct LinkLine {
148    /// The start files, before the user's objects.
149    pub start: Vec<PathBuf>,
150    /// The libraries, after the user's objects.
151    pub libraries: Vec<PathBuf>,
152    /// The end files, after the libraries.
153    pub end: Vec<PathBuf>,
154}
155
156impl LinkLine {
157    /// The line for a link against this sysroot, whichever libc the target names.
158    ///
159    /// The dispatch rather than the line, and it is [`libc`] that decides: a real archive, a stub
160    /// shared object, or nothing. The three methods below are the three answers. A target whose
161    /// sysroot we do not produce yet still gets the right shape, because the shape follows from
162    /// whether the libc on the target machine is an archive or a shared object and that is known
163    /// before any of it is built.
164    #[must_use]
165    pub fn for_target(sysroot: &Sysroot, mode: LinkMode) -> Self {
166        match libc(sysroot.target()) {
167            Libc::None => LinkLine::freestanding(sysroot),
168            Libc::Archive => LinkLine::musl(sysroot, mode),
169            Libc::Stub => LinkLine::glibc(sysroot, mode),
170            Libc::Import => LinkLine::mingw(sysroot, mode),
171        }
172    }
173
174    /// The line for a freestanding link against this sysroot, which is our runtime and nothing else.
175    ///
176    /// Section 8.2's first row: nine compiler headers and no link inputs. There is no `crt1.o`,
177    /// because nothing here decides what runs before `main` or whether there is a `main` at all, and
178    /// no `crti.o` or `crtn.o`, because those come from a libc too. A kernel or a bootloader brings
179    /// its own start file and says so with `-nostartfiles`, which it would have to pass anyway.
180    ///
181    /// `librucc_builtins.a` stays, because it is ours rather than the platform's.
182    /// `spec/cross-compile/10-runtime.md` is the argument: an architecture with no division
183    /// instruction needs `__divti3` whether there is a libc in the picture or not, and freestanding
184    /// code that does 64-bit arithmetic on a 32-bit target reaches it without asking.
185    ///
186    /// The mode is not a parameter because it changes nothing here. Every difference between the
187    /// modes is a start file and there are none.
188    #[must_use]
189    pub fn freestanding(sysroot: &Sysroot) -> Self {
190        LinkLine {
191            start: Vec::new(),
192            libraries: vec![sysroot.lib().join(BUILTINS)],
193            end: Vec::new(),
194        }
195    }
196
197    /// The line for a musl link against this sysroot.
198    ///
199    /// `crt1.o` runs before `main` and calls it. `crti.o` and `crtn.o` are the prologue and the
200    /// epilogue of the `.init` and `.fini` sections, which is why one is at the front and the other
201    /// is at the very back. `libc.a` carries musl's whole C library, and `librucc_builtins.a`
202    /// carries the operations the architecture does not have an instruction for, which
203    /// `spec/cross-compile/10-runtime.md` says has to be ours rather than the platform's.
204    ///
205    /// The builtins go after `libc.a` because musl calls some of them, and an archive that is
206    /// searched before the thing that needs it contributes nothing.
207    ///
208    /// `libc.a` on every mode including the dynamic ones, because what a musl sysroot here holds is
209    /// musl built static: section 9.3 takes musl first precisely because one tarball built one way
210    /// exercises the whole pipeline, and a shared musl is a second build of it that buys nothing
211    /// until somebody asks for a dynamically linked musl program.
212    #[must_use]
213    pub fn musl(sysroot: &Sysroot, mode: LinkMode) -> Self {
214        let lib = sysroot.lib();
215        LinkLine {
216            start: start_files(&lib, mode),
217            libraries: vec![lib.join("libc.a"), lib.join(BUILTINS)],
218            end: vec![lib.join("crtn.o")],
219        }
220    }
221
222    /// The line for a link against a generated stub, which is glibc and every other hosted libc that
223    /// is not musl.
224    ///
225    /// Named for glibc because glibc is the case `spec/cross-compile/09-libc-stubs.md` is written
226    /// about and the hard one. bionic, the BSDs and illumos reach the same line for the same reason:
227    /// their libc is a shared object on the target machine, so a list of the names it exports is
228    /// enough to link against it, and that is what `rucc-stub` produces. The paragraphs below about
229    /// `libc_nonshared.a` and `libm` are glibc's own.
230    ///
231    /// The same start files as musl's and a different library, and the library is the whole
232    /// difference between them here. A stub libc is linked against dynamically, so what goes on the
233    /// line is the generated `libc.so` from `rucc-stub` rather than an archive, and the version nodes
234    /// in it are what make a program built here run on an older machine.
235    ///
236    /// `libc_nonshared.a` is deliberately absent and it is a known gap rather than a decision.
237    /// glibc's own `libc.so` is a linker script naming `libc.so.6`, `libc_nonshared.a` and the
238    /// loader as a group, and that archive holds real compiled objects: `atexit`, `__stack_chk_fail_local`
239    /// on i386, and the `stat` family on releases before 2.33. None of it can be synthesized from a
240    /// description, because a stub is a list of names and these are bodies, so it has to be built
241    /// from glibc's sources and that is M9.5's work. A program that needs nothing from it links
242    /// today and a program that does gets an undefined symbol, which is the loud failure rather
243    /// than the quiet one.
244    ///
245    /// `libm.so` is not here either, and that is a decision. glibc's `libm` is real code and a
246    /// program that wants it passes `-lm`, which every build system that does arithmetic already
247    /// does, so putting it on every line would record a dependency the program does not have.
248    ///
249    /// A static glibc link is not one of these: there is no `libc.a` in a sysroot whose libc is a
250    /// stub, because a stub is a list of names and a static link needs bodies.
251    /// [`crate::argv::argv`] refuses that combination by name rather than producing this line with
252    /// `-static` in front of it.
253    #[must_use]
254    pub fn glibc(sysroot: &Sysroot, mode: LinkMode) -> Self {
255        let lib = sysroot.lib();
256        LinkLine {
257            start: start_files(&lib, mode),
258            libraries: vec![lib.join("libc.so"), lib.join(BUILTINS)],
259            end: vec![lib.join("crtn.o")],
260        }
261    }
262
263    /// The line for a mingw-w64 link against this sysroot.
264    ///
265    /// One start file and no end file, which is the first thing that is different from every ELF
266    /// line above. `crt2.o` runs before `main` and calls it, `dllcrt2.o` is its counterpart for a
267    /// DLL, and there is no `crti.o` and no `crtn.o` because PE has no `.init` and `.fini` sections
268    /// for a pair of files to open and close. What those two bracket on ELF is done on Windows by a
269    /// table of pointers in the `.CRT$XC` sections, which the linker sorts by section name, so the
270    /// ordering problem the three groups exist for does not arise here.
271    ///
272    /// `crtbegin.o` and `crtend.o` are deliberately absent. They are GCC's files rather than
273    /// mingw-w64's, they bracket GCC's own list of constructors, and a toolchain that is not GCC
274    /// writes that list the way the platform writes it instead. Ours is not written yet: a mingw
275    /// link runs `main` and does not run a file scope constructor, which is a known gap that belongs
276    /// with the sysroot build rather than with the line, and the gap is in the codegen for the
277    /// format rather than here.
278    ///
279    /// The libraries are a set rather than one file, because the C library on Windows is several
280    /// DLLs and the CRT calls into the system ones. `libmingw32.a` holds the start code `crt2.o`
281    /// calls, `libmoldname.a` is the layer that gives the old unprefixed spellings of the names
282    /// Microsoft deprecated, `libmingwex.a` is everything C requires that msvcrt does not have, and
283    /// `libmsvcrt.a` is the import library for the CRT itself. Then the four Win32 libraries that
284    /// mingw-w64's own code calls into, which are on the line for the same reason they are on gcc's:
285    /// a program that uses none of them directly still reaches `kernel32` through `malloc`.
286    ///
287    /// The order is the one a single pass linker needs, which is GNU ld's PE port: a library after
288    /// everything that calls into it. `librucc_builtins.a` is last for the reason it is last on the
289    /// musl line, which is that the things before it call it and it calls none of them. lld's COFF
290    /// linker resolves archives to a fixed point and does not care about any of this, and writing
291    /// the line for the stricter of the two is what makes one line serve both.
292    #[must_use]
293    pub fn mingw(sysroot: &Sysroot, mode: LinkMode) -> Self {
294        let lib = sysroot.lib();
295        let start = match mode {
296            LinkMode::Shared => "dllcrt2.o",
297            _ => "crt2.o",
298        };
299        let libraries = [
300            "libmingw32.a",
301            "libmoldname.a",
302            "libmingwex.a",
303            "libmsvcrt.a",
304            "libadvapi32.a",
305            "libshell32.a",
306            "libuser32.a",
307            "libkernel32.a",
308            BUILTINS,
309        ];
310        LinkLine {
311            start: vec![lib.join(start)],
312            libraries: libraries.iter().map(|name| lib.join(name)).collect(),
313            end: Vec::new(),
314        }
315    }
316
317    /// Every input, in the order they reach the linker, with the caller's objects in the middle.
318    ///
319    /// The one function that knows the whole order, so that a caller cannot assemble the three
320    /// groups in the wrong sequence.
321    #[must_use]
322    pub fn with_objects(&self, objects: &[PathBuf]) -> Vec<PathBuf> {
323        let mut all = self.start.clone();
324        all.extend_from_slice(objects);
325        all.extend(self.libraries.iter().cloned());
326        all.extend(self.end.iter().cloned());
327        all
328    }
329}
330
331/// The start files for one mode, in the order they go on the line.
332///
333/// Two files, and which the first one is says how the reference to `main` inside it is written.
334/// `crt1.o` refers to it absolutely, `Scrt1.o` through the global offset table so that a loader may
335/// place the program anywhere, and `rcrt1.o` does that and relocates the program itself before
336/// `main` runs, which is what a static position independent executable needs because there is no
337/// loader to do it. A shared object has none of them.
338///
339/// `crti.o` is always second and `crtn.o` is always last, which is [`LinkLine`]'s three groups
340/// rather than anything here.
341fn start_files(lib: &Path, mode: LinkMode) -> Vec<PathBuf> {
342    let first = match mode {
343        LinkMode::Static | LinkMode::DynamicNoPie => Some("crt1.o"),
344        LinkMode::StaticPie => Some("rcrt1.o"),
345        LinkMode::Dynamic => Some("Scrt1.o"),
346        LinkMode::Shared => None,
347    };
348    first.map(|name| lib.join(name)).into_iter().chain([lib.join("crti.o")]).collect()
349}
350
351/// The absolute path the target's loader is installed at, or [`None`] for a target that has none.
352///
353/// The libc picks the table and the architecture picks the row. [`None`] is the right answer for
354/// three different reasons: a freestanding target has no libc, WASI has no loader of this kind at
355/// all, and Darwin and Windows have one whose path is not written on the link line.
356#[must_use]
357pub fn loader(target: TargetTuple) -> Option<&'static str> {
358    match (target.os(), target.env()) {
359        (Os::Linux, Env::Musl) => Some(musl_loader(target)),
360        (Os::Linux, Env::Gnu) => Some(glibc_loader(target)),
361        // Bionic's is one path per word size and not one per architecture, because Android fixes
362        // the filesystem layout rather than leaving it to the port.
363        (Os::Linux, Env::Android) => Some(match target.pointer_width() {
364            64 => "/system/bin/linker64",
365            _ => "/system/bin/linker",
366        }),
367        _ => None,
368    }
369}
370
371/// The absolute path musl's loader is installed at on the target.
372///
373/// It goes in the program header of a dynamically linked binary, so it is a string about the target
374/// machine's filesystem and not about ours, and it has to be right without anything to check it
375/// against at link time. A wrong one produces a binary that the kernel refuses to start with a
376/// message about a missing file that is on nobody's disk.
377///
378/// 32-bit ARM is the row with two answers, because musl names the hard float and soft float builds
379/// differently and they are not interchangeable. PowerPC is the other row with two, and there the
380/// endianness picks, because musl treats the two byte orders as separate ports.
381#[must_use]
382pub fn musl_loader(target: TargetTuple) -> &'static str {
383    match target.arch() {
384        Arch::X86_64 => match target.data_model() {
385            DataModel::Ilp32On64 => "/lib/ld-musl-x32.so.1",
386            _ => "/lib/ld-musl-x86_64.so.1",
387        },
388        Arch::X86 => "/lib/ld-musl-i386.so.1",
389        Arch::Aarch64 | Arch::Arm64Ec => "/lib/ld-musl-aarch64.so.1",
390        Arch::Arm => match target.resolved_abi() {
391            Abi::DoubleFloat => "/lib/ld-musl-armhf.so.1",
392            _ => "/lib/ld-musl-arm.so.1",
393        },
394        Arch::Riscv64 => "/lib/ld-musl-riscv64.so.1",
395        Arch::Riscv32 => "/lib/ld-musl-riscv32.so.1",
396        Arch::S390x => "/lib/ld-musl-s390x.so.1",
397        Arch::PowerPc64 => match target.endian() {
398            Endian::Little => "/lib/ld-musl-powerpc64le.so.1",
399            Endian::Big => "/lib/ld-musl-powerpc64.so.1",
400        },
401        Arch::LoongArch64 => "/lib/ld-musl-loongarch64.so.1",
402        // musl has no wasm port and wasm has no loader. The caller that gets here asked for a
403        // dynamic musl link on a target with neither, which is a driver bug rather than a user
404        // one, and a path that cannot exist is a better report than a plausible wrong one.
405        Arch::Wasm32 => "/lib/ld-musl-none.so.1",
406    }
407}
408
409/// The absolute path glibc's loader is installed at on the target.
410///
411/// A different table from musl's and not a different spelling of it. musl names every loader after
412/// the architecture in one directory; glibc's names come from each port's history, so three of them
413/// are called `ld64.so` with a number that means something different per architecture, two are in
414/// `/lib64` rather than `/lib`, and i386's carries no architecture in its name at all because it was
415/// the only one when it was named.
416///
417/// The rows with more than one answer are the ones where the loader and the program have to agree
418/// about register usage. 32-bit ARM has the hard float and soft float split, RISC-V and LoongArch
419/// spell the float ABI and the data model into the name, and AArch64 has a byte order in it.
420/// Getting one wrong produces a binary the kernel will not start, with a message about a missing
421/// file, and it is a string nothing at link time can check.
422#[must_use]
423pub fn glibc_loader(target: TargetTuple) -> &'static str {
424    let narrow = target.data_model() == DataModel::Ilp32On64;
425    let hard = matches!(target.resolved_abi(), Abi::DoubleFloat);
426    match target.arch() {
427        Arch::X86_64 if narrow => "/libx32/ld-linux-x32.so.2",
428        Arch::X86_64 => "/lib64/ld-linux-x86-64.so.2",
429        Arch::X86 => "/lib/ld-linux.so.2",
430        Arch::Aarch64 | Arch::Arm64Ec => match (target.endian(), narrow) {
431            (Endian::Little, false) => "/lib/ld-linux-aarch64.so.1",
432            (Endian::Little, true) => "/lib/ld-linux-aarch64_ilp32.so.1",
433            (Endian::Big, false) => "/lib/ld-linux-aarch64_be.so.1",
434            (Endian::Big, true) => "/lib/ld-linux-aarch64_be_ilp32.so.1",
435        },
436        // The one row where the number differs rather than the name. ARM's loader went to 3 when
437        // EABI replaced OABI, and the hard float build is a separate file because passing a double
438        // in a float register is not compatible with passing it in a pair of integer ones.
439        Arch::Arm if hard => "/lib/ld-linux-armhf.so.3",
440        Arch::Arm => "/lib/ld-linux.so.3",
441        Arch::Riscv64 if hard => "/lib/ld-linux-riscv64-lp64d.so.1",
442        Arch::Riscv64 => "/lib/ld-linux-riscv64-lp64.so.1",
443        Arch::Riscv32 if hard => "/lib/ld-linux-riscv32-ilp32d.so.1",
444        Arch::Riscv32 => "/lib/ld-linux-riscv32-ilp32.so.1",
445        // `ld64` here means 64-bit z/Architecture and the 1 is glibc's ABI version for the port,
446        // which is not the 2 on PowerPC's file of the same name. It is in `/lib` and PowerPC's is in
447        // `/lib64`, so the two rows have nothing in common but the stem.
448        Arch::S390x => "/lib/ld64.so.1",
449        // ELFv2, both byte orders, which is the only PowerPC ABI
450        // `spec/cross-compile/06-abis.md` admits. The ELFv1 big-endian world uses `ld64.so.1` and
451        // is out of scope, so a wrong answer here is impossible rather than merely unlikely.
452        Arch::PowerPc64 => "/lib64/ld64.so.2",
453        Arch::LoongArch64 if hard => "/lib64/ld-linux-loongarch-lp64d.so.1",
454        Arch::LoongArch64 => "/lib64/ld-linux-loongarch-lp64s.so.1",
455        // There is no glibc for wasm and no loader for it either. Same reasoning as the musl table
456        // above: a path nothing will ever open beats a plausible one.
457        Arch::Wasm32 => "/lib/ld-linux-wasm32.so.1",
458    }
459}