dSYM+.crash解析

一、dSYM

1.dSYM

调试符号表,是苹果为调试和定位问题而使用的一种调试文件。调试符号信息在构建应用时就保存在Mach-O文件中了,而.dSYM就是从Mach-O文件中抽取调试信息而得到的一个单独的文件目录。它使用的是 DWARF 结构,在 .xcarchive 目录中其层次结构如下:

1
2
3
4
5
6
.xcarchive
--dSYMs
|--Your.app.dSYM
|--Contents
|--Resources
|--DWARF

要生成 dSYM 文件,你可以在 Xcode 的Build Settings中这样设置:

1
2
Generate Debug Symbols = YES
Debug Information Format = "DWARF with dSYM File"

反之,如果你不想生成 dSYM 文件,则可以这样配置:

1
2
3
Generate Debug Symbols = NO
或者
Debug Information Format = "DWARF"

2.DWARF

DWARF(DebuggingWith Arbitrary Record Formats),是 ELF 和 Mach-O 等文件格式中用来存储和处理调试信息的标准格式,.dSYM中真正保存符号表数据的是DWARF文件。DWARF 中不同的数据都保存在相应的section(节)中,ELF文件里所有的 section 名称都以.debug_开头,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
| Section Name         | Contents                                          |
| -------------------- | ------------------------------------------------ |
| .debug_abbrev | Abbreviations used in the .debug_info section |
| .debug_aranges | A mapping between memory address and compilation |
| .debug_frame | Call Frame Information |
| .debug_info | The core DWARF data containing DIEs |
| .debug_line | Line Number Program |
| .debug_loc | Macro descriptions |
| .debug_macinfo | A lookup table for global objects and functions |
| .debug_pubnames | A lookup table for global objects and functions |
| .debug_pubtypes | A lookup table for global types |
| .debug_ranges | Address ranges referenced by DIEs |
| .debug_str | String table used by .debug_info

Mach-O中关于section的命名和ELF稍有区别,把名称前的.换成了_,例如.debug_info变成了_debug_info

保存在 DAWARF 中的信息是高度压缩的,可以通过dwarfdump命令从中提取出可读信息。前文所述的那些 section 中,定位闪退日志只需要用到.debug_info.debug_line。由于解析出来的数据量较大,为了方便查看,就将其保存在文本中。两个 section 的数据提取方式如下:

  • .debug_info
1
$ dwarfdump -e --debug-info YourApp.dSYM/Contents/Resources/DWARF > info-e.txt
  • .debug_line
1
$ dwarfdump -e --debug-line YourApp.dSYM/Contents/Resources/DWARF > line-e.txt

3.闪退解析过程

  • 计算闪退地址对应符号表中的地址

下面的第二章节中会详细讲解闪退日志中的stack addressload address以及怎样计算闪退地址在符号表中的 symbol address,这里以 0x52846 为例。

  • .debug_info

.debug_info中最基本的描述单元为DIE(Debug Information Entry),首先我们要根据符号表闪退地址0x52846.debug_info中取出包含这个地址的DIE单元。为了简单起见,直接贴出了从 info-e.txt 中取出的对应DIE,其部分内容如下:

1
2
3
4
5
6
7
8
9
10
0x00062112:     function [99] *
low pc( 0x000502e0 )
high pc( 0x00053730 )
frame base( r7 )
object pointer( {0x0006212a} )
name( "-[OBDFirstConnectViewController showOilPricePickerView]" )
decl file( "/YourSourcePath/OBDFirstConnectViewController.m" )
decl line( 870 )
prototyped( 0x01 )
APPLE instruction set architecture( 0x01 )

可以看出,该DIE包含的是方法-[OBDFirstConnectViewController showOilPricePickerView]的内容,其地址范围是 0x000502e0–0x00053730,我们的目标地址 0x52846 正是在这个范围内,所以可以判定闪退发生在该方法的某一行中。

需要指出的是,上面这段DIE是为了介绍方便直接贴出来的,实际应用的时候需要通过搜索算法找出包含目标符号表闪退地址(这里是0x52846)的DIE。

