獲取任意線程調用棧目前有兩種方式。第一方式拿到棧的指針(StackPointer)以及棧幀指針(FramePointer),遞歸到棧底。
系統提供了 task_threads 方法,可以獲取到所有的線程,注意這里的線程是最底層的 mach 線程.
對于每一個線程,可以用 thread_get_state 方法獲取它的所有信息,信息填充在 _STRUCT_MCONTEXT 類型的參數中(這個方法中有兩個參數隨著 CPU 架構的不同而改變).
我們需要存儲線程的StackPointer以及 頂部的FramePointer, 通過遞歸獲取到整個調用棧.
根據棧幀的 Frame Pointer 獲取到這個函數調用的符號名
實現思路:
- 獲取線程的StackPointer 以及 FramePointer
- 找到FramePointer屬于哪一個鏡像文件(.m)
- 獲取鏡像文件的符號表
- 在符號表中找到函數調用地址對應的符號名
- return 到上一級調用函數的FramePointer, 重復第2步
- 到達棧底, 退出
這種方式是KSCrash的作者想到的,他曾提過一個問題Printing a stack trace from another thread,不過最后他自己想出這種方式給解決了。bestswifter基于此寫了BSBacktraceLogger,在OC中還是很好用的,但是在Swift沒法很好的打印出結果,不知道為什么,有知道的還希望能告知一下。
在這個提問下Printing a stack trace from another thread,有人通過Signal handling實現了。
Signal
這里介紹一下大致需要了解的知識點。
信號的本質
是軟件層次上對中斷的一種模擬。它是一種異步通信的處理機制,事實上,進程并不知道信號何時到來。
信號來源:
- 程序錯誤,如非法訪問內存
- 外部信號,如按下了CTRL+C
- 通過kill或sigqueue向另外一個進程發送信號
信號處理函數的過程
- 注冊信號處理函數
信號的處理是由內核來代理的,首先程序通過sigal或sigaction函數為每個信號注冊處理函數,而內核中維護一張信號向量表,對應信號處理機制。這樣,在信號在進程中注銷完畢之后,會調用相應的處理函數進行處理。 - 信號的檢測與響應時機
- 處理過程
基本的信號處理函數
信號操作最常用的方法是信號的屏蔽,信號屏蔽主要用到以下幾個函數:
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)
- 然后遍歷通過dladdr獲得某個地址符號信息
- 使用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
參考:
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
獲取任意線程調用棧的那些事