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_object::{Binding, Place};
19use rucc_target::ObjectFormat;
20
21use crate::data::Variable;
22
23/// The directives one object format wraps a function in.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum Directives {
26    /// ELF, which is Linux and the freestanding targets.
27    Elf,
28    /// Mach-O, which is Apple's.
29    MachO,
30    /// COFF, which is Windows.
31    Coff,
32}
33
34impl Directives {
35    /// The directives that go with that object format.
36    #[must_use]
37    pub const fn of(format: ObjectFormat) -> Directives {
38        match format {
39            ObjectFormat::Elf => Directives::Elf,
40            ObjectFormat::MachO => Directives::MachO,
41            ObjectFormat::Coff => Directives::Coff,
42        }
43    }
44
45    /// What goes in front of a C name to make the name the linker sees.
46    ///
47    /// Mach-O keeps the underscore that every Unix linker once had, so `main` in C is `_main` in
48    /// the object, and a listing that leaves it off refers to a symbol nothing defines.
49    #[must_use]
50    pub const fn symbol(self) -> &'static str {
51        match self {
52            Directives::Elf | Directives::Coff => "",
53            Directives::MachO => "_",
54        }
55    }
56
57    /// What goes in front of a label that belongs to one function and leaves no symbol behind.
58    #[must_use]
59    pub const fn local(self) -> &'static str {
60        match self {
61            Directives::Elf | Directives::Coff => ".L",
62            Directives::MachO => "L",
63        }
64    }
65
66    /// The directive that opens the section code goes in.
67    #[must_use]
68    pub const fn text(self) -> &'static str {
69        match self {
70            Directives::Elf | Directives::Coff => "\t.text",
71            Directives::MachO => "\t.section\t__TEXT,__text,regular,pure_instructions",
72        }
73    }
74
75    /// What is said about a function before its first instruction.
76    ///
77    /// Every function is global, because a machine function does not carry the linkage the C did
78    /// and nothing below the driver could ask. That is wrong for a `static` function and is the
79    /// reason `-S` output is a thing to read rather than a thing to link, until the object writer
80    /// gives the machine IR somewhere to keep it.
81    pub fn open(self, out: &mut String, name: &str) {
82        let symbol = self.symbol();
83        out.push_str("\t.p2align\t4, 0x90\n");
84        let _ = writeln!(out, "\t.globl\t{symbol}{name}");
85        match self {
86            Directives::Elf => {
87                let _ = writeln!(out, "\t.type\t{name}, @function");
88            }
89            // Windows says the same thing as a storage class and a type code: two is external and
90            // thirty two is a function, and the two numbers together are what ELF's one word says.
91            Directives::Coff => {
92                let _ = writeln!(out, "\t.def\t{name}\n\t.scl\t2\n\t.type\t32\n\t.endef");
93            }
94            Directives::MachO => {}
95        }
96        let _ = writeln!(out, "{symbol}{name}:");
97    }
98
99    /// The directive that opens the section a variable goes in.
100    ///
101    /// The three formats disagree about the names and about how much has to be said. ELF and COFF
102    /// have a directive per section that every assembler knows, and both want the flags spelled
103    /// out for a section the program named, since nothing else says whether it may be written to.
104    /// Mach-O has one directive and a segment in front of every section name.
105    pub fn section(self, out: &mut String, place: &Place) {
106        match (self, place) {
107            // A tentative definition is not in a section at all, and the caller is what decides
108            // that. It is answered here as the section it would otherwise have gone in, so that
109            // the match stays about sections and nothing has to be said twice.
110            (Directives::Elf | Directives::Coff, Place::Written | Place::Merged) => {
111                out.push_str("\t.data\n");
112            }
113            (Directives::Elf | Directives::Coff, Place::Zero) => out.push_str("\t.bss\n"),
114            (Directives::Elf, Place::ReadOnly) => out.push_str("\t.section\t.rodata\n"),
115            (Directives::Coff, Place::ReadOnly) => out.push_str("\t.section\t.rdata,\"dr\"\n"),
116            (Directives::Elf, Place::Named(name)) => {
117                let _ = writeln!(out, "\t.section\t{name},\"aw\",@progbits");
118            }
119            (Directives::Coff, Place::Named(name)) => {
120                let _ = writeln!(out, "\t.section\t{name},\"dw\"");
121            }
122            (Directives::MachO, Place::ReadOnly) => out.push_str("\t.section\t__TEXT,__const\n"),
123            // A Mach-O section name carries the segment it is in, so a program that named one
124            // named both halves and there is nothing to add to it.
125            (Directives::MachO, Place::Named(name)) => {
126                let _ = writeln!(out, "\t.section\t{name}");
127            }
128            (Directives::MachO, _) => out.push_str("\t.section\t__DATA,__data\n"),
129        }
130    }
131
132    /// What is said about a variable before its image, and whether an image follows.
133    ///
134    /// Two kinds of variable are one directive rather than a section, a label and bytes. A
135    /// tentative definition is a request to the linker for that much zeroed space on every format,
136    /// and on Mach-O so is a variable whose image is all zeros, because the section that would
137    /// hold it is one nothing may write bytes into.
138    pub fn variable(self, out: &mut String, var: &Variable) -> bool {
139        let symbol = self.symbol();
140        let align = var.align.max(1).trailing_zeros();
141        match (self, &var.place) {
142            (_, Place::Merged) => {
143                let comm = if var.binding == Binding::Local { ".lcomm" } else { ".comm" };
144                let name = &var.name;
145                let _ = writeln!(out, "\t{comm}\t{symbol}{name},{},{}", var.size, var.align);
146                return false;
147            }
148            (Directives::MachO, Place::Zero) => {
149                let name = &var.name;
150                let _ =
151                    writeln!(out, "\t.zerofill\t__DATA,__bss,{symbol}{name},{},{align}", var.size);
152                return false;
153            }
154            _ => {}
155        }
156        self.section(out, &var.place);
157        match var.binding {
158            Binding::Global => {
159                let _ = writeln!(out, "\t.globl\t{symbol}{}", var.name);
160            }
161            Binding::Weak => {
162                let _ = writeln!(out, "\t.weak\t{symbol}{}", var.name);
163            }
164            // Nothing, which is what makes it invisible outside the file. A name no directive
165            // mentions is still in the symbol table as a local one, which is what `static` is.
166            Binding::Local => {}
167        }
168        let _ = writeln!(out, "\t.p2align\t{align}");
169        if self == Directives::Elf {
170            let _ = writeln!(out, "\t.type\t{}, @object", var.name);
171        }
172        let _ = writeln!(out, "{symbol}{}:", var.name);
173        true
174    }
175
176    /// What is said about a function after its last instruction.
177    ///
178    /// The size, on the format that has one. It is written as the distance from the label to here
179    /// rather than as a number, because the assembler is the one that knows how long an
180    /// instruction turned out to be and this file is what it is about to find out from.
181    pub fn close(self, out: &mut String, name: &str) {
182        if self == Directives::Elf {
183            let _ = writeln!(out, "\t.size\t{name}, .-{name}");
184        }
185    }
186
187    /// What is said once, after every function.
188    pub fn end(self, out: &mut String) {
189        match self {
190            // Without this the stack is executable, which is not a default anybody chose.
191            Directives::Elf => out.push_str("\t.section\t.note.GNU-stack,\"\",@progbits\n"),
192            // What lets the linker throw away a function nothing calls, which it cannot do
193            // without being told that the boundaries between them are real.
194            Directives::MachO => out.push_str("\t.subsections_via_symbols\n"),
195            Directives::Coff => {}
196        }
197    }
198}
199
200#[cfg(test)]
201mod tests {
202    use super::*;
203
204    #[test]
205    fn a_mach_o_symbol_is_the_c_name_with_an_underscore_in_front_of_it() {
206        let mut out = String::new();
207        Directives::MachO.open(&mut out, "main");
208        assert!(out.contains("\t.globl\t_main\n"), "{out}");
209        assert!(out.contains("\n_main:\n"), "{out}");
210        // No type and no size, neither of which Mach-O has.
211        assert!(!out.contains(".type"), "{out}");
212        let mut close = String::new();
213        Directives::MachO.close(&mut close, "main");
214        assert_eq!(close, "");
215    }
216
217    #[test]
218    fn an_elf_function_says_what_it_is_and_how_long_it_is() {
219        let mut out = String::new();
220        Directives::Elf.open(&mut out, "main");
221        Directives::Elf.close(&mut out, "main");
222        assert!(out.contains("\t.type\tmain, @function\n"), "{out}");
223        assert!(out.contains("\t.size\tmain, .-main\n"), "{out}");
224    }
225
226    #[test]
227    fn an_elf_file_says_the_stack_is_not_executable() {
228        // The absence of this is what makes it executable, so the test is that it is there
229        // rather than that it is spelled a particular way.
230        let mut out = String::new();
231        Directives::Elf.end(&mut out);
232        assert!(out.contains(".note.GNU-stack"), "{out}");
233    }
234
235    #[test]
236    fn every_object_format_has_directives() {
237        for format in [ObjectFormat::Elf, ObjectFormat::MachO, ObjectFormat::Coff] {
238            let directives = Directives::of(format);
239            assert!(directives.text().starts_with('\t'));
240            let mut out = String::new();
241            directives.open(&mut out, "f");
242            directives.close(&mut out, "f");
243            directives.end(&mut out);
244            assert!(out.ends_with('\n'), "{format:?} left a line unfinished");
245        }
246    }
247}