rucc-object 0.19.0

ELF, Mach-O and COFF object writers.
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
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
//! What Mach-O answers, where it answers differently from the other two formats.
//!
//! Design: `spec/cross-compile/07-object-formats.md` section 7.3. The sibling of [`crate::elf`] and
//! [`crate::coff`], and for now only for AArch64, which is every Mac sold since 2023 and every
//! iPhone. An x86-64 Mac is the same format with its own table of relocations, which is not
//! written yet.
//!
//! # A section is two names
//!
//! Every section is in a segment and says so: code is `__TEXT,__text` and writable data is
//! `__DATA,__data`. The listing writes both halves and so does anyone who writes assembly for a
//! Mac, so the name of a part here is the pair with the comma between them. What a section is,
//! zero filled, strings, constructors or a thread's image, is a type in the low byte of its
//! flags rather than something the name implies, and the attributes above that byte say whether it
//! holds instructions. [`section_flags`] is the one place both are worked out.
//!
//! # A relocation keeps its addend somewhere else
//!
//! A Mach-O relocation has no addend field. An address in data keeps it in the bytes the address
//! goes in, which the writer underneath puts there. An instruction has no room for one, so the
//! relocation is preceded by an `ARM64_RELOC_ADDEND` that carries it in the field that would
//! otherwise name a symbol, which has twenty four bits. The writer underneath makes that pair
//! too, and what is left here is refusing an addend that does not fit, and one on a load through
//! a table slot, where the addend would be an offset into the slot and means nothing.
//!
//! # A name the linker splits at
//!
//! A file that ends with `.subsections_via_symbols` tells the linker every symbol starts a piece it
//! may move or throw away, which is how a Mac drops a function nothing calls. A label inside a
//! function must then not be a symbol, or the linker would cut the function in two. An `L` in
//! front is how the format says a name is only the assembler's, and a relocation against one is
//! written against the symbol before it with the distance added, the way Apple's assembler does
//! it.

use object::macho;
use object::write::SymbolSection;
use object::write::{MachOBuildVersion, Mangling, Object as Writer, Relocation, Symbol};
use object::{
    Architecture, BinaryFormat, Endianness, RelocationEncoding, RelocationFlags, RelocationKind,
    SectionFlags, SectionKind, SymbolFlags,
};
use rucc_base::hash::{Map, Set};
use rucc_target::TargetInfo;
use rucc_target::aarch64::Fixup;
use rucc_tuple::{Env, Os, Version};

use crate::file::{Error, Flavour, scope_of};
use crate::section::{Binding, Info, Reference};
use crate::source::{Assembled, Held, Name, Sort};

