rucc-asm 0.10.5

Instruction encoders, the integrated assembler, inline assembly and relaxation.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
//! What an assembler is told about a function or a variable, which is the object format's answer
//! rather than the machine's.
//!
//! Design: `spec/11-asm-objects-debug.md` section 11.3, which is about the object files
//! themselves. The directives here are the same facts said in text: which section code and data go
//! in, how a symbol is spelled, which symbols leave the file, and where each one ends.
//!
//! They are not the same on the three formats and the differences are not cosmetic. A Mach-O
//! symbol carries an underscore in front of the C name and an ELF one does not, so a listing that
//! got that wrong would fail to link against every library on the machine. A local label is
//! spelled `.L` on ELF and COFF and `L` on Mach-O, and a label that is not spelled the local way
//! ends up in the symbol table, where it is a name a debugger and a backtrace will show. And ELF
//! wants a marker saying the stack is not executable, whose absence makes it executable, which
//! section 11.3 calls out as a real and recurring security bug.

use std::fmt::Write as _;

use rucc_mir as mir;
use rucc_object::{Alias, Binding, Place, Visibility};
use rucc_target::ObjectFormat;

use crate::data::Variable;

/// The directives one object format wraps a function in.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Directives {
    /// ELF, which is Linux and the freestanding targets.
    Elf,
    /// Mach-O, which is Apple's.
    MachO,
    /// COFF, which is Windows.
    Coff,
}

impl Directives {
    /// The directives that go with that object format.
    #[must_use]
    pub const fn of(format: ObjectFormat) -> Directives {
        match format {
            ObjectFormat::Elf => Directives::Elf,
            ObjectFormat::MachO => Directives::MachO,
            ObjectFormat::Coff => Directives::Coff,
            // No assembler in this crate writes wasm, and the caller that asked has a target it
            // cannot emit for. ELF's directives are the ones nothing here depends on being right
            // for a target it will not reach.
            ObjectFormat::Wasm => Directives::Elf,
        }
    }

