APM_符号化原理

前言

符号化(Symbolication) 就是把一个内存地址,翻译成人能看懂的函数名、源文件和代码行号的一个过程。

未符号化前,调用栈中的地址:

swift 复制代码
  0   APMDEMO   0x0000000104000958
  1   APMDEMO   0x00000001040008a0

符号化后:

swift 复制代码
  0   APMDEMO   add(_:_:)      main.swift:11
  1   APMDEMO   main           main.swift:15

在开始之前,需要了解一些基本的概念:

  • DWARF
  • dSYM
  • Mach-O

DWARF

DWARF 是一种 "调试信息的数据格式标准"。它把编译后的机器地址、寄存器和内存位置,重新关联到函数、源文件、行号、变量和类型。它通常由编译器、汇编器或链接器生成,由调试器和符号化工具读取。DWARF 官方标准

就是一种数据格式,记录了调试信息,通过机器指令就能找到对应的源码:

swift 复制代码
  源代码
  UserService.swift 第 42 行
  loadUser()
          ↓ 编译
  机器指令地址
  0x10401234
          ↑ DWARF 保存对应关系

它的原始内容是二进制字节,可以使用 dwarfdump/llvm-dwarfdump 来解码。

假设源码是:

swift 复制代码
  int add(int a, int b) {
      int sum = a + b;
      return sum;
  }

CPU 在执行的时候,看不到 addsum 这些东西,只能看到:

swift 复制代码
  地址 0x00:某条 arm64 指令
  地址 0x0c:某条 arm64 指令
  地址 0x1c:某条 arm64 指令

编译器就使用 DWARF 来记录:

swift 复制代码
  函数 add 对应地址范围 [0x00, 0x28)
  变量 sum 的类型是 int
  变量 sum 位于函数帧基准位置 + 4
  地址 0x1c 对应源文件第 3 行、第 12 列

然后通过符号化的工具就可以通过地址,找到:

swift 复制代码
  0x1c
  → 位于函数 add
  → 对应第 3 行、第 12 列
  → return sum

这是编译器在构建时候提前记录好的映射关系。

数据结构

原始 DWARF 是二进制字节:

swift 复制代码
  00000000000000c8  00000066 08010005 00000000 001d0001
  00000000000000d8  08030201 00000000 04000000 00005000

dwarfdump 解码之后:

swift 复制代码
  DW_TAG_subprogram
    DW_AT_low_pc    [DW_FORM_addrx]  (0x0000000000000000)
    DW_AT_high_pc   [DW_FORM_data4]  (0x00000028)
    DW_AT_frame_base[DW_FORM_exprloc](DW_OP_reg31 WSP)
    DW_AT_name      [DW_FORM_strx1]  ("add")
    DW_AT_decl_line [DW_FORM_data1]  (1)
    DW_AT_type      [DW_FORM_ref4]   (0x00000065 "int")

  DW_TAG_variable
    DW_AT_location  [DW_FORM_exprloc](DW_OP_fbreg +4)
    DW_AT_name      [DW_FORM_strx1]  ("sum")
    DW_AT_decl_line [DW_FORM_data1]  (2)
    DW_AT_type      [DW_FORM_ref4]   (0x00000065 "int")
    
...

几个常见前缀的含义:

名称 身份和作用
DIE Debugging Information Entry,一条描述函数、变量、类型等对象的记录
DW_TAG_* 记录的种类,例如函数、变量或类型
DW_AT_* 记录的属性,例如名称、地址、行号和类型
DW_FORM_* 属性在二进制中使用什么编码形式
DW_OP_* 计算变量位置或值的小型表达式指令
CU Compilation Unit,一次编译输入对应的一组调试信息

例如:

swift 复制代码
  DW_TAG_variable
  DW_AT_name ("sum")
  DW_AT_type (指向 int 类型)
  DW_AT_location (DW_OP_fbreg +4)

整体含义是:

这是一条变量记录,变量名为 sum,类型是 int,在当前函数帧基准位置加 4 的地方可以找到它。

主要 sections

