iOS獲取任意線程調用棧

最近在寫一些東西須要獲取任意線程調用棧,而後看了現有的一些開源框架,寫的比較複雜並且對Swift的支持不是很好,因此寫了RCBacktracehtml

ARM幾種通用寄存器

ARM有15種通用寄存器,可是其實有些通用寄存器是有特殊用途的,PCS(Procedure Call Standard for Arm architecture)就定義了過程調用中,寄存器的特殊用途。linux

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。git

實際上,還有一個r11是optional的,被稱爲FP,即frame pointer,某些時刻咱們利用它保存棧底的地址。在arm64中LR是x30寄存器,FP是x29寄存器。github

ARM的棧幀

每一個線程都有本身的棧空間,線程中會有不少函數調用,每一個函數調用都有本身的stack frame棧幀,棧就是由一個一個棧幀組成。swift

下面這個是ARM的棧幀佈局圖: bash

130320150468341.png

main stack frame爲調用函數的棧幀,func1 stack frame爲當前函數(被調用者)的棧幀,棧底在高地址,棧向下增加。圖中FP就是棧基址,它指向函數的棧幀起始地址;SP則是函數的棧指針,它指向棧頂的位置。ARM壓棧的順序非常規矩,依次爲當前函數指針PC、返回指針LR、棧指針SP、棧基址FP、傳入參數個數及指針、本地變量和臨時變量。若是函數準備調用另外一個函數,跳轉以前臨時變量區先要保存另外一個函數的參數。app

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

相關文章
相關標籤/搜索