本文讲述的是符号化“残破”的栈,如果你有一个系统生成的crash日志,请交给Xcode自带的symbolicatecrash脚本。
Symbolicatecrash脚本的核心也是通过atos功能逐行符号化,但人家封装好了,比自己手动一行一行做快很多。
示例栈:
0 XSQSymbolicateDemo 0x00000001000ba530 XSQSymbolicateDemo + 25904
1 XSQSymbolicateDemo 0x00000001000ba4f0 XSQSymbolicateDemo + 25840
2 XSQSymbolicateDemo 0x00000001000ba4bc XSQSymbolicateDemo + 25788
3 XSQSymbolicateDemo 0x00000001000ba478 XSQSymbolicateDemo + 25720
4 UIKit 0x00000001966870ec <redacted> + 96
5 UIKit 0x000000019668706c <redacted> + 80
6 UIKit 0x00000001966715e0 <redacted> + 440
7 UIKit 0x0000000196686950 <redacted> + 576
8 UIKit 0x000000019668646c <redacted> + 2480
9 UIKit 0x0000000196681804 <redacted> + 3192
10 UIKit 0x0000000196652418 <redacted> + 340
11 UIKit 0x0000000196e4bf64 <redacted> + 2400
这是我写的一个demo app,并且在编译后期滤去了符号表,所以仅能看到一些奇怪的地址。
如何符号化第三方app内的符号
以第一行:
0 XSQSymbolicateDemo 0x00000001000ba530 XSQSymbolicateDemo + 25904
为例
需要条件:
(1)atos工具(Xcode安装时一般会自带)
(2)确认app运行的架构(armv7、arm64)
(3)app对应的dSYM文件(出包时获得)
(4)app代码载入到内存的基地址(后文详细介绍)
方法:
在命令行中输入:
xcrun atos -arch arm64 -o ./XSQSymbolicateDemo.app.dSYM/Contents/Resources/DWARF/XSQSymbolicateDemo -l 0x1000b4000 0x00000001000ba530
即可得到符号化后的结果:
-[ViewController helloWorld2] (in XSQSymbolicateDemo) (ViewController.m:100)
如何符号化系统动态库中的符号
以这一行为例:
4 UIKit 0x00000001966870ec <redacted> + 96
需要条件:
(1)atos工具(Xcode安装时一般会自带)
(2)确认app运行的架构(armv7、arm64)
(2)该OS版本、该动态库的符号文件(将该手机连接到电脑的Xcode上,会自动同步系统符号文件)
(3)该动态库载入到内存的基地址(后文详细介绍)
方法:
在命令行中输入:
xcrun atos -arch arm64 -o ~/Library/Developer/Xcode/iOS\ DeviceSupport/10.3.1\ \(14E304\)/Symbols/System/Library/Frameworks/UIKit.framework/UIKit -l 0x196642000 0x00000001966870ec
-[UIApplication sendAction:to:from:forEvent:] (in UIKit) + 96
即可得到符号化后的结果:
-[UIApplication sendAction:to:from:forEvent:] (in UIKit) + 96
如何获取基地址
注意:基地址在进程每次启动时决定,所以重启进程后,符号化时必须使用当次启动的基地址
方案一:从iOS生成的crash日志中获取
在iOS系统生成的crash日志中的下半部分,有这样的一些信息:
蓝色框圈出来的部分,即为app代码载入到内存的基地址
红色框圈出来的部分,即为各个动态库载入到内存的基地址
方案二:在app运行时打印
可以在app中调用如下代码获取各个image的基地址:
void printAllImage()
{
for (int i = 0; i < _dyld_image_count(); i++) {
char *image_name = (char *)_dyld_get_image_name(i);
const struct mach_header *mh = _dyld_get_image_header(i);
intptr_t vmaddr_slide = _dyld_get_image_vmaddr_slide(i);
NSLog(@"Image name %s at address 0x%llx and ASLR slide 0x%lx.\n",
image_name, (mach_vm_address_t)mh, vmaddr_slide);
}
}
得到如下输出:
Image name /var/containers/Bundle/Application/351121C8-CFE4-49AD-ACC0-A70C5BF1C4A6/XSQSymbolicateDemo.app/XSQSymbolicateDemo at address 0x1000b4000 and ASLR slide 0xb4000.
Image name /System/Library/Frameworks/Foundation.framework/Foundation at address 0x190f0c000 and ASLR slide 0xeedc000.
Image name /usr/lib/libarchive.2.dylib at address 0x190ee0000 and ASLR slide 0xeedc000.
Image name /usr/lib/libbz2.1.0.dylib at address 0x190e9e000 and ASLR slide 0xeedc000.
Image name /usr/lib/libSystem.B.dylib at address 0x18ef04000 and ASLR slide 0xeedc000.
Image name /usr/lib/system/libcache.dylib at address 0x18f35a000 and ASLR slide 0xeedc000.
......
可以看到第一行代表的是app自身,之后的每一行是app载入的动态库们。
介绍加载和ASLR
大致理解:
在进程启动的时候,内核加载器或者dyld会将指令加载到内存中。
ASLR全名Address Space Layout Randomization,地址空间布局随机化,用于防范恶意程序对已知地址进行攻击
在ASLR引入之前,由于加载的规则是固定的,所以理论上,一个进程不管重启多少次,每条指令对应的内存中的地址都是一样的。而每条指令对应到内存中的哪个地址,可以通过分析Mach-O文件分析出来。这就容易产生安全漏洞。
ASLR引入后,在进程启动前期的加载阶段,会生成一个随机数offset,让加载形成的内存整体偏移一个offset。
这样一个进程多次启动,每次行程的内存空间布局都不完全一致。同一个指令,经过多次启动,每次都会被布局到一个新计算出来的地址。
所以仅仅凭借“一个指令在内存中的地址”和dSYM文件,是无法进行符号化的,因为这个“地址”同时依赖于ASLR生成的offset。
我理解其实只需要一个offset,配合已知的架构、加载方式等信息,应该就能推测出app自身的基地址和各个库的基地址。尝试后也证明,各个库的基地址-offset后的值在同个设备的多次启动上是一致的。
但是为了图省事,还是自己打印一下所有库的基地址吧(´・ω・`)