/// A file of assembly for AArch64, as a Mach-O object.
///
/// The same three passes [`crate::assembled`] makes for the other two formats: every section, then
/// every name, then every relocation, since each wants the one before it finished. What differs is
/// in the module comment above, and where the debug information in `info` goes, which is the
/// last section below.
pub(crate) fn write(input: &Assembled, target: &TargetInfo, info: &Info) -> Result<Vec<u8>, Error> {
    let refused = |why: String| Error::Refused { why };
    let mut obj = Writer::new(BinaryFormat::MachO, Architecture::Aarch64, Endianness::Little);
    // The listing already wrote the underscore every C name has on this format, so the writer
    // underneath must not add a second one.
    obj.set_mangling(Mangling::None);
    if input.subsections {
        obj.set_subsections_via_symbols();
    }
    build_version(&mut obj, target);

    let mut made = Vec::with_capacity(input.parts.len());
    let mut layout = Vec::with_capacity(input.parts.len());
    for part in &input.parts {
        let (segment, section) = split(&part.name).map_err(refused)?;
        let kind = kind(segment, part.shape.mach);
        let id = obj.add_section(segment.as_bytes().to_vec(), section.as_bytes().to_vec(), kind);
        let flags = macho::SectionFlags(part.shape.mach);
        obj.section_mut(id).flags = SectionFlags::MachO { flags, reserved2: 0 };
        let align = part.align.max(1);
        if part.shape.bits {
            obj.append_section_data(id, &part.bytes, align);
        } else {
            obj.append_section_bss(id, part.size, align);
        }
        let size = if part.shape.bits { part.bytes.len() as u64 } else { part.size };
        layout.push(Placed { id, bss: kind.is_bss(), align, size });
        made.push(id);
    }

    // Which name each relocation is written against. A temporary is replaced by the last real
    // name at or before it in the same section, with the distance between them added, and one
    // with no real name in front of it is kept as a symbol after all, which is what the linker
    // then cuts the section at. That is at the start of what it was in front of, so nothing is cut
    // in two.
    let defined: Map<&str, &Name> =
        input.names.iter().map(|name| (name.name.as_str(), name)).collect();
    let mut atoms: Vec<Vec<(u64, &str)>> = vec![Vec::new(); input.parts.len()];
    for name in &input.names {
        if let Held::In { part, offset } = name.at {
            if !temporary(&name.name) && name.sort != Sort::File {
                atoms[part].push((offset, name.name.as_str()));
            }
        }
    }
    for list in &mut atoms {
        list.sort_unstable();
    }
    // An instruction's field carries what is added in an `ARM64_RELOC_ADDEND`, which holds less
    // than a section can be long. A temporary too far past the real name in front of it for that
    // is kept as a symbol as well, rather than refused, since it is a name the file may keep.
    let target_of = |symbol: &str, addend: i64, kind: Reference| -> (String, i64) {
        let Some(Held::In { part, offset }) = defined.get(symbol).map(|name| name.at) else {
            return (symbol.to_owned(), addend);
        };
        if !temporary(symbol) {
            return (symbol.to_owned(), addend);
        }
        let list = &atoms[part];
        match list.partition_point(|&(at, _)| at <= offset) {
            0 => (symbol.to_owned(), addend),
            after => {
                let (at, name) = list[after - 1];
                let moved = addend + (offset - at) as i64;
                let field = matches!(kind, Reference::Field(_));
                if field && !(-ADDEND..ADDEND).contains(&moved) {
                    return (symbol.to_owned(), addend);
                }
                (name.to_owned(), moved)
            }
        }
    };
    let kept: Set<String> = input
        .parts
        .iter()
        .flat_map(|part| &part.relocs)
        .map(|reloc| target_of(&reloc.symbol, reloc.addend, reloc.kind).0)
        .filter(|name| temporary(name))
        .collect();

    let mut symbols = Map::default();
    for name in &input.names {
        if temporary(&name.name) && !kept.contains(&name.name) {
            continue;
        }
        let (section, value, size) = match name.at {
            Held::In { part, offset } => {
                let Some(id) = made.get(part) else {
                    let why = format!(
                        "'{}' is in section {part} and there is no such section",
                        name.name
                    );
                    return Err(Error::Refused { why });
                };
                (SymbolSection::Section(*id), offset, name.size)
            }
            Held::Absolute(value) => (SymbolSection::Absolute, value, 0),
            // The boundary goes where an ELF writer puts it and the writer underneath moves it to
            // the few bits of the description Mach-O keeps it in.
            Held::Common { size, align } => (SymbolSection::Common, align, size),
            Held::Undefined => (SymbolSection::Undefined, 0, 0),
        };
        let id = obj.add_symbol(Symbol {
            name: name.name.clone().into_bytes(),
            value,
            size,
            kind: Flavour::MachO.sort(name.sort, name.binding),
            scope: scope_of(name.binding),
            weak: name.binding == Binding::Weak,
            section,
            flags: SymbolFlags::None,
        });
        Flavour::MachO.see(&mut obj, id, name.binding, name.visibility);
        symbols.insert(name.name.as_str(), id);
    }

    for (part, id) in input.parts.iter().zip(&made) {
        for reloc in &part.relocs {
            let (name, addend) = target_of(&reloc.symbol, reloc.addend, reloc.kind);
            let Some(&symbol) = symbols.get(name.as_str()) else {
                let why = format!("'{name}' is named by a relocation and by nothing else");
                return Err(Error::Refused { why });
            };
            let flags = self::reloc(reloc.kind, addend).map_err(refused)?;
            let record = Relocation { offset: reloc.at as u64, symbol, addend, flags };
            obj.add_relocation(*id, record).map_err(|why| refused(why.to_string()))?;
        }
    }
    described(&mut obj, &symbols, info, layout).map_err(refused)?;
    obj.write().map_err(|why| refused(why.to_string()))
}

/// A section as the writer underneath lays it out: whether it is zero filled, its alignment and
/// how many bytes it covers.
struct Placed {
    id: object::write::SectionId,
    bss: bool,
    align: u64,
    size: u64,
}

/// Where each section starts in the one segment of an object file, which is the address a symbol
/// in it has.
///
/// The writer underneath puts every section with bytes first, in the order they were made, each at
/// its alignment, and the zero filled ones after all of those, so a `__bss` is past the debug
/// sections as well. This is the same walk, since it does not say what it chose until the file is
/// written.
fn addresses(layout: &[Placed]) -> Map<object::write::SectionId, u64> {
    let mut at = Map::default();
    let mut address = 0u64;
    for bss in [false, true] {
        for placed in layout.iter().filter(|placed| placed.bss == bss) {
            address = address.next_multiple_of(placed.align.max(1));
            at.insert(placed.id, address);
            address += placed.size;
        }
    }
    at
}

