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

为什么 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.FileSystemGlobbing 或 System.IO 读取文件没问题,但别指望自动识别格式
常见错误是直接用 File.ReadAllBytes 拿到数据后,就按 ELF 或 PE 的偏移去硬读字段,结果在 magic 处就失败:Mach-O 的魔数是 0xFEEDFACF(32 位)或 0xFEEDFACF/0xFEEDFACF(64 位/大端变体),不是 0x454C4600(ELF)或 0x5A4D(MZ)。
实操建议:
- 先用
BinaryReader.ReadUInt32()读前 4 字节,比对是否为0xFEEDFACF或0xCFFAEDFE(字节序翻转) - 接着读
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_64vsLC_SYMTAB)布局完全不同 - 先读
cmd和cmdsize,再根据cmd值决定怎么读后续字节:比如LC_SEGMENT_64(值0x19)后面跟着segname[16]、vmaddr、vmsize等共 72 字节 - 用
BinaryReader.BaseStream.Seek()跳转,而不是靠结构体大小累加,避免因填充字节导致偏移错乱
想提取符号表或依赖库?重点看 LC_SYMTAB 和 LC_LOAD_DYLIB
LC_SYMTAB 告诉你符号字符串在哪(symoff)、有多少个符号(nsyms),但它不包含字符串内容;字符串实际存在另一个叫 LC_DYSYMTAB 的命令指定的 stroff 偏移处。而 LC_LOAD_DYLIB 的 dylib.name 是个相对偏移,必须加上 stroff 才能定位字符串起始。
实操建议:
- 先遍历所有
load command,缓存LC_SYMTAB的symoff/nsyms和LC_DYSYMTAB的stroff/strsize - 读符号表时,每个符号是
struct nlist_64(16 字节),其中n_un.n_strx是字符串表中的索引,不是地址 - 依赖库名读取:拿到
LC_LOAD_DYLIB中的name字段(其实是uint32偏移),加上之前缓存的stroff,再从字符串表中截取直到\0
真正麻烦的是 fat Mach-O(多架构合一文件),它开头是 fat_header,后面跟着多个 fat_arch 描述各 slice 的偏移和大小——不处理这个,你可能只读到了 x86_64 部分,却在 arm64 设备上运行失败。