从上述DIE中我们可以获取到这些信息:

  1. 闪退所在源码文件:/YourSourcePath/OBDFirstConnectViewController.m
  2. 发生闪退的方法:-[OBDFirstConnectViewController showOilPricePickerView]
  3. 发生闪退的方法在源文件中的行号:870
  • . debug_line

截止目前,我们可以获取到发生了闪退的方法的相关信息,但要想确定闪退发生的具体行号,还需要.debug_line 的帮助。

.debug_line 以一个方法为基本块,记了该方法中每一行对应的符号表地址。通过.debug_info 得知闪退发生的方法地址范围是 0x000502e0–0x00053730,通过起始地址 0x000502e0 再解析. debug_line 得到的 line-e.txt 中直接搜索即可得到闪退所在方法的. debug_line 数据,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
0x00000000000502e0    870 /YourSourcePath/OBDFirstConnectViewController.m
0x00000000000502e0 0
0x00000000000502f0 872
0x000000000005033c 873
0x0000000000050374 874
0x000000000005039e 875
0x00000000000503c8 876
...
0x0000000000052812 880
0x000000000005283e 881
0x0000000000052846 882
0x00000000000528c8 883
...

. debug_line 段的第一行内容标识了该方法的起始符号表地址,行号及方法所在文件路径,通过之前得到的闪退地址0x52846即可得知闪退发生在882行。

至此我们已经根据闪退地址解析出了闪退发生位置的详细信息:

  1. 闪退所在源码文件:/YourSourcePath/OBDFirstConnectViewController.m;
  2. 发生闪退的方法:-[OBDFirstConnectViewController showOilPricePickerView];
  3. 发生闪退的方法在源文件中的行号:870;
  4. 闪退发生在源文件中得行号:882。

二、.crash文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Incident Identifier: 28593639-9021-4BF9-A730-0A4E644EE52E
CrashReporter Key: cf719616d48fff95262c17eaf002e4de3d3f9842
Hardware Model: iPhone8,1
Process: WeChat [25431]
Path: /var/../WeChat.app/WeChat
Identifier: com.tencent.xin
Version: 6.5.7.32 (6.5.7)
Code Type: ARM-64 (Native)
Parent Process: launchd [1]

Date/Time: 2017-04-29 14:12:13.13 +0800
Launch Time: 2017-04-26 03:26:06.06 +0800
OS Version: iOS 9.3.5 (13G36)
Report Version: 105

Exception Type: 00000020
Exception Codes: 0x000000008badf00d
Exception Note: SIMULATED (this is NOT a crash)
Highlighted by Thread: 0

Thread 0 name: Dispatch queue: com.apple.main-thread
Thread 0:
0 libsystem_kernel.dylib 0x0000000180f99014 0x180f98000 + 4116
1 libdispatch.dylib 0x0000000180e763e8 0x180e64000 + 74728
2 FrontBoardServices 0x0000000182db63d4 0x182d94000 + 140244
3 FrontBoardServices 0x0000000182d9e910 0x182d94000 + 43280
15 GraphicsServices 0x0000000182be0088 0x182bd4000 + 49288
16 UIKit 0x00000001865e6088 0x186568000 + 516232
17 WeChat 0x00000001000fbea4 0x100058000 + 671396

Binary Images:
0x100058000 - 0x1033c3fff WeChat arm64 <8b7a21136f4434dfb2771b8a4218a0f1>
/var/containers/Bundle/Application/26524DA8-D1E8-430D-8ECA-9D5ABE3CE3CA/WeChat.app/WeChat
..

上面是一个crash文件的大致内容,为了简洁删除了一部分。

1.参数

  • uuid:
1
Incident Identifier: 28593639-9021-4BF9-A730-0A4E644EE52E

这是闪退文件中的 uuid ,根据这个 uuid 可确定与 dSYM 文件是否匹配,方法如下:

1
dwarfdump --uuid appName.app.dSYM/

这个命令行会打印出该 dSYM 文件内所有架构的 uuid。

  • 进程名[ID]