/// The debug sections, in `__DWARF`, which is the segment the linker leaves out of the image.
///
/// A Mac does not link DWARF. ld64 writes a map into the image that says which object each
/// function came from and where it went, and `dsymutil` reads each object's own sections through
/// that map, or a debugger does the same thing without it. So a place in one debug section that
/// names another is never moved by a linker, and is written as the offset it is with no relocation,
/// which is what clang does. What names a function or a variable is an address and keeps its
/// relocation, since that is how the map is matched to the object. The debug information knows
/// those by their C names, and the symbol is the same name with the underscore in front.
///
/// Such a relocation names the section the function or variable is in rather than its symbol, and
/// the bytes at the place hold the address it has in this object file with the addend on, which
/// is how clang writes them. `dsymutil` looks the address up in the map to learn where the linker
/// put that name, and a debugger reading the object takes the bytes as they are. Against the
/// symbol, with only the addend in the bytes, `dsymutil` put every function in a file at the
/// address of the first one, and `atos` and `lldb` named the wrong function for all the rest.
fn described(
    obj: &mut Writer<'_>,
    symbols: &Map<&str, object::write::SymbolId>,
    info: &Info,
    mut layout: Vec<Placed>,
) -> Result<(), String> {
    let debug = info.chunks.iter().map(|chunk| chunk.name.as_str()).collect::<Set<_>>();
    let mut made = Vec::with_capacity(info.chunks.len());
    for chunk in &info.chunks {
        let section = debug_section(&chunk.name)?;
        let id = obj.add_section(b"__DWARF".to_vec(), section.into_bytes(), SectionKind::Debug);
        layout.push(Placed { id, bss: false, align: 1, size: chunk.bytes.len() as u64 });
        made.push(id);
    }
    let at = addresses(&layout);
    for (chunk, &id) in info.chunks.iter().zip(&made) {
        // An ordinary section, which is a type of zero, holding debug information.
        let flags = macho::S_ATTR_DEBUG;
        obj.section_mut(id).flags = SectionFlags::MachO { flags, reserved2: 0 };
        let mut bytes = chunk.bytes.clone();
        let mut relocs = Vec::new();
        for reloc in &chunk.relocs {
            let Reference::Address { bytes: width } = reloc.kind else {
                return Err(format!("{:?} in the debug information", reloc.kind));
            };
            if debug.contains(reloc.symbol.as_str()) {
                let at = reloc.at;
                let Some(place) = bytes.get_mut(at..at + usize::from(width)) else {
                    return Err(format!("a place past the end of {}", chunk.name));
                };
                place.copy_from_slice(&reloc.addend.to_le_bytes()[..usize::from(width)]);
                continue;
            }
            let name = format!("_{}", reloc.symbol);
            let Some(&symbol) = symbols.get(name.as_str()) else {
                return Err(format!("'{name}' is named by the debug information and not defined"));
            };
            let flags = self::reloc(reloc.kind, reloc.addend)?;
            let (place, value) = (obj.symbol(symbol).section, obj.symbol(symbol).value);
            let (symbol, addend) = match place {
                SymbolSection::Section(section) => {
                    let start = at.get(&section).copied().unwrap_or(0);
                    let address = reloc.addend + (start + value) as i64;
                    (obj.section_symbol(section), address)
                }
                _ => (symbol, reloc.addend),
            };
            relocs.push(Relocation { offset: reloc.at as u64, symbol, addend, flags });
        }
        obj.append_section_data(id, &bytes, 1);
        for record in relocs {
            obj.add_relocation(id, record).map_err(|why| why.to_string())?;
        }
    }
    Ok(())
}

/// The Mach-O name of a DWARF section, which is its ELF name with two underscores for the dot, cut
/// to the sixteen bytes a section name has. `.debug_str_offsets` is `__debug_str_offs`, the same
/// as clang's.
fn debug_section(name: &str) -> Result<String, String> {
    let Some(rest) = name.strip_prefix(".debug_") else {
        return Err(format!("'{name}' is not a DWARF section"));
    };
    let mut spelled = format!("__debug_{rest}");
    spelled.truncate(16);
    Ok(spelled)
}

/// The largest addend an `ARM64_RELOC_ADDEND` can carry either way, which is what fits in the
/// twenty four bits it has for one.
const ADDEND: i64 = 1 << 23;

