如果要獲取當前線程的調用棧,可以直接使用現有API:[NSThread callStackSymbols]
。
但是並沒有相關API支持獲取任意線程的調用棧,所以只能自己編碼實現。
一個線程的調用棧是什麼樣的呢?
我的理解是應該包含當前線程的執行地址,並且從這個地址可以一級一級回溯到線程的入口地址,這樣就反向構成了一條鏈:線程入口執行某個方法,然後逐級嵌套調用到當前現場。
(圖片來源於維基百科)
如圖所示,每一級的方法調用,都對應了一張活動記錄,也稱為活動幀。也就是說,調用棧是由一張張幀結構組成的,可以稱之為棧幀。
我們可以看到,一張棧幀結構中包含著Return Address,也就是當前活動記錄執行結束後要返回的地址(展開)。<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPsTHw7SjrNTaztLDx7vxyKG1vdW71qG686Osvs2/ydLUzai5/be1u9i12Na3wLS9+NDQu9jL3cHLoaM8L3A+DQo8aDIgaWQ9"2-指令指針和基址指針">2. 指令指針和基址指針
我們明確了兩個目標:(1)當前執行的指令,(2)當前棧幀結構。
以x86為例,寄存器用途如下:
SP/ESP/RSP: Stack pointer for top address of the stack.
BP/EBP/RBP: Stack base pointer for holding the address of the current stack frame.
IP/EIP/RIP: Instruction pointer. Holds the program counter, the current instruction address.
可以看到,我們可以通過指令指針來獲取當前指令地址,以及通過棧基址指針獲取當前棧幀地址。
那麼問題來了,我們怎麼獲取到相關寄存器呢?
考慮到一個線程被掛起時,後續繼續執行需要恢復現場,所以在掛起時相關現場需要被保存起來,比如當前執行到哪條指令了。
那麼就要有相關的結構體來為線程保存運行時的狀態,經過一番查閱,得到如下信息:
The function thread_get_state returns the execution state (e.g. the machine registers) of target_thread as specified by flavor.
Function - Return the execution state for a thread.
SYNOPSIS
kern_return_t thread_get_state
(thread_act_t target_thread,
thread_state_flavor_t flavor,
thread_state_t old_state,
mach_msg_type_number_t old_state_count);
/*
* THREAD_STATE_FLAVOR_LIST 0
* these are the supported flavors
*/
#define x86_THREAD_STATE32 1
#define x86_FLOAT_STATE32 2
#define x86_EXCEPTION_STATE32 3
#define x86_THREAD_STATE64 4
#define x86_FLOAT_STATE64 5
#define x86_EXCEPTION_STATE64 6
#define x86_THREAD_STATE 7
#define x86_FLOAT_STATE 8
#define x86_EXCEPTION_STATE 9
#define x86_DEBUG_STATE32 10
#define x86_DEBUG_STATE64 11
#define x86_DEBUG_STATE 12
#define THREAD_STATE_NONE 13
/* 14 and 15 are used for the internal x86_SAVED_STATE flavours */
#define x86_AVX_STATE32 16
#define x86_AVX_STATE64 17
#define x86_AVX_STATE 18
所以我們可以通過這個API搭配相關參數來獲得想要的寄存器信息:
bool jdy_fillThreadStateIntoMachineContext(thread_t thread, _STRUCT_MCONTEXT *machineContext) {
mach_msg_type_number_t state_count = x86_THREAD_STATE64_COUNT;
kern_return_t kr = thread_get_state(thread, x86_THREAD_STATE64, (thread_state_t)&machineContext->__ss, &state_count);
return (kr == KERN_SUCCESS);
}
這裡引入了一個結構體叫_STRUCT_MCONTEXT
。
_STRUCT_MCONTEXT
在不同平台上的結構不同:
x86_64,如iPhone 6模擬器:
_STRUCT_MCONTEXT64
{
_STRUCT_X86_EXCEPTION_STATE64 __es;
_STRUCT_X86_THREAD_STATE64 __ss;
_STRUCT_X86_FLOAT_STATE64 __fs;
};
_STRUCT_X86_THREAD_STATE64
{
__uint64_t __rax;
__uint64_t __rbx;
__uint64_t __rcx;
__uint64_t __rdx;
__uint64_t __rdi;
__uint64_t __rsi;
__uint64_t __rbp;
__uint64_t __rsp;
__uint64_t __r8;
__uint64_t __r9;
__uint64_t __r10;
__uint64_t __r11;
__uint64_t __r12;
__uint64_t __r13;
__uint64_t __r14;
__uint64_t __r15;
__uint64_t __rip;
__uint64_t __rflags;
__uint64_t __cs;
__uint64_t __fs;
__uint64_t __gs;
};
x86_32,如iPhone 4s模擬器:
_STRUCT_MCONTEXT32
{
_STRUCT_X86_EXCEPTION_STATE32 __es;
_STRUCT_X86_THREAD_STATE32 __ss;
_STRUCT_X86_FLOAT_STATE32 __fs;
};
_STRUCT_X86_THREAD_STATE32
{
unsigned int __eax;
unsigned int __ebx;
unsigned int __ecx;
unsigned int __edx;
unsigned int __edi;
unsigned int __esi;
unsigned int __ebp;
unsigned int __esp;
unsigned int __ss;
unsigned int __eflags;
unsigned int __eip;
unsigned int __cs;
unsigned int __ds;
unsigned int __es;
unsigned int __fs;
unsigned int __gs;
};
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 */
};
ARMv7/v6,如iPhone 4s:
_STRUCT_MCONTEXT32
{
_STRUCT_ARM_EXCEPTION_STATE __es;
_STRUCT_ARM_THREAD_STATE __ss;
_STRUCT_ARM_VFP_STATE __fs;
};
_STRUCT_ARM_THREAD_STATE
{
__uint32_t __r[13]; /* General purpose register r0-r12 */
__uint32_t __sp; /* Stack pointer r13 */
__uint32_t __lr; /* Link register r14 */
__uint32_t __pc; /* Program counter r15 */
__uint32_t __cpsr; /* Current program status register */
};
可以對照《iOS ABI Function Call Guide》,其中在ARM64相關章節中描述到:
The frame pointer register (x29) must always address a valid frame record, although some functions–such as leaf functions or tail calls–may elect not to create an entry in this list. As a result, stack traces will always be meaningful, even without debug information
而在ARMv7/v6上描述到:
The function calling conventions used in the ARMv6 environment are the same as those used in the Procedure Call Standard for the ARM Architecture (release 1.07), with the following exceptions:
*The stack is 4-byte aligned at the point of function calls.
Large data types (larger than 4 bytes) are 4-byte aligned.
Register R7 is used as a frame pointer
Register R9 has special usage.*
所以,通過了解以上不同平台的寄存器結構,我們可以編寫出比較通用的回溯功能。
/**
* 關於棧幀的布局可以參考:
* https://en.wikipedia.org/wiki/Call_stack
* http://www.cs.cornell.edu/courses/cs412/2008sp/lectures/lec20.pdf
* http://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64/
*/
typedef struct JDYStackFrame {
const struct JDYStackFrame* const previous;
const uintptr_t returnAddress;
} JDYStackFrame;
//
int jdy_backtraceThread(thread_t thread, uintptr_t *backtraceBuffer, int limit) {
if (limit <= 0) return 0;
_STRUCT_MCONTEXT mcontext;
if (!jdy_fillThreadStateIntoMachineContext(thread, &mcontext)) {
return 0;
}
int i = 0;
uintptr_t pc = jdy_programCounterOfMachineContext(&mcontext);
backtraceBuffer[i++] = pc;
if (i == limit) return i;
uintptr_t lr = jdy_linkRegisterOfMachineContext(&mcontext);
if (lr != 0) {
/* 由於lr保存的也是返回地址,所以在lr有效時,應該會產生重復的地址項 */
backtraceBuffer[i++] = lr;
if (i == limit) return i;
}
JDYStackFrame frame = {0};
uintptr_t fp = jdy_framePointerOfMachineContext(&mcontext);
if (fp == 0 || jdy_copyMemory((void *)fp, &frame, sizeof(frame)) != KERN_SUCCESS) {
return i;
}
while (i < limit) {
backtraceBuffer[i++] = frame.returnAddress;
if (frame.returnAddress == 0
|| frame.previous == NULL
|| jdy_copyMemory((void *)frame.previous, &frame, sizeof(frame)) != KERN_SUCCESS) {
break;
}
}
return i;
}
如上。
後續iOS中線程Call Stack的捕獲和解析(二)。