Capture and parsing of thread Call Stack in iOS (1)
1. Obtain the Call Stack of any thread
To obtain the call stack of the current thread, you can directly use the existing API:[NSThread callStackSymbols]
.
However, there is no relevant API to support obtaining the call stack of any thread, so you can only implement it by yourself.
1. Infrastructure
What is the call stack of a thread?
My understanding is that it should include the execution address of the current thread, and the address can be traced back to the entry address of the thread at a level, which forms a chain in reverse order: the thread entry executes a method, then, the call is nested to the current site step by step.
(Image Source: Wikipedia)
Each level of method call corresponds to an activity record, also known as an activity frame. That is to say, the call stack is composed of a frame structure, which can be called a stack frame.
We can see that a stack frame structure contains the Return Address, which is the Address to be returned after the execution of the current activity record ends (expand ). <喎?http: www.bkjia.com kf ware vc " target="_blank" class="keylink"> Authorization/ydLUzai5/be1u9i12Na3wLS9 + ndqu9jl3jwam8l3a + DQo8aDIgaWQ9 "2-command pointer and base address Pointer"> 2. Command pointer and base address pointer
We have defined two objectives: (1) the currently executed command, and (2) the current stack frame structure.
Taking x86 as an example, the register usage is as follows:
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.
We can see that we can get the current instruction address through the instruction pointer and get the current stack frame address through the stack base address pointer.
So the question is, how can we get the relevant registers?
3. Thread execution status
When a thread is suspended, it needs to be restored for subsequent execution. Therefore, when the thread is suspended, the relevant field needs to be saved, for example, the instruction to be executed.
There must be a relevant struct to save the running state of the thread. After some reading, the following information is obtained:
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.SYNOPSISkern_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
Therefore, we can use this API with relevant parameters to obtain the desired register information:
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);}
Here we introduce a struct called_STRUCT_MCONTEXT
.
4. registers on different platforms
_STRUCT_MCONTEXT
The structures on different platforms are different:
X86_64, such as iPhone 6 simulator:
_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, such as iPhone 4s simulator:
_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, such as 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, such as iPhone 4 s:
_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 */};
Refer to the iOS ABI Function Call Guide, which is described in ARM64:
The frame pointer register (x29) must always address a valid frame record, although some functions-such as leaf functions or tail CILS-may elect not to create an entry in this list. as a result, stack traces will always be meaningful, even without debug information
In ARMv7/v6, the following is described:
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 CILS.
Large data types (larger than 4 bytes) are 4-byte aligned.
Register R7 is used as a frame pointer
Register R9 has special usage .*
Therefore, by understanding the register structure of different platforms above, we can compile general backtracking functions.
5. Algorithm Implementation
/*** Stack frame layout can be referred to: * 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 l Imit) {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) {/* Because lr stores the returned address, duplicate address items should be generated when lr is valid */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 ;}
As shown above.
2. coding implements symbolic resolution of an address
Capture and parse the thread Call Stack in iOS (2 ).