DWARF 不是把所有信息塞进一张表,而是拆成多个彼此引用的 Section,在 Mach-O 中通常放到 __DWARF segment 中。

Apple Mach-O 使用 __debug_* 命名,DWARF 标准文档通常写作 .debug_*

标准 DWARF 名 Mach-O 中的名字 作用
.debug_info __DWARF,__debug_info 主体 DIE 树,描述函数、变量、参数、类型和作用域
.debug_abbrev __DWARF,__debug_abbrev .debug_info 的格式字典
.debug_line __DWARF,__debug_line 机器地址与源文件、行号、列号的映射
.debug_line_str __DWARF,__debug_line_str 行号表使用的文件名和目录字符串
.debug_str __DWARF,__debug_str 公共字符串池,保存函数名、变量名和类型名等
.debug_str_offsets __DWARF,__debug_str_offs 从字符串编号映射到 .debug_str 偏移
.debug_addr __DWARF,__debug_addr 可供其他记录按索引引用的地址表
.debug_names __DWARF,__debug_names 名称到 DIE 的快速查找索引
.debug_loclists __DWARF,__debug_loclists 变量在不同指令范围内分别位于哪里
.debug_rnglists __DWARF,__debug_rnglists 函数或作用域对应的非连续代码范围
.debug_frame __DWARF,__debug_frame 恢复调用者寄存器、支持栈展开的调用帧信息
.debug_aranges __DWARF,__debug_aranges 从地址快速定位到编译单元的可选索引
.debug_macro __DWARF,__debug_macro 宏定义、取消定义及文件关系

它们之间大致这样协作:

swift 复制代码
                           .debug_abbrev
                           记录格式字典
                                 ↓
  .debug_names ───────→ .debug_info ───────→ .debug_line
  名称索引               程序实体主体              地址---源码位置
                            │                         │
                            │                         └→ .debug_line_str
                            │
                            ├→ .debug_str_offsets → .debug_str
                            ├→ .debug_addr
                            ├→ .debug_loclists
                            └→ .debug_rnglists

.debug_frame:相对独立地提供栈帧恢复规则

__debug_info

保存:

  • 函数
  • 参数
  • 局部变量
  • 类型
  • 结构体成员及偏移
  • 命名空间
  • 语法作用域
  • 内联函数
  • 函数地址范围
  • 声明位置
  • 变量位置规则

它是真正的程序描述,也是 DWARF 最核心的 Section。

__debug_abbrev

编码模板。

如果每个 DIE 都重复:

swift 复制代码
  这是一个函数
  下面有子节点
  属性一是 name
  属性二是 low_pc
  属性三是 high_pc
  ......

数据会非常大。

因此 __debug_abbrev 先定义模板:

swift 复制代码
  模板 2:
  类型 = DW_TAG_subprogram
  有子节点
  属性 = name、low_pc、high_pc、type......

__debug_info 中的 DIE 只需要记录 "我使用模板 2" 以及具体属性值。

所以:

__debug_abbrev 决定如何解码 __debug_info,两者必须配合读取。

__debug_str

共享字符串。

函数名、类型名、编译器名称和路径会大量重复。

__debug_info 可以不直接存储完整字符串,而只保存索引或偏移:

swift 复制代码
DW_AT_name → 字符串表中的第 N 项

再通过 __debug_str_offsets 找到 __debug_str 中真正的内容。

__debug_ranges / __debug_rnglists

优化后,一个函数或作用域对应的机器指令不一定连续。

例如一个函数的热路径和冷路径可能被分开放置:

swift 复制代码
  函数 foo:
  0x1000~0x1050
  0x3000~0x3020

这时仅使用一对 low_pc/high_pc 不够,需要地址范围列表。

DWARF 5 使用 .debug_rnglists 取代了之前版本的 .debug_ranges

__debug_loclists

它描述的是:

在某段机器地址范围内,调试器应该去哪里找这个变量。

例如优化后,变量 count 可能经历:

swift 复制代码
  0x1000~0x1010:在 x0 寄存器
  0x1010~0x1040:在栈上的 SP + 32
  0x1040~0x1050:无法恢复,已被优化掉