    /// What goes in front of a C name to make the name the linker sees.
    ///
    /// Mach-O keeps the underscore that every Unix linker once had, so `main` in C is `_main` in
    /// the object, and a listing that leaves it off refers to a symbol nothing defines.
    #[must_use]
    pub const fn symbol(self) -> &'static str {
        match self {
            Directives::Elf | Directives::Coff => "",
            Directives::MachO => "_",
        }
    }

    /// What goes in front of a label that belongs to one function and leaves no symbol behind.
    #[must_use]
    pub const fn local(self) -> &'static str {
        match self {
            Directives::Elf | Directives::Coff => ".L",
            Directives::MachO => "L",
        }
    }

    /// The directive that opens the section code goes in.
    #[must_use]
    pub const fn text(self) -> &'static str {
        match self {
            Directives::Elf | Directives::Coff => "\t.text",
            Directives::MachO => "\t.section\t__TEXT,__text,regular,pure_instructions",
        }
    }

    /// What is said about a function before its first instruction.
    ///
    /// The binding is written the way it is written for a variable, and a local one gets no
    /// directive at all: a name no directive mentions is still in the symbol table, as a local,
    /// which is what `static` is. Windows says the same thing as a storage class, where three is
    /// the local one and two the rest.
    ///
    /// `align` is in bytes and is a power of two, and the padding is `0x90` because the space in
    /// front of a function is reached by falling off the end of the one before it.
    pub fn open(
        self,
        out: &mut String,
        name: &str,
        align: u32,
        binding: Binding,
        visibility: Visibility,
    ) {
        let symbol = self.symbol();
        let _ = writeln!(out, "\t.p2align\t{}, 0x90", align.max(1).trailing_zeros());
        match binding {
            Binding::Global => {
                let _ = writeln!(out, "\t.globl\t{symbol}{name}");
            }
            Binding::Weak => {
                let _ = writeln!(out, "\t.weak\t{symbol}{name}");
            }
            Binding::Local => {}
        }
        self.seen(out, name, binding, visibility);
        match self {
            Directives::Elf => {
                let _ = writeln!(out, "\t.type\t{name}, @function");
            }
            // Windows says the storage class and the type code, and thirty two is a function.
            Directives::Coff => {
                let scl = if binding == Binding::Local { 3 } else { 2 };
                let _ = writeln!(out, "\t.def\t{name}\n\t.scl\t{scl}\n\t.type\t32\n\t.endef");
            }
            Directives::MachO => {}
        }
        let _ = writeln!(out, "{symbol}{name}:");
    }

    /// What is said about how far a name reaches outside a shared library, which is nothing at
    /// all in the ordinary case.
    ///
    /// A local name gets no directive whatever was asked for. `static` is already invisible to
    /// everything outside the file, so there is no dynamic symbol table for it to be in or out of,
    /// and gcc writes no visibility directive for one either.
    ///
    /// ELF says both of the other two and says them the same way an assembler expects. Mach-O has
    /// one of them: `.private_extern` is a symbol that leaves this object and does not leave the
    /// library, which is what hidden means, and there is no Mach-O spelling of protected because
    /// the format has no way to say a symbol is exported and cannot be interposed. COFF has
    /// neither, since what leaves a Windows DLL is decided by an export table the linker is given
    /// rather than by a bit on each symbol.
    pub fn seen(self, out: &mut String, name: &str, binding: Binding, visibility: Visibility) {
        if binding == Binding::Local || visibility == Visibility::Default {
            return;
        }
        let symbol = self.symbol();
        match (self, visibility) {
            (Directives::Elf, Visibility::Hidden) => {
                let _ = writeln!(out, "\t.hidden\t{name}");
            }
            (Directives::Elf, Visibility::Protected) => {
                let _ = writeln!(out, "\t.protected\t{name}");
            }
            (Directives::MachO, Visibility::Hidden) => {
                let _ = writeln!(out, "\t.private_extern\t{symbol}{name}");
            }
            (Directives::MachO, Visibility::Protected) | (Directives::Coff, _) => {}
            (_, Visibility::Default) => unreachable!("returned above"),
        }
    }

    /// The directive that opens the section a variable goes in.
    ///
    /// The three formats disagree about the names and about how much has to be said. ELF and COFF
    /// have a directive per section that every assembler knows, and both want the flags spelled
    /// out for a section the program named, since nothing else says whether it may be written to.
    /// Mach-O has one directive and a segment in front of every section name.
    pub fn section(self, out: &mut String, place: &Place) {
        match (self, place) {
            // A tentative definition is not in a section at all, and the caller is what decides
            // that. It is answered here as the section it would otherwise have gone in, so that
            // the match stays about sections and nothing has to be said twice.
            (Directives::Elf | Directives::Coff, Place::Written | Place::Merged) => {
                out.push_str("\t.data\n");
            }
            (Directives::Elf | Directives::Coff, Place::Zero) => out.push_str("\t.bss\n"),
            (Directives::Elf, Place::ReadOnly) => out.push_str("\t.section\t.rodata\n"),
            (Directives::Elf, Place::RelocReadOnly { local }) => {
                let name = if *local { ".data.rel.ro.local" } else { ".data.rel.ro" };
                let _ = writeln!(out, "\t.section\t{name},\"aw\",@progbits");
            }
            // COFF has no section of this kind and needs none. A Windows image is relocated as a
            // whole rather than a symbol at a time, and the loader makes whatever pages it has to
            // write writable for as long as it is writing them and puts them back afterwards, so
            // an address in a read only section costs a base relocation and nothing else.
            (Directives::Coff, Place::ReadOnly | Place::RelocReadOnly { .. }) => {
                out.push_str("\t.section\t.rdata,\"dr\"\n");
            }
            (Directives::Elf, Place::Named(name)) => {
                let _ = writeln!(out, "\t.section\t{name},\"aw\",@progbits");
            }
            (Directives::Coff, Place::Named(name)) => {
                let _ = writeln!(out, "\t.section\t{name},\"dw\"");
            }
            (Directives::MachO, Place::ReadOnly) => out.push_str("\t.section\t__TEXT,__const\n"),
            // Mach-O has the same problem and the same answer under a different name. A section in
            // `__TEXT` is never writable, so a constant holding an address goes in `__DATA,__const`
            // instead, which `dyld` writes and then protects. There is no `.local` half: the layout
            // hint is an ELF linker's, and this one has nothing to do with it.
            (Directives::MachO, Place::RelocReadOnly { .. }) => {
                out.push_str("\t.section\t__DATA,__const\n");
            }
            // A Mach-O section name carries the segment it is in, so a program that named one
            // named both halves and there is nothing to add to it.
            (Directives::MachO, Place::Named(name)) => {
                let _ = writeln!(out, "\t.section\t{name}");
            }
            (Directives::MachO, _) => out.push_str("\t.section\t__DATA,__data\n"),
        }
    }

    /// What is said about a variable before its image, and whether an image follows.
    ///
    /// Two kinds of variable are one directive rather than a section, a label and bytes. A
    /// tentative definition is a request to the linker for that much zeroed space on every format,
    /// and on Mach-O so is a variable whose image is all zeros, because the section that would
    /// hold it is one nothing may write bytes into.
    pub fn variable(self, out: &mut String, var: &Variable) -> bool {
        let symbol = self.symbol();
        let align = var.align.max(1).trailing_zeros();
        match (self, &var.place) {
            (_, Place::Merged) => {
                let comm = if var.binding == Binding::Local { ".lcomm" } else { ".comm" };
                let name = &var.name;
                let _ = writeln!(out, "\t{comm}\t{symbol}{name},{},{}", var.size, var.align);
                return false;
            }
            (Directives::MachO, Place::Zero) => {
                let name = &var.name;
                let _ =
                    writeln!(out, "\t.zerofill\t__DATA,__bss,{symbol}{name},{},{align}", var.size);
                return false;
            }
            _ => {}
        }
        self.section(out, &var.place);
        match var.binding {
            Binding::Global => {
                let _ = writeln!(out, "\t.globl\t{symbol}{}", var.name);
            }
            Binding::Weak => {
                let _ = writeln!(out, "\t.weak\t{symbol}{}", var.name);
            }
            // Nothing, which is what makes it invisible outside the file. A name no directive
            // mentions is still in the symbol table as a local one, which is what `static` is.
            Binding::Local => {}
        }
        self.seen(out, &var.name, var.binding, var.visibility);
        let _ = writeln!(out, "\t.p2align\t{align}");
        if self == Directives::Elf {
            let _ = writeln!(out, "\t.type\t{}, @object", var.name);
        }
        let _ = writeln!(out, "{symbol}{}:", var.name);
        true
    }

    /// What is said about a function after its last instruction.
    ///
    /// The size, on the format that has one. It is written as the distance from the label to here
    /// rather than as a number, because the assembler is the one that knows how long an
    /// instruction turned out to be and this file is what it is about to find out from.
    pub fn close(self, out: &mut String, name: &str) {
        if self == Directives::Elf {
            let _ = writeln!(out, "\t.size\t{name}, .-{name}");
        }
    }

    /// A second name for something the file already wrote down.
    ///
    /// The binding and then `.set`, which is all gcc writes and all an assembler needs: the type
    /// and the size of the new symbol are taken from the old one, so writing them again would
    /// only be a second chance to disagree. Nothing opens a section first, because the symbol is
    /// an entry in a table rather than a byte of anything, and no `.size` closes it for the same
    /// reason.
    pub fn alias(self, out: &mut String, alias: &Alias) {
        let symbol = self.symbol();
        match alias.binding {
            Binding::Global => {
                let _ = writeln!(out, "\t.globl\t{symbol}{}", alias.name);
            }
            Binding::Weak => {
                let _ = writeln!(out, "\t.weak\t{symbol}{}", alias.name);
            }
            Binding::Local => {}
        }
        self.seen(out, &alias.name, alias.binding, alias.visibility);
        let _ = writeln!(out, "\t.set\t{symbol}{},{symbol}{}", alias.name, alias.target);
    }

    /// What is said once, after every function.
    pub fn end(self, out: &mut String) {
        match self {
            // Without this the stack is executable, which is not a default anybody chose.
            Directives::Elf => out.push_str("\t.section\t.note.GNU-stack,\"\",@progbits\n"),
            // What lets the linker throw away a function nothing calls, which it cannot do
            // without being told that the boundaries between them are real.
            Directives::MachO => out.push_str("\t.subsections_via_symbols\n"),
            Directives::Coff => {}
        }
    }
}

