APM - iOS 基础功能 调用栈获取原理浅析
简介
函数调用关系可以帮助定位问题。
什么是调用栈(call stack)?
栈的特性前进后出
随着函数调用关系的执行,之前未执行完成的函数会先被压入栈中,执行当前作用域的函数。当前函数执行完成后,被在函数调用栈中被弹出。
iOS函数调用栈
start函数调用main,main函数调用UIApplicationMain,一直调用下去,所以叫做调用。如图所示,后进先出,先进后出(FILO),是一个标准的栈结构,叫做调用栈,也称作堆栈(backtrace),backtrace倒是有种往前回溯的意思
在start函数调用main的时候,start函数并没有退出,需要有一片内存区域存储传递的参数,局部变量等。函数内部每调用一个函数,就会把这些东西进行一次压栈操作。
每个线程都有自己的调用栈,进程/线程模型中,进程是最小的持有资源单位,而线程是最小的可调度单位,每个线程各自调度执行,有自己的调用栈。
调用栈常用格式
0 SLOCCrashDemo 0x000000010d10d46a -[ViewController viewDidLoad] + 74
1 UIKitCore 0x0000000110a718ed -[UIViewController _sendViewDidLoadWithAppearanceProxyObjectTaggingEnabled] + 88
2 UIKitCore 0x0000000110a76273 -[UIViewController loadViewIfRequired] + 1084
3 UIKitCore 0x0000000110a7665d -[UIViewController view] + 27
4 UIKitCore 0x00000001111e8d0f -[UIWindow addRootViewControllerViewIfPossible] + 313
5 UIKitCore 0x00000001111e83fd -[UIWindow _updateLayerOrderingAndSetLayerHidden:actionBlock:] + 219
6 UIKitCore 0x00000001111e93c1 -[UIWindow _setHidden:forced:] + 362
7 UIKitCore 0x00000001111fc3d4 -[UIWindow _mainQueue_makeKeyAndVisible] + 42
8 UIKitCore 0x0000000111438814 -[UIWindowScene _makeKeyAndVisibleIfNeeded] + 202
9 UIKitCore 0x0000000110601097 +[UIScene _sceneForFBSScene:create:withSession:connectionOptions:] + 1671
10 UIKitCore 0x00000001111aba92 -[UIApplication _connectUISceneFromFBSScene:transitionContext:] + 1114
11 UIKitCore 0x00000001111abdc1 -[UIApplication workspace:didCreateScene:withTransitionContext:completion:] + 289
12 UIKitCore 0x0000000110c983f3 -[UIApplicationSceneClientAgent scene:didInitializeWithEvent:completion:] + 358
13 FrontBoardServices 0x00000001194ae0ae -[FBSScene _callOutQueue_agent_didCreateWithTransitionContext:completion:] + 391
14 FrontBoardServices 0x00000001194d6b41 __94-[FBSWorkspaceScenesClient createWithSceneID:groupID:parameters:transitionContext:completion:]_block_invoke.176 + 102
15 FrontBoardServices 0x00000001194bbad5 -[FBSWorkspace _calloutQueue_executeCalloutFromSource:withBlock:] + 209
16 FrontBoardServices 0x00000001194d680f __94-[FBSWorkspaceScenesClient createWithSceneID:groupID:parameters:transitionContext:completion:]_block_invoke + 352
17 libdispatch.dylib 0x000000010efec9c8 _dispatch_client_callout + 8
18 libdispatch.dylib 0x000000010efef910 _dispatch_block_invoke_direct + 295
19 FrontBoardServices 0x00000001194fc7a5 __FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__ + 30
20 FrontBoardServices 0x00000001194fc48b -[FBSSerialQueue _targetQueue_performNextIfPossible] + 433
21 FrontBoardServices 0x00000001194fc950 -[FBSSerialQueue _performNextFromRunLoopSource] + 22
22 CoreFoundation 0x000000010d9ff37a __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
23 CoreFoundation 0x000000010d9ff272 __CFRunLoopDoSource0 + 180
24 CoreFoundation 0x000000010d9fe7b6 __CFRunLoopDoSources0 + 346
25 CoreFoundation 0x000000010d9f8f1f __CFRunLoopRun + 878
26 CoreFoundation 0x000000010d9f86c6 CFRunLoopRunSpecific + 567
27 GraphicsServices 0x00000001197fcdb3 GSEventRunModal + 139
28 UIKitCore 0x00000001111aa187 -[UIApplication _run] + 912
29 UIKitCore 0x00000001111af038 UIApplicationMain + 101
30 SLOCCrashDemo 0x000000010d10de72 main + 114
31 libdyld.dylib 0x000000010f07b409 start + 1
- 第一列是序号
- 第二列是Image名称(在LLDB中使用image list命令可以看到所有的image,iOS中把这样的库称作image)
- 第三列是内存地址
- 第四列是函数符号信息
- 第五列是相对Image的偏移地址。
调用栈获取原理
一个调用栈是由一个个栈帧组成。这些栈帧包含着子流程的状态信息,且是机器依赖和ABI依赖的。每个栈帧都表示这个子流程还没有结束和返回。
每个栈帧包含了:
- 传递给这个流程的参数
- 返回调用者的返回地址
- 为该流程本地变量开辟的空间
在谈调用栈的结构之前,我们先聊一聊CPU如何执行指令,以及CPU的小助手寄存器。
寄存器
寄存器是最高级的存储介质,是直接给CPU使用的存储。由于已经是硬件设备,那么对于不同的架构,当前寄存器的数量和用途是不一样的,当然功能上是近似的。由于iPhone的CPU主要是arm64架构,我们拿arm64来举例子。
常用的arm64寄存器
通用目的寄存器 x0-x28
FP (Frame Pointer) x29
帧指针指向于当前的栈帧(Stack Frame),当函数返回的时候也用来存储栈指针(Stack Pointer)。x29寄存器通常用来存储上一个帧指针的值,所以当函数返回的时候,上一个帧指针可以被恢复。x29寄存器也被用来存储Link Register(LR),LR存返回地址。
其中栈帧包含
- 本地变量
- 返回地址
- 函数参数
LR (Link Register) x30
用来持有函数调用的返回地址
SP (Stack Pointer) x31
指向于当前栈帧(stack frame)的顶部
PC (Program Counter)
指向于下一条将被执行的指令
CPSR (Current Program Status Register)
这个寄存器持有当前进程的状态,包含了运行模式,中断标记和其他控制位
获取调用栈
[NSThread callstackSymbols]
@property (class, readonly, copy) NSArray<NSString *> *callStackSymbols API_AVAILABLE(macos(10.6), ios(4.0), watchos(2.0), tvos(9.0));
直接获取当前线程的调用栈,在很多应用场景下,比如开启一个线程监听Runloop的卡顿,或者开启一个线程跟主线程做Ping-Pong的时候,调用这个方法只能获取当前监听线程的调用栈,并不满足需求。
backtrace_symbols
void* callstack[128];
int framesCount = backtrace(callstack, 128);
char **strs = backtrace_symbols(callstack, framesCount);
int i;
NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:framesCount];
for (i = 0; i < 4; i++) {
[backtrace addObject:[NSString stringWithUTF8String:strs[i]]];
}
free(strs);
通过遍历Frame Pointer获取
iOS内核
iOS是基于Darwin内核,由kernel、XNU和Runtime祖册。Mach微内核负责进程和线程抽象、虚拟内存管理、任务管理以及进程间通信和消息传递机制。其他的比如文件操作和设备访问由BSD层实现。
Task/Thread模型
Mach微内核没有提供进程逻辑,而是使用了Task作为资源容器,Task在BSD模型中是一一对应的关系,所以Task可以看成是进程的近似。
获取线程列表
进程是最小的持有资源单位,线程是最小的调度单位,但是不持有资源。所以近似进程的Task包含了进程列表,可以通过task_threads获取指定Task的线程列表。
kern_return_t task_threads
(
task_t target_task,
thread_act_array_t *act_list,
mach_msg_type_number_t *act_listCnt
);
获取线程状态
我们可以通过thread_info来查询指定线程的信息。
kern_return_t thread_info
(
thread_act_t target_act,
thread_flavor_t flavor,
thread_info_t thread_info_out,
mach_msg_type_number_t *thread_info_outCnt
);
获取到线程之后,我们可以通过thread_get_state获取线程的所有信息,其中包括了寄存器等信息,信息使用_STRUCT_MCONTEXT结构体定义。由于_STRUCT_MCONTEXT是跟硬件架构环境有关,所以代码使用宏来差分不同硬件架构。
#if defined __arm__
#define THREAD_STATE_FLAVOR ARM_THREAD_STATE
#define THREAD_STATE_COUNT ARM_THREAD_STATE_COUNT
#define __framePointer __r[7]
#elif defined __arm64__
#define THREAD_STATE_FLAVOR ARM_THREAD_STATE64
#define THREAD_STATE_COUNT ARM_THREAD_STATE64_COUNT
#define __framePointer __fp
#else
#error "Current CPU Architecture is not supported"
#endif
_STRUCT_MCONTEXT
ARM64架构的MachineContext结构如下
#ifndef _STRUCT_MCONTEXT64
#if __DARWIN_UNIX03
#define _STRUCT_MCONTEXT64 struct __darwin_mcontext64
_STRUCT_MCONTEXT64
{
_STRUCT_ARM_EXCEPTION_STATE64 __es;
_STRUCT_ARM_THREAD_STATE64 __ss;
_STRUCT_ARM_NEON_STATE64 __ns;
};
_STRUCT_ARM_THREAD_STATE
在_STRUCT_ARM_THREAD_STATE中我们可以得到SP、LR等关键字段。
#if __DARWIN_UNIX03
#define _STRUCT_ARM_THREAD_STATE64 struct __darwin_arm_thread_state64
#if __DARWIN_OPAQUE_ARM_THREAD_STATE64
_STRUCT_ARM_THREAD_STATE64
{
__uint64_t __x[29]; /* General purpose registers x0-x28 */
void* __opaque_fp; /* Frame pointer x29 */
void* __opaque_lr; /* Link register x30 */
void* __opaque_sp; /* Stack pointer x31 */
void* __opaque_pc; /* Program counter */
__uint32_t __cpsr; /* Current program status register */
__uint32_t __opaque_flags; /* Flags describing structure format */
};
#else /* __DARWIN_OPAQUE_ARM_THREAD_STATE64 */
_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 */
};
#endif /* __DARWIN_OPAQUE_ARM_THREAD_STATE64 */
完整代码
- 通过Link Register获取当前函数的返回地址
- 遍历Frame Pointer获取调用函数的地址
#include "backtrace.h"
#include <stdio.h>
#include <stdlib.h>
#include <machine/_mcontext.h>
#if defined __arm__
#define THREAD_STATE_FLAVOR ARM_THREAD_STATE
#define THREAD_STATE_COUNT ARM_THREAD_STATE_COUNT
#define __framePointer __r[7]
#elif defined __arm64__
#define THREAD_STATE_FLAVOR ARM_THREAD_STATE64
#define THREAD_STATE_COUNT ARM_THREAD_STATE64_COUNT
#define __framePointer __fp
#else
#error "Current CPU Architecture is not supported"
#endif
int df_backtrace(thread_t thread, void** stack, int maxSymbols) {
_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);
if (kret != KERN_SUCCESS) {
return 0;
}
int i = 0;
stack[i] = (void *)machineContext.__ss.__lr;
++i;
void **currentFramePointer = (void **)machineContext.__ss.__framePointer;
while (i < maxSymbols && currentFramePointer) {
void **previousFramePointer = *currentFramePointer;
if (!previousFramePointer) {
break;
}
stack[i] = *(currentFramePointer + 1);
currentFramePointer = previousFramePointer;
++i;
}
return i;
}
Tips
MachThread/PThread
mach内核的线程模型和BSD中的pthread线程的结构体不相同,可以通过线程名称来匹配和转化
内联函数、尾调用优化会使得调用栈发生变化
压栈和出栈都会造成一定的开销,所以在编译器的内联中,会把部分函数的计算步骤直接合并入调用函数中,尾调用的优化会复用一个栈帧,那么函数调用栈会出现变化
获取调用栈,是否需要挂起所有线程
获取调用栈的时候,并非一定要挂起所有线程,获取对应调用栈的时候会造成该线程短暂的停顿。挂起所有线程有助于获取当前整个进程的快照,在死锁排除,条件竞赛,性能分析和崩溃排查的时候可能会有帮助,相对的也会对应用的响应造成影响