最近在寫(xiě)一些東西需要獲取任意線程調(diào)用棧,然后看了現(xiàn)有的一些開(kāi)源框架,寫(xiě)的比較復(fù)雜而且對(duì)Swift的支持不是很好,所以寫(xiě)了RCBacktrace。
ARM幾種通用寄存器
ARM有15種通用寄存器蒲列,但是其實(shí)有些通用寄存器是有特殊用途的,PCS(Procedure Call Standard for Arm architecture)就定義了過(guò)程調(diào)用中搀罢,寄存器的特殊用途蝗岖。
r15:PC The Program Counter,也稱作程序計(jì)數(shù)器PC榔至,指令寄存器保存的是下一條將要執(zhí)行的指令的內(nèi)存地址抵赢。
r14:LR The Link Register,也稱作子程序連接寄存器(Subroutine Link Register)即連接寄存器LR唧取,LR寄存器則保存著最后一次函數(shù)調(diào)用指令的下一條指令的內(nèi)存地址铅鲤,即保存了返回地址。
r13:SP The Stack Pointer枫弟,堆棧指針邢享,sp寄存器在任意時(shí)刻會(huì)保存我們棧頂?shù)牡刂贰?br>
r12:IP The Intra-Procedure-call scratch register,可簡(jiǎn)單的認(rèn)為暫存SP淡诗。
實(shí)際上骇塘,還有一個(gè)r11是optional的,被稱為FP韩容,即frame pointer款违,某些時(shí)刻我們利用它保存棧底的地址。在arm64中LR是x30寄存器群凶,F(xiàn)P是x29寄存器插爹。
ARM的棧幀
每個(gè)線程都有自己的棧空間座掘,線程中會(huì)有很多函數(shù)調(diào)用递惋,每個(gè)函數(shù)調(diào)用都有自己的stack frame棧幀柔滔,棧就是由一個(gè)一個(gè)棧幀組成溢陪。
下面這個(gè)是ARM的棧幀布局圖:
main stack frame為調(diào)用函數(shù)的棧幀萍虽,func1 stack frame為當(dāng)前函數(shù)(被調(diào)用者)的棧幀,棧底在高地址形真,棧向下增長(zhǎng)杉编。圖中FP就是棧基址咆霜,它指向函數(shù)的棧幀起始地址邓馒;SP則是函數(shù)的棧指針,它指向棧頂?shù)奈恢枚昱鳌RM壓棧的順序很是規(guī)矩光酣,依次為當(dāng)前函數(shù)指針PC、返回指針LR脉课、棧指針SP救军、棧基址FP倘零、傳入?yún)?shù)個(gè)數(shù)及指針唱遭、本地變量和臨時(shí)變量。如果函數(shù)準(zhǔn)備調(diào)用另一個(gè)函數(shù)呈驶,跳轉(zhuǎn)之前臨時(shí)變量區(qū)先要保存另一個(gè)函數(shù)的參數(shù)拷泽。
backtrace
從上圖我們可以看到當(dāng)前棧幀中FP的值存儲(chǔ)的是上一個(gè)棧幀的FP地址。拿到本函數(shù)的FP寄存器袖瞻,所指示的棧地址司致,出棧,就能得到調(diào)用函數(shù)的LR寄存器的值聋迎,然后就能通過(guò)dynsym動(dòng)態(tài)鏈接表脂矫,找到對(duì)應(yīng)的函數(shù)名。
void **currentFramePointer = (void **)machineContext.__ss.__framePointer;
while (i < maxSymbols) {
void **previousFramePointer = *currentFramePointer;
if (!previousFramePointer) break;
stack[i] = *(currentFramePointer+1);
currentFramePointer = previousFramePointer;
++i;
}
線程執(zhí)行狀態(tài)
上面我們可以看到拿到某個(gè)線程的LR和FP寄存器就能進(jìn)行backtrace砌庄,那怎么拿到呢?
Thread是對(duì)pthread的封裝羹唠,在Foundation/Thread.swift,可以看到用pthread封裝Thread的詳細(xì)代碼娄昆。
不同的操作會(huì)設(shè)計(jì)自己的線程模型, 所以底層 API 是不相同的, 但是 POSIX提供的pthread就是相當(dāng)于對(duì)底層進(jìn)行了一次封裝, 讓不同平臺(tái)運(yùn)行得到相同的效果.
Unix 系統(tǒng)提供的 thread_get_state 和 task_threads 等方法佩微,操作的都是內(nèi)核線程,每個(gè)內(nèi)核線程由 thread_t 類型的 id 來(lái)唯一標(biāo)識(shí)萌焰,pthread 的唯一標(biāo)識(shí)是 pthread_t 類型哺眯。
內(nèi)核線程和 pthread 的轉(zhuǎn)換(也即是 thread_t 和 pthread_t 互轉(zhuǎn))很容易,因?yàn)?pthread 誕生的目的就是為了抽象內(nèi)核線程扒俯。
_STRUCT_MCONTEXT
類型的結(jié)構(gòu)體中奶卓,存儲(chǔ)了當(dāng)前線程的SP和最頂部棧幀的FP一疯,_STRUCT_MCONTEXT
在不同平臺(tái)上的結(jié)構(gòu)不同,如:
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就可以通過(guò)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獲取某個(gè)地址的符號(hào)信息
接著就可以通過(guò)dladdr函數(shù)和Dl_info獲得某個(gè)地址的符號(hào)信息
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方法沒(méi)有問(wèn)題,因?yàn)橹卣?guī)則比較簡(jiǎn)單盏浙,就是符號(hào)前加了一個(gè)'_'眉睹,但是Swift的命名重整比較復(fù)雜,所以方法經(jīng)過(guò)命名重整很難辨認(rèn)废膘,如下:
$s15RCBacktraceDemo14ViewControllerC3baryyF
所以我們需要調(diào)用swift_demangle
對(duì)重整過(guò)的符號(hào)進(jìn)行還原竹海,所以還原成原本的樣子后如下:
RCBacktraceDemo.ViewController.bar() -> ()
更詳細(xì)的Swift的命名重整可以看Friday Q&A 2014-08-08: Swift Name Mangling。
參考文章
ARM FP寄存器及frame pointer介紹
iOS中線程Call Stack的捕獲和解析(一)
ARM函數(shù)調(diào)用過(guò)程分析
Friday Q&A 2014-08-08: Swift Name Mangling
獲取任意線程調(diào)用棧的那些事