APM - iOS 基础功能 堆栈符号化原理和实践

1,592 阅读14分钟

原理

符号化

使用符号表对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项目的归档构建流程:

  1. 准备构建环境,构建目录
  2. 编译主工程依赖的Pods工程的静态库或者Framework (=== BUILD TARGET Aspects OF PROJECT Pods WITH CONFIGURATION Debug ===)
  3. 编译主工程的源代码文件 (CompileC)
  4. 链接生成主工程对应的可执行文件 (Ld)
  5. 拷贝图片,localized字符串等资源文件 (CpResource)
  6. 编译storyboard文件 (CompileStoryboard)
  7. CompileAssetCatalog
  8. 处理pinfo.list文件 (ProcessInfoPlistFile)
  9. 生成符号表文件(GenerateDSYMFile)
  10. 链接StoryBoard(LinkStoryboards)
  11. 执行配置的脚本文件(PhaseScriptExecution)
  12. 打包生成app文件,不是ipa文件(ProcessProductPackaging)
  13. 签名 (CodeSign)
  14. 校验 (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文件需要具备三个文件

  1. 获取symbolicatecrash工具

打开终端输入以下命令:

find /Applications/Xcode.app -name symbolicatecrash -type f

工具路径:

/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash

根据路径前往文件夹找到symbolicatecrash ,将其复制到刚才指定文件夹

  1. 获取dSYM文件
  2. 获取崩溃时产生的Crash文件,XXX.crash
  3. 打开终端,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:

  1. 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.
  2. 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.
  3. 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.
  4. 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 optionsWhere symbolication occurs
Don’t include bitcodeUpload symbolsThe service symbolicates the logs.
Include bitcodeUpload symbolsThe App Store compiles the bitcode and generates the dSYM files with full symbol names. Then the service symbolicates the logs.
Include bitcodeDon’t upload symbolsThe 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 symbolsXcode 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。

github.com/lksnmnn/Sym…

在这份开源代码中,我们可以找到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…