从一个常见的崩溃开始

昨天下班时候一朋友碰到一个下面的崩溃,但是全局断点却无法断到指定位置。鉴于不信邪的态度,决定尝试一番。然后将此事记录如下文。

1
2
3
4
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFConstantString hasPrefix:]: nil argument'
*** First throw call stack:
(0x18521ed8c .....)
libc++abi.dylib: terminating with uncaught exception of type NSException

崩溃表现

那老哥说他的APP运行之后,什么都不做,等待10s之后就会闪退,闪退的最重要信息是上面,栈结构上没有函数信息,只有地址。

打开全局断点,也无法直接断到指定的函数,这个信息我留了一张图,大致如下:(已脱敏)

崩溃截图

先简单的分析一下

  • 崩溃原因很明确:-[__NSCFConstantString hasPrefix:]: nil argument
  • __NSCFConstantStringNSString类簇里面的一个类,常见的类似的还有很多。
  • - (BOOL)hasPrefix:(NSString *)str;nil argument,也就是strnil
  • First throw call stack下方均为地址,说明此函数应该在一个.a/.framework中。
  • APP运行之后,什么都不做,等待10s之后就会闪退说明此崩溃为异步调用,至于是不是定时调用,这个真不好说。
  • 由于我技术有限,别的好像提炼不出有用信息了。。

复现一下崩溃

先来个简单的代码,验证一下 ** - (BOOL)hasPrefix:(NSString *)str;nil argument,也就是strnil。 **

1
2
3
4
5
6
#import <Foundation/Foundation.h>
int main(int argc, char *argv[]) {
@autoreleasepool {
NSLog(@"%@", @([@"" hasPrefix:nil]));
}
}

