经过Signal handling(信号处理)获取任意线程调用栈

获取任意线程调用栈目前有两种方式。第一方式拿到栈的指针(StackPointer)以及栈帧指针(FramePointer),递归到栈底。html

系统提供了 task_threads 方法,能够获取到全部的线程,注意这里的线程是最底层的 mach 线程.git

对于每个线程,能够用 thread_get_state 方法获取它的全部信息,信息填充在 _STRUCT_MCONTEXT 类型的参数中(这个方法中有两个参数随着 CPU 架构的不一样而改变).github

咱们须要存储线程的StackPointer以及 顶部的FramePointer, 经过递归获取到整个调用栈.swift

根据栈帧的 Frame Pointer 获取到这个函数调用的符号名bash

实现思路:架构

  1. 获取线程的StackPointer 以及 FramePointer
  2. 找到FramePointer属于哪个镜像文件(.m)
  3. 获取镜像文件的符号表
  4. 在符号表中找到函数调用地址对应的符号名
  5. return 到上一级调用函数的FramePointer, 重复第2步
  6. 到达栈底, 退出

这种方式是KSCrash的做者想到的,他曾提过一个问题Printing a stack trace from another thread,不过最后他本身想出这种方式给解决了。bestswifter基于此写了BSBacktraceLogger,在OC中仍是很好用的,可是在Swift无法很好的打印出结果,不知道为何,有知道的还但愿能告知一下。异步

在这个提问下Printing a stack trace from another thread,有人经过Signal handling实现了。函数

Signal

这里介绍一下大体须要了解的知识点。测试

信号的本质
是软件层次上对中断的一种模拟。它是一种异步通讯的处理机制,事实上,进程并不知道信号什么时候到来。ui

信号来源:

  1. 程序错误,如非法访问内存
  2. 外部信号,如按下了CTRL+C
  3. 经过kill或sigqueue向另一个进程发送信号

信号处理函数的过程

  1. 注册信号处理函数 信号的处理是由内核来代理的,首先程序经过sigal或sigaction函数为每一个信号注册处理函数,而内核中维护一张信号向量表,对应信号处理机制。这样,在信号在进程中注销完毕以后,会调用相应的处理函数进行处理。
  2. 信号的检测与响应时机
  3. 处理过程

基本的信号处理函数

信号操做最经常使用的方法是信号的屏蔽,信号屏蔽主要用到如下几个函数:

int sigemptyset(sigset_t *set): 函数初始化信号集set并将set设置为空

int sigfillset(sigset_t *set):函数初始化信号集,但将信号集set设置为全部信号的集合。

int sigaddset(sigset_t *set,int signo):将信号signo加入到信号集中去

int sigdelset(sigset_t *set,int signo):从信号集中删除signo信号。

int sigismemeber(sigset_t* set,int signo):检测信号是否被挂起。

int sigprocmask(int how,const sigset_t*set,sigset_t *oset):将指定的信号集合加入到进程的信号阻塞集合中去。若是提供了oset,那么当前的信号阻塞集合将会保存到oset集全中去。

对于信号集的初始化有两种方法: 一种是用sigemptyset使信号集中不包含任何信号,而后用sigaddset把信号加入到信号集中去。 另外一种是用sigfillset让信号集中包含全部信号,而后用sigdelset删除信号来初始化。

实现思路

1.经过sigaction注册信号处理函数

private func setupCallStackSignalHandler() {
    let action = __sigaction_u(__sa_sigaction: signalHandler)
    var sigActionNew = sigaction(__sigaction_u: action, sa_mask: sigset_t(), sa_flags: SA_SIGINFO)

    if sigaction(SIGUSR2, &sigActionNew, nil) != 0 {
        return
    }
}

private func signalHandler(code: Int32, info: UnsafeMutablePointer<__siginfo>?, uap: UnsafeMutableRawPointer?) -> Void {
    guard pthread_self() == targetThread else {
        return
    }

    callstack = frame()
}
复制代码

2.经过pthread_kill()向指定线程发送某个信号

if pthread_kill(threadId, SIGUSR2) != 0 {
     return nil
}

复制代码

3.在信号处理函数中经过backtrace得到函数调用栈(也能够使用NSThread.callstackSymbols)
4. 而后遍历经过dladdr得到某个地址符号信息
5. 使用swift_demangle函数进行符号名重整,这个是Swift特有的,能够看看Swift Name Mangling

6.用sigfillset让信号集中包含全部信号,而后用sigdelset删除信号来初始化

var mask = sigset_t()
sigfillset(&mask)
sigdelset(&mask, SIGUSR2)
复制代码

3,4,5的代码比较多,我就不贴了,能够看这里backtrace-swift,纯Swift写的,代码也不是不少。

测试效果

注意在Xcode的时候,由于Xcode屏蔽了signal的回调,咱们须要在lldb中输入如下命令,signal的回调就能够进来了

pro hand -p true -s false SIGUSR2
复制代码

Screen Shot 2019-08-19 at 10.34.25 PM.png

参考:

Getting a backtrace of other thread
Synchronization issue with usage of pthread_kill() to terminate thread blocked for I/O
Printing a stack trace from another thread
获取任意线程调用栈的那些事

相关文章
相关标签/搜索