fortrangoingonforty/afs-ld / 6ae056c

Browse files

Relax more loh kinds

Authored by espadonne
SHA
6ae056c49a591ddc97aba17ca88116f3de47e331
Parents
faaa755
Tree
0e3c5dd

2 changed files

StatusFile+-
M src/loh.rs 288 18
M tests/linker_run.rs 262 1
src/loh.rsmodified
@@ -50,6 +50,15 @@ struct LocatedWord {
5050
     insn: u32,
5151
 }
5252
 
53
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54
+enum LiteralLoadKind {
55
+    W,
56
+    X,
57
+    S,
58
+    D,
59
+    Q,
60
+}
61
+
5362
 pub fn parse_loh_blob(bytes: &[u8]) -> Result<Vec<LohEntry>, ReadError> {
5463
     let mut out = Vec::new();
5564
     let mut cursor = 0usize;
@@ -105,34 +114,165 @@ pub fn relax_layout(
105114
         return Ok(());
106115
     }
107116
 
108
-    let entries = parse_loh_blob(linkedit.loh_bytes())?;
117
+    let mut entries = parse_loh_blob(linkedit.loh_bytes())?;
118
+    entries.sort_by(|lhs, rhs| {
119
+        rhs.args
120
+            .len()
121
+            .cmp(&lhs.args.len())
122
+            .then_with(|| lhs.args.first().cmp(&rhs.args.first()))
123
+            .then_with(|| lhs.kind.cmp(&rhs.kind))
124
+    });
109125
     let mut rewritten = HashSet::new();
110126
     for entry in entries {
111
-        if entry.kind != LOH_ARM64_ADRP_ADD || entry.args.len() != 2 {
112
-            continue;
127
+        match entry.kind {
128
+            LOH_ARM64_ADRP_LDR => relax_adrp_ldr(layout, &entry, &mut rewritten)?,
129
+            LOH_ARM64_ADRP_LDR_GOT_LDR => relax_adrp_ldr_got_ldr(layout, &entry, &mut rewritten)?,
130
+            LOH_ARM64_ADRP_ADD => relax_adrp_add(layout, &entry, &mut rewritten)?,
131
+            LOH_ARM64_ADRP_LDR_GOT => relax_adrp_ldr_got(layout, &entry, &mut rewritten)?,
132
+            _ => {}
113133
         }
134
+    }
135
+    Ok(())
136
+}
114137
 
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
-        }
138
+fn relax_adrp_add(
139
+    layout: &mut Layout,
140
+    entry: &LohEntry,
141
+    rewritten: &mut HashSet<u64>,
142
+) -> Result<(), LohError> {
143
+    if entry.args.len() != 2 {
144
+        return Ok(());
145
+    }
146
+    let adrp_off = entry.args[0] as u64;
147
+    let add_off = entry.args[1] as u64;
148
+    if !claim_offsets(rewritten, &[adrp_off, add_off]) {
149
+        return Ok(());
150
+    }
151
+    let adrp = locate_word(layout, adrp_off)?;
152
+    let add = locate_word(layout, add_off)?;
153
+    let Some(target) = decode_adrp_add_target(adrp.insn, add.insn, adrp.addr) else {
154
+        return Ok(());
155
+    };
156
+    let dest = (add.insn & 0x1f) as u8;
157
+    let Some(adr) = encode_adr(target, adrp.addr, dest) else {
158
+        return Ok(());
159
+    };
160
+    write_word(layout, adrp, adr)?;
161
+    write_word(layout, add, NOP)?;
162
+    Ok(())
163
+}
120164
 
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
-        };
165
+fn relax_adrp_ldr(
166
+    layout: &mut Layout,
167
+    entry: &LohEntry,
168
+    rewritten: &mut HashSet<u64>,
169
+) -> Result<(), LohError> {
170
+    if entry.args.len() != 2 {
171
+        return Ok(());
172
+    }
173
+    let adrp_off = entry.args[0] as u64;
174
+    let ldr_off = entry.args[1] as u64;
175
+    if !claim_offsets(rewritten, &[adrp_off, ldr_off]) {
176
+        return Ok(());
177
+    }
178
+    let adrp = locate_word(layout, adrp_off)?;
179
+    let ldr = locate_word(layout, ldr_off)?;
180
+    let Some(target) = decode_adrp_ldr_target(adrp.insn, ldr.insn, adrp.addr) else {
181
+        return Ok(());
182
+    };
183
+    let Some(literal) = encode_ldr_literal(ldr.insn, target, ldr.addr) else {
184
+        return Ok(());
185
+    };
186
+    write_word(layout, adrp, NOP)?;
187
+    write_word(layout, ldr, literal)?;
188
+    Ok(())
189
+}
129190
 