1
Process:             WeChat [25431]

上面 “WeChat” 表示的是闪退所在进程的名字,括号中的数字即为此进程的ID。

  • CPU架构:
1
Code Type:           ARM-64 (Native)
  • 程序运行时的映射信息:(Binary Images)
1
2
3
Binary Images:
0x100058000 - 0x1033c3fff WeChat arm64 <8b7a21136f4434dfb2771b8a4218a0f1>
/var/containers/Bundle/Application/26524DA8-D1E8-430D-8ECA-9D5ABE3CE3CA/WeChat.app/WeChat
  1. 第一列,应用二进制文件中代码段的起始地址~终止地址;(0x100058000 - 0x1033c3fff)
  2. 第二列,映射文件名;(WeChat)
  3. 第三列,uuid;(8b7a21136f4434dfb2771b8a4218a0f1)
  4. 第四列,映射文件路径;
  • 闪退的堆栈信息(Thread Backtrace):
1
2
3
4
5
6
7
8
0   libsystem_kernel.dylib            0x0000000180f99014 0x180f98000 + 4116
1 libdispatch.dylib 0x0000000180e763e8 0x180e64000 + 74728
2 FrontBoardServices 0x0000000182db63d4 0x182d94000 + 140244
3 FrontBoardServices 0x0000000182d9e910 0x182d94000 + 43280
..
15 GraphicsServices 0x0000000182be0088 0x182bd4000 + 49288
16 UIKit 0x00000001865e6088 0x186568000 + 516232
17 WeChat 0x00000001000fbea4 0x100058000 + 671396
  1. 第一列:调用顺序;(0、1、2、3)
  2. 第二列:二进制库名;(WeChat、UIKit)
  3. 第三列:进程在运行时发生闪退处的地址;(stack address)
  4. 第四列:进程运行时的起始地址;(load address)
  5. 第五列:闪退处距离进程起始地址的偏移量;(slide)

注意!!这里的 “stack address” 与 “load address” 都是16进制的,而 “slide” 则是10进制的。

2.stack address

计算一下你就会发现,上面堆栈信息中,第四列+第五列 的内容与第三列在实质上是一样的。以下面的闪退堆栈信息的第 9 行为例:

1
2
3
4
5
6
7
8
9
10
Thread 0:
0 libobjc.A.dylib 0x33f10f60 0x33efe000 + 77664
1 Foundation 0x273526ac 0x2734a000 + 34476
9 Your 0x000f0846 0xa2000 + 321606
28 Your 0x0024643a 0xa2000 + 1721402
29 libdyld.dylib 0x34484aac 0x34483000 + 6828

Binary Images:
0xa2000 - 0x541fff Your armv7
/var/mobile/Containers/Bundle/Application/645D3184-4C20-4161-924B-BDE170FA64CC/Your.app/Your
1
2
3
stack address = 0x000f0846;
load address = 0xa2000;
slide = 321606;

将 slide 转换为 16 进制后(即 0x4E846)与 load address 相加:

1
0x000f0846 = 0xa2000 + 0x4E846;

所以,我们可以得出这么一个公式:

1
stack address = load address + slide;

注意!!这里 “stack address” 和 “load address” 均为程序在运行时的地址。要想利用符号表解析出闪退对应位置,需要计算出符号表中对应的闪退堆栈地址(symbol address)。

3.symbol address

我们打开一个应用时,内核会为该应用创建一个新的进程,并把应用的二进制文件加载到一片虚拟地址空间中。iOS4.3 后为了阻止内存溢出攻击苹果在iOS中使用了 ASLR 技术。这样进程每次启动时,地址空间都会被简单地随机化,所以每次加载时地址都不一样。这里的随机只是偏移不是搅乱,实现方式是通过内核将 Mach-O 的段“平移”某个随机数。

根据虚拟内存偏移量不变原理,只要提供了符号表 __TEXT 段的起始地址(vmaddr),再加上偏移量(这里为0x4E846)就能得到符号表中的堆栈地址(symbol address),计算方法为:

