1. 程式人生 > IOS開發 >iOS獲取任意執行緒呼叫棧

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
獲取任意執行緒呼叫棧的那些事