130
-        write_word(layout, adrp, adr)?;
131
-        write_word(layout, add, NOP)?;
191
+fn relax_adrp_ldr_got(
192
+    layout: &mut Layout,
193
+    entry: &LohEntry,
194
+    rewritten: &mut HashSet<u64>,
195
+) -> Result<(), LohError> {
196
+    if entry.args.len() != 2 {
197
+        return Ok(());
198
+    }
199
+    let adrp_off = entry.args[0] as u64;
200
+    let ldr_off = entry.args[1] as u64;
201
+    if !claim_offsets(rewritten, &[adrp_off, ldr_off]) {
202
+        return Ok(());
203
+    }
204
+    let adrp = locate_word(layout, adrp_off)?;
205
+    let ldr = locate_word(layout, ldr_off)?;
206
+    let Some(got_slot_addr) = decode_adrp_ldr_target(adrp.insn, ldr.insn, adrp.addr) else {
207
+        return Ok(());
208
+    };
209
+    if pageoff_load_kind(ldr.insn) != Some(LiteralLoadKind::X) {
210
+        return Ok(());
211
+    }
212
+    let Some(local_target) = read_u64_at_addr(layout, got_slot_addr) else {
213
+        return Ok(());
214
+    };
215
+    if !points_into_output(layout, local_target) {
216
+        return Ok(());
217
+    }
218
+    let dest = (ldr.insn & 0x1f) as u8;
219
+    let Some(adr) = encode_adr(local_target, adrp.addr, dest) else {
220
+        return Ok(());
221
+    };
222
+    write_word(layout, adrp, adr)?;
223
+    write_word(layout, ldr, NOP)?;
224
+    Ok(())
225
+}
226
+
227
+fn relax_adrp_ldr_got_ldr(
228
+    layout: &mut Layout,
229
+    entry: &LohEntry,
230
+    rewritten: &mut HashSet<u64>,
231
+) -> Result<(), LohError> {
232
+    if entry.args.len() != 3 {
233
+        return Ok(());
234
+    }
235
+    let adrp_off = entry.args[0] as u64;
236
+    let got_ldr_off = entry.args[1] as u64;
237
+    let final_ldr_off = entry.args[2] as u64;
238
+    if !claim_offsets(rewritten, &[adrp_off, got_ldr_off, final_ldr_off]) {
239
+        return Ok(());
240
+    }
241
+    let adrp = locate_word(layout, adrp_off)?;
242
+    let got_ldr = locate_word(layout, got_ldr_off)?;
243
+    let final_ldr = locate_word(layout, final_ldr_off)?;
244
+    let Some(got_slot_addr) = decode_adrp_ldr_target(adrp.insn, got_ldr.insn, adrp.addr) else {
245
+        return Ok(());
246
+    };
247
+    if pageoff_load_kind(got_ldr.insn) != Some(LiteralLoadKind::X) {
248
+        return Ok(());
249
+    }
250
+    let got_dest = (got_ldr.insn & 0x1f) as u8;
251
+    if load_base_reg(final_ldr.insn) != Some(got_dest) {
252
+        return Ok(());
253
+    }
254
+    let Some(local_target) = read_u64_at_addr(layout, got_slot_addr) else {
255
+        return Ok(());
256
+    };
257
+    if !points_into_output(layout, local_target) {
258
+        return Ok(());
132259
     }
260
+    let Some(adr) = encode_adr(local_target, adrp.addr, got_dest) else {
261
+        return Ok(());
262
+    };
263
+    write_word(layout, adrp, adr)?;
264
+    write_word(layout, got_ldr, NOP)?;
133265
     Ok(())
134266
 }
135267
 