/// Which relocation one reference is, or why there is none.
///
/// The branches, the page of an address and the low twelve bits of it are the four the code
/// generator uses for a name it can reach directly, and the loads through a table slot are the two
/// it uses for one another image may define. The low twelve bits are one relocation whatever size
/// the access is, since the linker reads the size off the instruction it is patching rather than
/// being told it, which is the difference from ELF's five. A conditional branch, `tbz` and a
/// literal load have no relocation on this format at all, and the assembler resolves every one of
/// those that stays inside a section.
pub(crate) fn reloc(reference: Reference, addend: i64) -> Result<RelocationFlags, String> {
    // A four byte distance from where it is written, which is how an unwind record says where its
    // function and its common record are. arm64 has no relocation for a distance in data, and the
    // way the format says one is an `ARM64_RELOC_SUBTRACTOR` naming the place, followed by an
    // `ARM64_RELOC_UNSIGNED` naming the target, with what is added kept in the bytes. The writer
    // underneath makes that pair from a generic distance, against a name it puts at the front of
    // the section the distance is written in, which is what clang's records do as well.
    if reference == Reference::Data {
        return Ok(RelocationFlags::Generic {
            kind: RelocationKind::Relative,
            encoding: RelocationEncoding::Generic,
            size: 32,
        });
    }
    let (r_type, r_pcrel, r_length) = match reference {
        Reference::Field(Fixup::Call26 | Fixup::Jump26) => (macho::ARM64_RELOC_BRANCH26, true, 2),
        Reference::Field(Fixup::AdrPage21) => (macho::ARM64_RELOC_PAGE21, true, 2),
        Reference::Field(
            Fixup::AddLo12
            | Fixup::Ldst8Lo12
            | Fixup::Ldst16Lo12
            | Fixup::Ldst32Lo12
            | Fixup::Ldst64Lo12
            | Fixup::Ldst128Lo12,
        ) => (macho::ARM64_RELOC_PAGEOFF12, false, 2),
        Reference::Field(Fixup::GotPage21) => (macho::ARM64_RELOC_GOT_LOAD_PAGE21, true, 2),
        Reference::Field(Fixup::GotLo12) => (macho::ARM64_RELOC_GOT_LOAD_PAGEOFF12, false, 2),
        Reference::Field(Fixup::GotTprelPage21) => (macho::ARM64_RELOC_TLVP_LOAD_PAGE21, true, 2),
        Reference::Field(Fixup::GotTprelLo12Nc) => {
            (macho::ARM64_RELOC_TLVP_LOAD_PAGEOFF12, false, 2)
        }
        Reference::Address { bytes: 8 } => (macho::ARM64_RELOC_UNSIGNED, false, 3),
        Reference::Address { bytes: 4 } => (macho::ARM64_RELOC_UNSIGNED, false, 2),
        Reference::Field(field) => {
            return Err(format!(
                "{} has no Mach-O relocation, and a branch that leaves its section has to be a \
                 `b` or a `bl`",
                field.name()
            ));
        }
        other => return Err(format!("no Mach-O relocation for arm64 is {other:?}")),
    };
    let slot = matches!(
        r_type,
        macho::ARM64_RELOC_GOT_LOAD_PAGE21
            | macho::ARM64_RELOC_GOT_LOAD_PAGEOFF12
            | macho::ARM64_RELOC_TLVP_LOAD_PAGE21
            | macho::ARM64_RELOC_TLVP_LOAD_PAGEOFF12
    );
    if slot && addend != 0 {
        return Err(format!("a load through a table slot with {addend} added to it"));
    }
    if r_type != macho::ARM64_RELOC_UNSIGNED && !(-ADDEND..ADDEND).contains(&addend) {
        return Err(format!("{addend} added to a name, which is more than Mach-O can say"));
    }
    Ok(RelocationFlags::MachO { r_type, r_pcrel, r_length })
}

/// The segment and the section a part's name says, split at the comma.
pub(crate) fn split(name: &str) -> Result<(&str, &str), String> {
    let why = || {
        format!(
            "'{name}' is not a Mach-O section name, which is a segment and a section of up to \
             sixteen bytes each with a comma between them"
        )
    };
    let (segment, section) = name.split_once(',').ok_or_else(why)?;
    let fits = |half: &str| !half.is_empty() && half.len() <= 16 && !half.contains(',');
    if !fits(segment) || !fits(section) {
        return Err(why());
    }
    Ok((segment, section))
}

/// The type and attributes a section has, from what `.section` said after its name or from the
/// name alone when it said nothing.
///
/// `kind` is the third field of the directive and `attributes` the fourth, split at each `+`. The
/// names with a type of their own are the ones Apple's assembler knows without being told, so
/// `.section __TEXT,__cstring` is strings whether or not `cstring_literals` follows.
///
/// # Errors
///
/// A type or an attribute Apple's assembler does not take, as a sentence.
pub(crate) fn section_flags(
    segment: &str,
    section: &str,
    kind: Option<&str>,
    attributes: &[&str],
) -> Result<u32, String> {
    let typ = match kind.map(str::trim) {
        Some(kind) => match kind {
            "regular" => macho::S_REGULAR,
            "zerofill" => macho::S_ZEROFILL,
            "cstring_literals" => macho::S_CSTRING_LITERALS,
            "4byte_literals" => macho::S_4BYTE_LITERALS,
            "8byte_literals" => macho::S_8BYTE_LITERALS,
            "16byte_literals" => macho::S_16BYTE_LITERALS,
            "literal_pointers" => macho::S_LITERAL_POINTERS,
            "non_lazy_symbol_pointers" => macho::S_NON_LAZY_SYMBOL_POINTERS,
            "mod_init_funcs" => macho::S_MOD_INIT_FUNC_POINTERS,
            "mod_term_funcs" => macho::S_MOD_TERM_FUNC_POINTERS,
            "coalesced" => macho::S_COALESCED,
            "interposing" => macho::S_INTERPOSING,
            "thread_local_regular" => macho::S_THREAD_LOCAL_REGULAR,
            "thread_local_zerofill" => macho::S_THREAD_LOCAL_ZEROFILL,
            "thread_local_variables" => macho::S_THREAD_LOCAL_VARIABLES,
            "thread_local_variable_pointers" => macho::S_THREAD_LOCAL_VARIABLE_POINTERS,
            "thread_local_init_function_pointers" => macho::S_THREAD_LOCAL_INIT_FUNCTION_POINTERS,
            other => return Err(format!("'{other}' is not a Mach-O section type")),
        },
        None => match (segment, section) {
            (_, "__cstring") => macho::S_CSTRING_LITERALS,
            (_, "__bss" | "__common") => macho::S_ZEROFILL,
            (_, "__thread_bss") => macho::S_THREAD_LOCAL_ZEROFILL,
            (_, "__thread_data") => macho::S_THREAD_LOCAL_REGULAR,
            (_, "__thread_vars") => macho::S_THREAD_LOCAL_VARIABLES,
            (_, "__mod_init_func") => macho::S_MOD_INIT_FUNC_POINTERS,
            (_, "__mod_term_func") => macho::S_MOD_TERM_FUNC_POINTERS,
            (_, "__literal4") => macho::S_4BYTE_LITERALS,
            (_, "__literal8") => macho::S_8BYTE_LITERALS,
            (_, "__literal16") => macho::S_16BYTE_LITERALS,
            _ => macho::S_REGULAR,
        },
    };
    let mut flags = u32::from(typ.0);
    // `__TEXT,__text` holds instructions whatever the directive said, and a file that opened it
    // with no attributes still has its code in it.
    let code = (segment, section) == ("__TEXT", "__text");
    for attribute in attributes.iter().map(|attribute| attribute.trim()) {
        flags |= match attribute {
            "pure_instructions" => macho::S_ATTR_PURE_INSTRUCTIONS.0,
            "no_toc" => macho::S_ATTR_NO_TOC.0,
            "strip_static_syms" => macho::S_ATTR_STRIP_STATIC_SYMS.0,
            "no_dead_strip" => macho::S_ATTR_NO_DEAD_STRIP.0,
            "live_support" => macho::S_ATTR_LIVE_SUPPORT.0,
            "self_modifying_code" => macho::S_ATTR_SELF_MODIFYING_CODE.0,
            "debug" => macho::S_ATTR_DEBUG.0,
            "" => 0,
            other => return Err(format!("'{other}' is not a Mach-O section attribute")),
        };
    }
    if code {
        flags |= macho::S_ATTR_PURE_INSTRUCTIONS.0;
    }
    // The assembler's own attribute, which says the linker may find code here. Every section the
    // program said is instructions has some.
    if flags & macho::S_ATTR_PURE_INSTRUCTIONS.0 != 0 {
        flags |= macho::S_ATTR_SOME_INSTRUCTIONS.0;
    }
    Ok(flags)
}

