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

4,503 阅读4分钟

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

系统提供了 task_threads 方法,可以获取到所有的线程,注意这里的线程是最底层的 mach 线程.

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

我们需要存储线程的StackPointer以及 顶部的FramePointer, 通过递归获取到整个调用栈.

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

实现思路:

  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

这里介绍一下大致需要了解的知识点。

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

信号来源:

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