C# 解析Mach-O文件 C#如何读取macOS/iOS可执行文件的结构

7次阅读

c# 默认无法解析 mach-o 文件,因其运行时无内置 mach-o 解析器,需手动识别魔数(0xfeedfacf等)、cpu 类型、加载命令及对齐规则,且须处理 fat 格式多架构切片

C# 解析Mach-O文件 C#如何读取macOS/iOS可执行文件的结构

为什么 C# 默认读不了 Mach-O 文件

Mach-O 是 macOS/ios 原生二进制格式,和 PE(windows)或 ELF(linux)互不兼容。C# 运行时(.NET)本身不提供 MachOReader 或类似内置类型,System.IO.BinaryReader 可以读字节,但无法理解段(__TEXT)、负载(LC_LOAD_DYLIB)、CPU 架构标识(0x01000007 表示 arm64)这些语义——你得自己解析头、加载命令、节表。

microsoft.Extensions.FileSystemGlobbingSystem.IO 读取文件没问题,但别指望自动识别格式

常见错误是直接用 File.ReadAllBytes 拿到数据后,就按 ELF 或 PE 的偏移去硬读字段,结果在 magic 处就失败:Mach-O 的魔数是 0xFEEDFACF(32 位)或 0xFEEDFACF/0xFEEDFACF(64 位/大端变体),不是 0x454C4600(ELF)或 0x5A4D(MZ)。

实操建议:

  • 先用 BinaryReader.ReadUInt32() 读前 4 字节,比对是否为 0xFEEDFACF0xCFFAEDFE(字节序翻转)
  • 接着读 cpu_type(4 字节)和 cpu_subtype(4 字节),确认是不是 0x01000007(arm64)、0x0100000B(arm64e)或 0x00000007(x86_64)
  • 别跳过 filetype 字段(如 0x00000002 表示可执行,0x00000008 表示动态库),它影响后续加载命令的解释方式

解析 load command 时最容易踩内存越界和字段对齐坑

Mach-O 的加载命令(Struct load_command)是变长结构,每个命令以 cmd(uint32)和 cmdsize(uint32)开头,后面紧跟命令专属数据。.NET 默认不按 8 字节对齐读结构体,而 Mach-O 要求所有字段自然对齐(比如 uint64 必须从 8 字节边界开始)。

实操建议:

  • 不要用 [StructLayout(LayoutKind.Sequential)] 直接映射整个 load_command —— 因为不同 cmd 类型(LC_SEGMENT_64 vs LC_SYMTAB)布局完全不同
  • 先读 cmdcmdsize,再根据 cmd 值决定怎么读后续字节:比如 LC_SEGMENT_64(值 0x19)后面跟着 segname[16]vmaddrvmsize 等共 72 字节
  • BinaryReader.BaseStream.Seek() 跳转,而不是靠结构体大小累加,避免因填充字节导致偏移错乱

想提取符号表或依赖库?重点看 LC_SYMTABLC_LOAD_DYLIB

LC_SYMTAB 告诉你符号字符串在哪(symoff)、有多少个符号(nsyms),但它不包含字符串内容;字符串实际存在另一个叫 LC_DYSYMTAB 的命令指定的 stroff 偏移处。而 LC_LOAD_DYLIBdylib.name 是个相对偏移,必须加上 stroff 才能定位字符串起始。

实操建议:

  • 先遍历所有 load command,缓存 LC_SYMTABsymoff/nsymsLC_DYSYMTABstroff/strsize
  • 读符号表时,每个符号是 struct nlist_64(16 字节),其中 n_un.n_strx 是字符串表中的索引,不是地址
  • 依赖库名读取:拿到 LC_LOAD_DYLIB 中的 name 字段(其实是 uint32 偏移),加上之前缓存的 stroff,再从字符串表中截取直到

真正麻烦的是 fat Mach-O(多架构合一文件),它开头是 fat_header,后面跟着多个 fat_arch 描述各 slice 的偏移和大小——不处理这个,你可能只读到了 x86_64 部分,却在 arm64 设备上运行失败。

text=ZqhQzanResources