最近在寫一些東西須要獲取任意線程調用棧,而後看了現有的一些開源框架,寫的比較複雜並且對Swift的支持不是很好,因此寫了RCBacktrace。html
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
每一個線程都有本身的棧空間,線程中會有不少函數調用,每一個函數調用都有本身的stack frame棧幀,棧就是由一個一個棧幀組成。swift
下面這個是ARM的棧幀佈局圖: bash
main stack frame爲調用函數的棧幀,func1 stack frame爲當前函數(被調用者)的棧幀,棧底在高地址,棧向下增加。圖中FP就是棧基址,它指向函數的棧幀起始地址;SP則是函數的棧指針,它指向棧頂的位置。ARM壓棧的順序非常規矩,依次爲當前函數指針PC、返回指針LR、棧指針SP、棧基址FP、傳入參數個數及指針、本地變量和臨時變量。若是函數準備調用另外一個函數,跳轉以前臨時變量區先要保存另外一個函數的參數。app
從上圖咱們能夠看到當前棧幀中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函數和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!)
}
複製代碼
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
獲取任意線程調用棧的那些事