CrashReporter 是 Mac OS X 下的调试工具,其会记录 Mac 下所有应用程序的崩溃信息。这些日志信息通常保存在路径 ~/Library/Logs/CrashReporter/ 下,当然,如果 CrashReporter 无法确定崩溃程序的所属用户,或者其用户是根用户,或者所属用户的路径无效或不可写,那么,崩溃日志信息会保存在路径 /Library/Logs/CrashReporter/ 下。
每个崩溃日志信息均会分开保存在一个文件中,文件的命名形如: PPP_YYYY-MM-DD-HHMMSS_NNN.crash ,PPP 是应用程序名称,YYYY-MM-DD-HHMMSS 是崩溃发生的日期及时间,NNN 则是主机用户名。当然,CrashReporter 也会创建名称如 .PPP_NNN_CrashHistory.plist 的隐藏文件。
为了避免日志信息占用大量存储空间,这些日志文件限制为20个。
在 Mac OS X 10.5 之前,CrashReporter 创建的日志文件名称形如:PPP.crash.log ,PPP 是程序名称,同一个应用产生的崩溃信息均会被追加到同一个文件中,并且没有存储容量的限制。
崩溃日志文件中包含许多信息,阅读理解这些信息有利于问题的查找。
崩溃日志信息的第一部分记录的是崩溃的应用程序的信息,其中包含程序线程号、父线程号、程序路径、程序唯一标识(bundle identifier)、版本等信息,如下:
Process: TextEdit [8752]
Path: /Applications/TextEdit.app/Contents/MacOS/TextEdit
Identifier: com.apple.TextEdit
Version: 1.5 (244)
Build Info: TextEdit-2440000~2
Code Type: X86 (Native)
Parent Process: launchd [241]
该部分记录的是崩溃日志本身的相关信息,包括崩溃发生的时间、系统版本、CrashReporter 版本等,如下:
Date/Time: 2008-01-29 12:32:46.239 +0000
OS Version: Mac OS X 10.5.1 (9B18)
Report Version: 6
该部分记录了导致本次崩溃的异常信息,包含异常类型、异常码、崩溃线程等,如下:
Exception Type: EXC_BAD_ACCESS (SIGBUS)
Exception Codes: KERN_PROTECTION_FAILURE at 0x0000000000000000
Crashed Thread: 0
最常见的异常类型如下:
异常类型 | 含义 |
---|---|
EXC_BAD_ACCESS/KERN_INVALID_ADDRESS |
访问未分配的内存地址造成,多是由数据访问或提取指令触发 |
EXC_BAD_ACCESS/KERN_PROTECTION_FAILURE |
向只读区域写数据造成 |
EXC_BAD_INSTRUCTION |
线程执行了非法指令 |
EXC_ARITHMETIC/EXC_I386_DIV |
在基于 intel 计算机上执行了除零操作 |
该部分保存了崩溃进程的所有线程的方法栈信息,如下:
Thread 0 Crashed:
0 ??? 0000000000 0 + 0
1 com.apple.CoreFoundation 0x942cf0fe CFRunLoopRunSpecific + 18…
2 com.apple.CoreFoundation 0x942cfd38 CFRunLoopRunInMode + 88
3 com.apple.HIToolbox 0x919e58a4 RunCurrentEventLoopInMode…
4 com.apple.HIToolbox 0x919e56bd ReceiveNextEventCommon + …
5 com.apple.HIToolbox 0x919e5531 BlockUntilNextEventMatchi…
6 com.apple.AppKit 0x9390bd5b _DPSNextEvent + 657
7 com.apple.AppKit 0x9390b6a0 -[NSApplication nextEvent…
8 com.apple.AppKit 0x939046d1 -[NSApplication run] + 79…
9 com.apple.AppKit 0x938d19ba NSApplicationMain + 574
10 com.apple.TextEdit 0x00001df6 0x1000 + 3574
当崩溃的程序是多线程时,那么每个线程都会有一个方法栈信息记录,而至于哪个记录是崩溃的栈信息,则需要开发者自己注意 Thread Crashed: 标识。方法调用栈记录的每一行信息都包含4个部分,每部分含义如下:
该部分记录的是崩溃的线程状态,这个记录与电脑系统架构相关,不同架构,记录的信息不同。
PowerPC 是 IBM 与 Apple 公司联合生产的个人台式机,其记录信息如下:
Thread 0 crashed with PPC Thread State 64:
srr0: 0x0000000000000000 srr1: 0x000000004000d030 …
cr: 0x44022282 … lr: 0x000000009000a6bc…
r0: 0x00000000ffffffe1 r1: 0x00000000bfffeb10 r2: 0x00000000a…
r4: 0x0000000003000006 r5: 0x0000000000000000 r6: 0x000000000…
r8: 0x0000000000000000 r9: 0x0000000000000000 r10: 0x000000000…
r12: 0x000000009000a770 r13: 0x0000000000000000 r14: 0x000000000…
r16: 0x0000000000000000 r17: 0x0000000000000000 r18: 0x000000000…
r20: 0x00000000101a7026 r21: 0x00000000be5b19d8 r22: 0x000000000…
r24: 0x0000000000000450 r25: 0x0000000000001203 r26: 0x000000000…
r28: 0x0000000000000000 r29: 0x0000000003000006 r30: 0x000000000…
这里要关注的是 ssr0 、lr 及崩溃地址,ssr 记录了程序发生崩溃时指令的地址,lr 记录着函数调用的返回地址。对于内存访问时的异常,有以下情况:
对于32位 intel 架构的电脑,其记录的线程状态信息如下:
Thread 0 crashed with X86 Thread State (32-bit):
eax: 0x00000000 ebx: 0x942cea07 ecx: 0xbfffed1c edx: 0x94b3a8e6
edi: 0x00000000 esi: 0x00000000 ebp: 0xbfffed58 esp: 0xbfffed1c
ss: 0x0000001f efl: 0x00010206 eip: 0x00000000 cs: 0x00000017
ds: 0x0000001f es: 0x0000001f fs: 0x00000000 gs: 0x00000037
cr2: 0x00000000
这里的信息,主要关注 eip 与崩溃地址,eip 是异常发生时,程序计数器中的值,所以其是导致异常发生的指令的地址。对于内存访问时的异常,有以下情况:
因为 intel 架构的工作方式不同,其相较于 PowerPC 更难获取线程的状态信息,如,在 PowerPC 中,返回的地址保存在寄存器 lr 中,而 intel 架构中的返回地址则保存在栈中。
对于64位 intel 架构的电脑,其记录的线程状态信息如下:
Thread 0 crashed with X86 Thread State (64-bit):
rax: 0x0000000000000000 rbx: 0x0000000000000000 rcx: 0x00007fff5fbfec48…
rdi: 0x00007fff5fbfed40 rsi: 0x0000000003000006 rbp: 0x00007fff5fbfeca0…
r8: 0x0000000000001003 r9: 0x0000000000000000 r10: 0x0000000000000450…
r12: 0x0000000000001003 r13: 0x0000000000000450 r14: 0x00007fff5fbfed40…
rip: 0x0000000000000000 rfl: 0x0000000000010206 cr2: 0x0000000000000000
该信息的解读同32位 intel 架构类似,不同的是 eip 中的值如今保存在 rip 中。
该部分保存所有加载到进程中的二进制映像的描述,其信息格式如下:
Binary Images:
0x1000 - 0x18feb com.apple.TextEdit 1.5 (244)
对于日志中无标识符号的回溯信息,可根据此描述信息找到相应的标识符号。另外,可以查看所有加载到进程的插件和库。信息中的括号中的值是映像的 UUID 值,该值对于查找无标识符号的信息很有用处。
当进程在运行时使用了 Rosetta ,那么其崩溃信息中则会记录一些额外信息,如下,进程信息中包含 Code Type 记录项,则说明 Rosetta 被使用了。
Process: TextEdit [9031]
Path: /Applications/TextEdit.app/Contents/MacOS/TextEdit
Identifier: com.apple.TextEdit
Version: 1.5 (244)
Code Type: PPC (Translated)
Parent Process: launchd [241]
另外,Translated 编码信息会被添加在二进制映像部分后面,包含有 Rosetta 版本号、进程命令行参数、崩溃信息、线程状态。
Translated Code Information:
Rosetta Version: 20.44
Args: /Applications/TextEdit.app/Contents/MacOS/TextEdit -psn_0_2761378
Exception: EXC_BAD_ACCESS (0x0001)
Thread 0: Crashed (0xb7fff9d0, 0xb80bc8c8)
0x40400000: No symbol
0x90a9b35c: /System/Library/Frameworks/AppKit.framework/Versions/C/AppKit…
0x90a9b290: /System/Library/Frameworks/AppKit.framework/Versions/C/AppKit…
0x90a9a7a8: /System/Library/Frameworks/AppKit.framework/Versions/C/AppKit…
0x90a9a0e0: /System/Library/Frameworks/AppKit.framework/Versions/C/AppKit…
0x90a99a1c: /System/Library/Frameworks/AppKit.framework/Versions/C/AppKit…
0x90a98458: /System/Library/Frameworks/AppKit.framework/Versions/C/AppKit…
0x90a6b8f4: /System/Library/Frameworks/AppKit.framework/Versions/C/AppKit…
0x909d8ed8: /System/Library/Frameworks/AppKit.framework/Versions/C/AppKit…
0x909a9930: /System/Library/Frameworks/AppKit.framework/Versions/C/AppKit…
0x00001e18: /Applications/TextEdit.app/Contents/MacOS/TextEdit : start + …
0x00000000: /Applications/TextEdit.app/Contents/MacOS/TextEdit : + 0
PPC Thread State
srr0: 0x00000000 srr1: 0x00000000 vrsave: 0x00000000
cr: 0xXXXXXXXX xer: 0x00000000 lr: 0x90a9b35c ctr: 0x0000e814
r00: 0x90a9b35c r01: 0xbfffe5d0 r02: 0xa0bcf924 r03: 0x00234840
r04: 0x00020000 r05: 0x002adce0 r06: 0x002adce0 r07: 0x002adce0
r08: 0xa1b1c1d3 r09: 0x00000000 r10: 0x00000004 r11: 0x00000001
r12: 0x0000e814 r13: 0xa01da174 r14: 0xa01da174 r15: 0xa01da174
r16: 0xa01da174 r17: 0xa01da174 r18: 0x00000000 r19: 0x002c00b0
r20: 0xbfffe760 r21: 0xa01da174 r22: 0xa01da174 r23: 0xa01da174
r24: 0xa01ea174 r25: 0x002adce0 r26: 0x002475c0 r27: 0x00015dd4
r28: 0x00234840 r29: 0x00020000 r30: 0x00234840 r31: 0x90a9b2f0
调试时使用的标识符号会一定程度的影响代码的大小及执行速度,所以,对于现在的 Xcode 可以在分发应用给用户之前取消标识符号,在分析日志信息时,将16进制数值翻译为相应的标识符号。
在使用 Xcode 编写程序时,其需要处理两种完全不同的符号类型:
Mach-O 也被分为两种类型:
Xcode 支持两种格式的调试符号:
Xcode 中 DWARF 的使用使得分发程序时,分离调试符号变得简单,只需要在 Build Setting 中将配置项 Debug Information Format 设置为 DWARF with dSYM File(DEBUG_INFORMATION_FORMAT = dwarf-with-dsym)即可,这样,在创建分发应用包时,Xcode 会自动提取程序中所有的调试信息,并保存在 .dSYM 文件中。当未来某个时刻,需要调试分发的程序时,只需要将程序和 .dSYM 文件放在相同路径下,GDB 就会自动找到并使用调试符号。
一些 Mach-O 符号是由动态连接器在运行时时翻译的,这些符号在构建应用时不能分离。而在构建不同的应用时,要进行不同的设置。首先,要在 Build Setting 中将 Deployment Postprocessing 设置为 YES(DEPLOYMENT_POSTPROCESSING = YES),而后,根据不同情况设置 Strip Style 的值:
对于分离 Mach-O 符号,通常有两种处理方式:
对于第一种情况,只需在 Build Setting 中 设置 Strip Style 的值为 Debugging Symbols 即可,但第二种需要根据工程目标的不同而定,对于应用和命令行工具,将值设为 All Symbols ,对于包、框架、动态库应分离本地符号,要将 Strip Style 设置为 Non-Global Symbols ,当然,也可以设置 Exported Symbols File 项,明确需要分离的符号,相反,也可以用其他方法明确需保留的符号。
将工程正确设置后,CrashReporter 产生的日志信息会因设置不同而不同,若保留了本地符号,那么日志中会记录崩溃的函数名称,若分离了所有符号,那么,日志中不会记录相应的函数名称。幸运的是,可以使用 .dSYM 文件获取相关源码层的信息,这个获取信息的过程分为位置相关的代码与位置不相关的代码两种,区别不大。
对于位置相关的代码(应用程序及命令行工具),使用 GDB 将数值地址转化为符号,较为简单,只需将分发的应用与 .dSYM 文件放在同一个路径下,然后使用 GDB 命令即可,如下:
$ # Get the numeric values from the backtrace...
$ grep "Thread 0 Crashed:" -A 19 NoSymbolsTest_[…]_guy-smiley.crash
Thread 0 Crashed:
0 ...le.dts.NoSymbolsTest.Bundle 0x107cbf99 0x107cb000 + 3993
1 ...le.dts.NoSymbolsTest.Bundle 0x107cbfcb 0x107cb000 + 4043
2 ...dts.NoSymbolsTest.Framework 0x10005f2e 0x10005000 + 3886
3 ...dts.NoSymbolsTest.Framework 0x10005f59 0x10005000 + 3929
4 com.apple.dts.NoSymbolsTest 0x10000edf 0x10000000 + 3807
5 com.apple.AppKit 0x939dcf94 -[NSApplication sendAction…
6 com.apple.AppKit 0x939dced4 -[NSControl sendAction:to:…
7 com.apple.AppKit 0x939dcd5a -[NSCell _sendActionFrom:]…
8 com.apple.AppKit 0x939dc3bb -[NSCell trackMouse:inRect…
9 com.apple.AppKit 0x939dbc12 -[NSButtonCell trackMouse:…
10 com.apple.AppKit 0x939db4cc -[NSControl mouseDown:] + …
11 com.apple.AppKit 0x939d9d9b -[NSWindow sendEvent:] + 5…
12 com.apple.AppKit 0x939a6a2c -[NSApplication sendEvent:…
13 com.apple.AppKit 0x93904705 -[NSApplication run] + 847…
14 com.apple.AppKit 0x938d19ba NSApplicationMain + 574
15 com.apple.dts.NoSymbolsTest 0x10000e36 0x10000000 + 3638
16 com.apple.dts.NoSymbolsTest 0x10000e02 0x10000000 + 3586
17 com.apple.dts.NoSymbolsTest 0x10000d29 0x10000000 + 3369
$ # Run GDB to get the symbolic information for the address in frame 4.
$ gdb NoSymbolsTest.app
GNU gdb 6.3.50-20050815 (Apple version gdb-768) […]
(gdb) info line *0x10000edf
Line 86 of "/Users/quinn/Crash Reporter/NoSymbolsTest/AppDelegate.m" \
starts at address
0x10000edf <-[AppDelegate testAction:]+104> and ends at \
0x10000ee1 <-[AppDelegate testAction:]+106>.
这里需要注意以下几点:
使用 UUID 判断获取的符号是否与应用程序一致
$ # Get the UUID from the binary images part of the crash log.
$ # The UUID is displayed in angle brackets.
$ grep "0x.*com.apple.dts.NoSymbolsTest .*<" NoSymbolsTest_[…]_guy-smiley.crash
0x10000000 - 0x10000ffe com.apple.dts.NoSymbolsTest ??? (1.0) \
<6264534bd26d5d39f7960cea770c4ea8> /Users/quinn/Crash Reporter/NoSymbolsTest/\
build/Release/NoSymbolsTest.app/Contents/MacOS/NoSymbolsTest
$ # Get the UUID from the binary that we have.
$ dwarfdump --uuid NoSymbolsTest.app/Contents/MacOS/NoSymbolsTest
UUID: 6264534B-D26D-5D39-F796-0CEA770C4EA8 (i386) NoSymbolsTest.app[…]
UUID: AA201B24-D09B-49E2-55E5-AB15AF63B12A (ppc) NoSymbolsTest.app[…]
$ # Get the UUIDs from the .dSYM file.
$ dwarfdump --uuid NoSymbolsTest.app.dSYM
UUID: 6264534B-D26D-5D39-F796-0CEA770C4EA8 (i386) NoSymbolsTest.app.dSYM
UUID: AA201B24-D09B-49E2-55E5-AB15AF63B12A (ppc) NoSymbolsTest.app.dSYM
$ # Note that all three UUIDs match!
对于位置不相关的代码,如框架、动态库或者包,这些代码的分析要复杂点。分析者需要找出代码名义上加载的位置与实际中加载的位置的差值,这个值叫做偏移量(slide)。
从崩溃日志信息中的二进制映射信息部分,可以找到代码实际加载的地址。如下,是 __TEXT 段的加载地址:
$ # Determine the addresses that the programs were loaded.
$ grep "0x.*com.apple.dts" NoSymbolsTest_[…]_guy-smiley.crash
0x10000000 - 0x10000ffe com.apple.dts.NoSymbolsTest ??? (1.0) […]
0x10005000 - 0x10005ffd com.apple.dts.NoSymbolsTest.Framework ??? (1.0) […]
0x107cb000 - 0x107cbffc com.apple.dts.NoSymbolsTest.Bundle ??? (1.0) […]
另外,可以使用 otool 获取 __TEXT 段的目的加载地址,如下:
$ otool -l NoSymbolsTest.app/Contents/MacOS/NoSymbolsTest \
| grep -B 3 -A 2 -m 1 "__TEXT"
Load command 1
cmd LC_SEGMENT
cmdsize 192
segname __TEXT
vmaddr 0x10000000
vmsize 0x00001000
$ otool -l NoSymbolsTest.app/Contents/Frameworks/Framework.framework/Framework \
| grep -B 3 -A 8 -m 1 "__TEXT"
Load command 0
cmd LC_SEGMENT
cmdsize 192
segname __TEXT
vmaddr 0x01000000
vmsize 0x00001000
$ otool -l NoSymbolsTest.app/Contents/Resources/Bundle.bundle/Contents/MacOS/Bundle \
| grep -B 3 -A 8 -m 1 "__TEXT"
Load command 0
cmd LC_SEGMENT
cmdsize 192
segname __TEXT
vmaddr 0x00000000
vmsize 0x00001000
将得到结果,进行计算,得到偏移量,如下:
程序 | 实际加载地址(A) | 目的加载地址(I) | 偏移量(slide = A - I) |
---|---|---|---|
main executable | 0x10000000 | 0x10000000 | 0 |
framework | 0x10005000 | 0x01000000 | 0x0F005000 |
bundle | 0x107cb000 | 0x00000000 | 0x107cb000 |
对于主函数中执行的代码(main executable),总是位置相关的。
如果使用 atos 获取符号,可以使用参数可选项 -S 填充获取的偏移量,如果使用 GDB ,那么过程较复杂,操作如下:
$ # Get the addresses from the backtrace.
$ grep "Thread 0 Crashed:" -A 5 NoSymbolsTest_2008-02-04-111412_guy-smiley.crash
Thread 0 Crashed:
0 ...le.dts.NoSymbolsTest.Bundle 0x107cbf99 0x107cb000 + 3993
1 ...le.dts.NoSymbolsTest.Bundle 0x107cbfcb 0x107cb000 + 4043
2 ...dts.NoSymbolsTest.Framework 0x10005f2e 0x10005000 + 3886
3 ...dts.NoSymbolsTest.Framework 0x10005f59 0x10005000 + 3929
4 com.apple.dts.NoSymbolsTest 0x10000edf 0x10000000 + 3807
$ # Now map the addresses for the frames in the bundle (0 and 1).
$ # Run GDB with no arguments.
$ gdb
GNU gdb 6.3.50-20050815 (Apple version gdb-768) […]
(gdb) # Disable shared library preloading. See below for why.
(gdb) set sharedlibrary preload-libraries off
(gdb) # Target the bundle.
(gdb) file Bundle.bundle/Contents/MacOS/Bundle
Reading symbols from […]
(gdb) # Subtract the bundle slide from the frame 0 address and then map it.
(gdb) p/x 0x107cbf99-0x107cb000
$1 = 0xf99
(gdb) info line *$1
Line 28 of "/Users/quinn/Crash Reporter/NoSymbolsTest/Bundle.m" starts at address \
0xf94 <-[Bundle testInner]+50> and ends at \
0xfa5 <-[Bundle testInner]+67>.
(gdb) # Subtract the bundle slide from the frame 1 address and then map it.
(gdb) p/x 0x107cbfcb-0x107cb000
$2 = 0xfcb
(gdb) info line *$2
Line 34 of "/Users/quinn/Crash Reporter/NoSymbolsTest/Bundle.m" starts at address \
0xfcb <-[Bundle testOuter]+36> and ends at \
0xfd1 <-[Bundle testOuter]+42>.
(gdb) quit
$ # Now do the same for the framework.
$ gdb
GNU gdb 6.3.50-20050815 (Apple version gdb-768) […]
(gdb) # Disable shared library preloading.
(gdb) set sharedlibrary preload-libraries off
(gdb) # Target the framework.
(gdb) file Framework.framework/Framework
Reading symbols from […]
(gdb) # Subtract the framework slide from the frame 2 address and then map it.
(gdb) p/x 0x10005f2e-0x0F005000
$1 = 0x1000f2e
(gdb) info line *$1
Line 39 of "/Users/quinn/Crash Reporter/NoSymbolsTest/Framework.m" starts at address \
0x1000f2e <-[Framework testInner]+308> and ends at
0x1000f35 <-[Framework testOuter]>.
(gdb) # Subtract the framework slide from the frame 3 address and then map it.
(gdb) p/x 0x10005f59-0x0F005000
$2 = 0x1000f59
(gdb) info line *$2
Line 44 of "/Users/Crash Reporter/NoSymbolsTest/Framework.m" starts at address \
0x1000f59 <-[Framework testOuter]+36> and ends at \
0x1000f5f <-[Framework testOuter]+42>.
默认情况下,在 GDB 中使用 file 命令设置目标文件时,GDB 会加载目标文件的所有符号,以及共享库中的符号,而共享库中的符号可能会覆盖程序中的符号,所以在设置目标文件之前,使用 set sharedlibrary preload-libraries off 命令取消加载共享库中的符号。
CrashReporterPrefs 是 Xcode 中开发者工具的一种,用于记录崩溃日志,有三种模式:
CrashReporter 现在仍有一些限制,如第三方开发者是不能够访问 CrashReporter 产生的日志文件的,当然,一些第三方开发者实现了自己的日志报告机制。另外,如果在日志中加入栈信息,那么有利于一些崩溃的原因查找。
在 Mac OS X 10.5 之前,若崩溃是由调用系统方法 abort 导致的,那么 CrashReporter 不会生产崩溃信息日志,并且如果程序导致了异常,但是这个异常有其相应的处理方法,CrashReporter 仍然会生成一个崩溃日志记录。