原理
符号化
使用符号表对APP发生Crash的程序堆栈进行解析和还原。如下所示:
符号表
iOS的符号表是dSYM文件,符号表是内存地址与函数名、文件名、行号的映射表,逻辑数据结构是HashMap。符号表元素如下所示:
<起始地址> <结束地址> <函数> [<文件名:行号>]
符号表文件结构是Mach-O,如下所示:
Mach64 Header中包含了Magic Number/CPU Type/CPU SubType等字段信息。
通过build setting里的Debug Information Format,可以设置dSYM的生成规则。通常Debug模式构建的app会把Debug符号表存储在编译好的Binary信息中,Release模式构建的app会把Debug符号表存储在dSYM文件中以节省体积。
dSYM文件存放在.xcarchive包文件中,每次archive会生成一份。dSYM是一个带后缀的文件夹形式的文件,如下所示:
DUApp.app.dSYM/
└── Contents
├── Info.plist
└── Resources
└── DWARF
└── DUApp
从目录结构可以看出iOS使用的是DWARF文件结构(Debugging With Attributed Record Formats),DWARF设计之初是跟ELF文件共同产生的,但实际上是独立于对象文件的一种调试文件结构标准。
*更多关于DWARF,www.dwarfstd.org/doc/Debuggi…
在静态链接linker的阶段会做符号的绑定,之后会生成符号表。
iOS项目的归档构建流程:
- 准备构建环境,构建目录
- 编译主工程依赖的Pods工程的静态库或者Framework (=== BUILD TARGET Aspects OF PROJECT Pods WITH CONFIGURATION Debug ===)
- 编译主工程的源代码文件 (CompileC)
- 链接生成主工程对应的可执行文件 (Ld)
- 拷贝图片,localized字符串等资源文件 (CpResource)
- 编译storyboard文件 (CompileStoryboard)
- CompileAssetCatalog
- 处理pinfo.list文件 (ProcessInfoPlistFile)
- 生成符号表文件(GenerateDSYMFile)
- 链接StoryBoard(LinkStoryboards)
- 执行配置的脚本文件(PhaseScriptExecution)
- 打包生成app文件,不是ipa文件(ProcessProductPackaging)
- 签名 (CodeSign)
- 校验 (Validate)
像Bugly要求我们在工程配置的Build Phases里添加它的脚本,用于将生成的符号表上传到bugly。根据归档构建流程,我们知道生成符号表的步骤是在处理pinfo.plst文件之后,所以我们配置的bugly的执行脚本必须放在链接这个步骤之后,否则会导致找不到符号表文件。
另外最初生成的符号表是放在构建的一个临时目录中,最后才拷贝到归档目录下的。临时目录如下:
~/Library/Developer/Xcode/DerivedData/DUApp-ajmvyrkvoxmeqecuyqlnxbgtlaoy/Build/Intermediates.noindex/ArchiveIntermediates/Here/BuildProductsPath/Debug-iphoneos/DUApp.app.dSYM
APP符号表
系统函数库、Flutter等共享动态库拥有自己独立的符号表,或者设置不产生符号表的库之外,其他的库和主APP的符号表会被统一的打成dSYM。
系统函数库符号表
Frameworks UIKit.framework等
PrivateFrameworks MetricKitCore.framework等
路径:
~/Library/Developer/Xcode/iOS DeviceSupport/
我们在链接真机调试的时候,有时候会看见Xcode显示正在同步符号表的操作,是Xcode在拷贝手机对应iOS系统的系统函数库符号表。
Crash Report
Incident Identifier: 98A0FCF9-844B-439F-B62F-605BC2917421
CrashReporter Key: a074974b834d1ed14788be80200b4cb777a24e78
Hardware Model: iPhone12,1
Process: DUApp [14615]
Path: /private/var/containers/Bundle/Application/AFFEF50C-73ED-4AC5-AA09-2975ADB583C3/DUApp.app/DUApp
Identifier: com.siwuai.duapp
Version: 4.61.0.1 (4.61.0)
Code Type: ARM-64 (Native)
Role: Foreground
Parent Process: launchd [1]
Coalition: com.siwuai.duapp [389]
Date/Time: 2020-12-23 14:07:58.1763 +0800
Launch Time: 2020-12-23 11:47:18.0352 +0800
OS Version: iPhone OS 14.0.1 (18A393)
Release Type: User
Baseband Version: 2.00.01
Report Version: 104
Exception Type: EXC_BAD_ACCESS (SIGBUS)
Exception Subtype: KERN_PROTECTION_FAILURE at 0x000000018952462c
VM Region Info: 0x18952462c is in 0x189524000-0x189528000; bytes after start: 1580 bytes before end: 14803
REGION TYPE START - END [ VSIZE] PRT/MAX SHRMOD REGION DETAIL
__TEXT 1891b4000-189524000 [ 3520K] r-x/r-x SM=COW ...iftCore.dylib
---> __TEXT 189524000-189528000 [ 16K] rw-/rwx SM=COW ...iftCore.dylib
__TEXT 189528000-1895e0000 [ 736K] r-x/r-x SM=COW ...iftCore.dylib
Termination Signal: Bus error: 10
Termination Reason: Namespace SIGNAL, Code 0xa
Terminating Process: exc handler [14615]
Triggered by Thread: 18
Thread 0 name: Dispatch queue: com.apple.main-thread
Thread 0:
0 libdispatch.dylib 0x0000000127748478 0x127740000 + 33912
1 libdispatch.dylib 0x0000000127748428 0x127740000 + 33832
2 libdispatch.dylib 0x000000012775cef0 0x127740000 + 118512
3 libdispatch.dylib 0x00000001277546d8 0x127740000 + 83672
4 CoreFoundation 0x00000001856431e4 0x1855a5000 + 647652
5 CoreFoundation 0x000000018563d3b4 0x1855a5000 + 623540
6 CoreFoundation 0x000000018563c4bc 0x1855a5000 + 619708
7 GraphicsServices 0x000000019c0c1820 0x19c0be000 + 14368
8 UIKitCore 0x0000000187fe0734 0x187463000 + 12048180
9 UIKitCore 0x0000000187fe5e10 0x187463000 + 12070416
10 DUApp 0x000000010481100c 0x1047e4000 + 184332
11 libdyld.dylib 0x0000000185303e60 0x185303000 + 3680
Thread 1 name: gputools.smt_poll.0x2816daee0
Thread 1:
0 libsystem_kernel.dylib 0x00000001b15f7d30 0x1b15d0000 + 163120
1 libsystem_c.dylib 0x000000018e7c57bc 0x18e752000 + 473020
2 libsystem_c.dylib 0x000000018e7c568c 0x18e752000 + 472716
3 GPUToolsCore 0x00000001368e174c 0x1368dc000 + 22348
4 libsystem_pthread.dylib 0x00000001ccc1eca8 0x1ccc1d000 + 7336
5 libsystem_pthread.dylib 0x00000001ccc27788 0x1ccc1d000 + 42888
(此处省略其他Thread)
Thread 18 crashed with ARM Thread State (64-bit):
x0: 0x000000016cd56580 x1: 0x000000000082d200 x2: 0x0000000000000101 x3: 0x000000000082d201
x4: 0x0082d1000082d200 x5: 0x000000000082d201 x6: 0x000000000082d201 x7: 0x000000000ee66848
x8: 0x000000016cd56580 x9: 0x0000450000004500 x10: 0x0000000282750e98 x11: 0x00000000009f80d1
x12: 0x0000450000004502 x13: 0x0000450000004500 x14: 0x0000000000000100 x15: 0x0000000000000000
x16: 0x000000018952462c x17: 0x0082d1000082d200 x18: 0x0000000000000000 x19: 0x0000000281abfa80
x20: 0x00000001def76808 x21: 0x0000000000030002 x22: 0x000000016cd570e0 x23: 0x0000000000000000
x24: 0x0000000127743b48 x25: 0x00000001047ec2b8 x26: 0x0000000280dccd40 x27: 0x0000000000000000
x28: 0x0000000000000000 fp: 0x000000016cd56630 lr: 0x000000010bd67f7c
sp: 0x000000016cd56460 pc: 0x000000018952462c cpsr: 0x60000000
esr: 0x8200000f (Instruction Abort) Permission fault
Binary Images:
0x1047e4000 - 0x1115e7fff DUApp arm64 <8c59717147743160abd186fc60d70636> /var/containers/Bundle/Application/AFFEF50C-73ED-4AC5-AA09-2975ADB583C3/DUApp.app/DUApp
0x12757c000 - 0x127587fff libBacktraceRecording.dylib arm64e <93a857ee9eaf3d3caec7793bb53e9cb1> /Developer/usr/lib/libBacktraceRecording.dylib
0x127598000 - 0x12768ffff libMTLCapture.dylib arm64e <a263f80aa300381e97f0f57bde59d745> /usr/lib/libMTLCapture.dylib
0x1276d4000 - 0x12770bfff libViewDebuggerSupport.dylib arm64e <a25387306dfd317cb8a9b34b537df54a> /Developer/Library/PrivateFrameworks/DTDDISupport.framework/libViewDebuggerSupport.dylib
(此处省略其他Binary Image)
EOF
符号化工具
GUI工具
使用Xcode自动符号化Crash文件
- 如果你用的Mac就是打包机,并且得到了发生崩溃的手机,那么手机连接电脑,通过Xcode菜单Window -> Devices and Simulators -> Devices -> View Device Logs找到自己的日志,就是符号化过后的。如果没有符号化,就稍微等待一会儿,或者右击点出菜单选择Re-Symbolicate Log
- 如果只有Mac出包机,没有手机只有崩溃日志,那么同样可以通过Xcode菜单Window -> Devices and Simulators -> Devices -> View Device Logs把崩溃日志直接拖进去,就是符号化过后的,如果没有符号化,就稍微等待一会儿,或者右击点出菜单选择Re-Symbolicate Log
命令行
匹配UUID
只有dSYM,Crash Report中的uuid匹配的情况下,才可以正确的符号化。
Crash Report的UUID可以在Binary Images中的首行Binary Image看到。
dSYM中的UUID,可以直接在可以通过命令行获取。
% dwarfdump --uuid <PathToDSYMFile>/Contents/Resources/DWARF/<BinaryName>
% dwarfdump --uuid <PathToBinary>
也可以通过Mach-O Viewer的工具看到。
SymbolicateCrash
通过Mac自带的命令行工具SymbolicateCrash解析Crash文件需要具备三个文件
- 获取symbolicatecrash工具
打开终端输入以下命令:
find /Applications/Xcode.app -name symbolicatecrash -type f
工具路径:
/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash
根据路径前往文件夹找到symbolicatecrash ,将其复制到刚才指定文件夹
- 获取dSYM文件
- 获取崩溃时产生的Crash文件,XXX.crash
- 打开终端,cd到当前文件夹,输入命令
./symbolicatecrash XX.crash XX.app.dSYM > result.crash
如果报错
Error: "DEVELOPER_DIR" is not defined at ./symbolicatecrash
需要 执行命令
export DEVELOPER_DIR="/Applications/XCode.app/Contents/Developer"
symbolicatecrash需要使用 DEVELOPER_DIR
来找到系统库符号表,上文中我们提到了系统库符号表的路径。
然后重新 输入命令
./symbolicatecrash XX.crash XX.app.dSYM > result.crash
这样就看到一个名字result.crash 已经符号化的文件了。
ATOS
To symbolicate using atos:
- Find a frame in the backtrace that you want to symbolicate. Note the name of the binary image in the second column, and the address in the third column.
- Look for a binary image with that name in the list of binary images at the bottom of the crash report. Note the architecture and load address of the binary image.
- Locate the dSYM file for the binary. If you don’t know where the dSYM file is located, see Locate a dSYM Using Spotlight to find the dSYM file that matches the build UUID of the binary image.
- Symbolicate the addresses in the backtrace using atos with the formula, substituting the information you gathered in previous steps:
% atos -arch <BinaryArchitecture> -o <PathToDSYMFile>/Contents/Resources/DWARF/<BinaryName> -l <LoadAddress> <AddressesToSymbolicate>
符号化图示:
注意事项:
使用atos逐行符号化的时候,如果Binary Image Name是系统函数库,需要修改<PathToDSYMFile>/Contents/Resources/DWARF/<BinaryName>
为系统函数库符号表的位置。
其他
BitCode
Where symbolication occurs depends on the distribution options you select when you upload your app to App Store Connect.
如果中间码是BitCode的话,苹果会在应用商店做编译和链接,所以dSYM文件会由苹果提供。
Distribution options | Where symbolication occurs |
---|---|
Don’t include bitcodeUpload symbols | The service symbolicates the logs. |
Include bitcodeUpload symbols | The App Store compiles the bitcode and generates the dSYM files with full symbol names. Then the service symbolicates the logs. |
Include bitcodeDon’t upload symbols | The App Store compiles the bitcode and generates the dSYM files with obfuscated symbols. When you download the dSYM files, Xcode de-obfuscates the symbols using the .bcsymbolmap files located in the selected archive. |
Don’t include bitcodeDon’t upload symbols | Xcode symbolicates logs using the dSYM files in your archive or dSYM files it finds on your Mac that are indexed by Spotlight. If Xcode can’t find the dSYM files, the log will not be symbolicated. If you can provide the dSYM files later, you can try to symbolicate the crash log again. |
实践
卡顿堆栈解析主体流程
使用队列存储未解析的堆栈,通过脚本定时拉取的形式获取堆栈信息,调用atos对未解析的堆栈进行解析,解析成功后通过接口返回数据。
SymbolicateCrash
SymbolicateCrash需要符合苹果要求的Crash Report类型,需要有report version等字段,但是卡顿堆栈形成的crash report不包含这些字段,导致无论是PLCrashReport或者是KSCrashReport都无法使用symbolicatecrash。
在这份开源代码中,我们可以找到SymbolicateCrash对应的符号化逻辑。
crash report中需要有Report Version字段,且report_version的字段需要是102、103、104、105的类型。
从实践的经验来看,使用SymbolicateCrash完整地符号化一个Crash Report需要3秒左右的时间。但是Crash Report格式与字段的限制,SymbolicateCrash当前只能用在Crash的符号化解析中,不能给卡顿堆栈符号化解析使用。
atos
atos没有Crash Report格式的限制,但是需要逐行符号化。
0 libdispatch.dylib 0x0000000127748478 0x127740000 + 33912
1 libdispatch.dylib 0x0000000127748428 0x127740000 + 33832
2 libdispatch.dylib 0x000000012775cef0 0x127740000 + 118512
3 libdispatch.dylib 0x00000001277546d8 0x127740000 + 83672
4 CoreFoundation 0x00000001856431e4 0x1855a5000 + 647652
5 CoreFoundation 0x000000018563d3b4 0x1855a5000 + 623540
6 CoreFoundation 0x000000018563c4bc 0x1855a5000 + 619708
7 GraphicsServices 0x000000019c0c1820 0x19c0be000 + 14368
8 UIKitCore 0x0000000187fe0734 0x187463000 + 12048180
9 UIKitCore 0x0000000187fe5e10 0x187463000 + 12070416
10 DUApp 0x000000010481100c 0x1047e4000 + 184332
11 libdyld.dylib 0x0000000185303e60 0x185303000 + 3680
逐行遍历BackTrace,通过Binary Image Name(如DUApp,UIKitCore等),寻找对应的dSYM。
def get_system_dsym_dir(report):
system_version = get_system_version(report) # exp: 14.2.1
os_version = get_os_version(report) # exp: 18B121
cpu_arch = get_cpu_arch(report) # exp: arm64e/arm64/armv7
system_dsym_dir = "" # exp: 14.2.1 (18B121) arm64e/14.2.1 (18B121)
if cpu_arch == "arm64e":
system_dsym_dir = "{} ({}) {}".format(system_version, os_version, cpu_arch)
else:
system_dsym_dir = "{} ({})".format(system_version, os_version)
if len(system_dsym_dir) == 0:
return ""
ios_devicesupport_dir = os.path.expanduser("~/Library/Developer/Xcode/iOS DeviceSupport/")
dirs = os.listdir(ios_devicesupport_dir)
if len(dirs) > 0 and system_dsym_dir in dirs:
system_dsym_dir_path = ios_devicesupport_dir + system_dsym_dir
return system_dsym_dir_path
return ""
调用atos解析。
output = commands.getoutput("xcrun atos -o '{}' -arch {} -l {} {}".format(dsym_path, cpu_arch, image_load_address, instruction_address_str))
瓶颈
由于atos每次都需要dSYM加载到内存中,没有缓存机制。导致每次单行解析的时间在2s左右,整体卡顿堆栈在30行左右,所以整个report的解析需要1min左右的时间,这也成为了目前卡顿堆栈符号化的性能瓶颈。
优化
合并相同LoadAddress的AddressToSymbolicate
% atos -arch <BinaryArchitecture> -o <PathToDSYMFile>/Contents/Resources/DWARF/<BinaryName> -l <LoadAddress> <AddressesToSymbolicate>
AddressesToSymbolicate可以是一个拼接字符串,所以我们把LoadAddress相同的AddressesToSymbolicate做一次合并
atos -arch arm64e <PathToDSYMFile>/Contents/Resources/DWARF/<BinaryName> "AddressToSymbolicate1 AddressToSymbolicate2 AddressToSymbolicate3"
使用pm2管理脚本的进程
更多信息参照 pm2.keymetrics.io/docs/usage/…
Sentry Symbolic
由于SymbolicateCrash和atos都要求解析环境是MacOS,所以对机器资源的耗费要求高,所以是否有可以在linux系统环境做符号化解析?由于iOS中的dSYM也是遵循DWARF协议的,答案是肯定的。
更多信息参照 github.com/getsentry/s…