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::{Binding, Place};
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        }
44    }
45
46    /// What goes in front of a C name to make the name the linker sees.
47    ///
48    /// Mach-O keeps the underscore that every Unix linker once had, so `main` in C is `_main` in
49    /// the object, and a listing that leaves it off refers to a symbol nothing defines.
50    #[must_use]
51    pub const fn symbol(self) -> &'static str {
52        match self {
53            Directives::Elf | Directives::Coff => "",
54            Directives::MachO => "_",
55        }
56    }
57
58    /// What goes in front of a label that belongs to one function and leaves no symbol behind.
59    #[must_use]
60    pub const fn local(self) -> &'static str {
61        match self {
62            Directives::Elf | Directives::Coff => ".L",
63            Directives::MachO => "L",
64        }
65    }
66
67    /// The directive that opens the section code goes in.
68    #[must_use]
69    pub const fn text(self) -> &'static str {
70        match self {
71            Directives::Elf | Directives::Coff => "\t.text",
72            Directives::MachO => "\t.section\t__TEXT,__text,regular,pure_instructions",
73        }
74    }
75
76    /// What is said about a function before its first instruction.
77    ///
78    /// The binding is written the way it is written for a variable, and a local one gets no
79    /// directive at all: a name no directive mentions is still in the symbol table, as a local,
80    /// which is what `static` is. Windows says the same thing as a storage class, where three is
81    /// the local one and two the rest.
82    ///
83    /// `align` is in bytes and is a power of two, and the padding is `0x90` because the space in
84    /// front of a function is reached by falling off the end of the one before it.
85    pub fn open(self, out: &mut String, name: &str, align: u32, binding: Binding) {
86        let symbol = self.symbol();
87        let _ = writeln!(out, "\t.p2align\t{}, 0x90", align.max(1).trailing_zeros());
88        match binding {
89            Binding::Global => {
90                let _ = writeln!(out, "\t.globl\t{symbol}{name}");
91            }
92            Binding::Weak => {
93                let _ = writeln!(out, "\t.weak\t{symbol}{name}");
94            }
95            Binding::Local => {}
96        }
97        match self {
98            Directives::Elf => {
99                let _ = writeln!(out, "\t.type\t{name}, @function");
100            }
101            // Windows says the storage class and the type code, and thirty two is a function.
102            Directives::Coff => {
103                let scl = if binding == Binding::Local { 3 } else { 2 };
104                let _ = writeln!(out, "\t.def\t{name}\n\t.scl\t{scl}\n\t.type\t32\n\t.endef");
105            }
106            Directives::MachO => {}
107        }
108        let _ = writeln!(out, "{symbol}{name}:");
109    }
110
111    /// The directive that opens the section a variable goes in.
112    ///
113    /// The three formats disagree about the names and about how much has to be said. ELF and COFF
114    /// have a directive per section that every assembler knows, and both want the flags spelled
115    /// out for a section the program named, since nothing else says whether it may be written to.
116    /// Mach-O has one directive and a segment in front of every section name.
117    pub fn section(self, out: &mut String, place: &Place) {
118        match (self, place) {
119            // A tentative definition is not in a section at all, and the caller is what decides
120            // that. It is answered here as the section it would otherwise have gone in, so that
121            // the match stays about sections and nothing has to be said twice.
122            (Directives::Elf | Directives::Coff, Place::Written | Place::Merged) => {
123                out.push_str("\t.data\n");
124            }
125            (Directives::Elf | Directives::Coff, Place::Zero) => out.push_str("\t.bss\n"),
126            (Directives::Elf, Place::ReadOnly) => out.push_str("\t.section\t.rodata\n"),
127            (Directives::Coff, Place::ReadOnly) => out.push_str("\t.section\t.rdata,\"dr\"\n"),
128            (Directives::Elf, Place::Named(name)) => {
129                let _ = writeln!(out, "\t.section\t{name},\"aw\",@progbits");
130            }
131            (Directives::Coff, Place::Named(name)) => {
132                let _ = writeln!(out, "\t.section\t{name},\"dw\"");
133            }
134            (Directives::MachO, Place::ReadOnly) => out.push_str("\t.section\t__TEXT,__const\n"),
135            // A Mach-O section name carries the segment it is in, so a program that named one
136            // named both halves and there is nothing to add to it.
137            (Directives::MachO, Place::Named(name)) => {
138                let _ = writeln!(out, "\t.section\t{name}");
139            }
140            (Directives::MachO, _) => out.push_str("\t.section\t__DATA,__data\n"),
141        }
142    }
143
144    /// What is said about a variable before its image, and whether an image follows.
145    ///
146    /// Two kinds of variable are one directive rather than a section, a label and bytes. A
147    /// tentative definition is a request to the linker for that much zeroed space on every format,
148    /// and on Mach-O so is a variable whose image is all zeros, because the section that would
149    /// hold it is one nothing may write bytes into.
150    pub fn variable(self, out: &mut String, var: &Variable) -> bool {
151        let symbol = self.symbol();
152        let align = var.align.max(1).trailing_zeros();
153        match (self, &var.place) {
154            (_, Place::Merged) => {
155                let comm = if var.binding == Binding::Local { ".lcomm" } else { ".comm" };
156                let name = &var.name;
157                let _ = writeln!(out, "\t{comm}\t{symbol}{name},{},{}", var.size, var.align);
158                return false;
159            }
160            (Directives::MachO, Place::Zero) => {
161                let name = &var.name;
162                let _ =
163                    writeln!(out, "\t.zerofill\t__DATA,__bss,{symbol}{name},{},{align}", var.size);
164                return false;
165            }
166            _ => {}
167        }
168        self.section(out, &var.place);
169        match var.binding {
170            Binding::Global => {
171                let _ = writeln!(out, "\t.globl\t{symbol}{}", var.name);
172            }
173            Binding::Weak => {
174                let _ = writeln!(out, "\t.weak\t{symbol}{}", var.name);
175            }
176            // Nothing, which is what makes it invisible outside the file. A name no directive
177            // mentions is still in the symbol table as a local one, which is what `static` is.
178            Binding::Local => {}
179        }
180        let _ = writeln!(out, "\t.p2align\t{align}");
181        if self == Directives::Elf {
182            let _ = writeln!(out, "\t.type\t{}, @object", var.name);
183        }
184        let _ = writeln!(out, "{symbol}{}:", var.name);
185        true
186    }
187
188    /// What is said about a function after its last instruction.
189    ///
190    /// The size, on the format that has one. It is written as the distance from the label to here
191    /// rather than as a number, because the assembler is the one that knows how long an
192    /// instruction turned out to be and this file is what it is about to find out from.
193    pub fn close(self, out: &mut String, name: &str) {
194        if self == Directives::Elf {
195            let _ = writeln!(out, "\t.size\t{name}, .-{name}");
196        }
197    }
198
199    /// What is said once, after every function.
200    pub fn end(self, out: &mut String) {
201        match self {
202            // Without this the stack is executable, which is not a default anybody chose.
203            Directives::Elf => out.push_str("\t.section\t.note.GNU-stack,\"\",@progbits\n"),
204            // What lets the linker throw away a function nothing calls, which it cannot do
205            // without being told that the boundaries between them are real.
206            Directives::MachO => out.push_str("\t.subsections_via_symbols\n"),
207            Directives::Coff => {}
208        }
209    }
210}
211
212/// What the object file is told about a function's name, from what the machine function carries.
213///
214/// Two names for one set of three, because the machine IR is not allowed to know what an object
215/// file is and the object writer is not allowed to know what a machine function is. This crate is
216/// where they meet, which is where the two spellings are put side by side.
217#[must_use]
218pub(crate) fn binding(binding: mir::Binding) -> Binding {
219    match binding {
220        mir::Binding::Global => Binding::Global,
221        mir::Binding::Local => Binding::Local,
222        mir::Binding::Weak => Binding::Weak,
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use rucc_object::FUNC_ALIGN;
229
230    use super::*;
231
232    #[test]
233    fn a_mach_o_symbol_is_the_c_name_with_an_underscore_in_front_of_it() {
234        let mut out = String::new();
235        Directives::MachO.open(&mut out, "main", 16, Binding::Global);
236        assert!(out.contains("\t.globl\t_main\n"), "{out}");
237        assert!(out.contains("\n_main:\n"), "{out}");
238        // No type and no size, neither of which Mach-O has.
239        assert!(!out.contains(".type"), "{out}");
240        let mut close = String::new();
241        Directives::MachO.close(&mut close, "main");
242        assert_eq!(close, "");
243    }
244
245    #[test]
246    fn an_elf_function_says_what_it_is_and_how_long_it_is() {
247        let mut out = String::new();
248        Directives::Elf.open(&mut out, "main", 16, Binding::Global);
249        Directives::Elf.close(&mut out, "main");
250        assert!(out.contains("\t.type\tmain, @function\n"), "{out}");
251        assert!(out.contains("\t.size\tmain, .-main\n"), "{out}");
252    }
253
254    #[test]
255    fn a_function_that_asked_to_be_more_aligned_is_written_at_that_alignment() {
256        let mut out = String::new();
257        Directives::Elf.open(&mut out, "f", 256, Binding::Global);
258        // The directive counts in powers of two and the attribute counts in bytes, and two
259        // hundred and fifty six bytes is eight of them.
260        assert!(out.contains("\t.p2align\t8, 0x90\n"), "{out}");
261        let mut plain = String::new();
262        Directives::Elf.open(&mut plain, "f", FUNC_ALIGN, Binding::Global);
263        assert!(plain.contains("\t.p2align\t4, 0x90\n"), "{plain}");
264    }
265
266    #[test]
267    fn an_elf_file_says_the_stack_is_not_executable() {
268        // The absence of this is what makes it executable, so the test is that it is there
269        // rather than that it is spelled a particular way.
270        let mut out = String::new();
271        Directives::Elf.end(&mut out);
272        assert!(out.contains(".note.GNU-stack"), "{out}");
273    }
274
275    #[test]
276    fn every_object_format_has_directives() {
277        for format in [ObjectFormat::Elf, ObjectFormat::MachO, ObjectFormat::Coff] {
278            let directives = Directives::of(format);
279            assert!(directives.text().starts_with('\t'));
280            let mut out = String::new();
281            directives.open(&mut out, "f", 16, Binding::Global);
282            directives.close(&mut out, "f");
283            directives.end(&mut out);
284            assert!(out.ends_with('\n'), "{format:?} left a line unfinished");
285        }
286    }
287}