/// Whether a section with these flags carries no bytes in the file.
#[must_use]
pub(crate) fn zero_filled(flags: u32) -> bool {
    let typ = macho::SectionFlags(flags).typ();
    matches!(typ, macho::S_ZEROFILL | macho::S_GB_ZEROFILL | macho::S_THREAD_LOCAL_ZEROFILL)
}

/// What the writer underneath calls the nearest thing to a section with these flags.
///
/// It is told the flags in full afterwards, so this only decides whether the writer counts bytes
/// or holds them, and nothing it does with a kind for thread-local data is wanted: the listing
/// writes the descriptors itself.
pub(crate) fn kind(segment: &str, flags: u32) -> SectionKind {
    if zero_filled(flags) {
        SectionKind::UninitializedData
    } else if flags & macho::S_ATTR_PURE_INSTRUCTIONS.0 != 0 {
        SectionKind::Text
    } else if segment == "__TEXT" {
        SectionKind::ReadOnlyData
    } else {
        SectionKind::Data
    }
}

/// Whether a name is one only the assembler sees, which is what an `L` in front says on Mach-O.
///
/// A name with a `\u{1}` in it is one the assembler made for a numbered label or a frame, which is
/// the same kind of name under a spelling no program can write. A name that starts `_.L` is one
/// the compiler made with the prefix ELF keeps out of the symbol table, a string or the label a
/// computed goto jumps to, with the underscore every name in a Mach-O listing gets. No C name
/// starts with a dot, so it is just as private here. A label inside a function has to be one of
/// these: kept as a symbol of its own it is where the linker cuts the function in two.
pub(crate) fn temporary(name: &str) -> bool {
    name.starts_with('L') || name.starts_with("_.L") || name.contains('\u{1}')
}

/// The platform and the oldest version of it the file is for, which is `LC_BUILD_VERSION`.
///
/// The linker decides from the version how to lay out constructors and fixups, so it is not
/// cosmetic. The defaults are the oldest release that runs on Apple silicon for a Mac and the one
/// the SDK's own default is for an iPhone, the same two the preprocessor predefines.
pub(crate) fn build_version(obj: &mut Writer<'_>, target: &TargetInfo) {
    let (platform, default) = match (target.tuple.os(), target.tuple.env()) {
        (Os::MacOs, _) => (macho::PLATFORM_MACOS, Version::new(11, 0)),
        (Os::IOs, Env::Simulator) => (macho::PLATFORM_IOSSIMULATOR, Version::new(14, 0)),
        (Os::IOs, Env::MacAbi) => (macho::PLATFORM_MACCATALYST, Version::new(14, 0)),
        (Os::IOs, _) => (macho::PLATFORM_IOS, Version::new(14, 0)),
        _ => return,
    };
    let version = target.tuple.os_version().unwrap_or(default);
    let part = |part: Option<u32>| part.unwrap_or(0).min(255) as u8;
    let minos = macho::Version::new(
        version.major_part().min(u32::from(u16::MAX)) as u16,
        part(version.minor_part()),
        part(version.patch_part()),
    );
    let mut build = MachOBuildVersion::default();
    (build.platform, build.minos) = (platform, minos);
    obj.set_macho_build_version(build);
}

