Skip to main content

rucc_asm/
format.rs

1//! What an assembler is told about a function or a variable, which is the object format's answer
2//! rather than the machine's.
3//!
4//! Design: `spec/11-asm-objects-debug.md` section 11.3, which is about the object files
5//! themselves. The directives here are the same facts said in text: which section code and data go
6//! in, how a symbol is spelled, which symbols leave the file, and where each one ends.
7//!
8//! They are not the same on the three formats and the differences are not cosmetic. A Mach-O
9//! symbol carries an underscore in front of the C name and an ELF one does not, so a listing that
10//! got that wrong would fail to link against every library on the machine. A local label is
11//! spelled `.L` on ELF and COFF and `L` on Mach-O, and a label that is not spelled the local way
12//! ends up in the symbol table, where it is a name a debugger and a backtrace will show. And ELF
13//! wants a marker saying the stack is not executable, whose absence makes it executable, which
14//! section 11.3 calls out as a real and recurring security bug.
15
16use std::fmt::Write as _;
17
18use rucc_mir as mir;
19use rucc_object::{Alias, Binding, Place, Visibility};
20use rucc_target::ObjectFormat;
21
22use crate::data::Variable;
23
24/// The directives one object format wraps a function in.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum Directives {
27    /// ELF, which is Linux and the freestanding targets.
28    Elf,
29    /// Mach-O, which is Apple's.
30    MachO,
31    /// COFF, which is Windows.
32    Coff,
33}
34
35impl Directives {
36    /// The directives that go with that object format.
37    #[must_use]
38    pub const fn of(format: ObjectFormat) -> Directives {
39        match format {
40            ObjectFormat::Elf => Directives::Elf,
41            ObjectFormat::MachO => Directives::MachO,
42            ObjectFormat::Coff => Directives::Coff,
43            // No assembler in this crate writes wasm, and the caller that asked has a target it
44            // cannot emit for. ELF's directives are the ones nothing here depends on being right
45            // for a target it will not reach.
46            ObjectFormat::Wasm => Directives::Elf,
47        }
48    }
49
50    /// What goes in front of a C name to make the name the linker sees.
51    ///
52    /// Mach-O keeps the underscore that every Unix linker once had, so `main` in C is `_main` in
53    /// the object, and a listing that leaves it off refers to a symbol nothing defines.
54    #[must_use]
55    pub const fn symbol(self) -> &'static str {
56        match self {
57            Directives::Elf | Directives::Coff => "",
58            Directives::MachO => "_",
59        }
60    }
61
62    /// What goes in front of a label that belongs to one function and leaves no symbol behind.
63    #[must_use]
64    pub const fn local(self) -> &'static str {
65        match self {
66            Directives::Elf | Directives::Coff => ".L",
67            Directives::MachO => "L",
68        }
69    }
70
71    /// The directive that opens the section code goes in.
72    #[must_use]
73    pub const fn text(self) -> &'static str {
74        match self {
75            Directives::Elf | Directives::Coff => "\t.text",
76            Directives::MachO => "\t.section\t__TEXT,__text,regular,pure_instructions",
77        }
78    }
79
80    /// What is said about a function before its first instruction.
81    ///
82    /// The binding is written the way it is written for a variable, and a local one gets no
83    /// directive at all: a name no directive mentions is still in the symbol table, as a local,
84    /// which is what `static` is. Windows says the same thing as a storage class, where three is
85    /// the local one and two the rest.
86    ///
87    /// `align` is in bytes and is a power of two, and the padding is `0x90` because the space in
88    /// front of a function is reached by falling off the end of the one before it.
89    pub fn open(
90        self,
91        out: &mut String,
92        name: &str,
93        align: u32,
94        binding: Binding,
95        visibility: Visibility,
96    ) {
97        let symbol = self.symbol();
98        let _ = writeln!(out, "\t.p2align\t{}, 0x90", align.max(1).trailing_zeros());
99        match binding {
100            Binding::Global => {
101                let _ = writeln!(out, "\t.globl\t{symbol}{name}");
102            }
103            Binding::Weak => {
104                let _ = writeln!(out, "\t.weak\t{symbol}{name}");
105            }
106            Binding::Local => {}
107        }
108        self.seen(out, name, binding, visibility);
109        match self {
110            Directives::Elf => {
111                let _ = writeln!(out, "\t.type\t{name}, @function");
112            }
113            // Windows says the storage class and the type code, and thirty two is a function.
114            Directives::Coff => {
115                let scl = if binding == Binding::Local { 3 } else { 2 };
116                let _ = writeln!(out, "\t.def\t{name}\n\t.scl\t{scl}\n\t.type\t32\n\t.endef");
117            }
118            Directives::MachO => {}
119        }
120        let _ = writeln!(out, "{symbol}{name}:");
121    }
122
123    /// What is said about how far a name reaches outside a shared library, which is nothing at
124    /// all in the ordinary case.
125    ///
126    /// A local name gets no directive whatever was asked for. `static` is already invisible to
127    /// everything outside the file, so there is no dynamic symbol table for it to be in or out of,
128    /// and gcc writes no visibility directive for one either.
129    ///
130    /// ELF says both of the other two and says them the same way an assembler expects. Mach-O has
131    /// one of them: `.private_extern` is a symbol that leaves this object and does not leave the
132    /// library, which is what hidden means, and there is no Mach-O spelling of protected because
133    /// the format has no way to say a symbol is exported and cannot be interposed. COFF has
134    /// neither, since what leaves a Windows DLL is decided by an export table the linker is given
135    /// rather than by a bit on each symbol.
136    pub fn seen(self, out: &mut String, name: &str, binding: Binding, visibility: Visibility) {
137        if binding == Binding::Local || visibility == Visibility::Default {
138            return;
139        }
140        let symbol = self.symbol();
141        match (self, visibility) {
142            (Directives::Elf, Visibility::Hidden) => {
143                let _ = writeln!(out, "\t.hidden\t{name}");
144            }
145            (Directives::Elf, Visibility::Protected) => {
146                let _ = writeln!(out, "\t.protected\t{name}");
147            }
148            (Directives::MachO, Visibility::Hidden) => {
149                let _ = writeln!(out, "\t.private_extern\t{symbol}{name}");
150            }
151            (Directives::MachO, Visibility::Protected) | (Directives::Coff, _) => {}
152            (_, Visibility::Default) => unreachable!("returned above"),
153        }
154    }
155
156    /// The directive that opens the section a variable goes in.
157    ///
158    /// The three formats disagree about the names and about how much has to be said. ELF and COFF
159    /// have a directive per section that every assembler knows, and both want the flags spelled
160    /// out for a section the program named, since nothing else says whether it may be written to.
161    /// Mach-O has one directive and a segment in front of every section name.
162    pub fn section(self, out: &mut String, place: &Place) {
163        match (self, place) {
164            // A tentative definition is not in a section at all, and the caller is what decides
165            // that. It is answered here as the section it would otherwise have gone in, so that
166            // the match stays about sections and nothing has to be said twice.
167            (Directives::Elf | Directives::Coff, Place::Written | Place::Merged) => {
168                out.push_str("\t.data\n");
169            }
170            (Directives::Elf | Directives::Coff, Place::Zero) => out.push_str("\t.bss\n"),
171            (Directives::Elf, Place::ReadOnly) => out.push_str("\t.section\t.rodata\n"),
172            (Directives::Elf, Place::RelocReadOnly { local }) => {
173                let name = if *local { ".data.rel.ro.local" } else { ".data.rel.ro" };
174                let _ = writeln!(out, "\t.section\t{name},\"aw\",@progbits");
175            }
176            // COFF has no section of this kind and needs none. A Windows image is relocated as a
177            // whole rather than a symbol at a time, and the loader makes whatever pages it has to
178            // write writable for as long as it is writing them and puts them back afterwards, so
179            // an address in a read only section costs a base relocation and nothing else.
180            (Directives::Coff, Place::ReadOnly | Place::RelocReadOnly { .. }) => {
181                out.push_str("\t.section\t.rdata,\"dr\"\n");
182            }
183            (Directives::Elf, Place::Named(name)) => {
184                let _ = writeln!(out, "\t.section\t{name},\"aw\",@progbits");
185            }
186            (Directives::Coff, Place::Named(name)) => {
187                let _ = writeln!(out, "\t.section\t{name},\"dw\"");
188            }
189            (Directives::MachO, Place::ReadOnly) => out.push_str("\t.section\t__TEXT,__const\n"),
190            // Mach-O has the same problem and the same answer under a different name. A section in
191            // `__TEXT` is never writable, so a constant holding an address goes in `__DATA,__const`
192            // instead, which `dyld` writes and then protects. There is no `.local` half: the layout
193            // hint is an ELF linker's, and this one has nothing to do with it.
194            (Directives::MachO, Place::RelocReadOnly { .. }) => {
195                out.push_str("\t.section\t__DATA,__const\n");
196            }
197            // A Mach-O section name carries the segment it is in, so a program that named one
198            // named both halves and there is nothing to add to it.
199            (Directives::MachO, Place::Named(name)) => {
200                let _ = writeln!(out, "\t.section\t{name}");
201            }
202            (Directives::MachO, _) => out.push_str("\t.section\t__DATA,__data\n"),
203        }
204    }
205
206    /// What is said about a variable before its image, and whether an image follows.
207    ///
208    /// Two kinds of variable are one directive rather than a section, a label and bytes. A
209    /// tentative definition is a request to the linker for that much zeroed space on every format,
210    /// and on Mach-O so is a variable whose image is all zeros, because the section that would
211    /// hold it is one nothing may write bytes into.
212    pub fn variable(self, out: &mut String, var: &Variable) -> bool {
213        let symbol = self.symbol();
214        let align = var.align.max(1).trailing_zeros();
215        match (self, &var.place) {
216            (_, Place::Merged) => {
217                let comm = if var.binding == Binding::Local { ".lcomm" } else { ".comm" };
218                let name = &var.name;
219                let _ = writeln!(out, "\t{comm}\t{symbol}{name},{},{}", var.size, var.align);
220                return false;
221            }
222            (Directives::MachO, Place::Zero) => {
223                let name = &var.name;
224                let _ =
225                    writeln!(out, "\t.zerofill\t__DATA,__bss,{symbol}{name},{},{align}", var.size);
226                return false;
227            }
228            _ => {}
229        }
230        self.section(out, &var.place);
231        match var.binding {
232            Binding::Global => {
233                let _ = writeln!(out, "\t.globl\t{symbol}{}", var.name);
234            }
235            Binding::Weak => {
236                let _ = writeln!(out, "\t.weak\t{symbol}{}", var.name);
237            }
238            // Nothing, which is what makes it invisible outside the file. A name no directive
239            // mentions is still in the symbol table as a local one, which is what `static` is.
240            Binding::Local => {}
241        }
242        self.seen(out, &var.name, var.binding, var.visibility);
243        let _ = writeln!(out, "\t.p2align\t{align}");
244        if self == Directives::Elf {
245            let _ = writeln!(out, "\t.type\t{}, @object", var.name);
246        }
247        let _ = writeln!(out, "{symbol}{}:", var.name);
248        true
249    }
250
251    /// What is said about a function after its last instruction.
252    ///
253    /// The size, on the format that has one. It is written as the distance from the label to here
254    /// rather than as a number, because the assembler is the one that knows how long an
255    /// instruction turned out to be and this file is what it is about to find out from.
256    pub fn close(self, out: &mut String, name: &str) {
257        if self == Directives::Elf {
258            let _ = writeln!(out, "\t.size\t{name}, .-{name}");
259        }
260    }
261
262    /// A second name for something the file already wrote down.
263    ///
264    /// The binding and then `.set`, which is all gcc writes and all an assembler needs: the type
265    /// and the size of the new symbol are taken from the old one, so writing them again would
266    /// only be a second chance to disagree. Nothing opens a section first, because the symbol is
267    /// an entry in a table rather than a byte of anything, and no `.size` closes it for the same
268    /// reason.
269    pub fn alias(self, out: &mut String, alias: &Alias) {
270        let symbol = self.symbol();
271        match alias.binding {
272            Binding::Global => {
273                let _ = writeln!(out, "\t.globl\t{symbol}{}", alias.name);
274            }
275            Binding::Weak => {
276                let _ = writeln!(out, "\t.weak\t{symbol}{}", alias.name);
277            }
278            Binding::Local => {}
279        }
280        self.seen(out, &alias.name, alias.binding, alias.visibility);
281        let _ = writeln!(out, "\t.set\t{symbol}{},{symbol}{}", alias.name, alias.target);
282    }
283
284    /// What is said once, after every function.
285    pub fn end(self, out: &mut String) {
286        match self {
287            // Without this the stack is executable, which is not a default anybody chose.
288            Directives::Elf => out.push_str("\t.section\t.note.GNU-stack,\"\",@progbits\n"),
289            // What lets the linker throw away a function nothing calls, which it cannot do
290            // without being told that the boundaries between them are real.
291            Directives::MachO => out.push_str("\t.subsections_via_symbols\n"),
292            Directives::Coff => {}
293        }
294    }
295}
296
297/// What the object file is told about a function's name, from what the machine function carries.
298///
299/// Two names for one set of three, because the machine IR is not allowed to know what an object
300/// file is and the object writer is not allowed to know what a machine function is. This crate is
301/// where they meet, which is where the two spellings are put side by side.
302#[must_use]
303pub(crate) fn binding(binding: mir::Binding) -> Binding {
304    match binding {
305        mir::Binding::Global => Binding::Global,
306        mir::Binding::Local => Binding::Local,
307        mir::Binding::Weak => Binding::Weak,
308    }
309}
310
311/// What the object file is told about how far a name reaches outside a shared library, from what
312/// the machine function carries.
313///
314/// Two spellings of one set of three, for the reason [`binding`] above has two.
315#[must_use]
316pub(crate) fn visibility(visibility: mir::Visibility) -> Visibility {
317    match visibility {
318        mir::Visibility::Default => Visibility::Default,
319        mir::Visibility::Hidden => Visibility::Hidden,
320        mir::Visibility::Protected => Visibility::Protected,
321    }
322}
323
324#[cfg(test)]
325mod tests {
326    use rucc_object::FUNC_ALIGN;
327
328    use super::*;
329
330    #[test]
331    fn a_mach_o_symbol_is_the_c_name_with_an_underscore_in_front_of_it() {
332        let mut out = String::new();
333        Directives::MachO.open(&mut out, "main", 16, Binding::Global, Visibility::Default);
334        assert!(out.contains("\t.globl\t_main\n"), "{out}");
335        assert!(out.contains("\n_main:\n"), "{out}");
336        // No type and no size, neither of which Mach-O has.
337        assert!(!out.contains(".type"), "{out}");
338        let mut close = String::new();
339        Directives::MachO.close(&mut close, "main");
340        assert_eq!(close, "");
341    }
342
343    #[test]
344    fn an_elf_function_says_what_it_is_and_how_long_it_is() {
345        let mut out = String::new();
346        Directives::Elf.open(&mut out, "main", 16, Binding::Global, Visibility::Default);
347        Directives::Elf.close(&mut out, "main");
348        assert!(out.contains("\t.type\tmain, @function\n"), "{out}");
349        assert!(out.contains("\t.size\tmain, .-main\n"), "{out}");
350    }
351
352    #[test]
353    fn a_function_that_asked_to_be_more_aligned_is_written_at_that_alignment() {
354        let mut out = String::new();
355        Directives::Elf.open(&mut out, "f", 256, Binding::Global, Visibility::Default);
356        // The directive counts in powers of two and the attribute counts in bytes, and two
357        // hundred and fifty six bytes is eight of them.
358        assert!(out.contains("\t.p2align\t8, 0x90\n"), "{out}");
359        let mut plain = String::new();
360        Directives::Elf.open(&mut plain, "f", FUNC_ALIGN, Binding::Global, Visibility::Default);
361        assert!(plain.contains("\t.p2align\t4, 0x90\n"), "{plain}");
362    }
363
364    /// The two directives that say a name does not leave the shared library, or leaves it and
365    /// cannot be replaced.
366    ///
367    /// The listing half of tamnd/rucc#733. It matters that this is written in the listing and not
368    /// only in the object writer, because the two are the same compiler taking two roads out and a
369    /// program built through `-S` and an assembler has to come out the same as one built straight
370    /// to an object.
371    #[test]
372    fn a_name_that_does_not_leave_the_library_says_so_in_the_listing() {
373        let mut out = String::new();
374        Directives::Elf.open(&mut out, "f", 16, Binding::Global, Visibility::Hidden);
375        assert!(out.contains("\t.globl\tf\n"), "still global to the static linker: {out}");
376        assert!(out.contains("\t.hidden\tf\n"), "{out}");
377        let mut protected = String::new();
378        Directives::Elf.open(&mut protected, "f", 16, Binding::Global, Visibility::Protected);
379        assert!(protected.contains("\t.protected\tf\n"), "{protected}");
380        // Mach-O's one spelling of the one of these it has, and it carries the underscore every
381        // other Apple symbol does.
382        let mut apple = String::new();
383        Directives::MachO.open(&mut apple, "f", 16, Binding::Global, Visibility::Hidden);
384        assert!(apple.contains("\t.private_extern\t_f\n"), "{apple}");
385    }
386
387    /// A `static` name gets no visibility directive whatever it asked for.
388    ///
389    /// gcc writes none for one either, and an assembler that is handed `.hidden` for a name that
390    /// was never `.globl` has been told something about a symbol that is not in anybody's dynamic
391    /// table to begin with.
392    #[test]
393    fn a_static_name_is_told_nothing_about_a_dynamic_linker_it_will_never_meet() {
394        for seen in [Visibility::Default, Visibility::Hidden, Visibility::Protected] {
395            let mut out = String::new();
396            Directives::Elf.open(&mut out, "f", 16, Binding::Local, seen);
397            assert!(!out.contains(".hidden"), "{seen:?}: {out}");
398            assert!(!out.contains(".protected"), "{seen:?}: {out}");
399        }
400    }
401
402    #[test]
403    fn an_elf_file_says_the_stack_is_not_executable() {
404        // The absence of this is what makes it executable, so the test is that it is there
405        // rather than that it is spelled a particular way.
406        let mut out = String::new();
407        Directives::Elf.end(&mut out);
408        assert!(out.contains(".note.GNU-stack"), "{out}");
409    }
410
411    #[test]
412    fn every_object_format_has_directives() {
413        for format in [ObjectFormat::Elf, ObjectFormat::MachO, ObjectFormat::Coff] {
414            let directives = Directives::of(format);
415            assert!(directives.text().starts_with('\t'));
416            let mut out = String::new();
417            directives.open(&mut out, "f", 16, Binding::Global, Visibility::Default);
418            directives.close(&mut out, "f");
419            directives.end(&mut out);
420            assert!(out.ends_with('\n'), "{format:?} left a line unfinished");
421        }
422    }
423}