这个 Mac 应用是不是通用二进制?查看 Mach-O 里的 Apple 芯片与 Intel 切片
本页目录
结论先行:通用二进制(universal binary,也叫 fat binary)文件开头是魔数 0xcafebabe 和一张切片表。每个切片都是一个完整的、针对某一种 CPU 的 Mach-O 映像,通常是给 Apple 芯片的 arm64 和给 Intel 的 x86_64。在 Mac 上,lipo -archs <文件> 就能列出来;在任何电脑上(包括 Windows 和 Linux),本站的 ELF / PE / Mach-O 查看器 都能在浏览器里列出切片并逐个打开,文件不上传。
关键在于:每个切片都是一次独立的构建。同一个文件里的两个切片,最低系统版本可以不同,签名状态可以不同,依赖的库也可以不同。本文自己编了几个通用二进制,逐个切片对比差异,还复现了两个真实问题:依赖库缺切片,以及某个切片没有签名。下面的输出全部来自一台装了 Xcode 的 Apple 芯片 Mac(macOS 27)。
适合谁读:发布自己的 Mac 应用、插件或命令行工具的开发者;以及在其他系统上收到 .app、.dylib 或 framework、需要知道它能在哪种机器上跑的开发者。
你会学到:
- 通用二进制的结构,以及怎么在
.app里找到真正的可执行文件 - 怎么在浏览器里、用
lipo和file、用 REA 6.4.0 列出并对比切片 - 怎么用 Xcode、CMake、Go 或
lipo -create生成通用二进制 - 怎么排查 “incompatible architecture” 和切片未签名的问题
只检查你自己构建的应用和库、开源软件,或你有权分析的文件。
通用二进制长什么样
文件开头是一个很短的大端序头:魔数 FAT_MAGIC(0xcafebabe,64 位变体是 0xcafebabf)、切片数量,然后每个切片一条记录,写着 CPU 类型、子类型、文件偏移、大小和对齐。后面就是各个切片,每个切片从自己的偏移开始,是一个普通的瘦 Mach-O 文件。
常见的架构:
arm64:Apple 芯片上的应用。x86_64:Intel Mac,以及在 Apple 芯片上通过 Rosetta 2 运行。arm64e:带指针认证(pointer authentication)的 arm64,Apple 自己的系统程序在用。第三方应用一般发arm64。
进程启动时,内核和 dyld 会为当前 CPU 挑最合适的切片。之后进程加载的每一个库,也都必须有匹配的切片。
第 1 步:找到真正的可执行文件
.app 其实是个文件夹。可执行文件在 Contents/MacOS 里,名字由 Contents/Info.plist 的 CFBundleExecutable 决定:
plutil -extract CFBundleExecutable raw Hello.app/Contents/Info.plist
Hello
主程序不是唯一的 Mach-O 文件。framework、dylib、辅助程序和插件都有各自的切片,只要有一个库只带 Intel 切片,应用在 Apple 芯片上就可能起不来。检查包里所有 Mach-O 文件:
find Hello.app -type f -exec sh -c 'lipo -archs "$1" >/dev/null 2>&1 && echo "$1: $(lipo -archs "$1")"' _ {} \;
我的测试包里故意放了一个只有 Intel 切片的库:
Hello.app/Contents/MacOS/Hello: x86_64 arm64
Hello.app/Contents/Frameworks/libutil.dylib: x86_64
第 2 步:在浏览器里打开各个切片
打开二进制查看器,选中可执行文件(在 Windows 或 Linux 上,先把 .app 从 .zip 或 .dmg 里解出来)。如果是通用二进制,报告里会多一个“通用二进制切片”区块,每个切片一个按钮,标着架构和大小。默认选中第一个切片,点另一个,整份报告就切换过去。
hello-universal 是用 clang -arch arm64 -arch x86_64 -mmacosx-version-min=11.0 编的,查看器列出:
| 切片 | 架构 | 偏移 | 大小 |
|---|---|---|---|
| 0 | x86_64 |
4096 | 12744 |
| 1 | arm64 |
32768 | 50160 |
“基本信息”区块里同时有整个文件的 SHA-256 和当前切片单独的“切片 SHA-256”。对比两次构建时用切片哈希:两个通用二进制的 arm64 切片可以完全一样,整个文件的哈希却不同。报告里节区和段的偏移都是相对于切片的,和 otool 的打印方式一致。
然后逐个切片对比:
- 平台、最低系统版本和 SDK:来自
LC_BUILD_VERSION,或较老的LC_VERSION_MIN_MACOSX。 - 链接的库:
LC_LOAD_DYLIB等加载命令,包括@rpath开头的名字。 - 代码签名和 Hardened Runtime:在“安全特性”表里。
切片之间可以不一致
我把两个架构分开编译,再合并:
clang -O2 -arch arm64 -mmacosx-version-min=11.0 hello.c -o hello-arm64
clang -O2 -arch x86_64 -mmacosx-version-min=10.13 hello.c -o hello-x86_64
lipo -create hello-arm64 hello-x86_64 -output hello-lipo
查看器里,x86_64 切片的最低系统版本是 10.13,arm64 切片是 11.0。用 otool 看,两个切片连记录版本的加载命令都不一样:
otool -l -arch x86_64 hello-lipo | grep -A4 LC_VERSION_MIN
otool -l -arch arm64 hello-lipo | grep -A4 LC_BUILD_VERSION
cmd LC_VERSION_MIN_MACOSX
cmdsize 16
version 10.13
sdk 27.0
--
cmd LC_BUILD_VERSION
cmdsize 32
platform 1
minos 11.0
sdk 27.0
这是正常的(Apple 芯片的 Mac 从 macOS 11 起步),但它意味着“本应用支持 macOS 10.13”只对 Intel Mac 成立。
第 3 步:在 Mac 上交叉核对
file hello-universal
lipo -archs hello-universal
lipo -detailed_info hello-universal
hello-universal: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64:Mach-O 64-bit executable arm64]
hello-universal (for architecture x86_64): Mach-O 64-bit executable x86_64
hello-universal (for architecture arm64): Mach-O 64-bit executable arm64
x86_64 arm64
Fat header in: hello-universal
fat_magic 0xcafebabe
nfat_arch 2
architecture x86_64
cputype CPU_TYPE_X86_64
cpusubtype CPU_SUBTYPE_X86_ALL
capabilities
offset 4096
size 12744
align 2^12 (4096)
architecture arm64
cputype CPU_TYPE_ARM64
cpusubtype CPU_SUBTYPE_ARM64_ALL
capabilities
offset 32768
size 50160
align 2^14 (16384)
偏移和大小与查看器的切片列表一致。想在 Apple 芯片上指定运行某个切片,arch -x86_64 ./hello-universal 会通过 Rosetta 2 启动 Intel 切片。两个切片都正常打印了 hello, world。
用 REA 6.4.0
REA(morluto 的开源项目)有一个 rea list-architectures 命令,内部调用的是 lipo -detailed_info(dist/native/NativeMacOSProvider.js),所以需要 macOS 和 Xcode 命令行工具。参数可以是文件,也可以是 .app:
rea list-architectures /path/to/Hello.app --format json
结果节选:
"subject":{"name":"Hello", ... "architecture":"arm64","local_path":".../Hello.app/Contents/MacOS/Hello"},
"normalized_result":{"architectures":{"items":[
{"name":"x86_64","cpu_type":"CPU_TYPE_X86_64","cpu_subtype":"CPU_SUBTYPE_X86_ALL","file_offset":4096,"size":12744,"alignment":4096},
{"name":"arm64","cpu_type":"CPU_TYPE_ARM64","cpu_subtype":"CPU_SUBTYPE_ARM64_ALL","file_offset":32768,"size":50160,"alignment":16384}]
有两个限制要知道。对 .app,它只报告主程序,不管包里的 framework,所以上面那条遍历整个包的 find 还是要跑。另外,这个文件的第一个切片是 x86_64,但证据里的 subject.architecture 在我的 Mac 上写的是 arm64;6.4.0 的结果里没有单独的字段说明读的是哪个切片。REA 的其他 macOS 工具见用 REA 分析 macOS 应用。
第 4 步:自己生成通用二进制
-
Xcode:Architectures 的默认值是 Standard Architectures(
ARCHS_STANDARD),Xcode 27 的 macOS 规格文件里它就是arm64和x86_64。Debug 配置通常把 Build Active Architecture Only(ONLY_ACTIVE_ARCH)设为 Yes,只出一个切片;用 Release 归档,再用lipo -archs确认。 -
直接用 clang:像上面那样写两次
-arch。 -
CMake:配置时传
-DCMAKE_OSX_ARCHITECTURES="arm64;x86_64"。 -
Go:分别编两个架构再合并,Go 1.27.1 实测可用:
GOOS=darwin GOARCH=arm64 go build -o app-arm64 . GOOS=darwin GOARCH=amd64 go build -o app-amd64 . lipo -create app-arm64 app-amd64 -output app -
预编译的第三方库:每一个都必须两个切片齐全。
lipo -create只能把不同架构的瘦文件拼在一起,没法把 Intel 库变成 Apple 芯片的库。
常见问题排查
“incompatible architecture (have ‘x86_64’, need ‘arm64’)”
我先把 app 编成通用二进制,链接一个同样是通用二进制的 libutil.dylib,然后把这个库换成只有 Intel 切片的版本,模拟一个没更新的第三方 SDK 混进来的情况。在 Apple 芯片上运行:
dyld[92577]: Library not loaded: @rpath/libutil.dylib
Referenced from: <196030DD-79A2-3BB0-9B35-3985D8230067> .../dep/app
Reason: tried: '.../dep/libutil.dylib' (mach-o file, but is an incompatible architecture (have 'x86_64', need 'arm64e, arm64e.v1, arm64, arm64')), ...
Referenced from 里的 UUID 就是 arm64 切片的 LC_UUID,查看器在这个切片上显示的是同一个值。如果一开始就链接这个 Intel 版的库,构建阶段就会报错:
ld: warning: ignoring file './libutil.dylib': found architecture 'x86_64', required architecture 'arm64'
Undefined symbols for architecture arm64:
REA 的 rea trace-dylib-resolution 不用运行程序就能发现这个问题。对 app 加 --architecture arm64 运行,@rpath/libutil.dylib 的候选结果是 "outcome":"architecture-missing",findings 里写着:
"kind":"required-load-unresolved", ... "explanation":"No candidate for @rpath/libutil.dylib exists in the analyzed root; dyld would fail to load this required dependency for app (arm64) unless the image is supplied elsewhere."
修复要在构建这一侧做:向库的提供方要通用(或 arm64)版本,或者自己重新编译。
有一个切片没签名
在 Apple 芯片上,链接器会自动给 arm64 产物做 ad hoc 签名,但不会给 x86_64 产物签。所以链接器直接输出的通用二进制,arm64 切片有签名,x86_64 切片没有。查看器里,切片 0 的代码签名显示“否”(no LC_CODE_SIGNATURE),切片 1 显示 adhoc,linker-signed。再看:
codesign -v hello-universal
hello-universal: code object is not signed at all
REA 的 rea inspect-signature 返回了 "signed": true,但在 limitations 里写了 "Unsigned Mach-O slices: x86_64."。要看这个字段,不能只看布尔值。修复方法是用你的证书给最终的通用二进制签名(codesign --sign "Developer ID Application: ..." --options runtime),它会给每个切片都签上。
常听到一种说法:“先签名再 lipo,签名一定会失效。“我的测试结果不是这样:用 lipo -create 合并两个各自 ad hoc 签过名的切片,或者用 lipo -remove、-thin 从已签名的通用二进制里去掉一个切片,得到的文件都能通过 codesign -v,因为每个切片带着自己的签名。我没有用 Developer ID 签名或已公证的应用测试过。稳妥的做法是:最后一次 lipo 之后再签名,然后验证。
常见问题
没有 Mac 能查吗?
能。查看器在任何现代浏览器里都能用,fat 头和每个切片都由它自己解析。lipo、codesign 和 REA 的 macOS 工具则需要 macOS。
arm64e 是什么?我的应用要不要带?
它是带指针认证的 arm64,Apple 的系统程序在用。应用一般发 arm64 就够了。
为什么节区偏移这么小? 报告里的偏移是相对于当前切片的。加上切片列表里的切片偏移,才是在整个文件里的位置。
查看器会上传文件吗? 不会。解析和哈希都在浏览器的 Web Worker 里完成。
下一步
- 检查你的构建:ELF / PE / Mach-O 查看器
- 检查 PIE、Hardened Runtime 等构建选项:检查二进制加固选项
- 深入分析 Mac 应用:用 REA 分析 macOS 应用
- 反编译某个切片:用 REA 和 Ghidra 免费分析原生二进制
- 参考:REA 的 DISCLAIMER.md