前言
关于 Crash report 的符号化问题,网上能搜到很多教程,在通常情况下能够满足要求,在此不在赘述。 只想说一下奇葩的情况,比如我们公司这样自己收集 Crash report 的。
最近在做与 Crash report 相关的工作,趁机整理一下遇到的问题。
加载地址
在符号化 Crash report 的过程中,如果用 atos,具体的命令是这样的:
atos [-o executable] [-l loadAddress] [-arch architecture] [address ...]
iOS 为了防止破解保系统序安全,程序每次执行时的加载地址是随机的。应该从哪找这个加载地址呢?
正常的做法是从 Crash report 中的 Binary Images 这个段中来找,比如:
Binary Images:
0x100058000 - 0x10006bfff TheElements arm64 <77b672e2b9f53b0f95adbc4f68cb80d6> /var/mobile/Containers/Bundle/Application/CB86658C-F349-4C7A-B73B-CE3B4502D5A4/TheElements.app/TheElements
...
注:例子来源于 Apple 官方文档,下同。
意思就是从 0x100058000 到 0x10006bfff 这段地址空间是 “TheElements” 这个二进制镜像的,所以应用程序的加载地址就是 0x100058000。
上述情况是针对 iOS 系统产生的 Crash report,由于我们自己收集的 report 中并没有 “Binary Images”,所以这个方法就失效了,只能另辟蹊径。
先看一下堆栈:(再精简的 Crash report 也应该有堆栈吧……)
0 TheElements 0x00000001000effdc 0x1000e4000 + 49116
1 UIKit 0x000000018ca5c2ec -[UIViewAnimationState sendDelegateAnimationDidStop:finished:] + 184
2 UIKit 0x000000018ca5c1f4 -[UIViewAnimationState animationDidStop:finished:] + 100
3 QuartzCore 0x000000018c380f60 CA::Layer::run_animation_callbacks(void*) + 292
4 libdispatch.dylib 0x0000000198fb9368 _dispatch_client_callout + 12
5 libdispatch.dylib 0x0000000198fbd97c _dispatch_main_queue_callback_4CF + 928
6 CoreFoundation 0x000000018822dfa0 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 8
7 CoreFoundation 0x000000018822c048 __CFRunLoopRun + 1488
8 CoreFoundation 0x00000001881590a0 CFRunLoopRunSpecific + 392
9 GraphicsServices 0x00000001912fb5a0 GSEventRunModal + 164
10 UIKit 0x000000018ca8aaa0 UIApplicationMain + 1484
11 TheElements 0x00000001000e9800 0x1000e4000 + 22528
12 libdyld.dylib 0x0000000198fe2a04 start + 0
注意包含 “TheElements” 的条目,后面的偏移量全都是 0x1000e4000 + XXX
的形式,这个 “0x1000e4000” 就是 “TheElements” 的加载地址。
网上能搜到的资料基本上就到此为止了,但是在实践中发现了一个问题:上面的 Crash report 是在 release 版本中产生的,没有包涵任何的调试符号,如果是在 Debug 模式下,也就是包含了调试符号之后,生成的 Crash Report 是这样的:
...
24 GraphicsServices 0x18fa2b6fc GSEventRunModal + 168
25 UIKit 0x18add2fac UIApplicationMain + 1488
26 TheElements 0x1000fd2f4 main (main.m:55)
27 libdyld.dylib 0x198176a08 start + 4
不再是“加载地址+偏移量”的模式了,变成了“符号+偏移量”,无法从这样的堆栈信息里找到加载地址。
那么为什么会出现这样的行为呢?
很遗憾,水平有限,没搜到官方的文档说明,只能从源码入手了。
在 iOS 中,常用的获取堆栈的方法是 [NSThread callStackReturnAddresses]
,在官方文档里说这个方法返回的堆栈信息跟 backtrace_symbols
函数一样,这个函数的实现在 Libc 的 gen/backtrace.c 文件内,关键代码如下:
// ...
if (info->dli_sname) {
symbol = info->dli_sname;
symbol_offset = (uintptr_t)addr - (uintptr_t)info->dli_saddr;
} else if(info->dli_fname) {
symbol = image;
symbol_offset = (uintptr_t)addr - (uintptr_t)info->dli_fbase;
} else if(0 < snprintf(symbuf, sizeof(symbuf), "0x%lx", (uintptr_t)info->dli_saddr)) {
symbol = symbuf;
symbol_offset = (uintptr_t)addr - (uintptr_t)info->dli_saddr;
} else {
symbol_offset = (uintptr_t)addr;
}
// ...
这段代码在判断堆栈地址显示的的格式,其中,info
是 Dl_info
类型的,定义在 dlfcn.h 文件内:
typedef struct dl_info {
const char *dli_fname; /* Pathname of shared object */
void *dli_fbase; /* Base address of shared object */
const char *dli_sname; /* Name of nearest symbol */
void *dli_saddr; /* Address of nearest symbol */
} Dl_info;
因此上面的代码就好理解了,当 dli_sname
(也就是最近的符号)存在的时候,就用 “最近的符号+与最近符号的偏移量” 这种格式,当 dli_sname
不存在时(也就是在非 Debug 模式下),就用 “二进制加载地址+与加载地址偏移量” 的格式。
至此,上面的问题就有了答案。
仅凭堆栈信息,是不能够在 Debug 模式下取得加载地址的, 需要在 Crash report 增加额外的字段,获取加载地址的方法如下:
#include <mach-o/dyld.h>
uintptr_t get_load_address(void) {
const struct mach_header *exe_header = NULL;
for (uint32_t i = 0; i < _dyld_image_count(); i++) {
const struct mach_header *header = _dyld_get_image_header(i);
if (header->filetype == MH_EXECUTE) {
exe_header = header;
break;
}
}
return exe_header;
}
dSYM 文件
符号化 Crash report 另一个关键点是要找到对应的 dSYM 文件,这个 dSYM 文件其实就是编译时,给编译器加 -g
选项产生的带有调试符号的二进制文件(在 OS X/iOS 上也就是 Mach-O 文件)。可以用 file 以及 otool 命令查看一下:
~|⇒ file Test
Test: Mach-O 64-bit dSYM companion file x86_64
~|⇒ otool -hV Test
Test:
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
MH_MAGIC_64 X86_64 ALL 0x00 DSYM 7 4408 0x00000000
用 otool -lV
命令可以看到里面有一些叫 “__DWARF” 的段,里面就是各种调试信息。现在的 OS X/iOS 系统中调试信息是用一个叫 “DWARF” 的格式存储的,这是一个调试文件格式的标准,详见http://dwarfstd.org 。
UUID
可执行文件与 dSYM 文件是通过 UUID 来关联的,在 Mach-O 文件的 Load Command 结构里有个叫 “uuid_command” 的字段,用来存储这个二进制文件的 UUID,在编译时设定,与他对应的 dSYM 文件具有相同的 UUID。
可以用 otool 或者 dwarfdump 命令查看,比如:
~|⇒ dwarfdump -u Test
UUID: DBEC12D1-9A61-33DF-BC39-E2ED2CB1D8F1 (x86_64) Test
~|⇒ otool -lV Test | grep uuid
uuid DBEC12D1-9A61-33DF-BC39-E2ED2CB1D8F1
当然,也可以在程序运行中通过代码获取,比如:(代码来源于http://stackoverflow.com/a/10121277/2978270)
#import <mach-o/ldsyms.h>
NSString *executableUUID()
{
const uint8_t *command = (const uint8_t *)(&_mh_execute_header + 1);
for (uint32_t idx = 0; idx < _mh_execute_header.ncmds; ++idx) {
if (((const struct load_command *)command)->cmd == LC_UUID) {
command += sizeof(struct load_command);
return [NSString stringWithFormat:@"%02X%02X%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X%02X%02X%02X%02X",
command[0], command[1], command[2], command[3],
command[4], command[5],
command[6], command[7],
command[8], command[9],
command[10], command[11], command[12], command[13], command[14], command[15]];
} else {
command += ((const struct load_command *)command)->cmdsize;
}
}
return nil;
}
更新
根据 Apple 员工写的一篇文章《Apple's "Lazy" DWARF Scheme》来看,链接时,会调用一个叫 dsymutil 的工具,把 .o 文件里的调试信息提取出到 dSYM 文件里,并且把 dSYM 文件的 UUID 设置成跟编译结果的 Mach-O 文件一致,方便查找。
也可以手工生成 dSYM 文件:
$dsymutil YourAPP -o YourAPP.dSYM
当然,前提是 YourAPP 中有调试符号。
最后
写了一个符号化崩溃日志的 Mac App - SYM,欢迎使用~