XNU的一些概念
XNU
XNU是mac os和iOS的系统内核,分为三个组成部分Mach,BSD和I/O Kit
其中Mach可以看做XNU的内核,也就是内核的内核.提供调度,IPC等基础服务.
BSD和I/O Kit是更高层,BSD提供网络,进程,文件服务等,I/O是设备驱动服务.
Task: 任务, 与进程的概念接近,但是包含更多的组成部分.有着虚拟的内存空间.有一个或多个线程.
Thread: 线程,CPU调度的最小单位.
Ports: 内核对象object基于Mach实现功能,通过Mach message来互相通信,Ports即端口,通信通道,通过相关API(端口权限)来进行访问,一个端口可以接收多个发送者的消息,单同一时刻只能接收一个.
另外地址空间,内存分页,虚拟地址也是由Mach来实现的.虚拟地址可以看这篇.
BSD
位于Mach上层,提供进程,安全,文件系统,网络连接,用户管理等.
I/O Kit
I/O即input和output,用于数据交换,I/O Kit就是封装了一些基础类,用于实现具体的设备驱动程序.
异常与信号的一些概念
异常都会伴随信号,信号得不到预定的处理,就会终止程序,这些信号可能来自内核也可能是应用本身的进程.
Mach异常
是内核(kernel)异常,开发者可以捕获这一层级的异常;
比如一些内存问题, EXC_BAD_ACCESS就是访问了坏地址
UNIX信号
BSD在Mach异常机制之上构建的UNIX信号处理机制。异常信号首先被转换为Mach异常,如果没有被外界捕捉,则会被默认的异常处理ux_exception()转换为UNIX信号。
比如还是上面的访问了坏地址,会被转换成SIGSEGV信号.
UNIX信号 对应的Mach异常
SIGFPE EXC_ARITHMETIC
SIGSEGV EXC_BAD_ACCESS
SIGBUS EXC_BAD_ACCESS
SIGILL EXC_BAD_INSTRUCTION
SIGTRAP EXC_BREAKPOINT
SIGEMT EXC_EMULATION
SIGSYS EXC_UNIX_BAD_SYSCALL
SIGPIPE EXC_UNIX_BAD_PIPE
SIGABRT EXC_CRASH
SIGKILL EXC_SOFT_SIGNAL
常见的UNIX信号:
SIGABRT是系统调用abort()发出的信号,主动调用也是一样的,也就是说异常的来源可能是NSException也可能是Mach.
SIGSEGV是访问坏地址,比如访问未分配给进程的内存,或者已经释放的内存.
在堆栈中,如果异常的地址是常规数值,比如6位到10位左右的0x00002333ffff这种,这种情况一般是内存已经被释放;
如果很小的地址或者空地址,可能是未初始化的空指针.
如果是极大的数值,如0x2333ffff44ff这种,则可能是野指针,在一些情况下,指针指向的地址和预想的不同,就可以认为是野指针,比如在线程安全问题中可能会遇到.
SIGBUS是总线异常,访问或者写入非法地址, 比如向只读权限的内存写入,或者内存对齐出现问题,比如一些字符串及字符操作.一般只会由系统函数或者其他一些C函数触发.
SIGSYS是非法的系统调用.
SIGTRAP通常是swift运行错误引起,最常见的是给一个非可选类型赋值nil,或者失败的强制类型转换,DispatchSourceTimer在suspend状态时被释放等.
另外调用编译器的trap相关函数也可以主动触发,比如设置断点以及其他一些debug操作.
NSException异常
应用层异常,是OC代码运行过程中的逻辑错误抛出的异常.可以用@trycatch捕获.
OC封装了具体的NSException类型,比如:
NSInvalidArgumentException 非法参数,向数组或者字典插入了nil.
NSRangeException 越界.
NSGenericException 迭代器异常,比如快速遍历的时候修改元素.
NSInternalInconsistencyException 类型不符异常,比如可变性不符,非Mutable调用Mutable的方法.
NSFileHandleOperationException 文件操作异常,比如内存不足.
还有KVO的相关异常,比如尝试移除未注册的observer,重复移除observer,keypath是nil等.
在非主线程更新UI.
OOM异常,内存不足.
消息动态决议的unrecognized selector等等.
函数调用栈
函数调用栈就是一个线程的函数调用回溯,首先来看看函数调用的过程.
举个例子
int func1(int x, int y){
int a;
int b;
int c = func2(&a, &b);
return c;
}
func1里面调用了func2.这种嵌套调用是非常常见的.
当调用func1时,func1入栈,虽然发生了嵌套,但是func1在栈中的布局是连续的,每个函数在栈中的布局都是类似的,这种布局叫做栈帧.
栈是从高地址向低地址填充的,首先一个函数栈帧的高位是栈底,
然后将函数内的局部变量放入栈中,但是入栈顺序与架构有关,可能是a在高位也可能是b在高位.
然后讲实参从右到左入栈,所以y先入栈,x在后面,y是高位.
最后栈顶是函数返回地址.
这张图显示了一个函数调用栈,最下面的是最先调用的,最上面的是最后调用的,运行的是objc4源码.
首先是在main函数,
然后调用了一个OC方法,转到objc_msgSend,
快速查找失败,调用_objc_msgSend_uncached,
然后进入慢速查找lookUpImpOrForward,
然后在类对象中查找SEL,getMethodNoSuper_nolock,
最后调用objc_class的methods()函数获取method_array_t.
不过这个例子不太适用于日常开发,因为iOS程序的函数调用栈,不会出现上面几个函数.
这是点击按钮的函数调用栈,这期间会多次执行objc_msgSend,但是并未出现.
objc_msgSend等直接由汇编实现的函数,直接嵌在调用它的函数的栈帧中,不会开辟新的栈帧,所以调用栈里看不到.
异常堆栈
异常堆栈就是程序抛出异常的时候,使用相关API可以获取到的此时的函数调用栈,展示程序最后的行为.
对应Mach异常,UNIX信号,NSException都有不同的API.
如何实现获取异常堆栈,本篇不做详述,以后再慢慢研究,相关的库与工具有很多.
dSYM
dSYM即debug symbols,其实是一个文件夹,可以显示包内容,有一个DWARF文件夹,里面是dsym文件.mach-o类型是MH_DSYM.
可以用MachOView打开
Commands段和Sections段都没啥内容,最重要的是符号表symbols.
在这里还可以看到一个UUID,同样项目app包里的对应可执行文件(位于xxx.xcarchive/products/applications/xxx/xxx)也有一个UUID,在archive时,他们生成的值是一样的.需要相等才能对应上.
函数在mach-o文件中是一类符号,符号表记录了符号在内存中的偏移量,在程序加载到内存中时,会被分配到一个基地址,根据偏移量把函数加载到对应的内存,因为是虚拟内存,所以对得上号.
这是bugly上的一个错误堆栈
首先能看到线程0,UNIX信号SIGTRAP
libdispatch.dylib 0x00000001afbd0e94 0x00000001afb96000 + 241300
XXXX 0x0000000102a2688c 0x000000010278c000 + 2730124
左边表示符号所在的Mach-O,通常都是项目里的crash,一般来自xxx.app,如果项目中有其他系统库或者动态库,也可能来自这些.
这里第一行是libdispatch.dylib的函数,第二行是xxx.app的函数.
如果是某些静态库比如项目的组件xxx1.framework,或者类似AFNetworking.framework这些里出现了crash,则会显示xxx1或者AFNetworking.
0x00000001afbd0e94是栈帧的栈底
0x00000001afb96000是mach-o的基地址,一次运行中,同一个mach-o的基地址是相同的.
241300是偏移量. 0x00000001afb96000 + 241300 = 0x00000001afbd0e94.
当然考虑到不同库,有的也可能没有栈帧的地址,需要计算一下.
符号化
获取异常堆栈的时候,只能拿到程序的基地址,函数栈帧的地址,和偏移量
现在,地址有了(堆栈),符号表也有了(dsym),就可以还原成函数调用栈了,这一步叫做符号化.把地址还原成符号.
可执行文件路径下 dwarfdump --uuid xxx 查看app可执行文件的uuid
dsym路径下 dwarfdump --uuid xx.app.dSYM 查看dsym的uuid
使用atos工具符号化
~ % atos -help
Usage: atos [-p pid] [-o executable/dSYM] [-f file-of-input-addresses] [-s slide | -l loadAddress | -offset] [-arch architecture] [-printHeader] [-fullPath] [-inlineFrames] [-d delimiter] [address ...]
-d/--delimiter delimiter when outputting inline frames. Defaults to newline.
--fullPath show full path to source file
-i/--inlineFrames display inlined functions
--offset treat all following addresses as offsets into the binary
符号化: atos -arch 架构类型 -o 路径 -l 基地址 栈帧地址
trigger@T Desktop % atos -arch arm64 -o AppName.app.dSYM -l 0x00000001045f4000 0x00000001046ac7ec
-[GLPlayEngine isForbiddenCoordWithCurrentBoard:] (in AppName) (GLPlayManager.m:188)
有些时候不能定位到具体的行数,文件和行显示类似
#0 Thread
SIGTRAP
0 GLDispatchTimer.__ivar_destroyer (in AppName) (GLDispatchTimer.swift:0)
1 GLDispatchTimer.__deallocating_deinit (in AppName) (<compiler-generated>:0)
1是较先执行的函数,compiler-generated表示编译器生成文件,这里是OC和Swift混编,
__deallocating_deinit是析构函数,它既不在项目代码里,也不在某个静态库/动态库中,而是由编译器生成.
0 是最后执行的函数,定位到0行说明异常不是出现在该文件这一层,而是Mach异常,这里的UNIX信号是SIGTRAP,
__ivar_destroyer和__deallocating_deinit函数,应该是析构的时候出现了问题,
swift代码出现SIGTRAP, 除了是非可选类型被赋值nil,还有一些情况如DispatchSourceTimer在suspend状态时被释放,失败的强制类型转换等
以这些为基础基本可以找到问题所在.