最近在写一些东西需要获取任意线程调用栈,然后看了现有的一些开源框架,写的比较复杂而且对Swift的支持不是很好,所以写了RCBacktrace。
ARM几种通用寄存器
ARM有15种通用寄存器,但是其实有些通用寄存器是有特殊用途的,PCS(Procedure Call Standard for Arm architecture)就定义了过程调用中,寄存器的特殊用途。
r15:PC The Program Counter,也称作程序计数器PC,指令寄存器保存的是下一条将要执行的指令的内存地址。
r14:LR The Link Register,也称作子程序连接寄存器(Subroutine Link Register)即连接寄存器LR,LR寄存器则保存着最后一次函数调用指令的下一条指令的内存地址,即保存了返回地址。
r13:SP The Stack Pointer,堆栈指针,sp寄存器在任意时刻会保存我们栈顶的地址。
r12:IP The Intra-Procedure-call scratch register,可简单的认为暂存SP。
实际上,还有一个r11是optional的,被称为FP,即frame pointer,某些时刻我们利用它保存栈底的地址。在arm64中LR是x30寄存器,FP是x29寄存器。
ARM的栈帧
每个线程都有自己的栈空间,线程中会有很多函数调用,每个函数调用都有自己的stack frame栈帧,栈就是由一个一个栈帧组成。
下面这个是ARM的栈帧布局图:
main stack frame为调用函数的栈帧,func1 stack frame为当前函数(被调用者)的栈帧,栈底在高地址,栈向下增长。图中FP就是栈基址,它指向函数的栈帧起始地址;SP则是函数的栈指针,它指向栈顶的位置。ARM压栈的顺序很是规矩,依次为当前函数指针PC、返回指针LR、栈指针SP、栈基址FP、传入参数个数及指针、本地变量和临时变量。如果函数准备调用另一个函数,跳转之前临时变量区先要保存另一个函数的参数。
backtrace
从上图我们可以看到当前栈帧中FP的值存储的是上一个栈帧的FP地址。拿到本函数的FP寄存器,所指示的栈地址,出栈,就能得到调用函数的LR寄存器的值,然后就能通过dynsym动态链接表,找到对应的函数名。
void **currentFramePointer = (void **)machineContext.__ss.__framePointer;
while (i < maxSymbols) {
void **previousFramePointer = *currentFramePointer;
if (!previousFramePointer) break;
stack[i] = *(currentFramePointer+1);
currentFramePointer = previousFramePointer;
++i;
}
线程执行状态
上面我们可以看到拿到某个线程的LR和FP寄存器就能进行backtrace,那怎么拿到呢?
Thread是对pthread的封装,在Foundation/Thread.swift,可以看到用pthread封装Thread的详细代码。
不同的操作会设计自己的线程模型, 所以底层 API 是不相同的, 但是 POSIX提供的pthread就是相当于对底层进行了一次封装, 让不同平台运行得到相同的效果.
Unix 系统提供的 thread_get_state 和 task_threads 等方法,操作的都是内核线程,每个内核线程由 thread_t 类型的 id 来唯一标识,pthread 的唯一标识是 pthread_t 类型。
内核线程和 pthread 的转换(也即是 thread_t 和 pthread_t 互转)很容易,因为 pthread 诞生的目的就是为了抽象内核线程。
_STRUCT_MCONTEXT
类型的结构体中,存储了当前线程的SP和最顶部栈帧的FP,_STRUCT_MCONTEXT
在不同平台上的结构不同,如:
ARM64,如iPhone 5s
_STRUCT_MCONTEXT64
{
_STRUCT_ARM_EXCEPTION_STATE64 __es;
_STRUCT_ARM_THREAD_STATE64 __ss;
_STRUCT_ARM_NEON_STATE64 __ns;
};
_STRUCT_ARM_THREAD_STATE64
{
__uint64_t __x[29]; /* General purpose registers x0-x28 */
__uint64_t __fp; /* Frame pointer x29 */
__uint64_t __lr; /* Link register x30 */
__uint64_t __sp; /* Stack pointer x31 */
__uint64_t __pc; /* Program counter */
__uint32_t __cpsr; /* Current program status register */
__uint32_t __pad; /* Same size for 32-bit or 64-bit clients */
};
有了thread_t和_STRUCT_MCONTEXT就可以通过thread_get_state
获得线程的FP和SP等。
_STRUCT_MCONTEXT machineContext;
mach_msg_type_number_t stateCount = THREAD_STATE_COUNT;
kern_return_t kret = thread_get_state(thread, THREAD_STATE_FLAVOR, (thread_state_t)&(machineContext.__ss), &stateCount);
dladdr获取某个地址的符号信息
接着就可以通过dladdr函数和Dl_info获得某个地址的符号信息
extern int dladdr(const void *, Dl_info *);
/*
* Structure filled in by dladdr().
*/
public struct dl_info {
public var dli_fname: UnsafePointer<Int8>! /* Pathname of shared object */
public var dli_fbase: UnsafeMutableRawPointer! /* Base address of shared object */
public var dli_sname: UnsafePointer<Int8>! /* Name of nearest symbol */
public var dli_saddr: UnsafeMutableRawPointer! /* Address of nearest symbol */
public init()
public init(dli_fname: UnsafePointer<Int8>!, dli_fbase: UnsafeMutableRawPointer!, dli_sname: UnsafePointer<Int8>!, dli_saddr: UnsafeMutableRawPointer!)
}
Swift命名重整
OC方法没有问题,因为重整规则比较简单,就是符号前加了一个'_',但是Swift的命名重整比较复杂,所以方法经过命名重整很难辨认,如下:
$s15RCBacktraceDemo14ViewControllerC3baryyF
所以我们需要调用swift_demangle
对重整过的符号进行还原,所以还原成原本的样子后如下:
RCBacktraceDemo.ViewController.bar() -> ()
更详细的Swift的命名重整可以看Friday Q&A 2014-08-08: Swift Name Mangling。
参考文章
ARM FP寄存器及frame pointer介绍
iOS中线程Call Stack的捕获和解析(一)
ARM函数调用过程分析
Friday Q&A 2014-08-08: Swift Name Mangling
获取任意线程调用栈的那些事