Recently, I was writing something that needed to obtain the call stack of any thread. Then I looked at some existing open source frameworks, which were complicated to write and did not support Swift very well, so I wrote RCBacktrace.
ARM Several general purpose registers
ARM has 15 kinds of general purpose registers, but some general purpose registers have special uses. PCS(Procedure Call Standard for ARM Architecture) defines the special uses of registers in Procedure calls.
R15: PC The Program Counter, also known as Program Counter PC, The instruction register holds The memory address of The next instruction to be executed. R14: LR The Link Register, also known as Subroutine Link Register, is LR, which stores The memory address of The next instruction of The last function call instruction, that is, The return address. R13: SP The Stack Pointer, SP register will hold our address at The top of The Stack at any time. R12: IP The intra-procede-call scratch register, which can simply be considered as a temporary SP.
In fact, there’s an optional R11 called FP, frame Pointer, that we use at some point to hold the address at the bottom of the stack. In ARM64 LR is the X30 register and FP is the X29 register.
ARM’s stack frame
Each thread has its own stack space, and there will be many function calls in the thread. Each function call has its own stack frame, which is composed of one stack frame after another.
Here is a stack frame layout for ARM:
Func1 Stack frame is the stack frame of the current function (called), with the bottom of the stack at a high address and the stack growing downward. In the figure, FP is the stack base address, which points to the start address of the stack frame of the function; SP is the stack pointer to the function, pointing to the top of the stack. ARM pushes the current function pointer PC, return pointer LR, stack pointer SP, stack base address FP, number of passed parameters and Pointers, local variables and temporary variables. If a function is about to call another function, the temporary variable area should hold the other function’s arguments before jumping.
backtrace
From the figure above we can see that the value of FP in the current stack frame stores the address of FP in the previous stack frame. Get the FP register of this function, the indicated stack address, out of the stack, you can get the LR register value of the calling function, and then you can find the corresponding function name through dynsyM dynamic linked table.
void **currentFramePointer = (void **)machineContext.__ss.__framePointer;
while (i < maxSymbols) {
void **previousFramePointer = *currentFramePointer;
if(! previousFramePointer)break;
stack[i] = *(currentFramePointer+1);
currentFramePointer = previousFramePointer;
++i;
}
Copy the code
Thread execution state
The LR and FP registers of a thread can be backtraced.
A Thread encapsulates a pthread. In Foundation/Thread.swift, you can see the detailed code for encapsulating a pthread. Different operations design their own threading model, so the underlying API is different, but POSIX provides pThreads that encapsulate the underlying layer so that different platforms get the same results.
Unix provides methods such as thread_get_state and task_threads that operate on kernel threads. Each kernel thread is uniquely identified by an ID of type thread_T. A pthread is uniquely identified by type pthread_T.
Switching between kernel threads and pthreads (thread_t and pthread_t) is easy, because pthreads are designed to abstract kernel threads.
The _STRUCT_MCONTEXT type stores the SP of the current thread and the FP of the top stack frame. The _STRUCT_MCONTEXT type varies from platform to platform.
ARM64, like the 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 */
};
Copy the code
With thread_t and _STRUCT_MCONTEXT you can use thread_get_state to get the thread’s FP, SP, etc.
_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);
Copy the code
Dladdr Retrieves symbolic information for an address
You can then obtain symbolic information for an address through the dlADDR function and Dl_info
extern int dladdr(const void *, Dl_info *);
Copy the code
/*
* Structure filled inby 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!) }Copy the code
Swift named reforming
OC method has no problem, because the renormalization rule is relatively simple, that is, a ‘_’ is added before the symbol, but the Swift named renormalization is more complex, so the method is difficult to identify after the named renormalization, as follows:
$s15RCBacktraceDemo14ViewControllerC3baryyF
Copy the code
Therefore, we need to call swift_demangle to restore the reformed symbol, so the restoration is as follows:
RCBacktraceDemo.ViewController.bar() -> ()
Copy the code
For a more detailed renaming of Swift, see Friday Q&A 2014-08-08: Swift Name Mangling.
Refer to the article
Friday Q&A 2014-08-08: Swift Name Mangling fetches the Call Stack of any thread