DWARF 保存的是这种寻找规则,而不是保存崩溃时 count 的实际数值。

__debug_names

名称索引。

例如调试器搜索 add 时,可以先在 __debug_names 中找到对应 DIE,而不必扫描整个 __debug_info

它主要解决:

swift 复制代码
函数名 -> DIE

它是加速结构,不是全部调试信息本身。

__debug_line

行号程序。

它不是简单保存:

swift 复制代码
  地址 A → 第 7 行
  地址 B → 第 8 行

它保存的是一段由 DWARF 定义的小型指令序列,叫做 Line Number Program。

执行这些指令时,会维护一组状态,例如:

  • 当前机器地址
  • 当前文件
  • 当前源码行
  • 当前列
  • 是否适合作为断点
  • 是否是函数序言或尾声

比如:

swift 复制代码
  Address             Line  Column
  0x100000378            7       0
  0x100000380            8      24
  0x100000384            8      34
  0x100000388            8      26
  0x10000038c            8       9
  0x100000390            9      12

因此查询 0x100000388,可以得到:

swift 复制代码
  函数:add
  文件:<stdin>
  行:8
  列:26

.debug_info 和 .debug_line 的区别

这是最容易混淆的地方。

.debug_info 中可能有:

swift 复制代码
  DW_AT_name      ("add")
  DW_AT_decl_line (1)

这里的第 1 行表示:

函数 add 声明在第 1 行。

.debug_line 中可能有:

swift 复制代码
  Address 0x1c → Line 3, Column 12

这里的第 3 行表示:

CPU 执行到地址 0x1c 时,对应源码第 3 行。

因此:

swift 复制代码
DW_AT_decl_line:  这个函数或变量在哪里声明?
.debug_line:      当前机器地址对应哪一行正在执行的源码?

崩溃地址恢复源码行,主要依赖 .debug_line

Mach-O

Mach-O 是 Apple/Darwin 平台使用的一种二进制文件格式。

它解决的问题是:编译完成后,只有一串机器指令还不够,系统还必须知道:

  • 这些指令属于 arm64 还是 x86_64
  • 文件是可执行程序、动态库还是目标文件
  • 哪些字节应该映射到内存的什么位置?
  • 哪些区域可读、可写或可执行
  • 程序依赖哪些动态库
  • 程序从哪里开始执行
  • 哪些地址需要在加载时调整
  • 符号表、代码签名、调试信息位于哪里

Mach-O 就是组织这些信息的 "二进制容器格式"。

swift 复制代码
  Objective-C / Swift / C
          ↓ 编译
       机器指令
          ↓ 按 Mach-O 格式组织
        Mach-O 文件
          ↓ 被系统解析和加载
      内存中的可执行映像

整体结构

一个单架构 Mach-O,也称为 thin Mach-O,可以简化为:

真实文件里,各块的具体位置由 Load Commands 中的偏移和大小决定。

另外,__TEXT 的文件范围通常从偏移 0 开始,因此 Mach Header 和 Load Commands 往往也处于 __TEXT 的映射范围内。

部分 作用
Mach Header 说明文件是什么架构、什么类型
Load Commands 描述有哪些内容、位于哪里、应该如何加载
Segment 说明一段文件数据怎么映射到内存
Section 说明 Segment 中某一小块数据是什么
__TEXT 机器代码、只读常量、栈展开信息
__DATA 可写全局数据
__DWARF 调试信息,包括地址到源码行的映射
__LINKEDIT 动态链接、符号、字符串表、代码签名等辅助数据

Mach Header

64 位 Mach-O 使用 mach_header_64,大小为 32 字节,字段字段包括:

swift 复制代码
  mach_header_64
  ├── magic          是否为 64 位 Mach-O
  ├── cputype        CPU 架构
  ├── cpusubtype     CPU 子类型
  ├── filetype       文件类型
  ├── ncmds          Load Command 数量
  ├── sizeofcmds     所有 Load Command 的总大小
  └── flags          文件特性

