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
r12:IP The Intra-Procedure-call scratch register,可簡單的認為暫存SP。
實際上,還有一個r11是optional的,被稱為FP,即frame pointer,某些時刻我們利用它儲存棧底的地址。在arm64中LR是x30暫存器,FP是x29暫存器。
ARM的棧幀
每個執行緒都有自己的棧空間,執行緒中會有很多函式呼叫,每個函式呼叫都有自己的stack frame棧幀,棧就是由一個一個棧幀組成。
下面這個是ARM的棧幀佈局圖:
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
獲取任意執行緒呼叫棧的那些事