/// What the object file is told about a function's name, from what the machine function carries.
///
/// Two names for one set of three, because the machine IR is not allowed to know what an object
/// file is and the object writer is not allowed to know what a machine function is. This crate is
/// where they meet, which is where the two spellings are put side by side.
#[must_use]
pub(crate) fn binding(binding: mir::Binding) -> Binding {
    match binding {
        mir::Binding::Global => Binding::Global,
        mir::Binding::Local => Binding::Local,
        mir::Binding::Weak => Binding::Weak,
    }
}

/// What the object file is told about how far a name reaches outside a shared library, from what
/// the machine function carries.
///
/// Two spellings of one set of three, for the reason [`binding`] above has two.
#[must_use]
pub(crate) fn visibility(visibility: mir::Visibility) -> Visibility {
    match visibility {
        mir::Visibility::Default => Visibility::Default,
        mir::Visibility::Hidden => Visibility::Hidden,
        mir::Visibility::Protected => Visibility::Protected,
    }
}

#[cfg(test)]
mod tests {
    use rucc_object::FUNC_ALIGN;

    use super::*;

    #[test]
    fn a_mach_o_symbol_is_the_c_name_with_an_underscore_in_front_of_it() {
        let mut out = String::new();
        Directives::MachO.open(&mut out, "main", 16, Binding::Global, Visibility::Default);
        assert!(out.contains("\t.globl\t_main\n"), "{out}");
        assert!(out.contains("\n_main:\n"), "{out}");
        // No type and no size, neither of which Mach-O has.
        assert!(!out.contains(".type"), "{out}");
        let mut close = String::new();
        Directives::MachO.close(&mut close, "main");
        assert_eq!(close, "");
    }

