Rust · 18321 bytes Raw Blame History
1 //! `afs-ld --dump <path>` — otool-style summary of a Mach-O object.
2 //!
3 //! Sprint 1 prints the `mach_header_64` + every load command in `header.ncmds`.
4 //! Section bodies, symbols, strings, and relocations become visible as Sprint 2
5 //! and later sprints bring them into the reader's model.
6
7 use std::io::{self, Write};
8 use std::path::Path;
9
10 use crate::archive::{Archive, Flavor, SpecialMember};
11 use crate::input::ObjectFile;
12 use crate::macho::constants::*;
13 use crate::macho::dylib::{DylibFile, DylibLoadKind};
14 use crate::macho::exports::ExportKind;
15 use crate::macho::reader::{
16 BuildVersionCmd, DyldInfoCmd, DylibCmd, DysymtabCmd, LinkEditDataCmd, LoadCommand,
17 MachHeader64, RpathCmd, Section64Header, Segment64, SymtabCmd,
18 };
19 use crate::macho::tbd::parse_tbd;
20 use crate::reloc::{parse_raw_relocs, parse_relocs, Referent, Reloc, RelocKind};
21 use crate::section::InputSection;
22 use crate::symbol::{InputSymbol, SymKind};
23
24 pub fn dump_archive_file(path: &Path) -> io::Result<()> {
25 let bytes = std::fs::read(path)?;
26 let ar = Archive::open(path, &bytes)
27 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
28 let out = io::stdout();
29 let mut h = out.lock();
30 let flavor = match ar.flavor {
31 Flavor::Bsd => "BSD",
32 Flavor::Sysv => "SysV",
33 Flavor::GnuThin => "GNU-thin",
34 };
35 writeln!(h, "{}:", path.display())?;
36 writeln!(
37 h,
38 "archive: flavor={flavor} members={} symbols={}",
39 ar.members().len(),
40 ar.symbol_index().map(|i| i.len()).unwrap_or(0),
41 )?;
42 writeln!(h, "Members:")?;
43 for (i, m) in ar.members().iter().enumerate() {
44 let kind = match m.special {
45 SpecialMember::None => "obj",
46 SpecialMember::BsdSymIndex => "bsd-symindex",
47 SpecialMember::SysvSymIndex => "sysv-symindex",
48 SpecialMember::SysvLongNames => "sysv-longnames",
49 };
50 writeln!(
51 h,
52 " [{i}] @0x{:x} {kind:<16} {} ({} bytes)",
53 m.header_offset,
54 m.name,
55 m.body.len()
56 )?;
57 }
58 if let Some(idx) = ar.symbol_index() {
59 writeln!(h, "Symbols ({}):", idx.len())?;
60 for (i, e) in idx.entries.iter().enumerate().take(16) {
61 writeln!(
62 h,
63 " [{i}] {} -> member@0x{:x}",
64 e.name, e.member_header_offset
65 )?;
66 }
67 if idx.len() > 16 {
68 writeln!(h, " ... ({} more)", idx.len() - 16)?;
69 }
70 }
71 Ok(())
72 }
73
74 pub fn dump_tbd_file(path: &Path) -> io::Result<()> {
75 let src = std::fs::read_to_string(path)?;
76 let docs =
77 parse_tbd(&src).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
78 let out = io::stdout();
79 let mut h = out.lock();
80 writeln!(h, "{}:", path.display())?;
81 writeln!(h, "tbd: documents={}", docs.len())?;
82 for (i, tbd) in docs.iter().enumerate() {
83 let targets: Vec<String> = tbd.targets.iter().map(|t| t.as_string()).collect();
84 writeln!(
85 h,
86 " [{i}] install_name={:?} targets=[{}] current={} compat={}",
87 tbd.install_name,
88 targets.join(", "),
89 tbd.current_version.as_deref().unwrap_or("-"),
90 tbd.compatibility_version.as_deref().unwrap_or("-")
91 )?;
92 if !tbd.reexported_libraries.is_empty() {
93 let total: usize = tbd.reexported_libraries.iter().map(|s| s.value.len()).sum();
94 writeln!(h, " reexported-libraries: {total}")?;
95 }
96 let export_syms: usize = tbd.exports.iter().map(|s| s.value.total()).sum();
97 if export_syms > 0 {
98 writeln!(h, " exports: {export_syms} symbols total")?;
99 }
100 }
101 Ok(())
102 }
103
104 pub fn dump_dylib_file(path: &Path) -> io::Result<()> {
105 let bytes = std::fs::read(path)?;
106 let dy = DylibFile::parse(path, &bytes)
107 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
108 let out = io::stdout();
109 let mut h = out.lock();
110 writeln!(h, "{}:", path.display())?;
111 writeln!(
112 h,
113 "dylib: install_name={:?} current={} compat={}",
114 dy.install_name,
115 version_str(dy.current_version),
116 version_str(dy.compatibility_version)
117 )?;
118 writeln!(h, "Dependencies ({}):", dy.dependencies.len())?;
119 for d in &dy.dependencies {
120 let kind = match d.kind {
121 DylibLoadKind::Normal => "normal",
122 DylibLoadKind::Weak => "weak",
123 DylibLoadKind::Reexport => "reexport",
124 DylibLoadKind::Upward => "upward",
125 };
126 writeln!(
127 h,
128 " [{}] {kind:<8} {} current={} compat={}",
129 d.ordinal,
130 d.install_name,
131 version_str(d.current_version),
132 version_str(d.compatibility_version)
133 )?;
134 }
135 if !dy.rpaths.is_empty() {
136 writeln!(h, "Rpaths ({}):", dy.rpaths.len())?;
137 for (i, p) in dy.rpaths.iter().enumerate() {
138 writeln!(h, " [{i}] {p}")?;
139 }
140 }
141 let entries = dy
142 .exports
143 .entries()
144 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
145 writeln!(h, "Exports ({}):", entries.len())?;
146 for (i, e) in entries.iter().enumerate().take(32) {
147 let rhs = match &e.kind {
148 ExportKind::Regular { address } => format!("addr=0x{address:x}"),
149 ExportKind::ThreadLocal { address } => format!("tlv addr=0x{address:x}"),
150 ExportKind::Absolute { address } => format!("abs=0x{address:x}"),
151 ExportKind::Reexport {
152 ordinal,
153 imported_name,
154 } => {
155 if imported_name.is_empty() {
156 format!("reexport from dylib#{ordinal}")
157 } else {
158 format!("reexport from dylib#{ordinal} as {imported_name}")
159 }
160 }
161 ExportKind::StubAndResolver { stub, resolver } => {
162 format!("stub=0x{stub:x} resolver=0x{resolver:x}")
163 }
164 };
165 writeln!(h, " [{i}] {:<40} {rhs}", e.name)?;
166 }
167 if entries.len() > 32 {
168 writeln!(h, " ... ({} more)", entries.len() - 32)?;
169 }
170 Ok(())
171 }
172
173 pub fn dump_file(path: &Path) -> io::Result<()> {
174 let bytes = std::fs::read(path)?;
175 let obj = ObjectFile::parse(path, &bytes)
176 .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e.to_string()))?;
177 let out = io::stdout();
178 let mut h = out.lock();
179 writeln!(h, "{}:", path.display())?;
180 write_header(&mut h, &obj.header)?;
181 for (i, cmd) in obj.commands.iter().enumerate() {
182 write_command(&mut h, i, cmd)?;
183 }
184 write_sections(&mut h, &obj.sections)?;
185 write_symbols(&mut h, &obj)?;
186 Ok(())
187 }
188
189 fn write_header(w: &mut impl Write, hdr: &MachHeader64) -> io::Result<()> {
190 writeln!(
191 w,
192 "mach_header_64: {} {} ncmds={} sizeofcmds={} flags=0x{:08x}",
193 cpu_name(hdr.cputype),
194 filetype_name(hdr.filetype),
195 hdr.ncmds,
196 hdr.sizeofcmds,
197 hdr.flags
198 )
199 }
200
201 fn write_command(w: &mut impl Write, idx: usize, cmd: &LoadCommand) -> io::Result<()> {
202 writeln!(
203 w,
204 "Load command {}: {} cmdsize={}",
205 idx,
206 cmd_name(cmd.cmd()),
207 cmd.cmdsize()
208 )?;
209 match cmd {
210 LoadCommand::Segment64(s) => write_segment64(w, s),
211 LoadCommand::Symtab(s) => write_symtab(w, s),
212 LoadCommand::Dysymtab(d) => write_dysymtab(w, d),
213 LoadCommand::BuildVersion(b) => write_build_version(w, b),
214 LoadCommand::LinkerOptimizationHint(l) => write_linkedit_data(w, l, "LOH"),
215 LoadCommand::Dylib(d) => write_dylib(w, d),
216 LoadCommand::Rpath(r) => write_rpath(w, r),
217 LoadCommand::DyldInfoOnly(d) => write_dyld_info(w, d),
218 LoadCommand::DyldExportsTrie(l) => write_linkedit_data(w, l, "EXPORTS_TRIE"),
219 LoadCommand::DyldChainedFixups(l) => write_linkedit_data(w, l, "CHAINED_FIXUPS"),
220 LoadCommand::Raw { cmd, data, .. } => {
221 writeln!(w, " (raw — cmd=0x{:x}, payload {} bytes)", cmd, data.len())
222 }
223 }
224 }
225
226 fn write_dylib(w: &mut impl Write, d: &DylibCmd) -> io::Result<()> {
227 writeln!(
228 w,
229 " name={:?} timestamp={} current={} compat={}",
230 d.name,
231 d.timestamp,
232 version_str(d.current_version),
233 version_str(d.compatibility_version)
234 )
235 }
236
237 fn write_rpath(w: &mut impl Write, r: &RpathCmd) -> io::Result<()> {
238 writeln!(w, " path={:?}", r.path)
239 }
240
241 fn write_dyld_info(w: &mut impl Write, d: &DyldInfoCmd) -> io::Result<()> {
242 writeln!(
243 w,
244 " rebase=@{}..+{} bind=@{}..+{} weak_bind=@{}..+{} lazy_bind=@{}..+{} export=@{}..+{}",
245 d.rebase_off,
246 d.rebase_size,
247 d.bind_off,
248 d.bind_size,
249 d.weak_bind_off,
250 d.weak_bind_size,
251 d.lazy_bind_off,
252 d.lazy_bind_size,
253 d.export_off,
254 d.export_size
255 )
256 }
257
258 fn write_segment64(w: &mut impl Write, s: &Segment64) -> io::Result<()> {
259 writeln!(
260 w,
261 " segname={:<16} vmaddr=0x{:x} vmsize=0x{:x} fileoff={} filesize={} maxprot={} initprot={} nsects={} flags=0x{:x}",
262 format!("\"{}\"", s.segname_str()),
263 s.vmaddr,
264 s.vmsize,
265 s.fileoff,
266 s.filesize,
267 prot_str(s.maxprot),
268 prot_str(s.initprot),
269 s.sections.len(),
270 s.flags
271 )?;
272 for (i, sec) in s.sections.iter().enumerate() {
273 write_section(w, i, sec)?;
274 }
275 Ok(())
276 }
277
278 fn write_section(w: &mut impl Write, idx: usize, s: &Section64Header) -> io::Result<()> {
279 writeln!(
280 w,
281 " Section {}: {},{} addr=0x{:x} size=0x{:x} offset={} align=2^{} reloff={} nreloc={} flags=0x{:08x}",
282 idx,
283 s.segname_str(),
284 s.sectname_str(),
285 s.addr,
286 s.size,
287 s.offset,
288 s.align,
289 s.reloff,
290 s.nreloc,
291 s.flags
292 )
293 }
294
295 fn write_symtab(w: &mut impl Write, s: &SymtabCmd) -> io::Result<()> {
296 writeln!(
297 w,
298 " symoff={} nsyms={} stroff={} strsize={}",
299 s.symoff, s.nsyms, s.stroff, s.strsize
300 )
301 }
302
303 fn write_dysymtab(w: &mut impl Write, d: &DysymtabCmd) -> io::Result<()> {
304 writeln!(
305 w,
306 " ilocalsym={} nlocalsym={} iextdefsym={} nextdefsym={} iundefsym={} nundefsym={} indirectsymoff={} nindirectsyms={}",
307 d.ilocalsym,
308 d.nlocalsym,
309 d.iextdefsym,
310 d.nextdefsym,
311 d.iundefsym,
312 d.nundefsym,
313 d.indirectsymoff,
314 d.nindirectsyms
315 )
316 }
317
318 fn write_build_version(w: &mut impl Write, b: &BuildVersionCmd) -> io::Result<()> {
319 writeln!(
320 w,
321 " platform={} minos={} sdk={} ntools={}",
322 platform_name(b.platform),
323 version_str(b.minos),
324 version_str(b.sdk),
325 b.tools.len()
326 )?;
327 for t in &b.tools {
328 writeln!(w, " tool={} version={}", t.tool, version_str(t.version))?;
329 }
330 Ok(())
331 }
332
333 fn write_linkedit_data(w: &mut impl Write, l: &LinkEditDataCmd, kind: &str) -> io::Result<()> {
334 writeln!(w, " {kind} dataoff={} datasize={}", l.dataoff, l.datasize)
335 }
336
337 fn write_sections(w: &mut impl Write, secs: &[InputSection]) -> io::Result<()> {
338 if secs.is_empty() {
339 return Ok(());
340 }
341 writeln!(w, "Sections ({}):", secs.len())?;
342 for (i, s) in secs.iter().enumerate() {
343 writeln!(
344 w,
345 " [{i}] {},{:<16} {:?} addr=0x{:x} size=0x{:x} align=2^{} offset={} flags=0x{:08x}",
346 s.segname, s.sectname, s.kind, s.addr, s.size, s.align_pow2, s.offset, s.flags
347 )?;
348 if !s.data.is_empty() {
349 writeln!(w, " data: {}", hex_preview(&s.data, 16))?;
350 }
351 if s.nreloc > 0 {
352 writeln!(w, " relocs ({}):", s.nreloc)?;
353 match parse_raw_relocs(&s.raw_relocs, 0, s.nreloc).and_then(|raws| parse_relocs(&raws))
354 {
355 Ok(fused) => {
356 for (ri, r) in fused.iter().enumerate() {
357 writeln!(w, " [{ri}] {}", describe_reloc(r))?;
358 }
359 }
360 Err(e) => writeln!(w, " <parse error: {e}>")?,
361 }
362 }
363 }
364 Ok(())
365 }
366
367 fn write_symbols(w: &mut impl Write, obj: &ObjectFile) -> io::Result<()> {
368 if obj.symbols.is_empty() {
369 return Ok(());
370 }
371 writeln!(w, "Symbols ({}):", obj.symbols.len())?;
372 for (i, sym) in obj.symbols.iter().enumerate() {
373 let name = obj.symbol_name(sym).unwrap_or("<unresolved>");
374 writeln!(w, " [{i}] {:<32} {}", name, describe_symbol(sym))?;
375 }
376 Ok(())
377 }
378
379 fn describe_symbol(sym: &InputSymbol) -> String {
380 if let Some(stab) = sym.stab_kind() {
381 return format!(
382 "STAB kind=0x{stab:02x} sect={} value=0x{:x}",
383 sym.sect_idx(),
384 sym.value()
385 );
386 }
387 let mut parts: Vec<String> = Vec::new();
388 parts.push(
389 match sym.kind() {
390 SymKind::Undef => "UNDF",
391 SymKind::Abs => "ABS",
392 SymKind::Sect => "SECT",
393 SymKind::Indirect => "INDR",
394 }
395 .to_string(),
396 );
397 if sym.is_ext() {
398 parts.push("ext".into());
399 }
400 if sym.is_private_ext() {
401 parts.push("pext".into());
402 }
403 if sym.weak_def() {
404 parts.push("weak_def".into());
405 }
406 if sym.weak_ref() {
407 parts.push("weak_ref".into());
408 }
409 if sym.no_dead_strip() {
410 parts.push("no_dead_strip".into());
411 }
412 match sym.kind() {
413 SymKind::Sect => parts.push(format!("sect={} value=0x{:x}", sym.sect_idx(), sym.value())),
414 SymKind::Abs => parts.push(format!("value=0x{:x}", sym.value())),
415 SymKind::Undef => {
416 if let Some(sz) = sym.common_size() {
417 let a = sym.common_align_pow2().unwrap_or(0);
418 parts.push(format!("common size={sz} align=2^{a}"));
419 } else if let Some(ord) = sym.library_ordinal() {
420 parts.push(format!("ord={ord}"));
421 }
422 }
423 SymKind::Indirect => {
424 parts.push(format!("-> strx={}", sym.value() as u32));
425 }
426 }
427 parts.join(" ")
428 }
429
430 fn describe_reloc(r: &Reloc) -> String {
431 let kind = match r.kind {
432 RelocKind::Unsigned => "Unsigned",
433 RelocKind::Branch26 => "Branch26",
434 RelocKind::Page21 => "Page21",
435 RelocKind::PageOff12 => "PageOff12",
436 RelocKind::GotLoadPage21 => "GotLoadPage21",
437 RelocKind::GotLoadPageOff12 => "GotLoadPageOff12",
438 RelocKind::PointerToGot => "PointerToGot",
439 RelocKind::TlvpLoadPage21 => "TlvpLoadPage21",
440 RelocKind::TlvpLoadPageOff12 => "TlvpLoadPageOff12",
441 RelocKind::Subtractor => "Subtractor",
442 };
443 let width = r.length.byte_width();
444 let mut parts = vec![
445 format!("offset=0x{:x}", r.offset),
446 kind.to_string(),
447 format!("len={width}"),
448 ];
449 if r.pcrel {
450 parts.push("pcrel".into());
451 }
452 parts.push(match r.referent {
453 Referent::Symbol(i) => format!("sym={i}"),
454 Referent::Section(i) => format!("sect={i}"),
455 });
456 if let Some(sub) = r.subtrahend {
457 parts.push(match sub {
458 Referent::Symbol(i) => format!("- sym={i}"),
459 Referent::Section(i) => format!("- sect={i}"),
460 });
461 }
462 if r.addend != 0 {
463 parts.push(format!("+ 0x{:x}", r.addend));
464 }
465 parts.join(" ")
466 }
467
468 fn hex_preview(bytes: &[u8], max: usize) -> String {
469 let shown = bytes.iter().take(max);
470 let hex: Vec<String> = shown.map(|b| format!("{b:02x}")).collect();
471 if bytes.len() > max {
472 format!("{} ... ({} more)", hex.join(" "), bytes.len() - max)
473 } else {
474 hex.join(" ")
475 }
476 }
477
478 // ---- pretty-printers ------------------------------------------------------
479
480 fn cpu_name(ct: u32) -> &'static str {
481 match ct {
482 CPU_TYPE_ARM64 => "arm64",
483 _ => "??",
484 }
485 }
486
487 fn filetype_name(ft: u32) -> &'static str {
488 match ft {
489 MH_OBJECT => "MH_OBJECT",
490 MH_EXECUTE => "MH_EXECUTE",
491 MH_DYLIB => "MH_DYLIB",
492 _ => "??",
493 }
494 }
495
496 fn cmd_name(cmd: u32) -> String {
497 match cmd {
498 LC_SEGMENT_64 => "LC_SEGMENT_64".into(),
499 LC_SYMTAB => "LC_SYMTAB".into(),
500 LC_DYSYMTAB => "LC_DYSYMTAB".into(),
501 LC_BUILD_VERSION => "LC_BUILD_VERSION".into(),
502 LC_LINKER_OPTIMIZATION_HINT => "LC_LINKER_OPTIMIZATION_HINT".into(),
503 LC_UUID => "LC_UUID".into(),
504 LC_MAIN => "LC_MAIN".into(),
505 LC_LOAD_DYLIB => "LC_LOAD_DYLIB".into(),
506 LC_LOAD_DYLINKER => "LC_LOAD_DYLINKER".into(),
507 LC_LOAD_WEAK_DYLIB => "LC_LOAD_WEAK_DYLIB".into(),
508 LC_ID_DYLIB => "LC_ID_DYLIB".into(),
509 LC_REEXPORT_DYLIB => "LC_REEXPORT_DYLIB".into(),
510 LC_RPATH => "LC_RPATH".into(),
511 LC_CODE_SIGNATURE => "LC_CODE_SIGNATURE".into(),
512 LC_FUNCTION_STARTS => "LC_FUNCTION_STARTS".into(),
513 LC_DATA_IN_CODE => "LC_DATA_IN_CODE".into(),
514 LC_SOURCE_VERSION => "LC_SOURCE_VERSION".into(),
515 LC_DYLD_INFO_ONLY => "LC_DYLD_INFO_ONLY".into(),
516 LC_DYLD_CHAINED_FIXUPS => "LC_DYLD_CHAINED_FIXUPS".into(),
517 LC_DYLD_EXPORTS_TRIE => "LC_DYLD_EXPORTS_TRIE".into(),
518 _ => format!("LC_0x{cmd:x}"),
519 }
520 }
521
522 fn platform_name(p: u32) -> &'static str {
523 match p {
524 PLATFORM_MACOS => "MACOS",
525 PLATFORM_IOS => "IOS",
526 _ => "UNKNOWN",
527 }
528 }
529
530 /// X.Y.Z packed 0xXXXXYYZZ → "X.Y.Z".
531 fn version_str(v: u32) -> String {
532 let x = (v >> 16) & 0xffff;
533 let y = (v >> 8) & 0xff;
534 let z = v & 0xff;
535 format!("{x}.{y}.{z}")
536 }
537
538 fn prot_str(p: u32) -> String {
539 let r = if p & 1 != 0 { 'r' } else { '-' };
540 let w = if p & 2 != 0 { 'w' } else { '-' };
541 let x = if p & 4 != 0 { 'x' } else { '-' };
542 format!("{r}{w}{x}")
543 }
544
545 #[cfg(test)]
546 mod tests {
547 use super::*;
548
549 #[test]
550 fn version_str_examples() {
551 assert_eq!(version_str(0x000B_0000), "11.0.0");
552 assert_eq!(version_str(0x000E_0200), "14.2.0");
553 assert_eq!(version_str(0x000E_0203), "14.2.3");
554 }
555
556 #[test]
557 fn prot_str_examples() {
558 assert_eq!(prot_str(0), "---");
559 assert_eq!(prot_str(7), "rwx");
560 assert_eq!(prot_str(5), "r-x");
561 }
562 }
563