常见 filetype

外在形式 Mach-O 类型 作用
编译生成的 .o MH_OBJECT 尚未完成最终链接的目标文件
App 内的主程序 MH_EXECUTE 可以启动的主可执行文件
.dylib MH_DYLIB 动态库
可加载插件 MH_BUNDLE 运行时加载的代码模块
dSYM 内部的 DWARF 文件 MH_DSYM 只保存配套调试信息

Mach-O 不只代表可执行程序,很多不同文件都可以是 Mach-O。

这里有几个容易混淆的地方:

  • .app 是一个目录包,不是 Mach-O 文件,它里面的主程序才是 Mach-O
  • .framework 也是目录结构,动态 Framework 里面的主二进制通常是 MH_DYLIB
  • .a 是 archive 容器,里面通常装着多个 Mach-O .o 文件
  • .dSYM 是目录包,里面真正保存 DWARF 的文件依然是一个 Mach-O,类型为 MH_DSYM
  • MH_BUNDLE 不等于 .app bundle,这里指可动态加载的 Mach-O 类型

Load Commands

Load Commands 是文件中的 "目录和加载说明"。

例如:

swift 复制代码
  LC_SEGMENT_64:__DWARF
  ├── __debug_info 的位置和大小
  ├── __debug_line 的位置和大小
  └── __debug_str 的位置和大小

它们和实际数据的关系是:

swift 复制代码
  Load Commands 中的 Section 描述
              │
              │ offset + size
              ▼
  文件中真正的 Section 数据

Segment 与 Section

Segment 是内存映射单位,Section 是数据分类单位。

完整名称通常写成:

Segment 名, Section 名

例如:

  • __TEXT 是 Segment,包含代码和只读数据
  • __text__TEXT 中保存机器指令的 Section
  • __DWARF 是可选 Segment
  • __debug_line 是其中保存地址与源码行号映射的 Section

Universal / Fat Mach-O

一个文件也可以同时包含多种 CPU 架构:

swift 复制代码
  ┌─────────────────────────────────┐
  │ Fat Header                      │
  │ 记录每个架构的位置和大小            │
  ├─────────────────────────────────┤
  │ arm64 Mach-O slice              │
  │ ├── Mach Header                 │
  │ ├── Load Commands               │
  │ └── Segment Data                │
  ├─────────────────────────────────┤
  │ x86_64 Mach-O slice             │
  │ ├── Mach Header                 │
  │ ├── Load Commands               │
  │ └── Segment Data                │
  └─────────────────────────────────┘

每个 slice 都是一份独立、完整的 Mach-O,外层 Fat Header 只负责把它们装在一个文件中。

怎样产生并运行?

swift 复制代码
  源代码
     ↓ 编译器、汇编器
  Mach-O .o(MH_OBJECT)
     ↓ 链接器 ld
  最终 Mach-O(MH_EXECUTE / MH_DYLIB)
     ↓ 启动程序
  内核识别 Mach-O
     ↓
  dyld 读取 Load Commands
     ↓
  映射 Segment、加载动态库、修正地址
     ↓
  进入程序入口
  • 编译器把源码转换成机器指令
  • .o 已经是 Mach-O,但还不能作为完整程序运行
  • 链接器合并多个 .o,生成最终 Mach-O
  • dyld 是 Apple 提供的动态加载器,用来读取 Mach-O
  • dyld 根据 Load Commands 加载依赖库并修正运行时地址

dSYM

dSYM 是某一次构建生成的二进制文件的外置调试信息包。

它不参与 App 运行,也不负责捕获崩溃。它的作用是留在开发侧,供调试器或符号化服务,把崩溃中的十六进制地址还原为:

  • 函数名
  • 源文件
  • 源码行号
  • 内联调用关系

整体结构

虽然经常说是 "dSYM 文件",但实际上它是一个目录包:

swift 复制代码
  MyApp.app.dSYM
  └── Contents
      ├── Info.plist
      └── Resources
          └── DWARF
              └── MyApp