268
+fn claim_offsets(rewritten: &mut HashSet<u64>, offsets: &[u64]) -> bool {
269
+    if offsets.iter().any(|offset| rewritten.contains(offset)) {
270
+        return false;
271
+    }
272
+    rewritten.extend(offsets.iter().copied());
273
+    true
274
+}
275
+
136276
 fn locate_word(layout: &Layout, file_offset: u64) -> Result<LocatedWord, LohError> {
137277
     for (section_idx, section) in layout.sections.iter().enumerate() {
138278
         for (atom_idx, atom) in section.atoms.iter().enumerate() {
@@ -194,6 +334,22 @@ fn decode_adrp_add_target(adrp: u32, add: u32, place: u64) -> Option<u64> {
194334
     Some((adrp_base as u64) + low)
195335
 }
196336
 
337
+fn decode_adrp_ldr_target(adrp: u32, ldr: u32, place: u64) -> Option<u64> {
338
+    let _kind = pageoff_load_kind(ldr)?;
339
+    let base = ((ldr >> 5) & 0x1f) as u8;
340
+    let adrp_reg = (adrp & 0x1f) as u8;
341
+    if adrp_reg == 31 || base != adrp_reg {
342
+        return None;
343
+    }
344
+    let adrp_immlo = ((adrp >> 29) & 0x3) as i64;
345
+    let adrp_immhi = ((adrp >> 5) & 0x7ffff) as i64;
346
+    let adrp_pages = sign_extend_21((adrp_immhi << 2) | adrp_immlo);
347
+    let adrp_base = ((place as i64) & !0xfff) + (adrp_pages << 12);
348
+    let shift = pageoff_shift(ldr);
349
+    let low = (((ldr >> 10) & 0xfff) as u64) << shift;
350
+    Some((adrp_base as u64) + low)
351
+}
352
+
197353
 fn encode_adr(target: u64, place: u64, reg: u8) -> Option<u32> {
198354
     if reg == 31 {
199355
         return None;
@@ -208,6 +364,28 @@ fn encode_adr(target: u64, place: u64, reg: u8) -> Option<u32> {
208364
     Some(0x1000_0000 | (immlo << 29) | (immhi << 5) | reg as u32)
209365
 }
210366
 
367
+fn encode_ldr_literal(insn: u32, target: u64, place: u64) -> Option<u32> {
368
+    let kind = pageoff_load_kind(insn)?;
369
+    let delta = (target as i64).wrapping_sub(place as i64);
370
+    if delta & 0b11 != 0 {
371
+        return None;
372
+    }
373
+    let imm = delta >> 2;
374
+    if !fits_signed(imm, 19) {
375
+        return None;
376
+    }
377
+    let encoded = (imm as u32) & 0x7ffff;
378
+    let rt = insn & 0x1f;
379
+    let base = match kind {
380
+        LiteralLoadKind::W => 0x1800_0000,
381
+        LiteralLoadKind::X => 0x5800_0000,
382
+        LiteralLoadKind::S => 0x1c00_0000,
383
+        LiteralLoadKind::D => 0x5c00_0000,
384
+        LiteralLoadKind::Q => 0x9c00_0000,
385
+    };
386
+    Some(base | (encoded << 5) | rt)
387
+}
388
+
211389
 fn is_adrp(insn: u32) -> bool {
212390
     (insn & 0x9f00_0000) == 0x9000_0000
213391
 }
@@ -216,6 +394,82 @@ fn is_add_imm_64(insn: u32) -> bool {
216394
     (insn & 0xffc0_0000) == 0x9100_0000
217395
 }
218396
 
397
+fn pageoff_load_kind(insn: u32) -> Option<LiteralLoadKind> {
398
+    match insn & 0xffc0_0000 {
399
+        0xb940_0000 => Some(LiteralLoadKind::W),
400
+        0xf940_0000 => Some(LiteralLoadKind::X),
401
+        0xbd40_0000 => Some(LiteralLoadKind::S),
402
+        0xfd40_0000 => Some(LiteralLoadKind::D),
403
+        0x3dc0_0000 => Some(LiteralLoadKind::Q),
404
+        _ => None,
405
+    }
406
+}
407
+
408
+fn load_base_reg(insn: u32) -> Option<u8> {
409
+    match insn & 0xffc0_0000 {
410
+        0xb940_0000 | 0xf940_0000 | 0xbd40_0000 | 0xfd40_0000 | 0x3dc0_0000 | 0x7940_0000
411
+        | 0x3940_0000 => Some(((insn >> 5) & 0x1f) as u8),
412
+        _ => None,
413
+    }
414
+}
415
+
416
+fn pageoff_shift(insn: u32) -> u64 {
417
+    if is_simd_fp_pageoff(insn) {
418
+        let size = ((insn >> 30) & 0b11) as u64;
419
+        let opc = ((insn >> 22) & 0b11) as u64;
420
+        if size == 0 && (opc & 0b10) != 0 {
421
+            4
422
+        } else {
423
+            size
424
+        }
425
+    } else {
426
+        ((insn >> 30) & 0b11) as u64
427
+    }
428
+}
429
+
430
+fn is_simd_fp_pageoff(insn: u32) -> bool {
431
+    ((insn >> 24) & 0b111) == 0b101
432
+}
433
+
434
+fn points_into_output(layout: &Layout, addr: u64) -> bool {
435
+    layout
436
+        .sections
437
+        .iter()
438
+        .any(|section| section.addr <= addr && addr < section.addr + section.size)
439
+}
440
+
441
+fn read_u64_at_addr(layout: &Layout, addr: u64) -> Option<u64> {
442
+    let bytes = read_bytes_at_addr(layout, addr, 8)?;
443
+    Some(u64::from_le_bytes(bytes.try_into().ok()?))
444
+}
445
+
446
+fn read_bytes_at_addr(layout: &Layout, addr: u64, len: usize) -> Option<Vec<u8>> {
447
+    for section in &layout.sections {
448
+        for atom in &section.atoms {
449
+            let start = section.addr + atom.offset;
450
+            let end = start + atom.data.len() as u64;
451
+            if start <= addr && addr + len as u64 <= end {
452
+                let word_off = (addr - start) as usize;
453
+                return Some(atom.data.get(word_off..word_off + len)?.to_vec());
454
+            }
455
+        }
456
+        if !section.synthetic_data.is_empty() {
457
+            let start = section.addr + section.synthetic_offset;
458
+            let end = start + section.synthetic_data.len() as u64;
459
+            if start <= addr && addr + len as u64 <= end {
460
+                let word_off = (addr - start) as usize;
461
+                return Some(
462
+                    section
463
+                        .synthetic_data
464
+                        .get(word_off..word_off + len)?
465
+                        .to_vec(),
466
+                );
467
+            }
468
+        }
469
+    }
470
+    None
471
+}
472
+
219473
 fn fits_signed(value: i64, bits: u32) -> bool {
220474
     let min = -(1i64 << (bits - 1));
221475
     let max = (1i64 << (bits - 1)) - 1;
@@ -279,4 +533,20 @@ mod tests {
279533
         let delta = sign_extend_21((immhi << 2) | immlo);
280534
         assert_eq!(place.wrapping_add_signed(delta), target);
281535
     }
536
+
537
+    #[test]
538
+    fn encode_ldr_literal_round_trips_x_load() {
539
+        let place = 0x1_0000_2004;
540
+        let target = place + 0x1fc;
541
+        let insn = 0xf940_0005u32;
542
+        let literal = encode_ldr_literal(insn, target, place).unwrap();
543
+        assert_eq!(literal & 0x1f, 5);
544
+        let imm = ((literal >> 5) & 0x7ffff) as i64;
545
+        let delta = if imm & (1 << 18) != 0 {
546
+            (imm | !0x7ffff) << 2
547
+        } else {
548
+            imm << 2
549
+        };
550
+        assert_eq!(place.wrapping_add_signed(delta), target);
551
+    }
282552
 }
tests/linker_run.rsmodified
@@ -9,7 +9,10 @@ use std::process::Command;
99
 mod common;
1010
 
1111
 use afs_ld::leb::read_uleb;
12
-use afs_ld::loh::{parse_loh_blob, LOH_ARM64_ADRP_ADD};
12
+use afs_ld::loh::{
13
+    parse_loh_blob, LOH_ARM64_ADRP_ADD, LOH_ARM64_ADRP_LDR, LOH_ARM64_ADRP_LDR_GOT,
14
+    LOH_ARM64_ADRP_LDR_GOT_LDR,
15
+};
1316
 use afs_ld::macho::constants::{
1417
     BIND_IMMEDIATE_MASK, BIND_OPCODE_ADD_ADDR_ULEB, BIND_OPCODE_DONE, BIND_OPCODE_DO_BIND,
1518
     BIND_OPCODE_DO_BIND_ADD_ADDR_IMM_SCALED, BIND_OPCODE_DO_BIND_ADD_ADDR_ULEB,
@@ -1819,6 +1822,13 @@ fn is_add_imm_64(insn: u32) -> bool {
18191822
     (insn & 0xffc0_0000) == 0x9100_0000
18201823
 }
18211824
 
1825
+fn is_ldr_literal(insn: u32) -> bool {
1826
+    matches!(
1827
+        insn & 0xff00_0000,
1828
+        0x1800_0000 | 0x5800_0000 | 0x1c00_0000 | 0x5c00_0000 | 0x9c00_0000
1829
+    )
1830
+}
1831
+
18221832
 fn decode_adr_target(insn: u32, place: u64) -> u64 {
18231833
     let immlo = ((insn >> 29) & 0x3) as i64;
18241834
     let immhi = ((insn >> 5) & 0x7ffff) as i64;
@@ -1826,6 +1836,16 @@ fn decode_adr_target(insn: u32, place: u64) -> u64 {
18261836
     place.wrapping_add_signed(delta)
18271837
 }
18281838
 
1839
+fn decode_ldr_literal_target(insn: u32, place: u64) -> u64 {
1840
+    let imm19 = ((insn >> 5) & 0x7ffff) as i64;
1841
+    let delta = if imm19 & (1 << 18) != 0 {
1842
+        (imm19 | !0x7ffff) << 2
1843
+    } else {
1844
+        imm19 << 2
1845
+    };
1846
+    place.wrapping_add_signed(delta)
1847
+}
1848
+
18291849
 fn sign_extend_26(value: i64) -> i64 {
18301850
     if value & (1 << 25) != 0 {
18311851
         value | !0x03ff_ffff
@@ -2174,6 +2194,247 @@ fn linker_run_no_loh_keeps_adrp_add_unrelaxed() {
21742194
     let _ = fs::remove_file(out);
21752195
 }
21762196
 
2197
+#[test]
2198
+fn linker_run_relaxes_adrp_ldr_loh_when_target_is_near() {
2199
+    if !have_xcrun() {
2200
+        eprintln!("skipping: xcrun unavailable");
2201
+        return;
2202
+    }
2203
+
2204
+    let obj = scratch("loh-adrp-ldr-near.o");
2205
+    let out = scratch("loh-adrp-ldr-near.out");
2206
+    let src = r#"
2207
+        .section __TEXT,__text,regular,pure_instructions
2208
+        .globl _main
2209
+        .globl _target
2210
+        _main:
2211
+        Lloh0:
2212
+            adrp x0, _target@PAGE
2213
+        Lloh1:
2214
+            ldr x1, [x0, _target@PAGEOFF]
2215
+            mov w0, #0
2216
+            ret
2217
+            .p2align 3
2218
+        _target:
2219
+            .quad 0x1122334455667788
2220
+        .loh AdrpLdr Lloh0, Lloh1
2221
+        .subsections_via_symbols
2222
+    "#;
2223
+    if let Err(e) = assemble(src, &obj) {
2224
+        eprintln!("skipping: assemble failed: {e}");
2225
+        return;
2226
+    }
2227
+
2228
+    let opts = LinkOptions {
2229
+        inputs: vec![obj.clone()],
2230
+        output: Some(out.clone()),
2231
+        kind: OutputKind::Executable,
2232
+        ..LinkOptions::default()
2233
+    };
2234
+    Linker::run(&opts).unwrap();
2235
+
2236
+    let bytes = fs::read(&out).unwrap();
2237
+    let loh = decode_loh(&bytes);
2238
+    assert_eq!(loh[0].kind, LOH_ARM64_ADRP_LDR);
2239
+    let target_addr = symbol_values(&bytes)["_target"];
2240
+    let (text_addr, text) = output_section(&bytes, "__TEXT", "__text").unwrap();
2241
+    let first = read_insn(&text, 0).unwrap();
2242
+    let second = read_insn(&text, 4).unwrap();
2243
+    assert_eq!(first, 0xd503_201f, "expected NOP after AdrpLdr relaxation");
2244
+    assert!(
2245
+        is_ldr_literal(second),
2246
+        "expected literal LDR, got {second:#010x}"
2247
+    );
2248
+    assert_eq!(
2249
+        decode_ldr_literal_target(second, text_addr + 4),
2250
+        target_addr
2251
+    );
2252
+
2253
+    let _ = fs::remove_file(obj);
2254
+    let _ = fs::remove_file(out);
2255
+}
2256
+
2257
+#[test]
2258
+fn linker_run_keeps_adrp_ldr_loh_when_target_is_far() {
2259
+    if !have_xcrun() {
2260
+        eprintln!("skipping: xcrun unavailable");
2261
+        return;
2262
+    }
2263
+
2264
+    let obj = scratch("loh-adrp-ldr-far.o");
2265
+    let out = scratch("loh-adrp-ldr-far.out");
2266
+    let src = r#"
2267
+        .section __TEXT,__text,regular,pure_instructions
2268
+        .globl _main
2269
+        .globl _target
2270
+        _main:
2271
+        Lloh0:
2272
+            adrp x0, _target@PAGE
2273
+        Lloh1:
2274
+            ldr x1, [x0, _target@PAGEOFF]
2275
+            mov w0, #0
2276
+            ret
2277
+            .space 0x200000
2278
+            .p2align 3
2279
+        _target:
2280
+            .quad 0x1122334455667788
2281
+        .loh AdrpLdr Lloh0, Lloh1
2282
+        .subsections_via_symbols
2283
+    "#;
2284
+    if let Err(e) = assemble(src, &obj) {
2285
+        eprintln!("skipping: assemble failed: {e}");
2286
+        return;
2287
+    }
2288
+
2289
+    let opts = LinkOptions {
2290
+        inputs: vec![obj.clone()],
2291
+        output: Some(out.clone()),
2292
+        kind: OutputKind::Executable,
2293
+        ..LinkOptions::default()
2294
+    };
2295
+    Linker::run(&opts).unwrap();
2296
+
2297
+    let bytes = fs::read(&out).unwrap();
2298
+    let target_addr = symbol_values(&bytes)["_target"];
2299
+    let (text_addr, text) = output_section(&bytes, "__TEXT", "__text").unwrap();
2300
+    let first = read_insn(&text, 0).unwrap();
2301
+    let second = read_insn(&text, 4).unwrap();
2302
+    assert!(is_adrp(first), "far AdrpLdr pair should stay ADRP");
2303
+    assert!(
2304
+        !is_ldr_literal(second),
2305
+        "far AdrpLdr pair should stay pageoff load, got literal {second:#010x}"
2306
+    );
2307
+    assert_eq!(
2308
+        decode_page_reference(&text, text_addr, 0, &PageRefKind::Load).unwrap(),
2309
+        target_addr
2310
+    );
2311
+
2312
+    let _ = fs::remove_file(obj);
2313
+    let _ = fs::remove_file(out);
2314
+}
2315
+
2316
+#[test]
2317
+fn linker_run_relaxes_adrp_ldr_got_loh_when_target_is_local() {
2318
+    if !have_xcrun() {
2319
+        eprintln!("skipping: xcrun unavailable");
2320
+        return;
2321
+    }
2322
+
2323
+    let obj = scratch("loh-adrp-ldr-got.o");
2324
+    let out = scratch("loh-adrp-ldr-got.out");
2325
+    let src = r#"
2326
+        .section __TEXT,__text,regular,pure_instructions
2327
+        .globl _main
2328
+        .globl _value
2329
+        _main:
2330
+        Lloh0:
2331
+            adrp x8, _value@GOTPAGE
2332
+        Lloh1:
2333
+            ldr x8, [x8, _value@GOTPAGEOFF]
2334
+            mov w0, #0
2335
+            ret
2336
+
2337
+        .section __DATA,__data
2338
+        .p2align 2
2339
+        _value:
2340
+            .long 7
2341
+        .loh AdrpLdrGot Lloh0, Lloh1
2342
+        .subsections_via_symbols
2343
+    "#;
2344
+    if let Err(e) = assemble(src, &obj) {
2345
+        eprintln!("skipping: assemble failed: {e}");
2346
+        return;
2347
+    }
2348
+
2349
+    let opts = LinkOptions {
2350
+        inputs: vec![obj.clone()],
2351
+        output: Some(out.clone()),
2352
+        kind: OutputKind::Executable,
2353
+        ..LinkOptions::default()
2354
+    };
2355
+    Linker::run(&opts).unwrap();
2356
+
2357
+    let bytes = fs::read(&out).unwrap();
2358
+    let loh = decode_loh(&bytes);
2359
+    assert_eq!(loh[0].kind, LOH_ARM64_ADRP_LDR_GOT);
2360
+    let target_addr = symbol_values(&bytes)["_value"];
2361
+    let (text_addr, text) = output_section(&bytes, "__TEXT", "__text").unwrap();
2362
+    let first = read_insn(&text, 0).unwrap();
2363
+    let second = read_insn(&text, 4).unwrap();
2364
+    assert!(is_adr(first), "expected ADR after AdrpLdrGot relaxation");
2365
+    assert_eq!(
2366
+        second, 0xd503_201f,
2367
+        "expected NOP after AdrpLdrGot relaxation"
2368
+    );
2369
+    assert_eq!(decode_adr_target(first, text_addr), target_addr);
2370
+
2371
+    let _ = fs::remove_file(obj);
2372
+    let _ = fs::remove_file(out);
2373
+}
2374
+
2375
+#[test]
2376
+fn linker_run_relaxes_adrp_ldr_got_ldr_loh_when_target_is_local() {
2377
+    if !have_xcrun() {
2378
+        eprintln!("skipping: xcrun unavailable");
2379
+        return;
2380
+    }
2381
+
2382
+    let obj = scratch("loh-adrp-ldr-got-ldr.o");
2383
+    let out = scratch("loh-adrp-ldr-got-ldr.out");
2384
+    let src = r#"
2385
+        .section __TEXT,__text,regular,pure_instructions
2386
+        .globl _main
2387
+        .globl _value
2388
+        _main:
2389
+        Lloh0:
2390
+            adrp x8, _value@GOTPAGE
2391
+        Lloh1:
2392
+            ldr x8, [x8, _value@GOTPAGEOFF]
2393
+        Lloh2:
2394
+            ldr w0, [x8]
2395
+            ret
2396
+
2397
+        .section __DATA,__data
2398
+        .p2align 2
2399
+        _value:
2400
+            .long 7
2401
+        .loh AdrpLdrGotLdr Lloh0, Lloh1, Lloh2
2402
+        .subsections_via_symbols
2403
+    "#;
2404
+    if let Err(e) = assemble(src, &obj) {
2405
+        eprintln!("skipping: assemble failed: {e}");
2406
+        return;
2407
+    }
2408
+
2409
+    let opts = LinkOptions {
2410
+        inputs: vec![obj.clone()],
2411
+        output: Some(out.clone()),
2412
+        kind: OutputKind::Executable,
2413
+        ..LinkOptions::default()
2414
+    };
2415
+    Linker::run(&opts).unwrap();
2416
+
2417
+    let bytes = fs::read(&out).unwrap();
2418
+    let loh = decode_loh(&bytes);
2419
+    assert_eq!(loh[0].kind, LOH_ARM64_ADRP_LDR_GOT_LDR);
2420
+    let target_addr = symbol_values(&bytes)["_value"];
2421
+    let (text_addr, text) = output_section(&bytes, "__TEXT", "__text").unwrap();
2422
+    let first = read_insn(&text, 0).unwrap();
2423
+    let second = read_insn(&text, 4).unwrap();
2424
+    let third = read_insn(&text, 8).unwrap();
2425
+    assert!(is_adr(first), "expected ADR after AdrpLdrGotLdr relaxation");
2426
+    assert_eq!(
2427
+        second, 0xd503_201f,
2428
+        "expected NOP after AdrpLdrGotLdr relaxation"
2429
+    );
2430
+    assert_eq!(decode_adr_target(first, text_addr), target_addr);
2431
+    assert_eq!(third, 0xb9400100, "expected final load to remain intact");
2432
+    assert_eq!(Command::new(&out).status().unwrap().code(), Some(7));
2433
+
2434
+    let _ = fs::remove_file(obj);
2435
+    let _ = fs::remove_file(out);
2436
+}
2437
+
21772438
 #[test]
21782439
 fn linker_run_emits_minimal_dylib_from_real_object() {
21792440
     if !have_xcrun() {