#[cfg(test)]
mod tests {
    use super::*;

    use object::read::{Object as _, ObjectSection as _, ObjectSymbol as _};

    use crate::section::{Reloc, Visibility};
    use crate::source::{Part, Shape};

    fn part(segment: &str, section: &str, bytes: Vec<u8>, size: u64) -> Part {
        Part {
            name: format!("{segment},{section}"),
            bytes,
            size,
            align: 8,
            shape: Shape::mach(segment, section, None, &[]).unwrap(),
            relocs: Vec::new(),
            group: None,
            link: None,
        }
    }

    fn name(name: &str, part: usize, offset: u64, binding: Binding) -> Name {
        Name {
            name: name.to_owned(),
            at: Held::In { part, offset },
            size: 0,
            sort: Sort::Untyped,
            binding,
            visibility: Visibility::Default,
        }
    }

    #[test]
    fn a_file_reads_back_as_the_sections_names_and_relocations_it_was_given() {
        let mut text = part("__TEXT", "__text", vec![0; 12], 12);
        for (at, fixup) in [(0, Fixup::AdrPage21), (4, Fixup::AddLo12)] {
            let kind = Reference::Field(fixup);
            text.relocs.push(Reloc { at, symbol: "_counter".into(), kind, addend: 0, after: 0 });
        }
        let mut data = part("__DATA", "__data", vec![0; 24], 24);
        let kind = Reference::Address { bytes: 8 };
        data.relocs.push(Reloc { at: 8, symbol: "Lstr".into(), kind, addend: 0, after: 0 });
        let strings = part("__TEXT", "__cstring", b"no\0hi\0".to_vec(), 6);
        let bss = part("__DATA", "__bss", Vec::new(), 64);
        let mut main = name("_main", 0, 0, Binding::Global);
        main.sort = Sort::Func;
        let input = Assembled {
            parts: vec![text, data, strings, bss],
            names: vec![
                main,
                name("Lmain_0", 0, 0, Binding::Local),
                name("_counter", 1, 0, Binding::Global),
                name("_s", 2, 0, Binding::Local),
                name("Lstr", 2, 3, Binding::Local),
                name("_zeros", 3, 0, Binding::Global),
            ],
            subsections: true,
        };
        let target = TargetInfo::new("aarch64-apple-darwin".parse().unwrap());
        let bytes = write(&input, &target, &Info::default()).unwrap();
        let file = object::File::parse(&bytes[..]).unwrap();
        assert_eq!(file.format(), BinaryFormat::MachO);
        assert_eq!(file.architecture(), Architecture::Aarch64);
        let names: Vec<_> = file.symbols().filter_map(|sym| sym.name().ok()).collect();
        assert!(names.contains(&"_main") && names.contains(&"_zeros"), "{names:?}");
        assert!(!names.iter().any(|name| name.starts_with('L')), "{names:?}");
        let text = file.section_by_name("__text").unwrap();
        assert_eq!(text.relocations().count(), 2);
        let bss = file.section_by_name("__bss").unwrap();
        assert_eq!((bss.size(), bss.data().unwrap().len()), (64, 0));
        // The temporary is gone and the address is the string before it with the distance on.
        let data = file.section_by_name("__data").unwrap();
        let (_, reloc) = data.relocations().next().unwrap();
        let object::RelocationTarget::Symbol(index) = reloc.target() else { panic!() };
        assert_eq!(file.symbol_by_index(index).unwrap().name(), Ok("_s"));
        assert_eq!(data.data().unwrap()[8], 3);
    }

    /// A computed goto table, which is the address of a label inside a function held in data. The
    /// listing spells the label `_.Llbl.0`, the underscore every name gets and the prefix ELF keeps
    /// out of the symbol table, and it has to be the function with the distance on here: kept as
    /// a symbol it cuts the function in two, and it was once written as an undefined external
    /// that no link could find (#2403).
    #[test]
    fn the_address_of_a_label_in_data_is_the_function_it_is_in() {
        let text = part("__TEXT", "__text", vec![0; 16], 16);
        let mut data = part("__DATA", "__const", vec![0; 16], 16);
        let kind = Reference::Address { bytes: 8 };
        for (at, label) in [(0, "_.Llbl.0"), (8, "_.Llbl.1")] {
            data.relocs.push(Reloc { at, symbol: label.into(), kind, addend: 0, after: 0 });
        }
        let mut f = name("_f", 0, 0, Binding::Global);
        f.sort = Sort::Func;
        let input = Assembled {
            parts: vec![text, data],
            names: vec![
                f,
                name("_.Llbl.0", 0, 4, Binding::Local),
                name("_.Llbl.1", 0, 12, Binding::Local),
                name("_dispatch_table.0", 1, 0, Binding::Local),
            ],
            subsections: true,
        };
        let target = TargetInfo::new("aarch64-apple-darwin".parse().unwrap());
        let bytes = write(&input, &target, &Info::default()).unwrap();
        let file = object::File::parse(&bytes[..]).unwrap();
        let names: Vec<_> = file.symbols().filter_map(|sym| sym.name().ok()).collect();
        assert!(!names.iter().any(|name| name.contains(".L")), "{names:?}");
        assert!(file.symbols().all(|sym| !sym.is_undefined()), "{names:?}");
        let data = file.section_by_name("__const").unwrap();
        let mut seen = Vec::new();
        for (at, reloc) in data.relocations() {
            let object::RelocationTarget::Symbol(index) = reloc.target() else { panic!() };
            let symbol = file.symbol_by_index(index).unwrap();
            assert_eq!(symbol.name(), Ok("_f"));
            seen.push((at, data.data().unwrap()[at as usize]));
        }
        seen.sort_unstable();
        assert_eq!(seen, [(0, 4), (8, 12)]);
    }