部分 作用
Myapp.app.dSYM 最外层目录包
Info.plist dSYM 包的基本元数据
Resources/DWARF/MyApp 真正保存调试信息的二进制文件

里面的 MyApp 是一个 MH_DSYM 类型的 Mach-O:

swift 复制代码
  MyApp.app.dSYM
  └── Contents/Resources/DWARF/MyApp
      ┌──────────────────────────────┐
      │ Mach Header                  │
      │ filetype = MH_DSYM           │
      ├──────────────────────────────┤
      │ Load Commands                │
      │ LC_UUID                      │
      │ LC_SEGMENT_64:__DWARF       │
      ├──────────────────────────────┤
      │ __DWARF                      │
      │ ├── __debug_info             │
      │ ├── __debug_line             │
      │ ├── __debug_str              │
      │ ├── __debug_abbrev           │
      │ └── ...                      │
      └──────────────────────────────┘

Apple 的 Mach-O 定义将 MH_DSYM 描述为只包含调试 Section 的配套文件。

所以:

dSYM 是一个目录包,包里面使用 Mach-O 文件承载调试数据。

怎样生成

基本生成流程是:

swift 复制代码
  源文件
     ↓ 编译
  多个 .o 目标文件
  每个 .o 中带有调试信息片段
     ↓ 链接
  最终可执行文件
  同时保留"最终地址对应哪个 .o"的信息
     ↓ dsymutil
  收集各个 .o 中的调试信息
  按照最终链接地址进行合并和调整
     ↓
  MyApp.app.dSYM

dsymutil 是 LLVM/Xcode 工具链提供的命令行工具。

它只负责在链接完成后收集并链接调试信息,不负责编译源码、不负责运行程序,也不负责捕获崩溃。

dsymutil 会根据最终可执行文件中的调试映射信息,找到各个目标文件里的 DWARF,将它们链接成默认的 .dSYM bundle。LLVM: dsymutil

可以这么理解:

swift 复制代码
  Foo.o
  ├── foo() 的调试信息
  └── foo() 在 Foo.o 中的临时地址

  最终链接结果
  └── foo() 被放到了最终地址 0x100003400

  dsymutil
  └── 生成最终调试映射:
      0x100003400
        → foo()
        → Foo.m
        → 第 42 行

所以 dSYM 里的地址信息对应的是最终链接结果,而不是某个 .o 中尚未链接的临时位置。

Xcode 什么时候生成

在 Build Setting 中搜 Debug Information Format,默认 Release 下的设置是:

DWARF with dSYM File

Archive 之后的目录结构:

swift 复制代码
  MyApp.xcarchive
  ├── Products
  │   └── Applications
  │       └── MyApp.app
  └── dSYMs
      └── MyApp.app.dSYM
  • Debug 构建默认可以把调试符号放在编译产物中
  • Release 构建通常把调试符号放入配套 dSYM,减少分发 App 的大小
  • Archive 时,Xcode 会将二进制和对应 dSYM 一起保存进 .xcarchive

dSYM 与构建怎么匹配

dSYM 最重要的身份信息是构建 UUID。

如果 App 的二进制:

swift 复制代码
UUID: A12B3456-7890-1234-5678-ABCDEF123456 (arm64)

那么对应的 dSYM 必须是:

swift 复制代码
UUID: A12B3456-7890-1234-5678-ABCDEF123456 (arm64)

只有两者的 UUID 一致,才能认为它们匹配。

即使用相同源码重新构建,只要工具链或构建设置发生变化,生成的二进制和 UUID 就可能不同,旧的 dSYM 不能用于新的二进制。

一个 App,多个 dSYM

一个 App 包可能有多个 dSYM:

swift 复制代码
  主 App Mach-O       → 主 App 的 dSYM
  动态 Framework      → Framework 自己的 dSYM
  App Extension       → Extension 自己的 dSYM
  其他动态库          → 各自对应的 dSYM

每个二进制 ------ 主程序、Framework、App Extension,都有自己的 dSYM。

对于多架构二进制,一个 dSYM 内部也可能包含多个架构 slice,每个架构有自己的 UUID。

