案例解析:一个符号引起的Crash

分享一个跟符号链接相关的Crash,一个小问题,但是开发没有相关意识,甚至也没自测就上线了。虽然听着是有点离谱,但确实发生了也挺合(li)理(pu)的。

某次应用灰度期间出现了一个Crash,编译链接正常,运行时Crash:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SIGABRT:
0 dyld ___abort_with_payload + 8
1 dyld _abort_with_payload_wrapper_internal + 104
2 dyld _dyldVersionString + 0
3 dyld dyld4::halt(char const*, dyld4::StructuredError const*) + 300
4 dyld dyld4::APIs::_dyld_dlsym_blocked() + 0
5 App -[KKMagicalManager handleResult:] (KKMagicalManager.mm:407)
6 App __41-[KKMagicalManager seek:completion:]_block_invoke (KKMagicalManager.mm:626)
7 libdispatch.dylib __dispatch_call_block_and_release + 32
8 libdispatch.dylib __dispatch_client_callout + 20
9 libdispatch.dylib __dispatch_lane_serial_drain + 744
10 libdispatch.dylib __dispatch_lane_invoke + 380
11 libdispatch.dylib __dispatch_root_queue_drain_deferred_wlh + 288
12 libdispatch.dylib __dispatch_workloop_worker_thread + 540
13 libsystem_pthread.dylib __pthread_wqthread + 288

Crash原因比较明确,就是运行时符号缺失,dyld接管后直接abort了。

Crash行的源码很简单,伪代码如下:

1
2
3
4
5
6
7
- (NSError *)handleResult:(NSInterger)code {
// ..
// KKMagicalManager.mm:626
NSError *error = KKMagicalMakeError(code, @"");

return error;
}

搜相关函数,其实是有实现的:

1
2
3
4
5
6
7
8
9
10
11
12
// KKType.h
NSError *KKMagicalMakeError(NSInteger code, NSString *fmt, ...) NS_FORMAT_FUNCTION(2, 3);

// KKType.m
NSError *_KKMagicalMakeError(NSInteger code, NSString *fmt, ...) {
va_list args;
va_start(args, fmt);
NSString *message = [[NSString alloc] initWithFormat:fmt arguments:args];
va_end(args);

return [NSError errorWithDomain:@"abc" code:code userInfo:@{}];
}

灰度版本该函数确实有改动,原实现伪代码如下:

1
2
3
4
5
6
7
8
9
// KKType.h
static NSError *_KKMagicalMakeError(NSInteger code, NSString *fmt, ...) {
va_list args;
va_start(args, fmt);
NSString *message = [[NSString alloc] initWithFormat:fmt arguments:args];
va_end(args);

return [NSError errorWithDomain:@"abc" code:code userInfo:@{}];
}

看出是什么问题没,运行期怎么就找不到符号了呢?

查看MachO的符号,如下

1
2
3
4
5
6
7
8
% nm App | grep KKMagicalVoiceMakeError
0000000000f18fd8 T _KKMagicalVoiceMakeError
0000000007c0a068 t _KKMagicalVoiceMakeError.island
U __Z23KKMagicalVoiceMakeErrorlP8NSStringz
0000000007c6c9bc t __Z23KKMagicalVoiceMakeErrorlP8NSStringz.island
jason@JASONZXCHEN-MB2 QQKSong.app % echo __Z23KKMagicalVoiceMakeErrorlP8NSStringz | c++filt
KKMagicalVoiceMakeError(long, NSString*, ...)
jason@JASONZXCHEN-MB2 QQKSong.app %

明显_KSMagicalVoiceMakeError符号是存在的!那导致Crash的找不到的符号是什么呢?
很容易发现,上面nm输出中,确实有一个未定义的符号:
U __Z23KKMagicalVoiceMakeErrorlP8NSStringz
这个是经过name mangling的符号,demangle后是KKMagicalVoiceMakeError(long, NSString*, ...)。这个跟函数声明都是一致的,看着好像也都没什么毛病。

等等,你肯定会想到了:
1、这里一个函数,为什么会生成了两个符号?
2、为什么发生了name mangling?
3、历史版本为什么没问题?

我们回头看灰度版本修改的实现:

1
2
3
4
5
6
7
8
9
10
11
12
// KKType.h
NSError *KKMagicalMakeError(NSInteger code, NSString *fmt, ...) NS_FORMAT_FUNCTION(2, 3);

// KKType.m
NSError *_KKMagicalMakeError(NSInteger code, NSString *fmt, ...) {
va_list args;
va_start(args, fmt);
NSString *message = [[NSString alloc] initWithFormat:fmt arguments:args];
va_end(args);

return [NSError errorWithDomain:@"abc" code:code userInfo:@{}];
}

name mangling是C++的典型的特性,主要是为了支持函数重载、命名空间等特性。而C通常不会发生name mangling。因此可以推测那就是这个函数是以C++的规则进行了编译。

经过简单确认,可以发现,_KKMagicalMakeError这个函数有多处调用,有部分调用入口是.mm的文件中。这样很容易实锤了。回答上面的问题。
首先,因为.mm文件中通过引入KKType.h文件引用了该函数,因此编译时,.h声明的函数以C++的规则进行了编译,也就发生了name mangling;而KKType.m中函数_KKMagicalMakeError却是以C的规则进行编译,因此最终生成了两个符号。.h声明的符号实际上并没有对应的实现,因此对应符号在链接后成了一个Undefined的符号。
而历史实现,直接在.h文件中将该函数声明为static并实现,因此符号是统一的。

修改方案就很简单,使用 extern “C”,这也是C/C++混编经常使用的、用来指定相关函数要以C的方式进行编译。

1
2
3
4
5
6
7
8
9
#if __cplusplus
extern "C" {
#endif

NSError *_KSMagicalVoiceError(NSInteger code, NSString *fmt, ...) NS_FORMAT_FUNCTION(2, 3);

#if __cplusplus
} //Extern C
#endif

評論