APM - iOS 基础功能 调用栈获取原理浅析

1,083 阅读7分钟

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线程的结构体不相同,可以通过线程名称来匹配和转化

内联函数、尾调用优化会使得调用栈发生变化

压栈和出栈都会造成一定的开销,所以在编译器的内联中,会把部分函数的计算步骤直接合并入调用函数中,尾调用的优化会复用一个栈帧,那么函数调用栈会出现变化

获取调用栈,是否需要挂起所有线程

获取调用栈的时候,并非一定要挂起所有线程,获取对应调用栈的时候会造成该线程短暂的停顿。挂起所有线程有助于获取当前整个进程的快照,在死锁排除,条件竞赛,性能分析和崩溃排查的时候可能会有帮助,相对的也会对应用的响应造成影响