1
symbol address = vmaddr + slide;

如何获取符号表中的 __TEXT 段起始地址呢?

1
$otool -l xx.app.dSYM/Contents/Resources/DWARF/xx

注意把 xx 替换为你自己的应用名。运行结果中的片段如下:

1
2
3
4
5
6
7
8
9
10
11
12
Load command 3
cmd LC_SEGMENT
cmdsize 736
segname __TEXT
vmaddr 0x00004000
vmsize 0x00700000
fileoff 0
filesize 0
maxprot 0x00000005
initprot 0x00000005
nsects 10
flags 0x0

其中的 vmaddr 0x00004000 字段即为 __TEXT 段的起始地址。

由公式:

1
symbol address = vmaddr + slide;

可得出:

1
0x52846 = 0x4000 + 0x4E846;

即符号表中的闪退地址 = 0x52846,接下来就可以根据这个地址解析出闪退位置了。

三、解析方案

1.dwarfdump

命令如下:

1
$dwarfdump --arch armv7 Your.app.dSYM --lookup 0x52846 | grep 'Line table'

需要注意的是:这里的 armv7 是运行设备的 CPU 指令集,而不是二进制文件的指令集。比如 armv7 指令集的二进制文件运行在 arm64 指令集的设备上,这个地方应该写 arm64。

  • –lookup 后面跟的一定是经过准确计算的符号表中的闪退地址
  • 使用 dwarfdump 解析的结果较杂乱,因此使用 grep 命令抓取其中关键点展示出来

运行结果如下:

1
2
Line table dir : '/data/.../Src/OBDConnectSetting/Controller'
Line table file: 'OBDFirstConnectViewController.m' line 882, column 5 with start address 0x000000000052768

其中第一行是编译时文件目录,第二行包含了闪退发生的文件名称以及文件中具体行号等信息,有了这些信息就能准确定位闪退原因啦。


2.atos

atos 命令可以解析出指定某一行的堆栈,使用方式如下:

1
$atos -o /Users/xxx/crash/AppName.app/AppName -arch armv7 0x52846

其执行结果如下:

1
-[OBDFirstConnectViewController showOilPricePickerView] (in Your) (OBDFirstConnectViewController.m:882)

3.无需计算闪退地址

atos 还提供了另外一种无需计算闪退地址对应的符号表地址的方式,命令格式如下:

1
$atos -o Your.app.dSYM/Contents/Resources/DWARF/Your -arch armv7 -l [load address] [stack address]

其中 -l 选项指定了二进制文件在运行时的 load address (0xa2000),后面跟的是闪退发生时的 stack address (0x000f0846)。解析结果:

1
-[OBDFirstConnectViewController showOilPricePickerView] (in Your) (OBDFirstConnectViewController.m:882)

4.shell脚本

桌面新建一个crash文件夹,把dSYM文件、友盟日志txt 及 shell 脚本放进来。

  • 友盟日志内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
