fortrangoingonforty/afs-ld / 336a3da

Browse files

decode LC_DYLD_INFO_ONLY and wire export trie through DylibFile

Authored by espadonne
SHA
336a3daa7d66e95640c9538126007d878ef7515f
Parents
015579b
Tree
20ce1a3

7 changed files

StatusFile+-
M src/args.rs 6 0
M src/dump.rs 93 2
M src/lib.rs 3 0
M src/macho/dylib.rs 30 13
M src/macho/reader.rs 100 0
M src/main.rs 10 0
A tests/dylib_integration.rs 103 0
src/args.rsmodified
@@ -68,6 +68,12 @@ pub fn parse(argv: &[String]) -> Result<LinkOptions, ArgsError> {
6868
                         .ok_or_else(|| ArgsError::MissingValue("--dump-archive".into()))?,
6969
                 ));
7070
             }
71
+            "--dump-dylib" => {
72
+                opts.dump_dylib = Some(PathBuf::from(
73
+                    it.next()
74
+                        .ok_or_else(|| ArgsError::MissingValue("--dump-dylib".into()))?,
75
+                ));
76
+            }
7177
             s if s.starts_with('-') => {
7278
                 return Err(ArgsError::UnknownFlag(s.to_string()));
7379
             }
src/dump.rsmodified
@@ -9,10 +9,12 @@ use std::path::Path;
99
 
1010
 use crate::archive::{Archive, Flavor, SpecialMember};
1111
 use crate::input::ObjectFile;
12
+use crate::macho::dylib::{DylibFile, DylibLoadKind};
13
+use crate::macho::exports::ExportKind;
1214
 use crate::macho::constants::*;
1315
 use crate::macho::reader::{
14
-    BuildVersionCmd, DylibCmd, DysymtabCmd, LinkEditDataCmd, LoadCommand, MachHeader64,
15
-    RpathCmd, Section64Header, Segment64, SymtabCmd,
16
+    BuildVersionCmd, DyldInfoCmd, DylibCmd, DysymtabCmd, LinkEditDataCmd, LoadCommand,
17
+    MachHeader64, RpathCmd, Section64Header, Segment64, SymtabCmd,
1618
 };
1719
 use crate::reloc::{parse_raw_relocs, parse_relocs, Referent, Reloc, RelocKind};
1820
 use crate::section::InputSection;
@@ -64,6 +66,75 @@ pub fn dump_archive_file(path: &Path) -> io::Result<()> {
6466
     Ok(())
6567
 }
6668
 
