Skip to main content

rucc_codegen/
elsewhere.rs

1//! Which names this file may not work the address of out for itself.
2//!
3//! Design: `spec/11-asm-objects-debug.md` section 11.3.
4//!
5//! Everything this compiler emits is position independent, so the address of a name is the distance
6//! from the instruction asking to the name, and that distance is a number the assembler leaves a
7//! hole for and the linker fills in. The linker can only fill it in when it is putting both ends in
8//! the same program. A name this file only declares may turn out to be in a shared library, and
9//! then there is no such distance and the link fails rather than guessing one.
10//!
11//! The way round it is a table: the linker gives the name one slot in the global offset table, fills
12//! the slot with whatever address the name ends up at, and the code loads the address out of the
13//! slot instead of working it out. The slot is in this program, so the distance to the slot is a
14//! number the linker has. It costs a load, and the linker takes the load back out again when the
15//! name turns out to have been in this program all along.
16//!
17//! Which names need it is a fact about the whole module and the code generator sees one function at
18//! a time, which is why this is worked out first and handed in rather than asked at the point of
19//! use.
20//!
21//! It is also a fact about which link is coming, which is [`rucc_ir::Pic`] and is why this is built
22//! from more than the module. Under `-fPIC` the link may be one that produces a shared library, and
23//! then a name this file exports is one the dynamic linker may find a different definition of, so
24//! reaching it from the instruction pointer would reach the wrong one. The static linker will not
25//! let that happen quietly: `R_X86_64_PC32` against a name it can see is replaceable is refused
26//! when it is making a shared object, which is how tamnd/rucc#756 was found.
27//!
28//! A thread-local variable is the other name this file cannot work the address of out for itself,
29//! and it is here for the same reason: which names are thread-local is a fact about the module and
30//! the code generator sees one function at a time. It is a harder case than the one above rather
31//! than a variation of it, because there is no address to work out at all. Every thread has its own
32//! copy, so what the link can say is only where the variable sits inside the block a thread gets,
33//! and turning that into an address is something the running program does. See [`Elsewhere::thread`].
34
35use std::collections::HashSet;
36
37use rucc_base::Symbol;
38use rucc_ir::{AttrSet, Linkage, Module, Pic, Visibility};
39use rucc_target::ObjectFormat;
40
41/// The names whose address only the linker knows.
42///
43/// Two ways in, and the first one holds whichever link is coming. A function this file only
44/// declares is one, because a function cannot be copied: it has exactly one address that every
45/// object in the program has to agree on, or two pointers to it compare unequal, so the one address
46/// is what the table holds and what everything reads. A variable can be copied, and in an
47/// executable it is, since the linker answers a reference to one another object defines by making
48/// room for it here and copying it there, so the name really does end up somewhere this file can
49/// measure to.
50///
51/// The second way in is `-fPIC`, where the link may be one that produces a shared library and the
52/// copying does not happen. There every replaceable name is in here, defined or not and function or
53/// variable, because the definition the process ends up using may be in another object however
54/// plainly this file defines it. What is not in here is what `-fPIC` costs nothing for: a `static`,
55/// and a name marked hidden or protected, which is the reason `-fPIC -fvisibility=hidden` is the
56/// combination a library that cares about its own speed is built with.
57///
58/// Both ways in are shut on a format with no such table, which is COFF. See `Self::table` for why
59/// the question has a different answer there rather than no answer.
60///
61/// A name this module has never heard of is not in here. Nothing the front end writes produces one,
62/// and treating an unknown name as a function would put the addresses the instrumentation takes of
63/// its own tables through a table of their own for no reason.
64///
65/// A thread-local variable is kept separately and answered by [`Self::thread`], because the two
66/// questions have different answers rather than one being a case of the other: the table slot of an
67/// ordinary name holds its address and the slot of a thread-local holds an offset, and reading
68/// either as though it were the other is a wrong answer rather than a slower one.
69#[derive(Debug, Clone, Default, PartialEq, Eq)]
70pub struct Elsewhere {
71    names: HashSet<Symbol>,
72    threads: HashSet<Symbol>,
73    twice: HashSet<Symbol>,
74    described: bool,
75}
76
77impl Elsewhere {
78    /// The names that link cannot reach from the instruction pointer.
79    ///
80    /// `copies` is whether the linker answers a reference from the instruction pointer to a
81    /// variable another object defines by copying the variable into the executable. x86-64 does,
82    /// even in a position independent executable. AArch64 and RISC-V do not: GNU ld refuses an
83    /// `adrp` against such a variable when it makes a PIE, which is the default link on every
84    /// distribution, and gcc reads the address out of the table there instead.
85    #[must_use]
86    pub fn of(module: &Module, pic: Pic, format: ObjectFormat, copies: bool) -> Self {
87        let threads = module
88            .globals()
89            .filter(|&id| module[id].tls.is_some())
90            .map(|id| module[id].name)
91            .collect();
92        let twice = module
93            .funcs()
94            .filter(|&id| module[id].attrs.set.contains(AttrSet::RETURNS_TWICE))
95            .map(|id| module[id].name)
96            .collect();
97        let described = format == ObjectFormat::MachO;
98        Self { threads, twice, described, ..Self::table(module, pic, format, copies) }
99    }
100
101    /// The half of the above that is about the global offset table, which is the older one.
102    ///
103    /// Empty on a format that has no such table. COFF is the one, and it is not that the question
104    /// goes unanswered there: a name this file only declares is reached from the instruction
105    /// pointer like any other, because whatever supplies it supplies a piece of this image to
106    /// measure to. A name the link resolves out of another object is in the image, and a name that
107    /// comes from a DLL arrives through an import library, which is an archive member holding a
108    /// jump under the plain name, so the name still stands for an address in this image and every
109    /// object that takes it gets the one the linker kept. Measured against gcc 13.2 for
110    /// `x86_64-w64-mingw32`, which writes `leaq other(%rip), %rax` for the address of a function it
111    /// has only seen declared. Asking for a table there instead reached the object writer as a
112    /// relocation it has no way to write, which is what tamnd/rucc#1443 was.
113    fn table(module: &Module, pic: Pic, format: ObjectFormat, copies: bool) -> Self {
114        if format == ObjectFormat::Coff {
115            return Self::default();
116        }
117        let funcs = module.funcs().filter(|&id| {
118            let func = &module[id];
119            func.is_declaration() || pic.replaceable(func.linkage, func.visibility)
120        });
121        // A weak variable nothing here defines is the one variable the copying above does not
122        // cover, since there may be no definition anywhere to copy and then its address is null. The
123        // distance from here to null is not a number the linker has, so lld refuses the
124        // `R_X86_64_PC32` and gcc reads the address out of a slot, which the linker fills with zero.
125        //
126        // Mach-O does no copying at all. `dyld` has no copy relocation, so a variable a library
127        // defines stays in the library and the only way to it is the slot. That is every variable
128        // this file only declares, unless it is hidden and so promised to be in the same image,
129        // and it is what clang writes: `_ext@GOTPAGE` on arm64 and `_ext@GOTPCREL` on x86-64.
130        let uncopied = format == ObjectFormat::MachO || !copies;
131        let globals = module
132            .globals()
133            .filter(|&id| {
134                let global = &module[id];
135                (global.is_declaration()
136                    && (global.linkage == Linkage::Weak
137                        || (uncopied && global.visibility == Visibility::Default)))
138                    || pic.replaceable(global.linkage, global.visibility)
139            })
140            .map(|id| module[id].name);
141        // An alias is a symbol of its own with a linkage and a visibility of its own, so it answers
142        // this for itself the same way it answered the visibility question in #752. What it points
143        // at is a separate name and is decided separately, which is what `weak, alias,
144        // visibility("hidden")` over an exported definition needs.
145        let aliases = module
146            .aliases()
147            .filter(|&id| pic.replaceable(module[id].linkage, module[id].visibility))
148            .map(|id| module[id].name);
149        funcs.map(|id| module[id].name).chain(globals).chain(aliases).collect()
150    }
151
152    /// Whether the address of that name has to be read out of the global offset table.
153    #[must_use]
154    pub fn holds(&self, name: Symbol) -> bool {
155        self.names.contains(&name)
156    }
157
158    /// Whether that name is a variable every thread has its own copy of.
159    ///
160    /// Asked before [`Self::holds`] and not instead of it, because the two answers are about
161    /// different things: a thread-local variable that another object may define is still reached
162    /// the same way, since the table slot holds an offset that is the same for every copy and the
163    /// question of whose copy is answered by the segment register rather than by the link.
164    #[must_use]
165    pub fn thread(&self, name: Symbol) -> bool {
166        self.threads.contains(&name)
167    }
168
169    /// Whether a call to that name may come back more than once, because a declaration of it said
170    /// `returns_twice`.
171    ///
172    /// Not a question about addresses like the two above, but it is the same kind of fact: it is
173    /// about the module, the function it changes is a different one from the function it is
174    /// written on, and the code generator sees one function at a time. See
175    /// [`crate::tail::comes_back`] for what the caller does with it.
176    #[must_use]
177    pub fn twice(&self, name: Symbol) -> bool {
178        self.twice.contains(&name)
179    }
180
181    /// Whether a thread-local variable is reached by calling through its descriptor, which is how
182    /// Mach-O does it on both architectures.
183    ///
184    /// The slot the table holds for such a variable is the address of the descriptor rather than an
185    /// offset from the thread pointer, and the first word of the descriptor is a function that takes
186    /// that address and gives back this thread's copy. So there is no thread pointer to add to,
187    /// and the answer is the value the call returns.
188    #[must_use]
189    pub const fn described(&self) -> bool {
190        self.described
191    }
192}
193
194/// The same set, written out by hand.
195///
196/// [`Elsewhere::of`] is how the driver builds one and is the only way a compilation does. This is
197/// for a test that wants to lower one function and say what is outside the file without building a
198/// module for it to be outside of.
199impl FromIterator<Symbol> for Elsewhere {
200    fn from_iter<T: IntoIterator<Item = Symbol>>(names: T) -> Self {
201        Self { names: names.into_iter().collect(), ..Self::default() }
202    }
203}
204
205impl Elsewhere {
206    /// The same set with those names said to be thread-local, for a test that lowers one function.
207    #[must_use]
208    pub fn with_threads<T: IntoIterator<Item = Symbol>>(mut self, threads: T) -> Self {
209        self.threads = threads.into_iter().collect();
210        self
211    }
212
213    /// The same set with thread-locals reached through a descriptor, for a test that lowers one
214    /// function the way Mach-O would.
215    #[must_use]
216    pub const fn with_descriptors(mut self) -> Self {
217        self.described = true;
218        self
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    use rucc_base::Interner;
227    use rucc_ir::{Alias, Func, Global, Linkage, Signature, TlsModel, Visibility};
228    use rucc_target::{Arch, Env, Os, TargetInfo, Triple};
229
230    /// A module with one of everything: a function with a body and one without, a variable with an
231    /// image and one without, a `static`, a hidden export, an alias and a thread-local.
232    fn module(names: &mut Interner) -> Module {
233        let target = TargetInfo::new(Triple::new(Arch::X86_64, Os::Linux, Env::Gnu));
234        let mut module = Module::new(names.intern("test.c"), &target);
235        let mut defined = Func::new(names.intern("here"), Signature::new());
236        defined.create_block();
237        module.add_func(defined);
238        module.add_func(Func::new(names.intern("exit"), Signature::new()));
239
240        let mut kept = Global::new(names.intern("kept"), 4, 4);
241        kept.init = Some(module.push_data(&[]));
242        module.add_global(kept);
243        module.add_global(Global::new(names.intern("away"), 4, 4));
244
245        let mut quiet = Global::new(names.intern("quiet"), 4, 4);
246        quiet.init = Some(module.push_data(&[]));
247        quiet.linkage = Linkage::Internal;
248        module.add_global(quiet);
249
250        let mut shy = Global::new(names.intern("shy"), 4, 4);
251        shy.init = Some(module.push_data(&[]));
252        shy.visibility = Visibility::Hidden;
253        module.add_global(shy);
254
255        let mut own = Global::new(names.intern("own"), 4, 4);
256        own.init = Some(module.push_data(&[]));
257        own.tls = Some(TlsModel::GlobalDynamic);
258        module.add_global(own);
259
260        module.add_alias(Alias::new(names.intern("second"), names.intern("here")));
261        module
262    }
263
264    #[test]
265    fn a_variable_every_thread_has_its_own_copy_of_is_one() {
266        let mut names = Interner::new();
267        let module = module(&mut names);
268        let elsewhere = Elsewhere::of(&module, Pic::Executable, ObjectFormat::Elf, true);
269        assert!(elsewhere.thread(names.intern("own")));
270    }
271
272    /// The question the other five ask is a different question, and a variable that is not
273    /// thread-local answering yes to this one would put an offset where an address belongs.
274    #[test]
275    fn an_ordinary_variable_is_not() {
276        let mut names = Interner::new();
277        let module = module(&mut names);
278        let elsewhere = Elsewhere::of(&module, Pic::Executable, ObjectFormat::Elf, true);
279        for name in ["kept", "away", "quiet", "shy", "here"] {
280            assert!(!elsewhere.thread(names.intern(name)), "{name} was called thread-local");
281        }
282    }
283
284    #[test]
285    fn a_function_this_file_only_declares_is_reached_through_the_table() {
286        let mut names = Interner::new();
287        let module = module(&mut names);
288        let elsewhere = Elsewhere::of(&module, Pic::Executable, ObjectFormat::Elf, true);
289        assert!(elsewhere.holds(names.intern("exit")));
290    }
291
292    #[test]
293    fn a_function_this_file_defines_is_not() {
294        let mut names = Interner::new();
295        let module = module(&mut names);
296        let elsewhere = Elsewhere::of(&module, Pic::Executable, ObjectFormat::Elf, true);
297        assert!(!elsewhere.holds(names.intern("here")));
298    }
299
300    #[test]
301    fn a_name_the_module_does_not_carry_at_all_is_not() {
302        let mut names = Interner::new();
303        let module = module(&mut names);
304        let elsewhere = Elsewhere::of(&module, Pic::Executable, ObjectFormat::Elf, true);
305        assert!(!elsewhere.holds(names.intern("nowhere")));
306    }
307
308    /// The whole of what an executable pays, which is one entry for the one function it calls in a
309    /// library. Every variable is reached from the instruction pointer, the one it does not define
310    /// included, because the linker copies that one in here.
311    #[test]
312    fn an_executable_pays_for_the_functions_and_for_nothing_else() {
313        let mut names = Interner::new();
314        let module = module(&mut names);
315        let elsewhere = Elsewhere::of(&module, Pic::Executable, ObjectFormat::Elf, true);
316        for name in ["kept", "away", "quiet", "shy", "second"] {
317            assert!(!elsewhere.holds(names.intern(name)), "{name} was in the table");
318        }
319    }
320
321    /// A weak variable nothing defines may be at zero, which no distance from the code reaches.
322    #[test]
323    fn a_weak_variable_this_file_only_declares_is_reached_through_the_table() {
324        let mut names = Interner::new();
325        let mut module = module(&mut names);
326        let mut maybe = Global::new(names.intern("maybe"), 4, 4);
327        maybe.linkage = Linkage::Weak;
328        module.add_global(maybe);
329        let elsewhere = Elsewhere::of(&module, Pic::Executable, ObjectFormat::Elf, true);
330        assert!(elsewhere.holds(names.intern("maybe")));
331    }
332
333    /// Mach-O never copies a variable into the executable, so the one this file only declares is
334    /// read through the table even in a program, and the ones it defines are still reached
335    /// directly.
336    #[test]
337    fn a_mach_o_executable_pays_for_the_variables_it_does_not_define_as_well() {
338        let mut names = Interner::new();
339        let mut module = module(&mut names);
340        let mut near = Global::new(names.intern("near"), 4, 4);
341        near.visibility = Visibility::Hidden;
342        module.add_global(near);
343        let elsewhere = Elsewhere::of(&module, Pic::Executable, ObjectFormat::MachO, true);
344        assert!(elsewhere.holds(names.intern("away")));
345        for name in ["kept", "quiet", "shy", "near"] {
346            assert!(!elsewhere.holds(names.intern(name)), "{name} was in the table");
347        }
348    }
349
350    /// An AArch64 executable pays for a variable it only declares, because the linker there makes
351    /// no copy for an `adrp` and refuses one in a PIE. bzip2 reading `stderr` is what found it.
352    #[test]
353    fn an_executable_that_gets_no_copies_pays_for_the_variables_it_does_not_define() {
354        let mut names = Interner::new();
355        let module = module(&mut names);
356        let elsewhere = Elsewhere::of(&module, Pic::Executable, ObjectFormat::Elf, false);
357        assert!(elsewhere.holds(names.intern("away")));
358        for name in ["kept", "quiet", "shy"] {
359            assert!(!elsewhere.holds(names.intern(name)), "{name} was in the table");
360        }
361        let copied = Elsewhere::of(&module, Pic::Executable, ObjectFormat::Elf, true);
362        assert!(!copied.holds(names.intern("away")));
363    }
364
365    /// A library pays for every name it exports, defined here or not, because the definition the
366    /// process uses may be in another object however plainly this file defines it.
367    #[test]
368    fn a_library_pays_for_every_name_something_else_may_define() {
369        let mut names = Interner::new();
370        let module = module(&mut names);
371        let elsewhere = Elsewhere::of(&module, Pic::Library, ObjectFormat::Elf, true);
372        for name in ["here", "exit", "kept", "away", "second"] {
373            assert!(elsewhere.holds(names.intern(name)), "{name} was not in the table");
374        }
375    }
376
377    /// A format with no table asks nothing of anybody, which is not the same as asking and being
378    /// told no. The name of a function this file only declares stands for an address in the image
379    /// on this format whether the link finds it in another object or in an import library, so the
380    /// instruction pointer reaches it and there is nothing left over to put in a table. gcc writes
381    /// the same `leaq other(%rip)` for the same declaration.
382    #[test]
383    fn a_format_with_no_table_puts_nothing_in_one() {
384        let mut names = Interner::new();
385        let module = module(&mut names);
386        let elsewhere = Elsewhere::of(&module, Pic::Executable, ObjectFormat::Coff, true);
387        for name in ["here", "exit", "kept", "away", "quiet", "shy", "second"] {
388            assert!(!elsewhere.holds(names.intern(name)), "{name} was in the table");
389        }
390    }
391
392    /// And the flag that fills the table on the other format does not fill it here either, since
393    /// there is no interposition on this one for it to be about.
394    #[test]
395    fn a_format_with_no_table_does_not_grow_one_under_the_library_flag() {
396        let mut names = Interner::new();
397        let module = module(&mut names);
398        let elsewhere = Elsewhere::of(&module, Pic::Library, ObjectFormat::Coff, true);
399        for name in ["here", "exit", "kept", "away", "second"] {
400            assert!(!elsewhere.holds(names.intern(name)), "{name} was in the table");
401        }
402    }
403
404    /// The other question this type answers is not the table's, so it keeps its answer whatever the
405    /// format. What a target with no thread-local storage does about it is the writer's refusal
406    /// rather than a name quietly left out here.
407    #[test]
408    fn a_format_with_no_table_still_says_which_variable_every_thread_has_a_copy_of() {
409        let mut names = Interner::new();
410        let module = module(&mut names);
411        let elsewhere = Elsewhere::of(&module, Pic::Executable, ObjectFormat::Coff, true);
412        assert!(elsewhere.thread(names.intern("own")));
413    }
414
415    /// And not for the names nothing outside can reach, which is what makes `-fvisibility=hidden`
416    /// worth writing next to it.
417    #[test]
418    fn a_library_pays_nothing_for_a_name_nothing_outside_it_can_see() {
419        let mut names = Interner::new();
420        let module = module(&mut names);
421        let elsewhere = Elsewhere::of(&module, Pic::Library, ObjectFormat::Elf, true);
422        assert!(!elsewhere.holds(names.intern("quiet")));
423        assert!(!elsewhere.holds(names.intern("shy")));
424    }
425}