    #[test]
    fn an_elf_function_says_what_it_is_and_how_long_it_is() {
        let mut out = String::new();
        Directives::Elf.open(&mut out, "main", 16, Binding::Global, Visibility::Default);
        Directives::Elf.close(&mut out, "main");
        assert!(out.contains("\t.type\tmain, @function\n"), "{out}");
        assert!(out.contains("\t.size\tmain, .-main\n"), "{out}");
    }

    #[test]
    fn a_function_that_asked_to_be_more_aligned_is_written_at_that_alignment() {
        let mut out = String::new();
        Directives::Elf.open(&mut out, "f", 256, Binding::Global, Visibility::Default);
        // The directive counts in powers of two and the attribute counts in bytes, and two
        // hundred and fifty six bytes is eight of them.
        assert!(out.contains("\t.p2align\t8, 0x90\n"), "{out}");
        let mut plain = String::new();
        Directives::Elf.open(&mut plain, "f", FUNC_ALIGN, Binding::Global, Visibility::Default);
        assert!(plain.contains("\t.p2align\t4, 0x90\n"), "{plain}");
    }

    /// The two directives that say a name does not leave the shared library, or leaves it and
    /// cannot be replaced.
    ///
    /// The listing half of tamnd/rucc#733. It matters that this is written in the listing and not
    /// only in the object writer, because the two are the same compiler taking two roads out and a
    /// program built through `-S` and an assembler has to come out the same as one built straight
    /// to an object.
    #[test]
    fn a_name_that_does_not_leave_the_library_says_so_in_the_listing() {
        let mut out = String::new();
        Directives::Elf.open(&mut out, "f", 16, Binding::Global, Visibility::Hidden);
        assert!(out.contains("\t.globl\tf\n"), "still global to the static linker: {out}");
        assert!(out.contains("\t.hidden\tf\n"), "{out}");
        let mut protected = String::new();
        Directives::Elf.open(&mut protected, "f", 16, Binding::Global, Visibility::Protected);
        assert!(protected.contains("\t.protected\tf\n"), "{protected}");
        // Mach-O's one spelling of the one of these it has, and it carries the underscore every
        // other Apple symbol does.
        let mut apple = String::new();
        Directives::MachO.open(&mut apple, "f", 16, Binding::Global, Visibility::Hidden);
        assert!(apple.contains("\t.private_extern\t_f\n"), "{apple}");
    }

    /// A `static` name gets no visibility directive whatever it asked for.
    ///
    /// gcc writes none for one either, and an assembler that is handed `.hidden` for a name that
    /// was never `.globl` has been told something about a symbol that is not in anybody's dynamic
    /// table to begin with.
    #[test]
    fn a_static_name_is_told_nothing_about_a_dynamic_linker_it_will_never_meet() {
        for seen in [Visibility::Default, Visibility::Hidden, Visibility::Protected] {
            let mut out = String::new();
            Directives::Elf.open(&mut out, "f", 16, Binding::Local, seen);
            assert!(!out.contains(".hidden"), "{seen:?}: {out}");
            assert!(!out.contains(".protected"), "{seen:?}: {out}");
        }
    }

    #[test]
    fn an_elf_file_says_the_stack_is_not_executable() {
        // The absence of this is what makes it executable, so the test is that it is there
        // rather than that it is spelled a particular way.
        let mut out = String::new();
        Directives::Elf.end(&mut out);
        assert!(out.contains(".note.GNU-stack"), "{out}");
    }

    #[test]
    fn every_object_format_has_directives() {
        for format in [ObjectFormat::Elf, ObjectFormat::MachO, ObjectFormat::Coff] {
            let directives = Directives::of(format);
            assert!(directives.text().starts_with('\t'));
            let mut out = String::new();
            directives.open(&mut out, "f", 16, Binding::Global, Visibility::Default);
            directives.close(&mut out, "f");
            directives.end(&mut out);
            assert!(out.ends_with('\n'), "{format:?} left a line unfinished");
        }
    }
}