iOS获取任意线程调用栈

最近在写一些东西须要获取任意线程调用栈,而后看了现有的一些开源框架,写的比较复杂并且对Swift的支持不是很好,因此写了RCBacktracehtml

ARM几种通用寄存器

ARM有15种通用寄存器,可是其实有些通用寄存器是有特殊用途的,PCS(Procedure Call Standard for Arm architecture)就定义了过程调用中,寄存器的特殊用途。linux

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。git

实际上,还有一个r11是optional的,被称为FP,即frame pointer,某些时刻咱们利用它保存栈底的地址。在arm64中LR是x30寄存器,FP是x29寄存器。github

ARM的栈帧

每一个线程都有本身的栈空间,线程中会有不少函数调用,每一个函数调用都有本身的stack frame栈帧,栈就是由一个一个栈帧组成。swift

下面这个是ARM的栈帧布局图: bash

130320150468341.png

main stack frame为调用函数的栈帧,func1 stack frame为当前函数(被调用者)的栈帧,栈底在高地址,栈向下增加。图中FP就是栈基址,它指向函数的栈帧起始地址;SP则是函数的栈指针,它指向栈顶的位置。ARM压栈的顺序非常规矩,依次为当前函数指针PC、返回指针LR、栈指针SP、栈基址FP、传入参数个数及指针、本地变量和临时变量。若是函数准备调用另外一个函数,跳转以前临时变量区先要保存另外一个函数的参数。app

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
获取任意线程调用栈的那些事

相关文章
相关标签/搜索