    /// A distance an unwind record holds, which arm64 says as the place taken from the target:
    /// a `SUBTRACTOR` naming the front of the table, then an `UNSIGNED` naming the function, with
    /// the place's own offset taken off what the bytes hold. That is the pair clang writes and the
    /// only one ld64 reads a record's distances as.
    #[test]
    fn a_distance_in_data_is_a_subtractor_and_an_unsigned() {
        use object::read::macho::MachOFile64;

        let text = part("__TEXT", "__text", vec![0; 8], 8);
        let attributes = ["no_toc", "strip_static_syms", "live_support"];
        let shape = Shape::mach("__TEXT", "__eh_frame", Some("coalesced"), &attributes).unwrap();
        let mut frames = Part { shape, ..part("__TEXT", "__eh_frame", vec![0; 32], 32) };
        let kind = Reference::Data;
        frames.relocs.push(Reloc { at: 8, symbol: "_f".into(), kind, addend: 0, after: 0 });
        let mut f = name("_f", 0, 0, Binding::Global);
        f.sort = Sort::Func;
        let input = Assembled { parts: vec![text, frames], names: vec![f], subsections: true };
        let target = TargetInfo::new("aarch64-apple-darwin".parse().unwrap());
        let bytes = write(&input, &target, &Info::default()).unwrap();
        let file = MachOFile64::<Endianness>::parse(&bytes[..]).unwrap();
        let section = file.section_by_name("__eh_frame").unwrap();
        let pairs: Vec<_> = section
            .macho_relocations()
            .unwrap()
            .iter()
            .map(|reloc| reloc.info(Endianness::Little))
            .map(|info| (info.r_address, info.r_type, info.r_extern, info.r_length))
            .collect();
        let subtractor = (8, macho::ARM64_RELOC_SUBTRACTOR, true, 2);
        assert_eq!(pairs, [subtractor, (8, macho::ARM64_RELOC_UNSIGNED, true, 2)]);
        assert_eq!(section.data().unwrap()[8..12], (-8i32).to_le_bytes());
    }

    /// A place in one debug section that names another is written as the offset it is, since no
    /// linker moves either, and an address keeps its relocation, against the section the function
    /// is in, found through its symbol with the underscore the debug information leaves off.
    #[test]
    fn debug_information_goes_in_the_dwarf_segment_with_only_its_addresses_relocated() {
        use crate::section::Chunk;

        let text = part("__TEXT", "__text", vec![0; 8], 8);
        let mut f = name("_f", 0, 0, Binding::Local);
        f.sort = Sort::Func;
        let input = Assembled { parts: vec![text], names: vec![f], subsections: true };
        let reloc = |at, symbol: &str, bytes, addend| Reloc {
            at,
            symbol: symbol.to_owned(),
            kind: Reference::Address { bytes },
            addend,
            after: 0,
        };
        let unit = Chunk {
            name: ".debug_info".to_owned(),
            bytes: vec![0; 24],
            relocs: vec![reloc(8, ".debug_abbrev", 4, 0x10), reloc(12, "f", 8, 4)],
        };
        let abbrev = Chunk { name: ".debug_abbrev".to_owned(), bytes: vec![0; 32], relocs: vec![] };
        let offsets =
            Chunk { name: ".debug_str_offsets".to_owned(), bytes: vec![0; 8], relocs: vec![] };
        let info = Info { chunks: vec![unit, abbrev, offsets], ..Info::default() };
        let target = TargetInfo::new("aarch64-apple-darwin".parse().unwrap());
        let bytes = write(&input, &target, &info).unwrap();
        let file = object::File::parse(&bytes[..]).unwrap();
        let unit = file.section_by_name("__debug_info").unwrap();
        assert_eq!(unit.segment_name(), Ok(Some("__DWARF")));
        let SectionFlags::MachO { flags, .. } = unit.flags() else { panic!() };
        assert_eq!(flags, macho::S_ATTR_DEBUG);
        let data = unit.data().unwrap();
        assert_eq!(data[8..12], 0x10u32.to_le_bytes());
        assert_eq!(data[12..20], 4u64.to_le_bytes());
        let relocs: Vec<_> = unit.relocations().collect();
        let [(12, reloc)] = relocs.as_slice() else { panic!("{relocs:?}") };
        let object::RelocationTarget::Section(index) = reloc.target() else { panic!("{reloc:?}") };
        assert_eq!(file.section_by_index(index).unwrap().name(), Ok("__text"));
        assert!(file.section_by_name("__debug_str_offs").is_some());
    }