三者的关系

概念 本质 解决的问题
DWARF 调试信息的数据格式 怎样表示函数、变量、源码文件、行号和地址映射
Mach-O Apple 平台的二进制容器格式 怎样组织代码、数据、加载信息和可选调试信息
dSYM 某个二进制的外置调试信息包 怎样把该二进制的调试信息独立保存、归档和上传

在结构关系上:

App 中的主程序:

swift 复制代码
  MyApp.app
  └── MyApp                         ← Mach-O 可执行文件
      ├── Mach Header
      ├── Load Commands
      │   ├── LC_UUID
      │   ├── LC_SEGMENT_64:__TEXT
      │   ├── LC_SEGMENT_64:__DATA
      │   └── ...
      ├── __TEXT                    ← 机器代码
      ├── __DATA                    ← 运行数据
      ├── __LINKEDIT                ← 链接、符号、签名信息
      └── __DWARF                   ← 可选

对应的 dSYM:

swift 复制代码
  MyApp.app.dSYM                    ← dSYM 目录包
  └── Contents
      └── Resources
          └── DWARF
              └── MyApp            ← MH_DSYM 类型的 Mach-O
                  ├── Mach Header
                  ├── Load Commands
                  │   ├── LC_UUID
                  │   └── LC_SEGMENT_64:__DWARF
                  └── __DWARF
                      ├── __debug_info
                      ├── __debug_line
                      ├── __debug_str
                      ├── __debug_abbrev
                      └── ...

所以其实是两个配套的 Mach-O:

swift 复制代码
  运行用 Mach-O                         调试用 Mach-O
  MyApp                                dSYM/.../DWARF/MyApp
  ├── LC_UUID = A                      ├── LC_UUID = A
  ├── __TEXT                           └── __DWARF
  ├── __DATA                               ├── __debug_info
  └── __LINKEDIT                          ├── __debug_line
                                          └── ...

它们通过 UUID 来对应。

所以:

dSYM 外层是目录包,里面仍然是一个 Mach-O,这个内部 Mach-O 主要承载 DWARF。

在 Release 构建中:

swift 复制代码
  源代码
     │
     │ 编译
     ▼
  多个 .o 文件
  每个 .o 本身也是 Mach-O
  ├── 机器代码片段
  └── DWARF 调试信息片段
     │
     │ 链接
     ▼
  最终 App Mach-O
  ├── 最终机器代码
  ├── 最终地址布局
  └── 调试映射信息
     │
     │ dsymutil 收集并链接各个 .o 中的 DWARF
     ▼
  MyApp.app.dSYM
  └── 内部 MH_DSYM Mach-O
      └── 最终地址对应的 DWARF

所以,三个阶段:

  • 编译器产生 DWARF
  • 链接器产生最终 Mach-O 地址布局
  • dsymutil 产生与该地址布局对应的 dSYM

符号化

假设你的崩溃报告中:

swift 复制代码
  运行地址:0x104001234
  映像加载地址:0x104000000
  映像 UUID:A12B...

对应 App Mach-O 的链接基址是:

0x100000000

首先,算出 ASLR,因为每次运行都会有一个随机地址偏移,根据运行加载地址和基地址可以算出来:

swift 复制代码
  ASLR slide
  = 0x104000000 - 0x100000000
  = 0x04000000

然后可以拿到链接时的地址:

swift 复制代码
  0x104001234 - 0x04000000
  = 0x100001234

然后根据 UUID(Mach-O 中有),找到对应的 dSYM,找到后:

swift 复制代码
  __debug_info
  └── 0x100001234 属于函数 sendRequest()

  __debug_line
  └── 0x100001234 对应 FRNetworkManager.m:128

最终得到:

swift 复制代码
  0x104001234
  → -[FRNetworkManager sendRequest:]
  → FRNetworkManager.m:128

因此三者的分工是:

swift 复制代码
  Mach-O
  └── 确认这是哪个映像,并完成运行地址换算

  dSYM
  └── 为这个具体构建提供独立调试资料

  DWARF
  └── 将链接地址解释为函数、文件和行号