Application received signal SIGSEGV
(null)
((
0 CoreFoundation 0x0000000190f311b8 0x0000000190e01000 + 1245624
1 libobjc.A.dylib 0x000000018f96855c objc_exception_throw + 56
2 CoreFoundation 0x0000000190f3108c 0x0000000190e01000 + 1245324
3 Foundation 0x00000001919e902c 0x000000019193b000 + 712748
4 UIKit 0x00000001976c5704 0x0000000196dd7000 + 9365252
5 UIKit 0x00000001976c5afc 0x0000000196dd7000 + 9366268
6 UIKit 0x00000001976c6f0c 0x0000000196dd7000 + 9371404
7 UIKit 0x00000001975f1c10 0x0000000196dd7000 + 8498192
8 UIKit 0x00000001975f1e28 0x0000000196dd7000 + 8498728
9 MyApp 0x0000000100223668 0x00000001000ac000 + 1537640
10 MyApp 0x00000001001e1588 0x00000001000ac000 + 1267080
11 UIKit 0x00000001973aef80 0x0000000196dd7000 + 6127488
12 UIKit 0x00000001973b2748 0x0000000196dd7000 + 6141768
13 UIKit 0x0000000196f7973c 0x0000000196dd7000 + 1713980
14 UIKit 0x0000000196e180f0 0x0000000196dd7000 + 266480
15 UIKit 0x00000001973a2680 0x0000000196dd7000 + 6076032
16 UIKit 0x00000001973a21e0 0x0000000196dd7000 + 6074848
17 UIKit 0x00000001973a149c 0x0000000196dd7000 + 6071452
18 UIKit 0x0000000196e1630c 0x0000000196dd7000 + 258828
19 UIKit 0x0000000196de6da0 0x0000000196dd7000 + 64928
20 MyApp 0x00000001005b01b4 __cxa_throw + 2250128
21 UIKit 0x00000001975d075c 0x0000000196dd7000 + 8361820
22 UIKit 0x00000001975ca130 0x0000000196dd7000 + 8335664
23 CoreFoundation 0x0000000190edeb5c 0x0000000190e01000 + 908124
24 CoreFoundation 0x0000000190ede4a4 0x0000000190e01000 + 906404
25 CoreFoundation 0x0000000190edc0a4 0x0000000190e01000 + 897188
26 CoreFoundation 0x0000000190e0a2b8 CFRunLoopRunSpecific + 444
27 GraphicsServices 0x00000001928be198 GSEventRunModal + 180
28 UIKit 0x0000000196e517fc 0x0000000196dd7000 + 501756
29 UIKit 0x0000000196e4c534 UIApplicationMain + 208
30 MyApp 0x00000001001b5804 0x00000001000ac000 + 1087492
31 libdyld.dylib 0x000000018fded5b8 0x000000018fde9000 + 17848
)

dSYM UUID: 1852C4B7-8391-3615-ADA5-2EE58D11DDED
CPU Type: armv7
Slide Address: 0x00004000
Binary Image: MyApp
Base Address: 0x000ca000
  • shell脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
DSYM=$1
LOGFILE=$2

if [ ! -n "$DSYM" ]; then
echo ">>>> DSYM is missing!!!"
exit
fi

if [ ! -n "$LOGFILE" ]; then
echo ">>>> log file is missing!!!"
exit
fi

echo ""
echo "/////////////////////"
echo " Info get... "
echo "/////////////////////"
echo ""

DSYM_UUID_KEY="dSYM UUID: "
DSYM_CPUTYPE_KEY="CPU Type: "
DSYM_BINARY_KEY="Binary Image: "

# first loop get uuid, cpu, binary
while read singleLine
do
str="$singleLine"

if [[ $str == $DSYM_UUID_KEY* ]]; then
UUID=${str#*"$DSYM_UUID_KEY"};
echo "Log UUID: "$UUID;
elif [[ $str == $DSYM_CPUTYPE_KEY* ]]; then
CPU=${str#*"$DSYM_CPUTYPE_KEY"};
echo "Log CPU: "$CPU;
elif [[ $str == $DSYM_BINARY_KEY* ]]; then
BINARY=${str#*"$DSYM_BINARY_KEY"};
echo "Log BINARY: "$BINARY;
fi

done < $LOGFILE

echo ""
echo "/////////////////////"
echo " Info check... "
echo "/////////////////////"
echo ""

checkUUID=`dwarfdump --uuid $DSYM -arch $CPU`
checkUUID=${checkUUID#*"UUID: "}
checkUUID=${checkUUID%" ($CPU)"*}
echo "DSYM UUID: $checkUUID"

if [[ "$UUID" == "$checkUUID" ]]; then
echo "UUID check passed."
else
echo "Warning!!!!!! UUID is not the same.. even though the code could be."
fi

echo ""
echo "/////////////////////"
echo " Log trace... "
echo "/////////////////////"
echo ""

#final loop get trace
while read singleLine
do
str="$singleLine"
if [[ $str == $DSYM_BINARY_KEY* ]]; then
#delete image
echo ""
else

# 匹配并删除第一个空格及其左边的所有字符,结果如下:
# MyApp 0x0000000100223668 0x00000001000ac000 + 1537640
str=${str#*" "}

# 如果当前行是我们自己项目的日志
if [[ $str == *$BINARY* ]]; then

# 匹配并删除第一个制表符及其左边的所有字符,即去掉第一列行号、第二列项目名,结果如下:
# 0x0000000100223668 0x00000001000ac000 + 1537640
str=${str#*" "};

# 匹配并删除"空格+空格"及其右边的所有字符,即去掉第一列行号、第二列项目名、第四列slide,结果如下:
# 0x0000000100223668 0x00000001000ac000
str=${str%" + "*}

# 以空格为分隔符,分别提取前后两部分字符
s1=$(echo $str | awk -F " " '{print $1}') # 获取第1部分并赋值给变量,即为stack address
s2=$(echo $str | awk -F " " '{print $2}') # 获取第2部分并赋值给变量,即为load address
# echo "stack address:$s1,load address:$s2"

# 开始解析日志
echo `atos -o $DSYM/Contents/Resources/DWARF/$BINARY -arch $CPU -l $s2 $s1`
fi
fi

done < $LOGFILE

执行脚本:

1
./trace_dsyms.sh Release.dSYM/ crash

解析后的闪退堆栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/////////////////////
Info get...
/////////////////////

Log UUID: 1852C4B7-8391-3615-ADA5-2EE58D11DDED
Log CPU: armv7
Log BINARY: MyApp

/////////////////////
Info check...
/////////////////////

DSYM UUID: 1852C4B7-8391-3615-ADA5-2EE58D11DDED
UUID check passed.

/////////////////////
Log trace...
/////////////////////

-[TLTaoBaoBindViewController setUps] (in MyApp) (TLTaoBaoBindViewController.m:234)
-[MBProgressHUD labelText] (in MyApp) (MBProgressHUD.h:313)
[invalid usage]: slide is not a recognized number
-[TLLoginManager httpTelLoginWithDistrict:mobile:captcha:fromVC:] (in MyApp) (TLLoginManager.m:277)

5.symbolicatecrash

symbolicatecrash 是Xcode自带的一个分析工具,可以通过机器上的闪退日志和应用的.dSYM文件定位发生闪退的位置,把crash日志中的地址替换成代码相应位置。

1、文件准备:.app.dSYM 文件

  • 拷贝 .crash 文件到桌面新建的crash文件夹内;
  • Xcode->Window->Organizer->APP->Show in Finder;
  • .xcarchive文件->显示包内容,找到 .app.dSYM 与 .app文件,并复制到 crash 目录中;

2、找到symbolicatecrash

  • 终端中执行下面命令:
1
find /Applications/Xcode.app -name symbolicatecrash -type f
  • 稍等就会输出symbolicatecrash路径
1
/Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash
  • 拷贝symbolicatecrash到crash目录中,与上面俩文件放到一起。
1
2
cp /Applications/Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework/Versions/A/Resources/symbolicatecrash /Users/xxx/Desktop/crash

3、执行symbolicatecrash

1
cd /Users/xxx/Desktop/crash
1
./symbolicatecrash /Users/xxx/Desktop/crash/xxx.crash /Users/xxx/Desktop/crash/xxx.app.dSYM > new_symbol.crash
  • 这时候终端有可能会出现:
1
Error: "DEVELOPER_DIR" is not defined at ./symbolicatecrash line 69.
  • 输入命令:
1
export DEVELOPER_DIR="/Applications/XCode.app/Contents/Developer"
  • 再执行,这时候终端将会进行处理了。

  • 终端完成后,在 crash 文件夹里会多出一个new_symbol.crash文件,打开即可查看BUG详情。


相关参考:

#©王中周-手动解析CrashLog之—-方法篇

#©王中周-手动解析CrashLog之—-原理篇

#©ctinusdev-Mach-O文件介绍之ASLR


dSYM+.crash解析
https://davidlii.cn/2019/03/12/dsym.html
作者
Davidli
发布于
2019年3月12日
许可协议