    /// What an address in the debug information holds is where the name is in this file with the
    /// addend on, the way clang writes it, because that is what `dsymutil` moves by the distance the
    /// linker moved the name. A function after the first one, a variable in `__data` and one in
    /// `__bss`, which the writer underneath puts after the debug sections, each read back as the
    /// address their own symbol has (#1992).
    #[test]
    fn an_address_in_the_debug_information_is_where_the_name_is_in_the_file() {
        use crate::section::Chunk;

        let text = part("__TEXT", "__text", vec![0; 32], 32);
        let data = part("__DATA", "__data", vec![0; 16], 16);
        let bss = part("__DATA", "__bss", Vec::new(), 64);
        let mut f = name("_f", 0, 0, Binding::Global);
        f.sort = Sort::Func;
        let mut g = name("_g", 0, 16, Binding::Local);
        g.sort = Sort::Func;
        let names = vec![f, g, name("_v", 1, 8, Binding::Global), name("_z", 2, 8, Binding::Local)];
        let input = Assembled { parts: vec![text, data, bss], names, subsections: true };
        let reloc = |at, symbol: &str, addend| Reloc {
            at,
            symbol: symbol.to_owned(),
            kind: Reference::Address { bytes: 8 },
            addend,
            after: 0,
        };
        let relocs = vec![reloc(0, "g", 0), reloc(8, "v", 0), reloc(16, "z", 4), reloc(24, "f", 2)];
        let unit = Chunk { name: ".debug_info".to_owned(), bytes: vec![0; 40], relocs };
        let info = Info { chunks: vec![unit], ..Info::default() };
        let target = TargetInfo::new("aarch64-apple-darwin".parse().unwrap());
        let bytes = write(&input, &target, &info).unwrap();
        let file = object::File::parse(&bytes[..]).unwrap();
        let address =
            |wanted: &str| file.symbols().find(|sym| sym.name() == Ok(wanted)).unwrap().address();
        let unit = file.section_by_name("__debug_info").unwrap();
        let data = unit.data().unwrap();
        let held = |at: usize| u64::from_le_bytes(data[at..at + 8].try_into().unwrap());
        // Each against the section its name is in, which is what makes `dsymutil` read the bytes
        // as the address to look up.
        let mut against: Vec<_> = unit
            .relocations()
            .map(|(at, reloc)| {
                let object::RelocationTarget::Section(index) = reloc.target() else {
                    panic!("{reloc:?}")
                };
                (at, file.section_by_index(index).unwrap().name().unwrap())
            })
            .collect();
        against.sort_unstable();
        assert_eq!(against, [(0, "__text"), (8, "__data"), (16, "__bss"), (24, "__text")]);
        assert_eq!(held(0), address("_g"));
        assert_eq!(held(8), address("_v"));
        assert_eq!(held(16), address("_z") + 4);
        assert_eq!(held(24), 2);
        // The three would all have read as their addend if the layout were not counted.
        assert_eq!(address("_g"), 16);
        assert_eq!(address("_v"), 40);
        assert!(address("_z") > address("_v"), "{}", address("_z"));
    }

    #[test]
    fn the_low_bits_are_one_relocation_whatever_the_access_is() {
        for fixup in [Fixup::AddLo12, Fixup::Ldst8Lo12, Fixup::Ldst64Lo12, Fixup::Ldst128Lo12] {
            let flags = reloc(Reference::Field(fixup), 8).unwrap();
            assert_eq!(
                flags,
                RelocationFlags::MachO {
                    r_type: macho::ARM64_RELOC_PAGEOFF12,
                    r_pcrel: false,
                    r_length: 2
                }
            );
        }
    }

    #[test]
    fn what_this_format_cannot_say_is_refused() {
        assert!(reloc(Reference::Field(Fixup::CondBr19), 0).is_err());
        assert!(reloc(Reference::Field(Fixup::GotLo12), 8).is_err());
        assert!(reloc(Reference::Field(Fixup::AdrPage21), 1 << 23).is_err());
        assert!(reloc(Reference::Field(Fixup::AdrPage21), -(1 << 23)).is_ok());
        assert!(reloc(Reference::Address { bytes: 8 }, 1 << 40).is_ok());
    }

    #[test]
    fn a_section_is_what_its_type_says_or_what_its_name_implies() {
        let text = section_flags("__TEXT", "__text", Some("regular"), &["pure_instructions"]);
        let text = text.unwrap();
        assert_eq!(text & 0xff, 0);
        assert_ne!(text & macho::S_ATTR_SOME_INSTRUCTIONS.0, 0);
        assert_eq!(section_flags("__TEXT", "__text", None, &[]).unwrap(), text);
        let bss = section_flags("__DATA", "__bss", None, &[]).unwrap();
        assert!(zero_filled(bss));
        let strings = section_flags("__TEXT", "__cstring", Some("cstring_literals"), &[]);
        assert_eq!(strings.unwrap(), u32::from(macho::S_CSTRING_LITERALS.0));
        assert!(section_flags("__DATA", "__data", Some("sideways"), &[]).is_err());
        assert_eq!(split("__DATA,__const"), Ok(("__DATA", "__const")));
        assert!(split(".data").is_err());
        assert!(split("__DATA,__a_name_longer_than_16").is_err());
    }
}