iOS获取任意线程调用栈

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容