点赞评论,感觉有用的朋友可以关注笔者公众号 iOS 成长指北,持续更新
原书为 iOS Crash Dump Analysis Book,已得作者授权,欢迎 star
在本章中,我们将研究应用程序中止崩溃。
通过崩溃报告异常类型来区分这类崩溃—— EXC_CRASH (SIGABRT)
。
我们关注这些从网上收集的大量崩溃信息。
一般原则
许多操作系统语言的支持模块和库都有用于检测致命程序错误的代码。一旦触发,操作系统将终止该应用程序,这会导致 SIGABRT
崩溃。
SIGABRT
并没有特别的原因。所以我们将查看各种示例,以便我们可以分析它们出现的各种情况。
有时,这种崩溃会在崩溃报告的 Application Specific Information
区域中提供一些信息。如果这不能揭示我们所需的详细信息,但通常可以找到引发崩溃的模块,并对代码进行逆向工程以了解具体是什么症状。
Kindle Create 崩溃
Kindle Create是一款应用程序,作者可以利用手稿(比如 docx
文件)创建电子书。它通过 QuartzCore 库进行大量的绘制。
在发布预览时,它发生崩溃并产生以下崩溃报告,为便于演示,将其截断:
Process: Kindle Create [3010]
Path: /Applications/Kindle Create.app/
Contents/MacOS/Kindle Create
Identifier: com.amazon.kc
Version: 1.10 (1.10)
Code Type: X86 (Native)
Parent Process: ??? [1]
Responsible: Kindle Create [3010]
User ID: 501
Crashed Thread: 16 Dispatch queue:
com.apple.root.default-qos
Exception Type: EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note: EXC_CORPSE_NOTIFY
Application Specific Information:
abort() called
Application Specific Signatures:
Graphics kernel error: 0xfffffffb
Thread 16 Crashed:: Dispatch queue: com.apple.root.default-qos
0 libsystem_kernel.dylib 0xa73e7ed6
__pthread_kill + 10
1 libsystem_pthread.dylib 0xa75a0427
pthread_kill + 363
2 libsystem_c.dylib 0xa7336956
abort + 133
3 libGPUSupportMercury.dylib 0xa2aa342d
gpusGenerateCrashLog + 160
4 com.apple.AMDRadeonX4000GLDriver 0x180cbb00
gpusKillClientExt + 23
5 libGPUSupportMercury.dylib 0xa2aa4857
gpusSubmitDataBuffers + 157
6 com.apple.AMDRadeonX4000GLDriver 0x180a293c
glrATI_Hwl_SubmitPacketsWithToken + 143
7 com.apple.AMDRadeonX4000GLDriver 0x180fd9b0
glrFlushContextToken + 68
8 libGPUSupportMercury.dylib 0xa2aa88c8
gldFlushContext + 24
9 GLEngine 0x9b416f5b
glFlushRender_Exec + 37
10 com.apple.QuartzCore 0x9c1c8412
CA::(anonymous namespace)::IOSurface::detach() + 166
11 com.apple.QuartzCore 0x9c1c7631
CAOpenGLLayerDraw(CAOpenGLLayer*, double, CVTimeStamp const*,
unsigned int) + 1988
12 com.apple.QuartzCore 0x9c1c6c9a
-[CAOpenGLLayer _display] + 618
13 com.apple.QuartzCore 0x9c179f62
-[CALayer display] + 158
14 com.apple.AppKit 0x916106ac
-[NSOpenGLLayer display] + 305
15 com.apple.QuartzCore 0x9c1c9f77
display_callback(void*, void*) + 59
16 com.apple.QuartzCore 0x9c1c9efa
CA::DispatchGroup::dispatch(bool) + 88
17 com.apple.QuartzCore 0x9c1c9e9a
CA::DispatchGroup::callback_0(void*) + 16
18 libdispatch.dylib 0xa72565dd
_dispatch_client_callout + 50
19 libdispatch.dylib 0xa7263679
_dispatch_queue_override_invoke + 779
20 libdispatch.dylib 0xa725818b
_dispatch_root_queue_drain + 660
21 libdispatch.dylib 0xa7257ea5
_dispatch_worker_thread3 + 100
22 libsystem_pthread.dylib 0xa759cfa5
_pthread_wqthread + 1356
23 libsystem_pthread.dylib 0xa759ca32
start_wqthread + 34
Thread 16 crashed with X86 Thread State (32-bit):
eax: 0x00000000 ebx: 0xb0a79000 ecx: 0xb0a78acc
edx: 0x00000000
edi: 0xa75a02ca esi: 0x0000002d ebp: 0xb0a78af8
esp: 0xb0a78acc
ss: 0x00000023 efl: 0x00000206 eip: 0xa73e7ed6
cs: 0x0000000b
ds: 0x00000023 es: 0x00000023 fs: 0x00000023
gs: 0x0000000f
cr2: 0xa9847340
Logical CPU: 0
Error Code: 0x00080148
Trap Number: 132
Binary Images:
0x18099000 - 0x1815efff com.apple.AMDRadeonX4000GLDriver
(1.68.20 - 1.6.8)
/System/Library/Extensions/AMDRadeonX4000GLDriver.bundle/
Contents/MacOS/AMDRadeonX4000GLDriver
0xa2aa2000 - 0xa2aacfff libGPUSupportMercury.dylib
(16.7.4)
/System/Library/PrivateFrameworks/GPUSupport.framework/
Versions/A/Libraries/libGPUSupportMercury.dylib
从堆栈回溯中可以看到 OpenGL 管道已刷新。
这导致 com.apple.AMDRadeonX4000GLDriver
检测到命令问题并触发崩溃。 我们看到该代码为崩溃报告提供了自定义信息。
3 libGPUSupportMercury.dylib 0xa2aa342d
gpusGenerateCrashLog + 160
我们可以在这里使用 Hopper 脱壳和逆向工程工具。
通过在我们的Mac上找到二进制文件,我们可以要求Hopper不仅分解有问题的代码,而且还生成伪代码。对于大多数开发人员来说,很难立即了解汇编代码,因为当今很少使用汇编代码。这就是伪代码最有价值的原因。
我们首先从崩溃报告的 Binary Images
部分找到二进制文件的位置。
/System/Library/PrivateFrameworks/GPUSupport.framework/
Versions/A/Libraries/libGPUSupportMercury.dylib
对于系统二进制文件而言,遍历文件层次结构可能会很麻烦,因为它们深深地嵌套在文件系统中。
如果 Hopper 已经正在运行,那么快速选择正确文件的方法是使用命令行。
'/Applications/Hopper Disassembler v4.app/Contents/
MacOS/hopper' -e /System/Library/PrivateFrameworks/
GPUSupport.framework/Versions/A/Libraries/
libGPUSupportMercury.dylib
如果Hopper没有运行,我们可以启动它。我们可以启动 Finder 程序并选择 'Go To Folder' 以选择文件夹
/System/Library/PrivateFrameworks/GPUSupport.framework/
Versions/A/Libraries/
然后,我们可以简单地将libGPUSupportMercury.dylib
从 Finder 中拖到 Hopper 应用的主面板中,它将开始处理文件。
我们需要选择要脱壳的体系结构。 它必须与我们正在诊断的内容匹配。从崩溃报告中我们可以看到它是 Code Type
X86 (Native)
,这意味着我们需要在 Hopper 中选择32位体系结构选项。
然后我们点击 Next,然后点击 OK
片刻之后,文件将被处理。 然后我们可以选择 Navigate -> Go To Address or Symbol 并提供地址 _gpusGenerateCrashLog
。注意,下划线是在前面的。 C 编译器会在生成目标文件之前自动将其放入。 在过去,这样做是为了使手写的汇编代码在链接期间不会与C 语言符号冲突。
在默认视图中,Hopper 将显示该功能的反汇编代码。
通过选择伪代码按钮(用红色圆圈显示),我们可以使 Hopper 对该功能生成更容易理解的描述。
这是 Hopper 的输出:
int _gpusGenerateCrashLog(int arg0, int arg1, int arg2) {
rdi = arg0;
r14 = arg2;
rbx = arg1;
if (*0xc678 != 0x0) {
rax = *___stack_chk_guard;
if (rax != *___stack_chk_guard) {
rax = __stack_chk_fail();
}
}
else {
if (rdi != 0x0) {
IOAccelDeviceGetName(*(rdi + 0x230), 0x0, 0x14);
}
if ((rbx & 0x20000000) == 0x0) {
rdx =
"Graphics kernel error: 0x%08x\n";
}
else {
rdx =
"Graphics hardware encountered an error and was reset:
0x%08x\n";
}
sprintf_l(var_A0, 0x0, rdx);
*0xc680 = var_A0;
rax = abort();
}
return rax;
}
在这里,我们可以看到两种报文。一种是 :
"Graphics kernel error: 0x%08x\n"
另一种是:
"Graphics hardware encountered an error and was reset: 0x%08x\n"
实际上,我们在崩溃报告中看到以下内容:
Application Specific Signatures:
Graphics kernel error: 0xfffffffb
不幸的是,尚不清楚此错误是什么意思。 我们需要应用程序的作者打开OpenGL命令级日志记录,以便了解图形驱动程序拒绝了哪些绘图命令。
通过使用不同的 Mac 和显卡配置将会是实验变得很有趣。让我们判断这是一个是特定的驱动问题,或一个通用的 OpenGL 问题。
类型混淆
编译器在静态类型检查方面做得非常出色。但在动态推断类型时,可能会出现问题。而 配置文件在这方面尤为麻烦。 很容易为配置参数设置错误的类型
我们借助示例代码icdab_nsdata
来说明我们的观点。
从配置文件中获取 NSData 对象
思考以下示例代码
- (BOOL)application:(UIApplication *)application
didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Override point for customization after application
// launch.
NSData *myToken = [[NSData alloc] initWithData:
[[NSUserDefaults standardUserDefaults]
objectForKey:@"SomeKey"]];
NSLog(@"My data is %@ - ok since we can handle a nil",
myToken);
id stringProperty = @"Some string";
NSData *problemToken = [[NSData alloc]
initWithData:stringProperty];
NSLog(@"My data is %@ - we have probably crashed by now",
problemToken);
return YES;
}
这段代码试图做两件事。 首先,它尝试从其配置中获取 token
。我们假设在之前的运行中,用户已经以 SomeKey
为键值保存了一个 NSData
格式的 token
。
按照设计, NSData
对象可以处理所提供的数据是否为 nil
的情况。 因此,如果尚未保存数据,代码仍可正常运行。
token
可能只是一个简单的十六进制字符串,例如 7893883873a705aec69e2942901f20d7b1e28dec
上面的代码中有一个字符串 stringProperty
,用于模拟以下情况:用户存存储的 token
是一个字符串而不是 NSData
对象。
可能是它被手动复制并粘贴到用户的 plist
文件中。 如果 initWithData
方法参数为 NSString
,那么就无法创建 NSData
对象。 然后发生了崩溃。
如果我们运行该代码,我们将得到以下崩溃报告,为了便于演示,将其截断:
反序列化崩溃报告
Incident Identifier: 12F72C5C-E9BD-495F-A017-832E3BBF285E
CrashReporter Key: 56ec2b40764a1453466998785343f1e51c8b3849
Hardware Model: iPod5,1
Process: icdab_nsdata [324]
Path: /private/var/containers/Bundle/Application/
98F79023-562D-4A76-BC72-5E56D378AD98/
icdab_nsdata.app/icdab_nsdata
Identifier: www.perivalebluebell.icdab-nsdata
Version: 1 (1.0)
Code Type: ARM (Native)
Parent Process: launchd [1]
Exception Type: EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note: EXC_CORPSE_NOTIFY
Triggered by Thread: 0
Filtered syslog:
None found
Last Exception Backtrace:
0 CoreFoundation 0x25aa3916
__exceptionPreprocess + 122
1 libobjc.A.dylib 0x2523ee12
objc_exception_throw + 33
2 CoreFoundation 0x25aa92b0
-[NSObject+ 1045168 (NSObject) doesNotRecognizeSelector:]
+ 183
3 CoreFoundation 0x25aa6edc
___forwarding___ + 695
4 CoreFoundation 0x259d2234
_CF_forwarding_prep_0 + 19
5 Foundation 0x2627e9a0
-[_NSPlaceholderData initWithData:] + 123
6 icdab_nsdata 0x000f89ba
-[AppDelegate application:
didFinishLaunchingWithOptions:] + 27066 (AppDelegate.m:26)
7 UIKit 0x2a093780
-[UIApplication _handleDelegateCallbacksWithOptions:
isSuspended:restoreState:] + 387
8 UIKit 0x2a2bb2cc
-[UIApplication _callInitializationDelegatesForMainScene:
transitionContext:] + 3075
9 UIKit 0x2a2bf280
-[UIApplication _runWithMainScene:transitionContext:
completion:] + 1583
10 UIKit 0x2a2d3838
__84-[UIApplication _handleApplicationActivationWithScene:
transitionContext:completion:]_block_invoke3286 + 31
11 UIKit 0x2a2bc7ae
-[UIApplication workspaceDidEndTransaction:] + 129
12 FrontBoardServices 0x27146c02
__FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__ + 13
13 FrontBoardServices 0x27146ab4
-[FBSSerialQueue _performNext] + 219
14 FrontBoardServices 0x27146db4
-[FBSSerialQueue _performNextFromRunLoopSource] + 43
15 CoreFoundation 0x25a65dfa
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
+ 9
16 CoreFoundation 0x25a659e8
__CFRunLoopDoSources0 + 447
17 CoreFoundation 0x25a63d56
__CFRunLoopRun + 789
18 CoreFoundation 0x259b3224
CFRunLoopRunSpecific + 515
19 CoreFoundation 0x259b3010
CFRunLoopRunInMode + 103
20 UIKit 0x2a08cc38
-[UIApplication _run] + 519
21 UIKit 0x2a087184
UIApplicationMain + 139
22 icdab_nsdata 0x000f8830
main + 26672 (main.m:14)
23 libdyld.dylib 0x2565b86e tlv_get_addr
+ 41
不幸的是,这种崩溃没有将更多的有用信息(例如“特定于应用程序的信息”)注入到崩溃报告中。
但是,我们确实会在系统日志(应用程序的控制台日志)中获取信息:
default 13:36:58.000000 +0000 icdab_nsdata
My data is <> - ok since we can handle a nil
default 13:36:58.000000 +0100 icdab_nsdata
-[__NSCFConstantString _isDispatchData]:
unrecognized selector sent to instance 0x3f054
default 13:36:58.000000 +0100 icdab_nsdata
*** Terminating app due to uncaught exception
'NSInvalidArgumentException', reason:
'-[__NSCFConstantString _isDispatchData]:
unrecognized selector sent to instance 0x3f054'
*** First throw call stack:
(0x25aa391b 0x2523ee17 0x25aa92b5 0x25aa6ee1 0x259d2238
0x2627e9a5 0x3d997
0x2a093785 0x2a2bb2d1 0x2a2bf285 0x2a2d383d 0x2a2bc7b3
0x27146c07
0x27146ab9 0x27146db9 0x25a65dff 0x25a659ed 0x25a63d5b
0x259b3229
0x259b3015 0x2a08cc3d 0x2a087189 0x3d80d 0x2565b873)
default 13:36:58.000000 +0100
SpringBoard Application
'UIKitApplication:www.perivalebluebell.icdab-nsdata[0x51b9]'
crashed.
default 13:36:58.000000 +0100
UserEventAgent
2769630555571: id=www.perivalebluebell.icdab-nsdata
pid=386, state=0
default 13:36:58.000000 +0000 ReportCrash
Formulating report for corpse[386] icdab_nsdata
default 13:36:58.000000 +0000 ReportCrash
Saved type '109(109_icdab_nsdata)'
report (2 of max 25) at
/var/mobile/Library/Logs/CrashReporter/
icdab_nsdata-2018-07-27-133658.ips
从这里我们可以看到问题是 __NSCFConstantString
无法响应 _isDispatchData
是因为 NSString
不是数据所提供的对象。
Apple SDK 具有私有实现类,以支持我们使用的公共对象。 错误报告将引用这些私有类。 因此,他们的名字可能不再令人熟悉。
可以通过一种简单的方法进行管理,并找出具体的表示映射到要搜索的类类型定义的哪个对象。
方便的是,其他工程师已经在框架上使用 class-dump
工具来生成所有 Objective-C 的类定义,并将它们存储在 GitHub上。他们使用 class-dump
工具。这使得 Objective-C 的所有私有框架符号都很容易搜索到。
我们可以找到关于 _isDispatchData
的定义。
/* Generated by RuntimeBrowser
Image: /System/Library/Frameworks/Foundation.framework/
Foundation
*/
@interface _NSDispatchData : NSData
+ (bool)supportsSecureCoding;
- (bool)_allowsDirectEncoding;
- (id)_createDispatchData;
- (bool)_isDispatchData;
- (Class)classForCoder;
- (id)copyWithZone:(struct _NSZone { }*)arg1;
- (void)encodeWithCoder:(id)arg1;
- (void)enumerateByteRangesUsingBlock:(id /* block */)arg1;
- (void)getBytes:(void*)arg1;
- (void)getBytes:(void*)arg1 length:(unsigned long long)arg2;
- (void)getBytes:(void*)arg1 range:(struct _NSRange
{ unsigned long long x1; unsigned long long x2; })arg2;
- (unsigned long long)hash;
- (id)initWithCoder:(id)arg1;
- (id)subdataWithRange:(struct _NSRange
{ unsigned long long x1; unsigned long long x2; })arg1;
@end
同样,我们可以查找 __NSCFConstantString
。
/* Generated by RuntimeBrowser
Image: /System/Library/Frameworks/CoreFoundation.framework/
CoreFoundation
*/
@interface __NSCFConstantString : __NSCFString
- (id)autorelease;
- (id)copyWithZone:(struct _NSZone { }*)arg1;
- (bool)isNSCFConstantString__;
- (oneway void)release;
- (id)retain;
- (unsigned long long)retainCount;
@end