iOS获取任意线程调用栈

6,222 阅读4分钟

最近在写一些东西需要获取任意线程调用栈,然后看了现有的一些开源框架,写的比较复杂而且对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的栈帧布局图:

130320150468341.png

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