运行一下,结果很意料之中,得出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Untitled.m:4:29: warning: null passed to a callee that requires a non-null argument [-Wnonnull]
NSLog(@"%@", @([@"" hasPrefix:nil]));
^ ~~~
1 warning generated.
Terminated due to signal: ABORT TRAP (6)
2018-07-25 10:44:50.569 Untitled[53770:899141] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFConstantString hasPrefix:]: nil argument'
*** First throw call stack:
(
0 CoreFoundation 0x00007fff30aae2db __exceptionPreprocess + 171
1 libobjc.A.dylib 0x00007fff57c58c76 objc_exception_throw + 48
2 CoreFoundation 0x00007fff30b3fd7d +[NSException raise:format:] + 205
3 CoreFoundation 0x00007fff30a01840 -[__NSCFString hasPrefix:] + 96
4 Untitled 0x000000010787fd60 main + 64
5 libdyld.dylib 0x00007fff58872015 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException

经过1 warning generated之后,果断崩溃了,还有明确的崩溃时候栈结构。

好,问题确定了就是这个1 warning generated造成的。改下就完事咯~~

寻找问题

尝试解除warning

远程进去之后我震惊了,warning: 999+。先快速找到问题,要什么自行车。。

Xcode中全局搜索hasPrefix关键字,带变量的全部加上断点。

run一下,问题依旧如此。。正如上面说的一样:

First throw call stack下方均为地址,说明此函数应该在一个.a/.framework中。

GO DIE…

PS.

  • 关于hasPrefix函数的OC声明swift声明均为完整字符串,所以安心全局搜索就能找到代码中已存在的hasPrefix函数
  • 顺便搜了一下该项目不包含第三方.a, 但是.framework不少。。有CocoaPods集成有直接拖进来的。。

尝试Method Swizzling一下

这玩意既然在别人的framework中,那就更新一下咯,但是全部更新并不可取。。

framework的更新可能导致一些不可预估的可变因素。比如说API修改、内部逻辑变更导致外部使用不兼容、某些开发者喜欢直接修改framework、等。

所以先确定是哪个framework。

Method Swizzling。将其-[__NSCFConstantString hasPrefix:]: nil argument直接过滤nil,岂不美滋滋~~

好了先测试一下:

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
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface MySafeString : NSObject
@end
@implementation MySafeString
+ (void)load {
Class originalClass = NSClassFromString(@"__NSCFConstantString");
Class swizzledClass = [self class];
SEL originalSelector = @selector(hasPrefix:);
SEL swizzledSelector = @selector(safe_hasPrefix:);
Method originalMethod = class_getInstanceMethod(originalClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector);

IMP originalIMP = method_getImplementation(originalMethod);
IMP swizzledIMP = method_getImplementation(swizzledMethod);
const char *originalType = method_getTypeEncoding(originalMethod);
const char *swizzledType = method_getTypeEncoding(swizzledMethod);

class_replaceMethod(originalClass,swizzledSelector,originalIMP,originalType);
class_replaceMethod(originalClass,originalSelector,swizzledIMP,swizzledType);
}
- (BOOL)safe_hasPrefix:(NSString *)str {
if (str == nil) {
return YES;
} else {
return [self safe_hasPrefix:str];
}
}

@end
int main(int argc, char *argv[]) {
@autoreleasepool {
NSLog(@"%@", @([@"" hasPrefix:nil]));
}
}

输出:

1
2
3
4
5
Untitled.m:33:29: warning: null passed to a callee that requires a non-null argument [-Wnonnull]
NSLog(@"%@", @([@"" hasPrefix:nil]));
^ ~~~
1 warnings generated.
2018-07-25 11:32:20.634 Untitled[54520:926937] 1

不崩溃了,赶紧粘过去,并在- (BOOL)safe_hasPrefix:(NSString *)str中的return YES;打上断点。

内心得到了极大的满足~ 美滋滋~~~

2分钟后。

断点停了,向- (BOOL)safe_hasPrefix:(NSString *)strstr传nil的调用者是[MOBFErrorReportService writeHTTPErrorMsg:error:]

MOBFErrorReportService放入Google看到第二个就是这个[MOBFDevice duid] - BUG提交- Mob官方论坛。文中指出SDK 版本: ShareSDKVersion-3.5.1

好了看一下ShareSDK这个库。emmm 这个库是拖拽进去的。老哥说这项目接手之后没更新过这个库。

事已至此,确定是ShareSDK的问题。

更新之,问题解决~ 真心的美滋滋~~

PS.

寻找的过程部分特写

上面的逻辑并不是一气呵成,而是由于粗心也走了部分弯路,特列出来引以为戒。

看到全局断点真的走不到时

由于代码是公司代码,也没有demo,所以布吉岛为啥确实断点不会走。。。

MMP 看到了First throw call stack之后第一反应是,这些地址应该可以还原到对应的位置。

然后想到了逆向里面的骚操作。顿时想逆向分析一下。。。

但由于看了一下.app中的可执行文件有 ** 40M ** ,算了算了,IDA载入的有点慢,这要好久才能分析完。。

想了想天要下雨,媳妇和娃都在外面。果断放弃分析一波的冲动,来Method Swizzling会更快。

看走眼的 Method Swizzling

之前看过这个Method Swizzling的各种姿势文章,印象深刻。

由于 ** Method Swizzling ** 风险不那么容易控制,所以并没用类簇相关的。

一顿cmd c / cmd v之后改了类名就扔了过去。当时大约这个样子:

1
2
3
4
5
6
7
8
- (BOOL)safe_hasPrefix:(NSString *)str {
if (str == nil) {
return YES;
} else {
// 此处会发生循环调用,是错误的示例
return [(NSString *)self hasPrefix:str];
}
}

正如我刚在上面的备注一样。

脑袋一热,这类簇会不会有别的坑,然后过分的自信忽略了safe_前缀,并写了个C的前缀比较:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (BOOL)safe_hasPrefix:(NSString *)str {
if (str == nil) {
return NO;
} else if (((NSString *)self).length < str.length) {
return NO;
} else {
// 此处逻辑并未经过很多的测试用例测试,只是临时修补。实际方案应该直接调用safe_hasPrefix
const char *str1 = [str cStringUsingEncoding:NSUTF8StringEncoding];
const char *str2 = [(NSString *)self cStringUsingEncoding:NSUTF8StringEncoding];

for (NSInteger idx = str.length; idx >= 0; idx--) {
if (str1[idx] != str2[idx]) {
return NO;
}
}
return YES;
}
}

一番梭哈之后,定位到了问题。

但是今天复盘的时候想到这个类簇的IMP正常的操作也不可能出现循环调用。。。。

然后发现昨天的一番梭哈真的是弱智。。惭愧惭愧。。。

写在最后

  • 解决问题的方案在于知识面的宽度。(一直为我没有通过First throw call stack直接定位而害羞)
  • 着急下班的时候别写太多逻辑,容易漏。要么别着急,要么万分小心。
  • 集成第三方SDK别瞎胡闹,使用代码直接集成(除非万不得已)。尽量使用CocoaPods这样的工具。
  • 代码中的 ** warning ** 记得消一下。成天999+ 容易漏掉关键信息。
  • 开发的第三方SDK一定要注意测试覆盖率。
  • swift大法好。swift不用!绝对不会有机会出现func hasPrefix(_ str: String) -> Bool中参数str传空的可能 :)