fortrangoingonforty/afs-ld / faaa755

Browse files

Relax adrp-add hints

Authored by espadonne
SHA
faaa755af0a1d10c8caa4decbc17a18c07dffd65
Parents
1e80916
Tree
b509fee

8 changed files

StatusFile+-
M src/args.rs 10 0
M src/lib.rs 11 0
M src/loh.rs 178 0
M src/macho/writer.rs 4 0
M src/main.rs 1 0
M tests/cli_diagnostics.rs 38 0
M tests/linker_run.rs 189 0
M tests/snapshots/help.txt 1 0
src/args.rsmodified
@@ -33,6 +33,7 @@ const KNOWN_FLAGS: &[&str] = &[
3333
     "-unexported_symbol",
3434
     "-S",
3535
     "-no_uuid",
36
+    "-no_loh",
3637
     "-dead_strip",
3738
     "-icf=safe",
3839
     "-icf=none",
@@ -312,6 +313,9 @@ pub fn parse(argv: &[String]) -> Result<LinkOptions, ArgsError> {
312313
             "-no_uuid" => {
313314
                 opts.emit_uuid = false;
314315
             }
316
+            "-no_loh" => {
317
+                opts.no_loh = true;
318
+            }
315319
             "-dead_strip" => {
316320
                 opts.dead_strip = true;
317321
             }
@@ -466,6 +470,12 @@ mod tests {
466470
         assert!(!opts.emit_uuid);
467471
     }
468472
 
473
+    #[test]
474
+    fn no_loh_flag_is_recorded() {
475
+        let opts = parse(&argv(&["-no_loh", "foo.o"])).unwrap();
476
+        assert!(opts.no_loh);
477
+    }
478
+
469479
     #[test]
470480
     fn dead_strip_icf_and_fixup_chain_flags_are_recorded() {
471481
         let opts = parse(&argv(&[
src/lib.rsmodified
@@ -103,6 +103,7 @@ pub struct LinkOptions {
103103
     pub strip_debug: bool,
104104
     pub emit_uuid: bool,
105105
     pub dead_strip: bool,
106
+    pub no_loh: bool,
106107
     pub icf_mode: IcfMode,
107108
     pub fixup_chains: bool,
108109
     pub all_load: bool,
@@ -152,6 +153,7 @@ impl Default for LinkOptions {
152153
             strip_debug: false,
153154
             emit_uuid: true,
154155
             dead_strip: false,
156
+            no_loh: false,
155157
             icf_mode: IcfMode::None,
156158
             fixup_chains: false,
157159
             all_load: false,
@@ -179,6 +181,7 @@ pub enum LinkError {
179181
     Synth(synth::SynthError),
180182
     Unwind(synth::unwind::UnwindError),
181183
     Icf(IcfError),
184
+    Loh(loh::LohError),
182185
     DuplicateSymbols(String),
183186
     UndefinedSymbols(String),
184187
     UnsupportedArch(String),
@@ -205,6 +208,7 @@ impl std::fmt::Display for LinkError {
205208
             LinkError::Synth(e) => write!(f, "{e}"),
206209
             LinkError::Unwind(e) => write!(f, "{e}"),
207210
             LinkError::Icf(e) => write!(f, "{e}"),
211
+            LinkError::Loh(e) => write!(f, "{e}"),
208212
             LinkError::DuplicateSymbols(msg) | LinkError::UndefinedSymbols(msg) => {
209213
                 write!(f, "{msg}")
210214
             }
@@ -304,6 +308,12 @@ impl From<IcfError> for LinkError {
304308
     }
305309
 }
306310
 
311
+impl From<loh::LohError> for LinkError {
312
+    fn from(value: loh::LohError) -> Self {
313
+        LinkError::Loh(value)
314
+    }
315
+}
316
+
307317
 /// The linker itself. Sprint 0 only validates that inputs exist; later sprints
308318
 /// grow this into the full pipeline described in `.docs/overview.md`.
309319
 pub struct Linker;
@@ -570,6 +580,7 @@ impl Linker {
570580
             &linkedit,
571581
             icf.as_ref().map(|plan| plan.redirects()),
572582
         )?;
583
+        loh::relax_layout(&mut layout, &linkedit, !opts.no_loh)?;
573584
         let folded_symbols = icf
574585
             .as_ref()
575586
             .map(|plan| plan.folded_symbols(&atom_table, &sym_table, &layout_inputs))
src/loh.rsmodified
@@ -4,13 +4,19 @@
44
 //! args...)` records. The args are file offsets of the participating
55
 //! instructions.
66
 
7
+use std::collections::HashSet;
8
+use std::fmt;
9
+
10
+use crate::layout::Layout;
711
 use crate::leb::{read_uleb, write_uleb};
812
 use crate::macho::reader::ReadError;
13
+use crate::macho::writer::LinkEditPlan;
914
 
1015
 pub const LOH_ARM64_ADRP_LDR: u32 = 2;
1116
 pub const LOH_ARM64_ADRP_LDR_GOT_LDR: u32 = 4;
1217
 pub const LOH_ARM64_ADRP_ADD: u32 = 7;
1318
 pub const LOH_ARM64_ADRP_LDR_GOT: u32 = 8;
19
+const NOP: u32 = 0xd503_201f;
1420
 
1521
 #[derive(Debug, Clone, PartialEq, Eq)]
1622
 pub struct LohEntry {
@@ -18,6 +24,32 @@ pub struct LohEntry {
1824
     pub args: Vec<u32>,
1925
 }
2026
 
27
+#[derive(Debug, Clone, PartialEq, Eq)]
28
+pub struct LohError(String);
29
+
30
+impl fmt::Display for LohError {
31
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32
+        write!(f, "LOH relaxation error: {}", self.0)
33
+    }
34
+}
35
+
36
+impl std::error::Error for LohError {}
37
+
38
+impl From<ReadError> for LohError {
39
+    fn from(value: ReadError) -> Self {
40
+        Self(value.to_string())
41
+    }
42
+}
43
+
44
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45
+struct LocatedWord {
46
+    section_idx: usize,
47
+    atom_idx: usize,
48
+    word_off: usize,
49
+    addr: u64,
50
+    insn: u32,
51
+}
52
+
2153
 pub fn parse_loh_blob(bytes: &[u8]) -> Result<Vec<LohEntry>, ReadError> {
2254
     let mut out = Vec::new();
2355
     let mut cursor = 0usize;
@@ -64,6 +96,140 @@ pub fn write_loh_blob(entries: &[LohEntry]) -> Vec<u8> {
6496
     out
6597
 }
6698
 
99
+pub fn relax_layout(
100
+    layout: &mut Layout,
101
+    linkedit: &LinkEditPlan,
102
+    enabled: bool,
103
+) -> Result<(), LohError> {
104
+    if !enabled || linkedit.loh.is_none() || linkedit.loh_bytes().is_empty() {
105
+        return Ok(());
106
+    }
107
+
108
+    let entries = parse_loh_blob(linkedit.loh_bytes())?;
109
+    let mut rewritten = HashSet::new();
110
+    for entry in entries {
111
+        if entry.kind != LOH_ARM64_ADRP_ADD || entry.args.len() != 2 {
112
+            continue;
113
+        }
114
+
115
+        let adrp_off = entry.args[0] as u64;
116
+        let add_off = entry.args[1] as u64;
117
+        if !rewritten.insert(adrp_off) || !rewritten.insert(add_off) {
118
+            continue;
119
+        }
120
+
121
+        let adrp = locate_word(layout, adrp_off)?;
122
+        let add = locate_word(layout, add_off)?;
123
+        let Some(target) = decode_adrp_add_target(adrp.insn, add.insn, adrp.addr) else {
124
+            continue;
125
+        };
126
+        let Some(adr) = encode_adr(target, adrp.addr, (adrp.insn & 0x1f) as u8) else {
127
+            continue;
128
+        };
129
+
130
+        write_word(layout, adrp, adr)?;
131
+        write_word(layout, add, NOP)?;
132
+    }
133
+    Ok(())
134
+}
135
+
136
+fn locate_word(layout: &Layout, file_offset: u64) -> Result<LocatedWord, LohError> {
137
+    for (section_idx, section) in layout.sections.iter().enumerate() {
138
+        for (atom_idx, atom) in section.atoms.iter().enumerate() {
139
+            let start = section.file_off + atom.offset;
140
+            let end = start + atom.data.len() as u64;
141
+            if !(start <= file_offset && file_offset + 4 <= end) {
142
+                continue;
143
+            }
144
+            let word_off = (file_offset - start) as usize;
145
+            let bytes = atom.data.get(word_off..word_off + 4).ok_or_else(|| {
146
+                LohError(format!(
147
+                    "instruction read OOB at file offset 0x{file_offset:x}"
148
+                ))
149
+            })?;
150
+            return Ok(LocatedWord {
151
+                section_idx,
152
+                atom_idx,
153
+                word_off,
154
+                addr: section.addr + atom.offset + word_off as u64,
155
+                insn: u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]),
156
+            });
157
+        }
158
+    }
159
+    Err(LohError(format!(
160
+        "LOH instruction offset 0x{file_offset:x} did not resolve to an output atom"
161
+    )))
162
+}
163
+
164
+fn write_word(layout: &mut Layout, word: LocatedWord, insn: u32) -> Result<(), LohError> {
165
+    let atom = &mut layout.sections[word.section_idx].atoms[word.atom_idx];
166
+    let bytes = atom
167
+        .data
168
+        .get_mut(word.word_off..word.word_off + 4)
169
+        .ok_or_else(|| {
170
+            LohError(format!(
171
+                "instruction write OOB at section {} atom {} word {}",
172
+                word.section_idx, word.atom_idx, word.word_off
173
+            ))
174
+        })?;
175
+    bytes.copy_from_slice(&insn.to_le_bytes());
176
+    Ok(())
177
+}
178
+
179
+fn decode_adrp_add_target(adrp: u32, add: u32, place: u64) -> Option<u64> {
180
+    if !is_adrp(adrp) || !is_add_imm_64(add) {
181
+        return None;
182
+    }
183
+    let rd = (adrp & 0x1f) as u8;
184
+    let add_rd = (add & 0x1f) as u8;
185
+    let add_rn = ((add >> 5) & 0x1f) as u8;
186
+    if rd == 31 || add_rd != rd || add_rn != rd {
187
+        return None;
188
+    }
189
+    let adrp_immlo = ((adrp >> 29) & 0x3) as i64;
190
+    let adrp_immhi = ((adrp >> 5) & 0x7ffff) as i64;
191
+    let adrp_pages = sign_extend_21((adrp_immhi << 2) | adrp_immlo);
192
+    let adrp_base = ((place as i64) & !0xfff) + (adrp_pages << 12);
193
+    let low = ((add >> 10) & 0xfff) as u64;
194
+    Some((adrp_base as u64) + low)
195
+}
196
+
197
+fn encode_adr(target: u64, place: u64, reg: u8) -> Option<u32> {
198
+    if reg == 31 {
199
+        return None;
200
+    }
201
+    let delta = (target as i64).wrapping_sub(place as i64);
202
+    if !fits_signed(delta, 21) {
203
+        return None;
204
+    }
205
+    let encoded = (delta as u32) & 0x1f_ffff;
206
+    let immlo = encoded & 0x3;
207
+    let immhi = (encoded >> 2) & 0x7ffff;
208
+    Some(0x1000_0000 | (immlo << 29) | (immhi << 5) | reg as u32)
209
+}
210
+
211
+fn is_adrp(insn: u32) -> bool {
212
+    (insn & 0x9f00_0000) == 0x9000_0000
213
+}
214
+
215
+fn is_add_imm_64(insn: u32) -> bool {
216
+    (insn & 0xffc0_0000) == 0x9100_0000
217
+}
218
+
219
+fn fits_signed(value: i64, bits: u32) -> bool {
220
+    let min = -(1i64 << (bits - 1));
221
+    let max = (1i64 << (bits - 1)) - 1;
222
+    (min..=max).contains(&value)
223
+}
224
+
225
+fn sign_extend_21(value: i64) -> i64 {
226
+    if value & (1 << 20) != 0 {
227
+        value | !0x1f_ffff
228
+    } else {
229
+        value
230
+    }
231
+}
232
+
67233
 #[cfg(test)]
68234
 mod tests {
69235
     use super::*;
@@ -101,4 +267,16 @@ mod tests {
101267
             }]
102268
         );
103269
     }
270
+
271
+    #[test]
272
+    fn encode_adr_round_trips_small_delta() {
273
+        let place = 0x1_0000_1000;
274
+        let target = place + 0x48;
275
+        let adr = encode_adr(target, place, 9).unwrap();
276
+        assert_eq!(adr & 0x1f, 9);
277
+        let immlo = ((adr >> 29) & 0x3) as i64;
278
+        let immhi = ((adr >> 5) & 0x7ffff) as i64;
279
+        let delta = sign_extend_21((immhi << 2) | immlo);
280
+        assert_eq!(place.wrapping_add_signed(delta), target);
281
+    }
104282
 }
src/macho/writer.rsmodified
@@ -748,6 +748,10 @@ impl LinkEditPlan {
748748
     pub fn lazy_bind_offset(&self, symbol: SymbolId) -> Option<u32> {
749749
         self.lazy_bind_offsets.get(&symbol).copied()
750750
     }
751
+
752
+    pub fn loh_bytes(&self) -> &[u8] {
753
+        &self.loh_bytes
754
+    }
751755
 }
752756
 
753757
 fn build_linkedit_plan(
src/main.rsmodified
@@ -36,6 +36,7 @@ Options:
3636
   -x                              Strip local symbols
3737
   -S                              Strip debug symbols (currently a no-op warning)
3838
   -no_uuid                        Omit LC_UUID
39
+  -no_loh                         Skip LOH relaxation
3940
   -dead_strip                     Dead-strip unreferenced code/data
4041
   -icf=safe | -icf=none | -icf=all
4142
                                   Configure identical code folding (`all` currently errors)
tests/cli_diagnostics.rsmodified
@@ -216,6 +216,44 @@ fn no_uuid_flag_omits_uuid_load_command() {
216216
     let _ = fs::remove_file(out_path);
217217
 }
218218
 
219
+#[test]
220
+fn no_loh_flag_links_successfully() {
221
+    if !have_xcrun() {
222
+        eprintln!("skipping: xcrun as unavailable");
223
+        return;
224
+    }
225
+
226
+    let exe = env!("CARGO_BIN_EXE_afs-ld");
227
+    let obj = match assemble_minimal_main("no-loh-main.o") {
228
+        Ok(obj) => obj,
229
+        Err(e) => {
230
+            eprintln!("skipping: assemble failed: {e}");
231
+            return;
232
+        }
233
+    };
234
+    let out_path = scratch("no-loh.out");
235
+    let out = Command::new(exe)
236
+        .arg("-no_loh")
237
+        .arg("-o")
238
+        .arg(&out_path)
239
+        .arg(&obj)
240
+        .output()
241
+        .expect("afs-ld should run");
242
+    assert!(
243
+        out.status.success(),
244
+        "-no_loh link should succeed:\nstderr:\n{}",
245
+        String::from_utf8_lossy(&out.stderr)
246
+    );
247
+    assert!(
248
+        out_path.is_file(),
249
+        "expected -no_loh link to produce {}",
250
+        out_path.display()
251
+    );
252
+
253
+    let _ = fs::remove_file(obj);
254
+    let _ = fs::remove_file(out_path);
255
+}
256
+
219257
 #[test]
220258
 fn strip_debug_flag_warns_but_links_successfully() {
221259
     if !have_xcrun() {
tests/linker_run.rsmodified
@@ -1807,6 +1807,25 @@ fn read_insn(bytes: &[u8], start: usize) -> Result<u32, String> {
18071807
     Ok(u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]]))
18081808
 }
18091809
 
1810
+fn is_adr(insn: u32) -> bool {
1811
+    (insn & 0x9f00_0000) == 0x1000_0000
1812
+}
1813
+
1814
+fn is_adrp(insn: u32) -> bool {
1815
+    (insn & 0x9f00_0000) == 0x9000_0000
1816
+}
1817
+
1818
+fn is_add_imm_64(insn: u32) -> bool {
1819
+    (insn & 0xffc0_0000) == 0x9100_0000
1820
+}
1821
+
1822
+fn decode_adr_target(insn: u32, place: u64) -> u64 {
1823
+    let immlo = ((insn >> 29) & 0x3) as i64;
1824
+    let immhi = ((insn >> 5) & 0x7ffff) as i64;
1825
+    let delta = sign_extend_21((immhi << 2) | immlo);
1826
+    place.wrapping_add_signed(delta)
1827
+}
1828
+
18101829
 fn sign_extend_26(value: i64) -> i64 {
18111830
     if value & (1 << 25) != 0 {
18121831
         value | !0x03ff_ffff
@@ -1985,6 +2004,176 @@ fn linker_run_preserves_loh_payloads_from_input_objects() {
19852004
     }
19862005
 }
19872006
 
2007
+#[test]
2008
+fn linker_run_relaxes_adrp_add_loh_when_target_is_near() {
2009
+    if !have_xcrun() {
2010
+        eprintln!("skipping: xcrun unavailable");
2011
+        return;
2012
+    }
2013
+
2014
+    let obj = scratch("loh-relax-near.o");
2015
+    let out = scratch("loh-relax-near.out");
2016
+    let src = r#"
2017
+        .section __TEXT,__text,regular,pure_instructions
2018
+        .globl _main
2019
+        .globl _target
2020
+        _main:
2021
+        Lloh0:
2022
+            adrp x0, _target@PAGE
2023
+        Lloh1:
2024
+            add x0, x0, _target@PAGEOFF
2025
+            mov w0, #0
2026
+            ret
2027
+        _target:
2028
+            ret
2029
+        .loh AdrpAdd Lloh0, Lloh1
2030
+        .subsections_via_symbols
2031
+    "#;
2032
+    if let Err(e) = assemble(src, &obj) {
2033
+        eprintln!("skipping: assemble failed: {e}");
2034
+        return;
2035
+    }
2036
+
2037
+    let opts = LinkOptions {
2038
+        inputs: vec![obj.clone()],
2039
+        output: Some(out.clone()),
2040
+        kind: OutputKind::Executable,
2041
+        ..LinkOptions::default()
2042
+    };
2043
+    Linker::run(&opts).unwrap();
2044
+
2045
+    let bytes = fs::read(&out).unwrap();
2046
+    let (text_addr, text) = output_section(&bytes, "__TEXT", "__text").unwrap();
2047
+    let target_addr = symbol_values(&bytes)["_target"];
2048
+    let first = read_insn(&text, 0).unwrap();
2049
+    let second = read_insn(&text, 4).unwrap();
2050
+    assert!(
2051
+        is_adr(first),
2052
+        "expected ADR after LOH relaxation, got {first:#010x}"
2053
+    );
2054
+    assert_eq!(second, 0xd503_201f, "expected NOP after LOH relaxation");
2055
+    assert_eq!(decode_adr_target(first, text_addr), target_addr);
2056
+
2057
+    let _ = fs::remove_file(obj);
2058
+    let _ = fs::remove_file(out);
2059
+}
2060
+
2061
+#[test]
2062
+fn linker_run_keeps_adrp_add_loh_when_target_is_far() {
2063
+    if !have_xcrun() {
2064
+        eprintln!("skipping: xcrun unavailable");
2065
+        return;
2066
+    }
2067
+
2068
+    let obj = scratch("loh-relax-far.o");
2069
+    let out = scratch("loh-relax-far.out");
2070
+    let src = r#"
2071
+        .section __TEXT,__text,regular,pure_instructions
2072
+        .globl _main
2073
+        .globl _target
2074
+        _main:
2075
+        Lloh0:
2076
+            adrp x0, _target@PAGE
2077
+        Lloh1:
2078
+            add x0, x0, _target@PAGEOFF
2079
+            mov w0, #0
2080
+            ret
2081
+            .space 0x200000
2082
+        _target:
2083
+            ret
2084
+        .loh AdrpAdd Lloh0, Lloh1
2085
+        .subsections_via_symbols
2086
+    "#;
2087
+    if let Err(e) = assemble(src, &obj) {
2088
+        eprintln!("skipping: assemble failed: {e}");
2089
+        return;
2090
+    }
2091
+
2092
+    let opts = LinkOptions {
2093
+        inputs: vec![obj.clone()],
2094
+        output: Some(out.clone()),
2095
+        kind: OutputKind::Executable,
2096
+        ..LinkOptions::default()
2097
+    };
2098
+    Linker::run(&opts).unwrap();
2099
+
2100
+    let bytes = fs::read(&out).unwrap();
2101
+    let (text_addr, text) = output_section(&bytes, "__TEXT", "__text").unwrap();
2102
+    let target_addr = symbol_values(&bytes)["_target"];
2103
+    let first = read_insn(&text, 0).unwrap();
2104
+    let second = read_insn(&text, 4).unwrap();
2105
+    assert!(
2106
+        is_adrp(first),
2107
+        "far LOH pair should stay ADRP, got {first:#010x}"
2108
+    );
2109
+    assert!(
2110
+        is_add_imm_64(second),
2111
+        "far LOH pair should keep ADD immediate, got {second:#010x}"
2112
+    );
2113
+    assert_eq!(
2114
+        decode_page_reference(&text, text_addr, 0, &PageRefKind::Add).unwrap(),
2115
+        target_addr
2116
+    );
2117
+
2118
+    let _ = fs::remove_file(obj);
2119
+    let _ = fs::remove_file(out);
2120
+}
2121
+
2122
+#[test]
2123
+fn linker_run_no_loh_keeps_adrp_add_unrelaxed() {
2124
+    if !have_xcrun() {
2125
+        eprintln!("skipping: xcrun unavailable");
2126
+        return;
2127
+    }
2128
+
2129
+    let obj = scratch("loh-no-relax.o");
2130
+    let out = scratch("loh-no-relax.out");
2131
+    let src = r#"
2132
+        .section __TEXT,__text,regular,pure_instructions
2133
+        .globl _main
2134
+        .globl _target
2135
+        _main:
2136
+        Lloh0:
2137
+            adrp x0, _target@PAGE
2138
+        Lloh1:
2139
+            add x0, x0, _target@PAGEOFF
2140
+            mov w0, #0
2141
+            ret
2142
+        _target:
2143
+            ret
2144
+        .loh AdrpAdd Lloh0, Lloh1
2145
+        .subsections_via_symbols
2146
+    "#;
2147
+    if let Err(e) = assemble(src, &obj) {
2148
+        eprintln!("skipping: assemble failed: {e}");
2149
+        return;
2150
+    }
2151
+
2152
+    let opts = LinkOptions {
2153
+        inputs: vec![obj.clone()],
2154
+        output: Some(out.clone()),
2155
+        kind: OutputKind::Executable,
2156
+        no_loh: true,
2157
+        ..LinkOptions::default()
2158
+    };
2159
+    Linker::run(&opts).unwrap();
2160
+
2161
+    let bytes = fs::read(&out).unwrap();
2162
+    let (text_addr, text) = output_section(&bytes, "__TEXT", "__text").unwrap();
2163
+    let target_addr = symbol_values(&bytes)["_target"];
2164
+    let first = read_insn(&text, 0).unwrap();
2165
+    let second = read_insn(&text, 4).unwrap();
2166
+    assert!(is_adrp(first), "expected ADRP when -no_loh is set");
2167
+    assert!(is_add_imm_64(second), "expected ADD when -no_loh is set");
2168
+    assert_eq!(
2169
+        decode_page_reference(&text, text_addr, 0, &PageRefKind::Add).unwrap(),
2170
+        target_addr
2171
+    );
2172
+
2173
+    let _ = fs::remove_file(obj);
2174
+    let _ = fs::remove_file(out);
2175
+}
2176
+
19882177
 #[test]
19892178
 fn linker_run_emits_minimal_dylib_from_real_object() {
19902179
     if !have_xcrun() {
tests/snapshots/help.txtmodified
@@ -30,6 +30,7 @@ Options:
3030
   -x                              Strip local symbols
3131
   -S                              Strip debug symbols (currently a no-op warning)
3232
   -no_uuid                        Omit LC_UUID
33
+  -no_loh                         Skip LOH relaxation
3334
   -dead_strip                     Dead-strip unreferenced code/data
3435
   -icf=safe | -icf=none | -icf=all
3536
                                   Configure identical code folding (`all` currently errors)