Highlight
Apple 解释了符号化(Symbolication)如何把运行时地址还原成函数名、文件名和行号,并给出 atos、otool、vmmap、nm、symbols、dwarfdump、codesign 等工具来诊断崩溃日志和 Instruments 采样信息。
核心内容
崩溃发生时,设备看到的是内存地址和机器指令。开发者看到的代码却是函数、文件和行号。两者之间缺少映射,崩溃日志就只能告诉你某个地址出错了。
Session 里的 MagicNumbers 程序就是这个场景。代码从 10 个随机数里选一个数字。程序崩溃后,原始 crash log 只能显示 MagicNumbers 在某处崩了。寄存器、地址、反汇编都很难直接指出问题。
Xcode Organizer 下载 dSYM 后,崩溃日志被重新处理。日志开始显示函数调用、文件名和行号,还能指出数组访问越界。原因是 MAGIC_CHOICE 超出了 10 个元素的数组范围。
性能分析也有同样的问题。Instruments 在没有完整符号信息时,只能显示部分 backtrace。高 CPU 区间和低 CPU 区间看起来像同一段代码。补上 dSYM 后,Instruments 能指出高 CPU 来自遗留的 debug code path。
Apple 在这场 session 中把符号化拆成两步。第一步,把运行时地址转回二进制文件里的地址。第二步,用调试信息把文件地址映射到源码。前一步依赖 Mach-O、load command 和 ASLR slide。后一步依赖 function starts、nlist symbol table 和 DWARF。
详细内容
用 atos 手动符号化一个地址
(02:51)atos 可以把一个崩溃地址还原成源码位置。前提是你有对应构建的 dSYM、架构、加载地址和崩溃地址。
atos -o MagicNumbers.dSYM/Contents/Resources/DWARF/MagicNumbers -arch arm64 -l 0x10045c000 -i 0x10045fb70
关键点:
atos是地址符号化工具,用来查询某个地址对应的符号信息。-o MagicNumbers.dSYM/Contents/Resources/DWARF/MagicNumbers指向 dSYM 包里的 DWARF 二进制。-arch arm64指定要查询的架构切片,Universal 2 App 必须明确架构。-l 0x10045c000提供__TEXT段的加载地址,工具用它计算 ASLR slide。-i 0x10045fb70要求atos同时考虑内联函数(inlined functions),这会让 backtrace 更接近源码调用关系。
这条命令对应 session 开头的崩溃示例。Xcode Organizer 自动做的事情,也可以用 atos 手动完成。
从 Mach-O 找回文件地址
(07:34)符号化第一步是“回到文件”。运行时地址会受 ASLR(Address Space Layout Randomization,地址空间布局随机化)影响。文件里的地址由 linker 写入 Mach-O(Apple 可执行文件格式)header 的 load commands。
otool -l MagicNumbers | grep LC_SEGMENT -A8
关键点:
otool -l打印 Mach-O 文件的 load commands。LC_SEGMENT_64描述一个 segment 的属性。vmaddr是 linker 分配给 segment 的文件地址。__TEXTsegment 存放函数和方法代码,是崩溃指令通常所在的位置。-A8让grep输出匹配行后面的 8 行,方便查看 segment 名称、地址和大小。
(11:32)运行中的加载地址可以用 vmmap 查看。crash log 的 Binary Images 区域也会记录这些加载地址。
vmmap MagicNumbers | grep __TEXT
关键点:
vmmap MagicNumbers枚举 MagicNumbers 进程的内存区域。grep __TEXT只保留__TEXTsegment。__TEXT的运行时加载地址记为L。otool查到的 linker 地址记为A。- ASLR slide 计算公式是
S = L - A。
拿到 slide 后,用 运行时地址 - S 得到文件地址。文件地址稳定,不会因为每次启动时 kernel 选择了不同 ASLR slide 而变化。
用反汇编确认崩溃指令
(10:31)当你已经把崩溃地址换算成文件地址,可以查看该位置的机器指令。session 示例里,otool 找到了 brk 指令,它表示程序触发了异常。
otool -tV MagicNumbers -arch arm64
关键点:
otool读取 Mach-O 文件。-t打印__TEXTsegment 里的文本段内容,也就是可执行指令。-V让输出使用符号化的反汇编格式。MagicNumbers是要检查的可执行文件。-arch arm64指定架构切片,避免 Universal 2 文件中查错架构。
这一步适合验证“这个地址到底执行了什么”。它不能直接给你源码行号,但能确认 crash log 中的地址是否落在异常指令上。
区分 function starts 和 nlist 符号表
(15:09)function starts 是最基础的调试信息。它只记录函数起始地址,不记录函数名。它存放在 __LINKEDIT segment,并由 LC_FUNCTION_STARTS load command 指向。
symbols -onlyFuncStartsData -arch arm64 MagicNumbers
关键点:
symbols用来检查二进制文件里的符号和调试信息。-onlyFuncStartsData只输出 function starts 数据。- 输出里会有一组地址和空占位。
- 这些地址能告诉调试器“某个地址距离函数开头多少字节”。
- 它无法告诉你函数名、文件名或行号。
(17:06)nlist symbol table 比 function starts 多一层信息。它使用 nlist_64 结构记录名称、类型、section、描述和值。
struct nlist_64 {
union {
uint32_t n_strx;
} n_un;
uint8_t n_type;
uint8_t n_sect;
uint16_t n_desc;
uint64_t n_value;
};
关键点:
n_un.n_strx指向字符串表中的名称。n_type描述符号类型,session 重点提到 direct symbol 和 indirect symbol。n_sect表示符号所在 section。n_desc保存额外描述信息。n_value保存地址或相关值。
(17:59)direct symbols 是 App 或 framework 自己定义并参与链接的函数或方法。Swift 符号通常经过 name mangling(名称修饰),需要 demangle 才好读。
nm -arch arm64 --defined-only --numeric-sort MagicNumbers | xcrun swift-demangle
关键点:
nm查看 Mach-O 的符号表。-arch arm64选择 arm64 架构。--defined-only只显示当前二进制定义的 direct symbols。--numeric-sort按地址排序,便于和崩溃地址对照。xcrun swift-demangle把 Swift mangled name 还原成可读名称。
(23:06)indirect symbols 表示来自外部 framework 或 library 的函数。例如 MagicNumbers 依赖 libswiftCore 中的 Swift 函数。
nm -m -arch arm64 --undefined-only --numeric-sort MagicNumbers
关键点:
-m显示符号来自哪个 framework 或 library。--undefined-only只显示当前二进制使用、但由外部依赖提供的符号。--numeric-sort按地址排序。- 这些信息能说明 App 与系统库或 Swift runtime 的链接关系。
- 它不会提供本地静态函数的源码行号。
Release 构建常常会 strip symbol table。Strip Linked Product、Strip Style、Strip Swift Symbols 会影响保留哪些符号。出现“有函数名但没有文件名和行号”的 backtrace 时,通常还缺 DWARF。
用 DWARF 获取文件名、行号和内联调用
(27:16)DWARF(调试信息格式)提供最完整的源码映射。dSYM bundle 里包含一个带 __DWARF segment 的二进制。session 提到的关键数据流有 debug_info、debug_abbrev 和 debug_line。
dwarfdump -v -debug-info -arch arm64 MagicNumbers.dSYM
关键点:
dwarfdump读取 DWARF 调试信息。-v输出详细内容。-debug-info查看debug_info数据流。-arch arm64指定架构。MagicNumbers.dSYM是要检查的 dSYM bundle。
DWARF 用 compile unit(编译单元)表示一个源文件,用 subprogram(子程序)表示一个函数。compile unit 是树根,subprogram 是子节点。debug_line 保存文件地址到文件名和行号的映射。
(29:25)如果 atos 没有使用 -i,内联函数的调用细节会丢失。session 中的输出仍然有函数名和行号,但缺少许多有价值的函数关系。
atos -o MagicNumbers.dSYM/Contents/Resources/DWARF/MagicNumbers -arch arm64 -l 0x10045c000 0x10045fb70
关键点:
- 这条命令查询同一个 dSYM 和同一个地址。
- 它保留
-o、-arch、-l和目标地址。 - 它没有
-i参数。 - 编译器优化会把函数调用替换成函数体,这叫 inlining(内联)。
- DWARF 用 inlined subroutine(内联子程序)记录这类关系。
内联信息解释了为什么 dSYM 对 Instruments 和 crash log 很重要。只有 DWARF 能表达函数被内联到哪里、调用点在源码哪一行、内联调用之间的父子关系。
检查 dSYM、UUID、DWARF 和签名
(32:29)静态库和 object file 也可能包含 DWARF。dsymutil 的 debug map 能显示函数来自哪些文件,工具随后可以扫描这些位置处理 DWARF。
dsymutil --dump-debug-map -arch arm64 MagicNumbers
关键点:
dsymutil是生成和检查 dSYM 的工具。--dump-debug-map输出 debug map。-arch arm64指定架构。MagicNumbers是被检查的二进制。- 输出能帮助你确认调试信息来自哪些 object file 或静态库。
(33:59)dSYM 必须和 App 构建匹配。匹配依据是 UUID。crash report 的 Binary Images 区域有 App UUID,dSYM 也有自己的 UUID。
symbols -uuid MagicNumbers.dSYM
关键点:
symbols可以读取 dSYM 的 UUID。-uuid只输出 UUID 信息。- UUID 是 load command 中的唯一标识。
- App 和 dSYM 的 UUID 必须一致。
- Bitcode App 的 dSYM 需要从 App Store Connect 下载对应版本。
(34:03)工具链偶尔可能生成无效 DWARF。Apple 建议用 dwarfdump 验证,并在发现错误时提交反馈。
dwarfdump --verify MagicNumbers.dSYM
关键点:
dwarfdump检查 DWARF 内容。--verify执行一致性验证。MagicNumbers.dSYM是被验证的调试符号文件。- 如果出现错误,说明符号化质量可能受影响。
- 单个二进制的 DWARF 数据有 4GB 上限,过大时需要拆分组件。
(35:09)如果 Instruments 找不到函数名,而你确认有 dSYM,还要检查签名和 entitlement。开发构建需要 get-task-allow entitlement,Instruments 才有权限符号化 App。
codesign --display -v --entitlements :- MagicApp.app
关键点:
codesign查看 App 的代码签名信息。--display显示签名元数据。-v输出更详细的信息。--entitlements :-把 entitlement 内容打印到标准输出。MagicApp.app是要检查的 App bundle。
如果缺少 get-task-allow,检查 Xcode 的 Code Signing Inject Base Entitlements build setting。使用 Profile action 时,Xcode 通常会自动设置。
核心启发
-
做什么:给 CI 增加 dSYM UUID 校验。 为什么值得做:session 强调 App 和 dSYM 必须 UUID 匹配,错配会让崩溃日志无法完整符号化。 怎么开始:构建产物生成后运行
symbols -uuid YourApp.app/YourApp和symbols -uuid YourApp.app.dSYM,比较 UUID 后再上传归档。 -
做什么:做一个崩溃地址快速查询脚本。 为什么值得做:
atos可以把单个地址映射到函数名、文件名和行号,适合处理用户发来的 crash log 片段。 怎么开始:从 Binary Images 里提取 load address,从 backtrace 提取崩溃地址,然后拼出atos -o ... -arch ... -l ... -i ...。 -
做什么:在性能排查手册里加入 Instruments dSYM 检查步骤。 为什么值得做:session 的 Instruments 示例说明,缺 dSYM 会让高耗时路径和普通路径看起来相同。 怎么开始:采样结果缺文件名或行号时,先定位对应 dSYM,再检查 UUID 和
get-task-allowentitlement。 -
做什么:为 Release 构建整理符号剥离策略。 为什么值得做:
Strip Linked Product和Strip Style会影响符号表可见性,保留错误会增加体积,剥离过度会降低诊断能力。 怎么开始:检查 target 的 strip build settings,结合nm、symbols -onlyNListData查看 direct symbols 是否符合预期。 -
做什么:建立大型项目的 dSYM 大小监控。 为什么值得做:session 提到单个二进制 DWARF 数据有 4GB 上限,过大的 dSYM 会影响工具处理。 怎么开始:归档后统计每个 dSYM 体积;接近上限的模块拆成 framework 或独立组件,让每个组件生成更小的 dSYM。
关联 Session
- Detect and diagnose memory issues — 学习用 Xcode、XCTest 和命令行工具定位内存问题。
- Triage TestFlight crashes in Xcode Organizer — 了解 Xcode Organizer 如何接收、聚类和符号化 TestFlight 崩溃报告。
- Discover breakpoint improvements — 掌握 Xcode 13 的列断点和未解析断点,补齐交互式调试流程。
- Explore advanced project configuration in Xcode — 理解复杂 Xcode 工程中的构建设置和文件依赖配置。
评论
GitHub Issues · utterances