总结

符号化的本质,是利用构建阶段提前生成的调试映射,把运行时机器地址反向转换为函数名、源文件和代码行号。

三个核心概念:

概念 核心作用
DWARF 规定函数、类型、变量、地址和源码行号等调试信息怎样编码
Mach-O 组织 Apple 平台的机器代码、运行数据、加载信息和可选调试数据
dSYM 独立保存某个具体 Mach-O 构建所对应的 DWARF 调试信息

它们在构建阶段形成了下面的关系:

swift 复制代码
  源代码
    ↓ 编译
  Mach-O .o
  ├── 机器代码片段
  └── DWARF 调试信息片段
    ↓ 链接
  最终 App Mach-O
  ├── 最终机器代码
  ├── 最终地址布局
  └── 构建 UUID
    ↓ dsymutil 收集并链接 DWARF
  dSYM 目录包
  └── MH_DSYM 类型的 Mach-O
      ├── 相同构建 UUID
      └── 最终地址对应的 DWARF

App 运行后,Mach-O 被加载为进程中的一个映像。由于 ASLR,调用栈记录的是运行时地址,而 DWARF 描述的是链接时地址,因此符号化需要以下数据:

  • 调用栈中的运行时地址
  • 映像的运行时加载地址
  • Mach-O 的链接基址
  • 映像的 CPU 架构和 UUID
  • UUID、架构相匹配的 dSYM

完整符号化过程是:

swift 复制代码
  运行时地址
    ↓ 根据地址范围确定所属映像
  读取映像加载地址、链接基址和 UUID
    ↓
  ASLR slide = 运行加载地址 - 链接基址
    ↓
  链接地址 = 运行时地址 - ASLR slide
    ↓ 根据 UUID 和架构选择正确 dSYM
  读取其中的 DWARF
    ↓
  __debug_info 确定函数、作用域和内联关系
  __debug_line 确定源文件、行号和列号
    ↓
  函数名 + 源文件 + 代码行号

因此三者最终分工:

swift 复制代码
  Mach-O
  └── 描述二进制映像、地址布局、架构和 UUID

  dSYM
  └── 保存这个具体构建对应的外置调试资料

  DWARF
  └── 将链接地址解释为函数、文件、行号和变量信息

需要注意的是:

  • Mach-O 不一定包含完整 DWARF
  • dSYM 不是 DWARF 格式本身,而是承载 DWARF 的目录包
  • dSYM 不负责捕获崩溃,也不参与 App 正常运行
  • 文件名、版本号和源码相同都不能证明 dSYM 匹配
  • 二进制与 dSYM 必须具有相同架构和构建 UUID
  • __debug_info 中的声明行号不等于崩溃执行行号,运行地址到源码行主要依赖 __debug_line
  • 优化可能导致函数内联、代码重排或变量被删除,因此即使 dSYM 正确,也不保证恢复所有源码级状态
相关推荐
花椒技术3 小时前
不是跑通一次 Demo:花椒 QA 怎样把真机 UI 自动化做成回归流程
ios·单元测试·测试
2501_915918416 小时前
Swift IDE 有哪些?按平台和需求选择Swift 开发工具
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
songgeb8 小时前
UITableView 局部刷新 Crash:Invalid update
ios·架构
2501_916007479 小时前
免费抓包工具 Windows 和 Mac 怎么选?根据平台来选择工具
网络协议·计算机网络·网络安全·ios·adb·https·udp
念何架构之路10 小时前
响应渲染 render(render/ 包)
ios·iphone·xcode
_瑞1 天前
APM_寄存器
ios·汇编语言
2501_916008891 天前
iOS App 请求地址查看,不用配置看到 App 调了哪些接口
网络协议·计算机网络·网络安全·ios·adb·https·udp
2501_915106321 天前
Swift 开发环境搭建指南:三条路径按目标对号入座
开发语言·vscode·ios·objective-c·个人开发·swift·敏捷流程
_瑞1 天前
APM_符号化
ios