經過Signal handling(信號處理)獲取任意線程調用棧

獲取任意線程調用棧目前有兩種方式。第一方式拿到棧的指針(StackPointer)以及棧幀指針(FramePointer),遞歸到棧底。html

系統提供了 task_threads 方法,能夠獲取到全部的線程,注意這裏的線程是最底層的 mach 線程.git

對於每個線程,能夠用 thread_get_state 方法獲取它的全部信息,信息填充在 _STRUCT_MCONTEXT 類型的參數中(這個方法中有兩個參數隨着 CPU 架構的不一樣而改變).github

咱們須要存儲線程的StackPointer以及 頂部的FramePointer, 經過遞歸獲取到整個調用棧.swift

根據棧幀的 Frame Pointer 獲取到這個函數調用的符號名bash

實現思路:markdown

  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

這裏介紹一下大體須要瞭解的知識點。函數

信號的本質
是軟件層次上對中斷的一種模擬。它是一種異步通訊的處理機制,事實上,進程並不知道信號什麼時候到來。oop

信號來源:

  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
獲取任意線程調用棧的那些事

相關文章
相關標籤/搜索