69
+pub fn dump_dylib_file(path: &Path) -> io::Result<()> {
70
+    let bytes = std::fs::read(path)?;
71
+    let dy = DylibFile::parse(path, &bytes)
72
+        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
73
+    let out = io::stdout();
74
+    let mut h = out.lock();
75
+    writeln!(h, "{}:", path.display())?;
76
+    writeln!(
77
+        h,
78
+        "dylib: install_name={:?} current={} compat={}",
79
+        dy.install_name,
80
+        version_str(dy.current_version),
81
+        version_str(dy.compatibility_version)
82
+    )?;
83
+    writeln!(h, "Dependencies ({}):", dy.dependencies.len())?;
84
+    for d in &dy.dependencies {
85
+        let kind = match d.kind {
86
+            DylibLoadKind::Normal => "normal",
87
+            DylibLoadKind::Weak => "weak",
88
+            DylibLoadKind::Reexport => "reexport",
89
+            DylibLoadKind::Upward => "upward",
90
+        };
91
+        writeln!(
92
+            h,
93
+            "  [{}] {kind:<8} {} current={} compat={}",
94
+            d.ordinal,
95
+            d.install_name,
96
+            version_str(d.current_version),
97
+            version_str(d.compatibility_version)
98
+        )?;
99
+    }
100
+    if !dy.rpaths.is_empty() {
101
+        writeln!(h, "Rpaths ({}):", dy.rpaths.len())?;
102
+        for (i, p) in dy.rpaths.iter().enumerate() {
103
+            writeln!(h, "  [{i}] {p}")?;
104
+        }
105
+    }
106
+    let entries = dy
107
+        .exports
108
+        .entries()
109
+        .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
110
+    writeln!(h, "Exports ({}):", entries.len())?;
111
+    for (i, e) in entries.iter().enumerate().take(32) {
112
+        let rhs = match &e.kind {
113
+            ExportKind::Regular { address } => format!("addr=0x{address:x}"),
114
+            ExportKind::ThreadLocal { address } => format!("tlv addr=0x{address:x}"),
115
+            ExportKind::Absolute { address } => format!("abs=0x{address:x}"),
116
+            ExportKind::Reexport {
117
+                ordinal,
118
+                imported_name,
119
+            } => {
120
+                if imported_name.is_empty() {
121
+                    format!("reexport from dylib#{ordinal}")
122
+                } else {
123
+                    format!("reexport from dylib#{ordinal} as {imported_name}")
124
+                }
125
+            }
126
+            ExportKind::StubAndResolver { stub, resolver } => {
127
+                format!("stub=0x{stub:x} resolver=0x{resolver:x}")
128
+            }
129
+        };
130
+        writeln!(h, "  [{i}] {:<40} {rhs}", e.name)?;
131
+    }
132
+    if entries.len() > 32 {
133
+        writeln!(h, "  ... ({} more)", entries.len() - 32)?;
134
+    }
135
+    Ok(())
136
+}
137
+
67138
 pub fn dump_file(path: &Path) -> io::Result<()> {
68139
     let bytes = std::fs::read(path)?;
69140
     let obj = ObjectFile::parse(path, &bytes)
@@ -108,6 +179,9 @@ fn write_command(w: &mut impl Write, idx: usize, cmd: &LoadCommand) -> io::Resul
108179
         LoadCommand::LinkerOptimizationHint(l) => write_linkedit_data(w, l, "LOH"),
109180
         LoadCommand::Dylib(d) => write_dylib(w, d),
110181
         LoadCommand::Rpath(r) => write_rpath(w, r),
182
+        LoadCommand::DyldInfoOnly(d) => write_dyld_info(w, d),
183
+        LoadCommand::DyldExportsTrie(l) => write_linkedit_data(w, l, "EXPORTS_TRIE"),
184
+        LoadCommand::DyldChainedFixups(l) => write_linkedit_data(w, l, "CHAINED_FIXUPS"),
111185
         LoadCommand::Raw { cmd, data, .. } => {
112186
             writeln!(
113187
                 w,
@@ -134,6 +208,23 @@ fn write_rpath(w: &mut impl Write, r: &RpathCmd) -> io::Result<()> {
134208
     writeln!(w, "  path={:?}", r.path)
135209
 }
136210
 
211
+fn write_dyld_info(w: &mut impl Write, d: &DyldInfoCmd) -> io::Result<()> {
212
+    writeln!(
213
+        w,
214
+        "  rebase=@{}..+{} bind=@{}..+{} weak_bind=@{}..+{} lazy_bind=@{}..+{} export=@{}..+{}",
215
+        d.rebase_off,
216
+        d.rebase_size,
217
+        d.bind_off,
218
+        d.bind_size,
219
+        d.weak_bind_off,
220
+        d.weak_bind_size,
221
+        d.lazy_bind_off,
222
+        d.lazy_bind_size,
223
+        d.export_off,
224
+        d.export_size
225
+    )
226
+}
227
+
137228
 fn write_segment64(w: &mut impl Write, s: &Segment64) -> io::Result<()> {
138229
     writeln!(
139230
         w,
src/lib.rsmodified
@@ -38,6 +38,8 @@ pub struct LinkOptions {
3838
     pub dump: Option<PathBuf>,
3939
     /// When set, afs-ld dumps the named static archive's structure.
4040
     pub dump_archive: Option<PathBuf>,
41
+    /// When set, afs-ld dumps the named MH_DYLIB's load commands + exports.
42
+    pub dump_dylib: Option<PathBuf>,
4143
 }
4244
 
4345
 impl Default for LinkOptions {
@@ -50,6 +52,7 @@ impl Default for LinkOptions {
5052
             kind: OutputKind::Executable,
5153
             dump: None,
5254
             dump_archive: None,
55
+            dump_dylib: None,
5356
         }
5457
     }
5558
 }
src/macho/dylib.rsmodified
@@ -127,29 +127,46 @@ impl DylibFile {
127127
     }
128128
 }
129129
 
130
-/// The export trie lives in `__LINKEDIT` pointed at either by
131
-/// `LC_DYLD_INFO_ONLY.export_off / export_size` (classic) or by
132
-/// `LC_DYLD_EXPORTS_TRIE` (chained-fixups era). We accept both; the latter
133
-/// shares the `linkedit_data_command` wire shape.
134
-///
135
-/// Dylibs built with older toolchains may have no export trie at all (the
136
-/// symbol table was the only source of exports). Return an empty trie so
137
-/// downstream code doesn't crash.
130
+/// Locate the export-trie bytes in either `LC_DYLD_INFO_ONLY.export_*` or
131
+/// `LC_DYLD_EXPORTS_TRIE` (chained-fixups era). Dylibs built by older
132
+/// toolchains may have no export trie; in that case return an empty trie.
138133
 fn locate_exports_trie(
139134
     commands: &[LoadCommand],
140135
     file_bytes: &[u8],
141136
 ) -> Result<ExportTrie, ReadError> {
142
-    // Sprint 5 intentionally surfaces only the raw trie bytes; the walker
143
-    // arrives in the next commit. Placeholder for now: return the empty trie.
144137
     for cmd in commands {
145
-        if let LoadCommand::LinkerOptimizationHint(_) = cmd {
146
-            // LC_LOH is not the trie — just here to keep the walk explicit.
138
+        match cmd {
139
+            LoadCommand::DyldInfoOnly(d) if d.export_size != 0 => {
140
+                return trie_slice(file_bytes, d.export_off, d.export_size);
141
+            }
142
+            LoadCommand::DyldExportsTrie(l) if l.datasize != 0 => {
143
+                return trie_slice(file_bytes, l.dataoff, l.datasize);
144
+            }
145
+            _ => {}
147146
         }
148147
     }
149
-    let _ = file_bytes;
150148
     Ok(ExportTrie::empty())
151149
 }
152150
 
151
+fn trie_slice(file_bytes: &[u8], off: u32, size: u32) -> Result<ExportTrie, ReadError> {
152
+    let start = off as usize;
153
+    let end = start
154
+        .checked_add(size as usize)
155
+        .ok_or(ReadError::Truncated {
156
+            need: usize::MAX,
157
+            have: file_bytes.len(),
158
+            context: "export trie (offset + size overflows)",
159
+        })?;
160
+    if end > file_bytes.len() {
161
+        return Err(ReadError::Truncated {
162
+            need: end,
163
+            have: file_bytes.len(),
164
+            context: "export trie",
165
+        });
166
+    }
167
+    Ok(ExportTrie::from_bytes(&file_bytes[start..end]))
168
+}
169
+
153170
 /// Look up the 1-based ordinal of a dependency by its install name. Used by
154171
 /// Sprint 14's symbol-table writer when encoding each undefined symbol's
155172
 /// two-level-namespace library ordinal into its `n_desc` high byte.
src/macho/reader.rsmodified
@@ -134,6 +134,14 @@ pub enum LoadCommand {
134134
     Dylib(DylibCmd),
135135
     /// `LC_RPATH` — one runtime-search path per entry.
136136
     Rpath(RpathCmd),
137
+    /// `LC_DYLD_INFO_ONLY` — classic locator for rebase/bind/lazy/weak/export
138
+    /// streams in `__LINKEDIT`.
139
+    DyldInfoOnly(DyldInfoCmd),
140
+    /// `LC_DYLD_EXPORTS_TRIE` — the modern chained-fixups alternative that
141
+    /// holds only the export trie (paired with `LC_DYLD_CHAINED_FIXUPS`).
142
+    DyldExportsTrie(LinkEditDataCmd),
143
+    /// `LC_DYLD_CHAINED_FIXUPS` — pointer to the chained-fixups blob.
144
+    DyldChainedFixups(LinkEditDataCmd),
137145
     /// A load command whose payload we haven't decoded yet. Preserves bytes
138146
     /// verbatim for byte-level round-trip.
139147
     Raw { cmd: u32, cmdsize: u32, data: Vec<u8> },
@@ -149,6 +157,9 @@ impl LoadCommand {
149157
             LoadCommand::LinkerOptimizationHint(_) => LC_LINKER_OPTIMIZATION_HINT,
150158
             LoadCommand::Dylib(d) => d.cmd,
151159
             LoadCommand::Rpath(_) => LC_RPATH,
160
+            LoadCommand::DyldInfoOnly(_) => LC_DYLD_INFO_ONLY,
161
+            LoadCommand::DyldExportsTrie(_) => LC_DYLD_EXPORTS_TRIE,
162
+            LoadCommand::DyldChainedFixups(_) => LC_DYLD_CHAINED_FIXUPS,
152163
             LoadCommand::Raw { cmd, .. } => *cmd,
153164
         }
154165
     }
@@ -162,6 +173,9 @@ impl LoadCommand {
162173
             LoadCommand::LinkerOptimizationHint(_) => LinkEditDataCmd::WIRE_SIZE,
163174
             LoadCommand::Dylib(d) => d.wire_size(),
164175
             LoadCommand::Rpath(r) => r.wire_size(),
176
+            LoadCommand::DyldInfoOnly(_) => DyldInfoCmd::WIRE_SIZE,
177
+            LoadCommand::DyldExportsTrie(_) => LinkEditDataCmd::WIRE_SIZE,
178
+            LoadCommand::DyldChainedFixups(_) => LinkEditDataCmd::WIRE_SIZE,
165179
             LoadCommand::Raw { cmdsize, .. } => *cmdsize,
166180
         }
167181
     }
@@ -431,6 +445,17 @@ fn decode_command(cmd: u32, cmdsize: u32, payload: &[u8]) -> Result<LoadCommand,
431445
         | LC_REEXPORT_DYLIB
432446
         | LC_LOAD_UPWARD_DYLIB => Ok(LoadCommand::Dylib(DylibCmd::parse(cmd, cmdsize, payload)?)),
433447
         LC_RPATH => Ok(LoadCommand::Rpath(RpathCmd::parse(cmdsize, payload)?)),
448
+        LC_DYLD_INFO_ONLY => Ok(LoadCommand::DyldInfoOnly(DyldInfoCmd::parse(cmdsize, payload)?)),
449
+        LC_DYLD_EXPORTS_TRIE => Ok(LoadCommand::DyldExportsTrie(LinkEditDataCmd::parse(
450
+            LC_DYLD_EXPORTS_TRIE,
451
+            cmdsize,
452
+            payload,
453
+        )?)),
454
+        LC_DYLD_CHAINED_FIXUPS => Ok(LoadCommand::DyldChainedFixups(LinkEditDataCmd::parse(
455
+            LC_DYLD_CHAINED_FIXUPS,
456
+            cmdsize,
457
+            payload,
458
+        )?)),
434459
         _ => Ok(LoadCommand::Raw {
435460
             cmd,
436461
             cmdsize,
@@ -452,6 +477,9 @@ pub fn write_commands(cmds: &[LoadCommand], out: &mut Vec<u8>) {
452477
             LoadCommand::LinkerOptimizationHint(l) => l.write(LC_LINKER_OPTIMIZATION_HINT, out),
453478
             LoadCommand::Dylib(d) => d.write(out),
454479
             LoadCommand::Rpath(r) => r.write(out),
480
+            LoadCommand::DyldInfoOnly(d) => d.write(out),
481
+            LoadCommand::DyldExportsTrie(l) => l.write(LC_DYLD_EXPORTS_TRIE, out),
482
+            LoadCommand::DyldChainedFixups(l) => l.write(LC_DYLD_CHAINED_FIXUPS, out),
455483
             LoadCommand::Raw { cmd, cmdsize, data } => {
456484
                 out.extend_from_slice(&cmd.to_le_bytes());
457485
                 out.extend_from_slice(&cmdsize.to_le_bytes());
@@ -865,6 +893,78 @@ fn pad8(n: usize) -> usize {
865893
     (n + 7) & !7
866894
 }
867895
 
896
+// ---------------------------------------------------------------------------
897
+// LC_DYLD_INFO_ONLY — classic locator for rebase/bind/lazy/weak/export.
898
+// ---------------------------------------------------------------------------
899
+
900
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
901
+pub struct DyldInfoCmd {
902
+    pub rebase_off: u32,
903
+    pub rebase_size: u32,
904
+    pub bind_off: u32,
905
+    pub bind_size: u32,
906
+    pub weak_bind_off: u32,
907
+    pub weak_bind_size: u32,
908
+    pub lazy_bind_off: u32,
909
+    pub lazy_bind_size: u32,
910
+    pub export_off: u32,
911
+    pub export_size: u32,
912
+}
913
+
914
+impl DyldInfoCmd {
915
+    pub const WIRE_SIZE: u32 = 8 + 40;
916
+
917
+    pub fn parse(cmdsize: u32, payload: &[u8]) -> Result<Self, ReadError> {
918
+        if cmdsize != Self::WIRE_SIZE {
919
+            return Err(ReadError::BadCmdsize {
920
+                cmd: LC_DYLD_INFO_ONLY,
921
+                cmdsize,
922
+                at_offset: 0,
923
+                reason: "LC_DYLD_INFO_ONLY cmdsize must be 48",
924
+            });
925
+        }
926
+        if payload.len() < 40 {
927
+            return Err(ReadError::Truncated {
928
+                need: 40,
929
+                have: payload.len(),
930
+                context: "dyld_info_command",
931
+            });
932
+        }
933
+        let g = |i: usize| u32_le(&payload[i * 4..(i + 1) * 4]);
934
+        Ok(DyldInfoCmd {
935
+            rebase_off: g(0),
936
+            rebase_size: g(1),
937
+            bind_off: g(2),
938
+            bind_size: g(3),
939
+            weak_bind_off: g(4),
940
+            weak_bind_size: g(5),
941
+            lazy_bind_off: g(6),
942
+            lazy_bind_size: g(7),
943
+            export_off: g(8),
944
+            export_size: g(9),
945
+        })
946
+    }
947
+
948
+    pub fn write(&self, out: &mut Vec<u8>) {
949
+        out.extend_from_slice(&LC_DYLD_INFO_ONLY.to_le_bytes());
950
+        out.extend_from_slice(&Self::WIRE_SIZE.to_le_bytes());
951
+        for v in [
952
+            self.rebase_off,
953
+            self.rebase_size,
954
+            self.bind_off,
955
+            self.bind_size,
956
+            self.weak_bind_off,
957
+            self.weak_bind_size,
958
+            self.lazy_bind_off,
959
+            self.lazy_bind_size,
960
+            self.export_off,
961
+            self.export_size,
962
+        ] {
963
+            out.extend_from_slice(&v.to_le_bytes());
964
+        }
965
+    }
966
+}
967
+
868968
 // ---------------------------------------------------------------------------
869969
 // linkedit_data_command — shared wire format for LC_LINKER_OPTIMIZATION_HINT,
870970
 // LC_FUNCTION_STARTS, LC_DATA_IN_CODE, LC_CODE_SIGNATURE, LC_DYLD_EXPORTS_TRIE,
src/main.rsmodified
@@ -33,6 +33,16 @@ fn main() -> ExitCode {
3333
         };
3434
     }
3535
 
36
+    if let Some(path) = &opts.dump_dylib {
37
+        return match dump::dump_dylib_file(path) {
38
+            Ok(()) => ExitCode::SUCCESS,
39
+            Err(e) => {
40
+                diag::error(&format!("{}: {}", path.display(), e));
41
+                ExitCode::from(1)
42
+            }
43
+        };
44
+    }
45
+
3646
     match Linker::run(&opts) {
3747
         Ok(()) => ExitCode::SUCCESS,
3848
         Err(LinkError::NoInputs) => {
tests/dylib_integration.rsadded
@@ -0,0 +1,103 @@
1
+//! Sprint 5 real-world gate: build a tiny dylib with `clang`, parse it via
2
+//! `DylibFile`, and confirm:
3
+//!
4
+//! - `install_name` is what `-install_name` requested;
5
+//! - the exported symbol surfaces in the trie;
6
+//! - at least one `LC_LOAD_DYLIB` dependency is present (every clang-linked
7
+//!   dylib picks up libSystem).
8
+//!
9
+//! Skipped if `xcrun clang` isn't available or fails for any reason.
10
+
11
+use std::path::PathBuf;
12
+use std::process::Command;
13
+
14
+use afs_ld::macho::dylib::DylibFile;
15
+use afs_ld::macho::exports::ExportKind;
16
+
17
+fn build_test_dylib(src: &str, out: &PathBuf) -> Result<(), String> {
18
+    let mut child = Command::new("xcrun")
19
+        .args([
20
+            "--sdk",
21
+            "macosx",
22
+            "clang",
23
+            "-x",
24
+            "c",
25
+            "-arch",
26
+            "arm64",
27
+            "-shared",
28
+            "-o",
29
+        ])
30
+        .arg(out)
31
+        .arg("-install_name")
32
+        .arg("@rpath/libafsldtest.dylib")
33
+        .arg("-")
34
+        .stdin(std::process::Stdio::piped())
35
+        .stdout(std::process::Stdio::piped())
36
+        .stderr(std::process::Stdio::piped())
37
+        .spawn()
38
+        .map_err(|e| format!("spawn: {e}"))?;
39
+    use std::io::Write;
40
+    child
41
+        .stdin
42
+        .as_mut()
43
+        .unwrap()
44
+        .write_all(src.as_bytes())
45
+        .map_err(|e| format!("write: {e}"))?;
46
+    let out = child.wait_with_output().map_err(|e| format!("wait: {e}"))?;
47
+    if !out.status.success() {
48
+        return Err(format!(
49
+            "clang failed: {}",
50
+            String::from_utf8_lossy(&out.stderr)
51
+        ));
52
+    }
53
+    Ok(())
54
+}
55
+
56
+#[test]
57
+fn small_clang_dylib_parses_and_exports_function() {
58
+    let which = Command::new("xcrun").arg("-f").arg("clang").output();
59
+    if !matches!(which, Ok(o) if o.status.success()) {
60
+        eprintln!("skipping: xcrun clang unavailable");
61
+        return;
62
+    }
63
+
64
+    use std::sync::atomic::{AtomicUsize, Ordering};
65
+    static SEQ: AtomicUsize = AtomicUsize::new(0);
66
+    let out_path = std::env::temp_dir().join(format!(
67
+        "afs-ld-dylib-{}-{}.dylib",
68
+        std::process::id(),
69
+        SEQ.fetch_add(1, Ordering::Relaxed)
70
+    ));
71
+
72
+    let src = r#"
73
+        int afsld_answer(int x) { return x + 42; }
74
+    "#;
75
+    if let Err(e) = build_test_dylib(src, &out_path) {
76
+        eprintln!("skipping: clang could not build test dylib: {e}");
77
+        return;
78
+    }
79
+
80
+    let bytes = std::fs::read(&out_path).expect("read test dylib");
81
+    let dy = DylibFile::parse(&out_path, &bytes).expect("parse test dylib");
82
+
83
+    assert_eq!(dy.install_name, "@rpath/libafsldtest.dylib");
84
+
85
+    // clang-linked dylibs pull libSystem in as a Normal dependency.
86
+    assert!(
87
+        dy.dependencies
88
+            .iter()
89
+            .any(|d| d.install_name.contains("libSystem")),
90
+        "expected a libSystem dependency; got {:?}",
91
+        dy.dependencies
92
+    );
93
+
94
+    let entries = dy.exports.entries().expect("decode exports");
95
+    let name = "_afsld_answer";
96
+    let found = entries.iter().find(|e| e.name == name).unwrap_or_else(|| {
97
+        let exported: Vec<&str> = entries.iter().map(|e| e.name.as_str()).collect();
98
+        panic!("expected {name} in exports; got {exported:?}")
99
+    });
100
+    assert!(matches!(found.kind, ExportKind::Regular { .. }));
101
+
102
+    let _ = std::fs::remove_file(&out_path);
103
+}