在学习线程的相关常识时,咱们必定触摸过 NSThread 类,它有一个类特点:@property (class, readonly, copy) NSArray<NSString *> *callStackSymbols 用来获取当时线程的函数调用仓库,该特点回来包括调用仓库符号的数组,每个元素都是一个 NSString 目标,其值的格局由 backtrace_symbols() 函数确认。初看此特点给人眼前一亮,可是看到它仅是一个类特点时,咱们仿佛认识到了什么,对,它有一个限制,此类特点回来值仅能描绘调用此办法时当时线程的调用仓库回溯。咱们常见的在一个子线程中抓取主线程的函数调用仓库的场景它就无法胜任了。例如:进行功能监控时通常会开一个子线程监控,如监控主线程的卡顿状况、CPU Usage 状况,当呈现主线程卡顿、某个线程 CPU Usage 占用过高时,需求进行抓栈,那么就需求经过其它办法获取非当时线程的调用栈了。

 在 Developer Documentation 中查找 callStackSymbols 时,发现 NSException 类中也有一个同名的实例特点:@property(readonly, copy) NSArray<NSString *> *callStackSymbols; 此实例特点描绘引发 Objective-C 反常时调用仓库回溯的字符串数组。每个字符串的格局由 backtrace_symbols() 确认,感兴趣的小伙伴能够测验在一个 Try-Catch 中制造一个 Objective-C 反常,打印一下 callStackSymbols 特点的内容试一下。

 下面咱们以大佬的 bestswifter/BSBacktraceLogger 代码为示例来研讨 iOS 下对函数调用栈进行回溯的进程。

Mach 线程(mach_port_t)

 持续开端之前咱们需求弥补一些 macOS 三种线程的联系:

  1. pthread_t pthread_self(void) 回来的是 pthread_t,glibc 库的线程 id。实践上是线程操控块 tcb 首地址。(pthread_self 是 POSIX 规范中的接口,pthread_t 是 POSIX 规范中线程的类型)
  2. syscall(SYS_gettid) 内核级线程 id,系统仅有。该函数为系统调用函数,glibc 可能没有该函数声明,此时需求运用 syscall(SYS_gettid)。能够参阅 pthread_self() VS syscall(SYS_gettid) 具体了解它们之间的联系。(此种线程不触及本篇的常识点,可直接疏忽)
  3. 此种正是 macOS 中 Mach 内核中的线程(在 mach 中或许咱们把线程了解为一个个端口,获取某条线程便是获取此线程的端口):mach_port_t mach_thread_self(void) 直接获取线程端口 mach_port_t,另外一种是:首要 pthread_t pthread_self(void) 获取 POSIX 规范线程:pthread_t,然后经过 mach_port_t pthread_mach_thread_np(pthread_t) 把它转换为 Mach 线程(端口):mach_port_tmach_port_t 是 macOS 特有的 id,实践上不能说是 thread id,而应该当做是线程端口,它是 Mach 中表明线程的一种办法。

 上面的第 3 条获取 Mach 线程的办法中,其间触及一些内存方面的问题,看到这个链接 一个“反诘”引发的内存反思 咱们能够具体阅览一下,增强一下对 extern mach_port_t mach_thread_self(void);mach_port_t pthread_mach_thread_np(pthread_t); 函数的认识,其间 np 是 not posix 的首字母缩写。

/* return the mach thread bound to the pthread 回来绑定到 pthread 的 mach 线程 */
__API_AVAILABLE(macos(10.4), ios(2.0))
mach_port_t pthread_mach_thread_np(pthread_t);

 针对上述文章中说到的优化点,咱们大约能够经过以下两种办法获取 Mach 线程(port):

// 办法 1
mach_port_t safe_thread_self(void) {
    // mach_thread_self 和 mach_port_deallocate 配对运用,运用结束后马上开释空间 
    mach_port_t thread_self = mach_thread_self();
    mach_port_deallocate(mach_task_self(), thread_self);
    return thread_self;
}
// 办法 2
mach_port_t safe_thread_self(void) {
    return pthread_mach_thread_np(pthread_self());
}

 然后 ARM 架构下:FP(x29) 栈底 SP 栈顶 PC 下一条指令 LR(x30) 函数回来后的下一个函数的第一条指令 寄存器相关的内容,网上好多大佬都做了总结,这儿就不再展开了。能够参阅这篇:iOS开发–探究iOS线程调用栈及符号化。

 下面咱们开端剖析 BSBacktraceLogger 源码。

DEFINE MACRO FOR DIFFERENT CPU ARCHITECTURE

 BSBacktraceLogger.m 文件最初,界说了一大组宏,首要针对不同的 CPU 架构体系界说的。这儿首要是 __arm64__(64 位 ARM 架构)、__arm__(32 位 ARM 架构)、__x86_64__(64 位 x86 架构)、__i386__(32 位 x86 架构),这儿咱们首要重视 __x86_64____arm64__ 就好了。

#pragma -mark DEFINE MACRO FOR DIFFERENT CPU ARCHITECTURE
#if defined(__arm64__)
// DETAG_INSTRUCTION_ADDRESS 是为了去掉指令地址中的指针标签,ARM 指令分为 Thumb 和 Normal,指令长度分别为 16 位和 32 位,ARM64 下指令长度为 32 位,所以至少是按照 4 字节对齐,指令地址必定是 4 的整数倍,指令最低 2 位正常总是为 0,系统可能会在这两位插入指针标签
// 所以要去掉这个标签的值,得到真正的指令地址,ARM64 去除低两位,armv7 下指令可能为 Thumb 和 Normal,同理去除最低一位
// x86_64 和 i386 中指令是可变长度的,一切位都是有意义的,不进行去除
// 地址与 ~(3UL) 做与操作,即把地址二进制表明的最终两方位 0
#define DETAG_INSTRUCTION_ADDRESS(A) ((A) & ~(3UL))
// ARM 架构 64 位下表明线程状况个数,此值没什么用,仅用于 thread_get_state 函数中的第四个暂时参数。x86 下值为 42
#define BS_THREAD_STATE_COUNT ARM_THREAD_STATE64_COUNT
// ARM 架构 64 位表明线程状况,此值没什么用,仅用于 thread_get_state 函数中的第二个暂时参数。x86 下值为 4
#define BS_THREAD_STATE ARM_THREAD_STATE64
// 栈底寄存器
#define BS_FRAME_POINTER __fp
// 栈顶寄存器
#define BS_STACK_POINTER __sp
// ARM 架构下:程序计数器寄存器
#define BS_INSTRUCTION_ADDRESS __pc
#elif defined(__arm__)
// 地址与 ~(1UL) 做与操作,最终一方位 0
#define DETAG_INSTRUCTION_ADDRESS(A) ((A) & ~(1UL))
// 同上的两个暂时参数
#define BS_THREAD_STATE_COUNT ARM_THREAD_STATE_COUNT
#define BS_THREAD_STATE ARM_THREAD_STATE
// 栈底寄存器
#define BS_FRAME_POINTER __r[7]
// 栈顶寄存器
#define BS_STACK_POINTER __sp
// ARM 架构下:程序计数器寄存器
#define BS_INSTRUCTION_ADDRESS __pc
#elif defined(__x86_64__)
// 不变
#define DETAG_INSTRUCTION_ADDRESS(A) (A)
// 同上的两个暂时参数
#define BS_THREAD_STATE_COUNT x86_THREAD_STATE64_COUNT
#define BS_THREAD_STATE x86_THREAD_STATE64
// 栈底寄存器
#define BS_FRAME_POINTER __rbp
// 栈顶寄存器
#define BS_STACK_POINTER __rsp
// x86 64 位架构,对应 ARM 架构下 PC 寄存器
#define BS_INSTRUCTION_ADDRESS __rip
#elif defined(__i386__)
// 坚持不变
#define DETAG_INSTRUCTION_ADDRESS(A) (A)
// 同上的两个暂时参数
#define BS_THREAD_STATE_COUNT x86_THREAD_STATE32_COUNT
#define BS_THREAD_STATE x86_THREAD_STATE32
// 栈底寄存器
#define BS_FRAME_POINTER __ebp
// 栈顶寄存器
#define BS_STACK_POINTER __esp
// x86 32 位架构,对应 ARM 架构下 PC 寄存器
#define BS_INSTRUCTION_ADDRESS __eip
#endif
#define CALL_INSTRUCTION_FROM_RETURN_ADDRESS(A) (DETAG_INSTRUCTION_ADDRESS((A)) - 1)
#if defined(__LP64__)
// 针对 64 位架构输出地址字符串的格局
#define POINTER_FMT       "0x%016lx"
#define POINTER_SHORT_FMT "0x%lx"
// 64 位架构中符号表数组中元素的类型
#define BS_NLIST struct nlist_64
#else
// 针对 32 位架构输出地址字符串的格局
#define POINTER_FMT       "0x%08lx"
#define POINTER_SHORT_FMT "0x%lx"
// 32 位架构中符号表数组中元素的类型
#define BS_NLIST struct nlist
#endif

BSStackFrameEntry

struct BSStackFrameEntry 是界说的用来表明函数栈帧内栈底指针(FP 寄存器)链表节点的结构体,这儿其实很简单可是描绘的有点绕,BSStackFrameEntry 结构体中的 const uintptr_t return_address 成员变量表明的是一个栈帧中的栈底寄存器(FP)的值,即 BSStackFrameEntry 结构体是用来构建栈底指针链表时的节点结构体。

// 用于表明栈帧栈底指针 FP 链表的结构体,实质结构为 x29、x30 组合,即 FP 和 LR 寄存器
typedef struct BSStackFrameEntry{
    const struct BSStackFrameEntry *const previous;
    const uintptr_t return_address;
} BSStackFrameEntry;
// 用于记载主线程 port 的全局变量
static mach_port_t main_thread_id;
+ (void)load {
    // main_thread_id 是在 load 办法中赋值,保证必定是在主线程
    main_thread_id = mach_thread_self();
}

 这儿直接运用的 mach_thread_self() 获取主线程,能够替换为上面的 safe_thread_self() 函数。

 持续向下看到界说在 .h 中四个 API 的完成,分别是:获取悉数线程的调用栈回溯、获取当时线程的调用栈回溯、获取主线程的调用栈回溯、获取指定 NSThread 的调用栈回溯。

#pragma -mark Implementation of interface
// 获取指定线程的调用栈回溯
+ (NSString *)bs_backtraceOfNSThread:(NSThread *)thread {
    // 调用 bs_machThreadFromNSThread 函数把 thread 转换为 thread_t(实践是 typedef mach_port_t thread_t)类型的 Mach 线程,然后调用 _bs_backtraceOfThread 获取调用栈回溯字符串,
    // 这儿并不是一个什么数据结构的转换进程,仅仅一个对应线程的查询:在当时 task 的一切线程中找到与指定 NSThread 对应的 Mach 线程。 
    return _bs_backtraceOfThread(bs_machThreadFromNSThread(thread));
}
// 获取当时线程的调用栈回溯
+ (NSString *)bs_backtraceOfCurrentThread {
    // 对 [NSThread currentThread] 当时线程进行调用栈回溯
    return [self bs_backtraceOfNSThread:[NSThread currentThread]];
}
// 获取主线程的调用栈回溯
+ (NSString *)bs_backtraceOfMainThread {
    // 对 [NSThread mainThread] 主线程进行调用栈回溯
    return [self bs_backtraceOfNSThread:[NSThread mainThread]];
}
// 对当时一切线程进行栈回溯(此函数能够先看下大约进程,下面咱们会对其间触及的函数进行具体讲解)
+ (NSString *)bs_backtraceOfAllThread {
    // 记载当时一切线程的 port
    thread_act_array_t threads;
    // 记载当时线程的数量
    mach_msg_type_number_t thread_count = 0;
    // 当时的 task
    const task_t this_task = mach_task_self();
    // 获取当时一切线程和线程数量,分别记载在 threads 和 thread_count 中
    kern_return_t kr = task_threads(this_task, &threads, &thread_count);
    // 假如获取失败的话,回来过错信息
    if (kr != KERN_SUCCESS) {
        return @"Fail to get information of all threads";
    }
    // 调用栈回溯字符串的最初拼接上线程数量字符串
    NSMutableString *resultString = [NSMutableString stringWithFormat:@"Call Backtrace of %u threads:\n", thread_count];
    // 然后循环对一切的线程进行调用栈回溯,把回溯的字符串拼接在 resultString 中
    for (int i = 0; i < thread_count; i++) {
        [resultString appendString:_bs_backtraceOfThread(threads[i])];
    }
    // 回来成果
    return [resultString copy];
}

bs_machThreadFromNSThread(Convert NSThread to Mach thread )

 将 NSThread 转换为 Mach 线程(mach_port_t),其实是在当时 task 的一切线程中找到与指定 NSThread 对应的 Mach 线程。

#pragma -mark Convert NSThread to Mach thread
/// NSThread 转换为 thread_t 类型的 Mach 线程
/// @param nsthread NSThread 线程目标
thread_t bs_machThreadFromNSThread(NSThread *nsthread) {
    char name[256];
    // 用来存储当时 task 的线程数量
    mach_msg_type_number_t count;
    // 用来存储当时一切线程的 mach_port_t 的数组(typedef mach_port_t thread_t; mach_port_t 是 thread_t 的别号)
    // 这儿咱们按住 command 点击查看一下 thread_act_array_t 的实践类型:
    // 首要 `typedef thread_act_t *thread_act_array_t;` 看到 thread_act_array_t 是一个 thread_act_t 指针,
    // 然后 `typedef mach_port_t thread_act_t;` 即 list 实践便是一个 mach_port_t 数组,实践便是一个 thread_t 数组。
    thread_act_array_t list;
    // 调用 task_threads 函数依据当时的 task 来获取一切线程(线程端口),保存在 list 变量中,count 记载线程的总数量
    // mach_task_self() 表明获取当时的 Mach task,它的类型其实也是 mach_port_t,这儿牵涉到 macOS 中 Mach 微内核用户态和内核态的一些的常识点。
    // mach_task_self() 获取当时 task,看到该函数回来的类型也是 mach_port_t:extern mach_port_t mach_task_self_;
    // #define mach_task_self() mach_task_self_
    task_threads(mach_task_self(), &list, &count);
    // 当时时刻戳
    NSTimeInterval currentTimestamp = [[NSDate date] timeIntervalSince1970];
    // 取出 nsthread 的 name 记载在 originName 中(大约率是空字符串,假如没有给 thread 设置 name 的话),然后取当时的时刻戳作为 nsthread 的新姓名
    NSString *originName = [nsthread name];
    // 这儿除了把 nsthread 的姓名设置为时刻戳,也会把 nsthread 对应的 pthread_t 的姓名设置为同一个值
    [nsthread setName:[NSString stringWithFormat:@"%f", currentTimestamp]];
    // 假如 nsthread 是主线程的话直接回来在 +load 函数中获取的主线程的 mach_port_t
    if ([nsthread isMainThread]) {
        // 这儿直接把 mach_port_t 强制转换为了 thread_t,由于实践 `typedef mach_port_t thread_t;`,mach_port_t 便是 thread_t 的别号
        return (thread_t)main_thread_id;
    }
    // 遍历 list 数组中的 mach_port_t
    for (int i = 0; i < count; ++i) {
        // _Nullable pthread_t pthread_from_mach_thread_np(mach_port_t);
        // 调用 pthread_from_mach_thread_np 函数,从 mach_port_t 转换为 pthread_t,留意这儿是 pthread_t 比上面的 thread_t 多了一个 p
        pthread_t pt = pthread_from_mach_thread_np(list[i]);
        printf("✈️✈️✈️ \t%p\n", pt);
        // 这儿的再一次的 if ([nsthread isMainThread]) {} 判别,没看懂,上面不是有了一个判别了吗?
        // 假如是主线程的话,再次回来主线程对应的 mach_port_t
        if ([nsthread isMainThread]) {
            // 假如 mach_port_t 相等的话,直接回来 list[i] 即可
            if (list[i] == main_thread_id) {
                return list[i];
            }
        }
        // 获取 pt 的姓名,然后与 nsthread 进行比较,获得 nsthread 对应的 thread_t
        if (pt) {
            name[0] = '\0';
            // 获取 pthread_t 的姓名,保存在 name char 数组中
            pthread_getname_np(pt, name, sizeof name);
            printf(" \t%s\n", name);
            printf(" \t%s\n", [nsthread name].UTF8String);
            // strcmp 函数是 string compare(字符串比较)的缩写,用于比较两个字符串并依据比较成果回来整数。基本形式为 strcmp(str1,str2),若 str1=str2,则回来零;若 str1<str2,则回来负数;若str1>str2,则回来正数。
            // 假如两者相等,表明找到了 nsthread 对应的 Mach 线程,然后把 nsthread 康复原名,并回来 list[i]
            if (!strcmp(name, [nsthread name].UTF8String)) {
                [nsthread setName:originName];
                return list[i];
            }
        }
    }
    // 未找到 nsthread 对应的 Mach 线程,把 nsthread 康复为原名
    [nsthread setName:originName];
    // 回来当时线程的 port(`mach_port_t`)
    return mach_thread_self();
}

 上面的铺陈和铺排都做好了,下面看下最重要的进程:return _bs_backtraceOfThread(bs_machThreadFromNSThread(thread));

_bs_backtraceOfThread(Get call backtrace of a mach_thread)

NSString *_bs_backtraceOfThread(thread_t thread) 函数获取指定 Mach 线程(thread_t)的调用栈回溯字符串。

 这儿触及到一个比较复杂的结构体:_STRUCT_MCONTEXT machineContext;。 下面咱们以 x86_64 渠道为例看下它的具体内容,首要是依据当时是 64 位 CPU 架构所以其指向了 _STRUCT_MCONTEXT64 结构体,然后 _STRUCT_MCONTEXT64 结构体中嵌套了三个结构体,咱们只需求重视其间的 _STRUCT_X86_THREAD_STATE64 __ss; 结构体,由于在后续获取线程状况的函数 thread_get_state 中,只传递了 &machineContext->__ss 作为参数,__ss 对应了一组寄存器列表,咱们也能够直接把它们了解为线程的执行上下文。所以 _bs_backtraceOfThread 函数中请求的 machineContext 变量正是用来记载指定线程当时上下文信息的(寄存器列表值)。

#define _STRUCT_MCONTEXT _STRUCT_MCONTEXT64
#define _STRUCT_MCONTEXT64      struct __darwin_mcontext64
_STRUCT_MCONTEXT64
{
    _STRUCT_X86_EXCEPTION_STATE64   __es; // 反常状况记载
    _STRUCT_X86_THREAD_STATE64      __ss; // 线程上下文(寄存器列表)
    _STRUCT_X86_FLOAT_STATE64       __fs; // 浮点寄存器
};
#define    _STRUCT_X86_THREAD_STATE64    struct __darwin_x86_thread_state64
_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;
};
#define    _STRUCT_X86_FLOAT_STATE64    struct __darwin_x86_float_state64
_STRUCT_X86_FLOAT_STATE64
{
    int             __fpu_reserved[2];
    _STRUCT_FP_CONTROL    __fpu_fcw;        /* x87 FPU control word */
    _STRUCT_FP_STATUS    __fpu_fsw;        /* x87 FPU status word */
    __uint8_t        __fpu_ftw;        /* x87 FPU tag word */
    __uint8_t        __fpu_rsrv1;        /* reserved */ 
    __uint16_t        __fpu_fop;        /* x87 FPU Opcode */
    /* x87 FPU Instruction Pointer */
    __uint32_t        __fpu_ip;        /* offset */
    __uint16_t        __fpu_cs;        /* Selector */
    __uint16_t        __fpu_rsrv2;        /* reserved */
    /* x87 FPU Instruction Operand(Data) Pointer */
    __uint32_t        __fpu_dp;        /* offset */
    __uint16_t        __fpu_ds;        /* Selector */
    ...
};

 下面咱们持续看 _bs_backtraceOfThread 函数的内容。

NSString *_bs_backtraceOfThread(thread_t thread) {
    // 默许栈深度是 50
    uintptr_t backtraceBuffer[50];
    int i = 0;
    // 调用栈回溯字符串变量 resultString,默许最初都是 thread ID(port)
    NSMutableString *resultString = [[NSMutableString alloc] initWithFormat:@"Backtrace of Thread %u:\n", thread];
    // 针对不同渠道的机器声明一个用来存储线程上下文的变量
    _STRUCT_MCONTEXT machineContext;
    // 1⃣️ 获取指定线程的上下文信息,假如获取失败的话直接回来过错描绘
    if(!bs_fillThreadStateIntoMachineContext(thread, &machineContext)) {
        return [NSString stringWithFormat:@"Fail to get information about thread: %u", thread];
    }
    // 2⃣️ 获取 __rip 寄存器的值(对应 ARM 架构下 PC 寄存器的值)
    const uintptr_t instructionAddress = bs_mach_instructionAddress(&machineContext);
    // 把 PC 寄存器的值记载在回溯缓冲数组中...
    backtraceBuffer[i] = instructionAddress;
    ++i;
    // FP(x29) 栈底 SP 栈顶 PC 下一条指令 LR(x30) 函数回来后的下一个函数的第一条指令
    // x29(FP) 栈底寄存器 SP 栈顶寄存器 LR(x30)是当时函数回来后,下一个函数的第一条指令 PC 下一条指令
    // 3⃣️ 读取 LR 寄存器的值,只要 ARM 渠道有,x86 渠道回来 0
    uintptr_t linkRegister = bs_mach_linkRegister(&machineContext);
    if (linkRegister) {
        // 把 LR 寄存器的值记载在回溯缓冲数组中...
        backtraceBuffer[i] = linkRegister;
        i++;
    }
    // 假如 instructionAddress 为 0 的话,即获取 PC 寄存器的值为 0,则回来一个过错字符串,感觉这个判别应该放在上面获取后直接判别吧,没必要读了 LR 寄存器再判别吧!
    if (instructionAddress == 0) {
        return @"Fail to get instruction address";
    }
    // 创立一个栈帧节点
    BSStackFrameEntry frame = {0};
    // 4⃣️ 获得 FP 栈底寄存器的值
    const uintptr_t framePtr = bs_mach_framePointer(&machineContext);
    // 5⃣️ bs_mach_copyMem 函数内部对 vm_read_overwrite 函数进行封装。
    // 运用 vm_read_overwrite() 函数,从目标进程 "读取" 内存。
    // 留意,这个函数与 vm_read() 不同,应该并没有做实践的数据仿制,而是将 [region.address ~ region.address + region.size] 规模对应的一切映射状况同步给了 [region_data ~ region_data + region.size],关于 Resident 的部分,两个进程中不同的虚拟内存地址对应的应该是相同的物理内存地址。
    // 假如 framePtr 等于 0 或许以 framePtr 为开端地址,仿制 sizeof(frame) 个长度的虚拟内存的数据到 frame 指针中去失败,则回来过错描绘,
    // 这儿 frame 变量是 struct BSStackFrameEntry 类型的结构体,它内部一个指针,一个无符号 long 变量,所以 sizeof(frame) 的值为 16,
    // 即这儿的作用是把 FP 栈底寄存器的值和 SP 栈顶寄存器的值仿制到 frame 中
    if (framePtr == 0 || bs_mach_copyMem((void *)framePtr, &frame, sizeof(frame)) != KERN_SUCCESS) {
        return @"Fail to get frame pointer";
    }
    // 循环 50 次,沿着栈底指针构建一个链表,链表的每个节点都是每个调用帧的栈底指针,即前一个函数帧的开端地址
    for (; i < 50; i++) {
        backtraceBuffer[i] = frame.return_address;
        // 直到 FP 为 0,前一个 FP 指向 0,内存读取失败,跳出循环
        if (backtraceBuffer[i] == 0 || frame.previous == 0 || bs_mach_copyMem(frame.previous, &frame, sizeof(frame)) != KERN_SUCCESS) {
            break;
        }
    }
    // 准备一个值 i 的 backtraceLength 长度的 Dl_info 数组
    int backtraceLength = i;
    Dl_info symbolicated[backtraceLength];
    // 7⃣️ 查找 backtraceBuffer 数组中地址对应的符号信息
    bs_symbolicate(backtraceBuffer, symbolicated, backtraceLength, 0);
    // 遍历调用栈回溯中的函数字符串拼接在 resultString 字符串中
    for (int i = 0; i < backtraceLength; ++i) {
        // 8⃣️ 依据 `BsBacktraceLogger               0x10fa4359c -[ViewController bar] + 12` 这个格局,把调用栈的回溯字符串拼接在一起
        [resultString appendFormat:@"%@", bs_logBacktraceEntry(i, backtraceBuffer[i], &symbolicated[i])];
    }
    [resultString appendFormat:@"\n"];
    return [resultString copy];
}

 下面咱们具体一个一个看一下 _bs_backtraceOfThread 函数中触及的其他函数。

bs_fillThreadStateIntoMachineContext(HandleMachineContext)

 获取 thread 的状况赋值到 machineContext 参数(实践只给 &machineContext->__ss 赋值),bool 类型回来值表明是否获取成功/失败,&machineContext->__ss 结构体在上面也现已看过了,是记载寄存器列表的一个结构体。

bool bs_fillThreadStateIntoMachineContext(thread_t thread, _STRUCT_MCONTEXT *machineContext) {
    mach_msg_type_number_t state_count = BS_THREAD_STATE_COUNT;
    // #define BS_THREAD_STATE_COUNT x86_THREAD_STATE64_COUNT
    // typedef _STRUCT_X86_THREAD_STATE64 x86_thread_state64_t;
    // #define x86_THREAD_STATE64_COUNT ((mach_msg_type_number_t) ( sizeof (x86_thread_state64_t) / sizeof (int) ))
    // #define BS_THREAD_STATE x86_THREAD_STATE64
    // x86_THREAD_STATE64 值为 4
    // state_count 值为 42
    // 42 * 4 和 21 * 8 都等于 168 
    // 获取指定 thread 的上下文,并赋值在 &machineContext->__ss 参数中(寄存器列表)
    kern_return_t kr = thread_get_state(thread, BS_THREAD_STATE, (thread_state_t)&machineContext->__ss, &state_count);
    return (kr == KERN_SUCCESS);
}
// 获取栈底寄存器的值
uintptr_t bs_mach_framePointer(mcontext_t const machineContext){
    return machineContext->__ss.BS_FRAME_POINTER;
}
// 获取栈顶寄存器的值
uintptr_t bs_mach_stackPointer(mcontext_t const machineContext){
    return machineContext->__ss.BS_STACK_POINTER;
}
// 获取 x86 渠道下 IP 寄存器的值,对应 ARM 架构下 PC 寄存器的值
uintptr_t bs_mach_instructionAddress(mcontext_t const machineContext){
    return machineContext->__ss.BS_INSTRUCTION_ADDRESS;
}
// 读取 LR 寄存器的值,LR 是当时函数结束后,下一个函数的第一条指令。x86 渠道没有这个寄存器,只要 ARM 渠道才有
uintptr_t bs_mach_linkRegister(mcontext_t const machineContext){
#if defined(__i386__) || defined(__x86_64__)
    return 0;
#else
    return machineContext->__ss.__lr;
#endif
}
// 仿制当时 task 指定方位的指定长度的虚拟内存空间中的内容,首要用于仿制寄存器空间中的值
kern_return_t bs_mach_copyMem(const void *const src, void *const dst, const size_t numBytes){
    vm_size_t bytesCopied = 0;
    return vm_read_overwrite(mach_task_self(), (vm_address_t)src, (vm_size_t)numBytes, (vm_address_t)dst, &bytesCopied);
}

 这部分的代码全和线程状况有关,读取线程寄存器的值。

bs_symbolicate(Symbolicate)

 bs_symbolicate 符号化,把指定地址进行符号化,即找到指定地址所对应的符号信息。

 这儿咱们首要看一下它的参数:const uintptr_t* const backtraceBuffer 这个是栈底指针的数组,也是每个函数调用栈开端地址的指针,咱们便是依据这个指针去 Image 中查找最接近的符号。Dl_info* const symbolsBuffer 是一个 Dl_info 数组(Dl_info symbolicated[backtraceLength]):

/*
 * Structure filled in by dladdr().
 */
typedef struct dl_info {
        const char      *dli_fname;     /* Pathname of shared object */
        void            *dli_fbase;     /* Base address of shared object */
        const char      *dli_sname;     /* Name of nearest symbol */
        void            *dli_saddr;     /* Address of nearest symbol */
} Dl_info;

 Dl_info 是咱们获取指定符号时会用到的结构体,然后 const int numEntriesconst uintptr_t* const backtraceBuffer 数组的长度。const int skippedEntries 可直接疏忽,它传递的是 0。

void bs_symbolicate(const uintptr_t* const backtraceBuffer,
                    Dl_info* const symbolsBuffer,
                    const int numEntries,
                    const int skippedEntries){
    int i = 0;
    // _bs_backtraceOfThread 函数内调用 bs_symbolicate 函数时,skippedEntries 传的到都是 0 可疏忽
    if (!skippedEntries && i < numEntries) {
        // 查找指定地址 address 最接近的符号的信息
        bs_dladdr(backtraceBuffer[i], &symbolsBuffer[i]);
        i++;
    }
    // 然后遍历查找指定地址 address 最接近的符号的信息,
    for (; i < numEntries; i++) {
        // CALL_INSTRUCTION_FROM_RETURN_ADDRESS(backtraceBuffer[i]) 去掉地址最终两位的地址签名,并数组偏移 1 个元素
        bs_dladdr(CALL_INSTRUCTION_FROM_RETURN_ADDRESS(backtraceBuffer[i]), &symbolsBuffer[i]);
    }
}
// 查找指定地址 address 最接近的符号的信息
bool bs_dladdr(const uintptr_t address, Dl_info* const info) {
    info->dli_fname = NULL;
    info->dli_fbase = NULL;
    info->dli_sname = NULL;
    info->dli_saddr = NULL;
    // 判别一个指定地址是否在当时现已加载的某个 Image 中并回来该 Image 在 _dyld_image_count 数值中的索引,即获得指定地址在某个 image 中并回来此 image 的索引
    const uint32_t idx = bs_imageIndexContainingAddress(address);
    // 假如回来 UINT_MAX 表明在当时现已加载的 Image 镜像中找不到 address 地址
    if (idx == UINT_MAX) {
        return false;
    }
    // 获得此 Image 镜像的 header 地址
    const struct mach_header* header = _dyld_get_image_header(idx);
    // 获得此 Image 内存地址的 slide 值
    const uintptr_t imageVMAddrSlide = (uintptr_t)_dyld_get_image_vmaddr_slide(idx);
    // 获得此 Image 内存地址的基地址
    const uintptr_t addressWithSlide = address - imageVMAddrSlide;
    // 获得 Image 在当时可执行文件中的虚拟地址的基地址然后加上 Slide
    const uintptr_t segmentBase = bs_segmentBaseOfImageIndex(idx) + imageVMAddrSlide;
    if (segmentBase == 0) {
        return false;
    }
    // Image 的姓名赋值给 dli_fname,实践的值是 Image 的完整路径
    info->dli_fname = _dyld_get_image_name(idx);
    // Base address of shared object
    info->dli_fbase = (void*)header;
    // Find symbol tables and get whichever symbol is closest to the address.
    // 查找符号表并获取最接近地址的符号
    // #define BS_NLIST struct nlist_64
    // 符号表中的每个元素正是这个 struct nlist_64/nlist 结构体
    const BS_NLIST* bestMatch = NULL;
    // 无符号 long 最大值
    uintptr_t bestDistance = ULONG_MAX;
    // 针对 64 位和非 64 位的可执行文件,内部的 +1 是跳过 __PAGEZERO 段
    uintptr_t cmdPtr = bs_firstCmdAfterHeader(header);
    if (cmdPtr == 0) {
        return false;
    }
    // 遍历 Image 的 Load Command
    for (uint32_t iCmd = 0; iCmd < header->ncmds; iCmd++) {
        const struct load_command* loadCmd = (struct load_command*)cmdPtr;
        // #define LC_SYMTAB 0x2 /* link-edit stab symbol table info */
        // 找到 LC_SYMTAB 段,
        if (loadCmd->cmd == LC_SYMTAB) {
// The symtab_command contains the offsets and sizes of the link-edit 4.3 BSD "stab" style symbol table information as described in the header files <nlist.h> and <stab.h>.
//            struct symtab_command {
//                uint32_t    cmd;        /* LC_SYMTAB */
//                uint32_t    cmdsize;    /* sizeof(struct symtab_command) */
//                uint32_t    symoff;        /* symbol table offset */
//                uint32_t    nsyms;        /* number of symbol table entries */
//                uint32_t    stroff;        /* string table offset */
//                uint32_t    strsize;    /* string table size in bytes */
//            };
            // 由于 loadCmd 是 LC_SYMTAB 类型,所以这儿可直接把 cmdPtr 强制转换为 struct symtab_command * 指针
            const struct symtab_command* symtabCmd = (struct symtab_command*)cmdPtr;
            // 直接基地址 + symbol table 偏移,获得符号表的首地址,且符号表中正是 struct nlist/nlist_64 类型数组,所以这儿直接强转为 BS_NLIST 指针
            const BS_NLIST* symbolTable = (BS_NLIST*)(segmentBase + symtabCmd->symoff);
            // 然后直接基地址 + string table 偏移,获得保存符号姓名符串的表的开端地址
            const uintptr_t stringTable = segmentBase + symtabCmd->stroff;
            // 然后对符号表中的符号进行遍历,找到最接近 address 的符号,保存在 bestMatch 变量中,
            for (uint32_t iSym = 0; iSym < symtabCmd->nsyms; iSym++) {
//                This is the symbol table entry structure for 64-bit architectures. 这是 64 位体系结构的符号表条目结构。
//                struct nlist_64 {
//                    union {
//                        uint32_t  n_strx; /* index into the string table */
//                    } n_un;
//                    uint8_t n_type;        /* type flag, see below */
//                    uint8_t n_sect;        /* section number or NO_SECT */
//                    uint16_t n_desc;       /* see <mach-o/stab.h> */
//                    uint64_t n_value;      /* value of this symbol (or stab offset) */
//                };
                // If n_value is 0, the symbol refers to an external object.
                // 假如 n_value 为 0,则该符号引用外部目标
                if (symbolTable[iSym].n_value != 0) {
                    // 获得当时符号的地址
                    uintptr_t symbolBase = symbolTable[iSym].n_value;
                    // 这儿没太了解,用 addressWithSlide 减去 symbolBase,理论上 addressWithSlide 的值应该会小于 symbolBase,硬减的话会得到一个负值,然后由于 currentDistance 是一个无符号 long,
                    // 所以这儿 currentDistance 的值是减法溢出后转换为无符号 long,
                    // 这儿遍历符号,每次记载当时符号和指定地址的间隔,记载下来
                    uintptr_t currentDistance = addressWithSlide - symbolBase;
                    // 然后记载到一个最接近指定地址的符号(每遇到一个最近的间隔值就更新一下 bestDistance 的值)
                    if ((addressWithSlide >= symbolBase) && (currentDistance <= bestDistance)) {
                        bestMatch = symbolTable + iSym;
                        bestDistance = currentDistance;
                    }
                }
            }
            // 找到 bestMatch 时,记载下当时 Image 的:
            if (bestMatch != NULL) {
                // dli_saddr 最近符号的地址
                info->dli_saddr = (void*)(bestMatch->n_value + imageVMAddrSlide);
                // dli_sname 最近符号的称号
                info->dli_sname = (char*)((intptr_t)stringTable + (intptr_t)bestMatch->n_un.n_strx);
                if (*info->dli_sname == '_') {
                    info->dli_sname++;
                }
                // This happens if all symbols have been stripped.
                // 假如一切符号都已被剥离,则会发生这种状况。
                if(info->dli_saddr == info->dli_fbase && bestMatch->n_type == 3) {
                    info->dli_sname = NULL;
                }
                break;
            }
        }
        // 偏移到下一个 Load Command
        cmdPtr += loadCmd->cmdsize;
    }
    return true;
}
/// 针对 64 位和非 64 位的可执行文件,这儿的 +1 是跳过 __PAGEZERO 段
/// @param header Image header
uintptr_t bs_firstCmdAfterHeader(const struct mach_header* const header) {
    switch(header->magic) {
        case MH_MAGIC:
        case MH_CIGAM:
            return (uintptr_t)(header + 1);
        case MH_MAGIC_64:
        case MH_CIGAM_64:
            return (uintptr_t)(((struct mach_header_64*)header) + 1);
        default:
            return 0;  // Header is corrupt
    }
}
/// 判别一个指定地址是否在当时现已加载的某个 Image 中并回来该 Image 在 _dyld_image_count 数值中的索引,即获得指定地址在某个 image 中并回来此 image 的索引
/// @param address 指定地址
uint32_t bs_imageIndexContainingAddress(const uintptr_t address) {
    // 当时 dyld 加载的 Image 镜像的数量
    const uint32_t imageCount = _dyld_image_count();
    // image header 的指针
    const struct mach_header* header = 0;
    // 开端遍历这些 Image 镜像
    for(uint32_t iImg = 0; iImg < imageCount; iImg++) {
        // 获得当时这个 image 的 header 指针
        header = _dyld_get_image_header(iImg);
        if (header != NULL) {
            // Look for a segment command with this address within its range.
            // address 减去 image 的 slide 随机值,获得它的基地址
            uintptr_t addressWSlide = address - (uintptr_t)_dyld_get_image_vmaddr_slide(iImg);
            // 当时 image 的第一个段的地址(撇开 __PAGEZERO 段)
            uintptr_t cmdPtr = bs_firstCmdAfterHeader(header);
            if (cmdPtr == 0) {
                continue;
            }
            // 然后再开端遍历这个 Image 中的一切 Load Command
            for (uint32_t iCmd = 0; iCmd < header->ncmds; iCmd++) {
                // 强转为 struct load_command * 指针
                const struct load_command* loadCmd = (struct load_command*)cmdPtr;
                // 然后仅需求遍历 LC_SEGMENT/LC_SEGMENT_64 类型的段
                if (loadCmd->cmd == LC_SEGMENT) {
                    // 强转为 struct segment_command * 指针
                    const struct segment_command* segCmd = (struct segment_command*)cmdPtr;
                    // 然后判别 addressWSlide 是否在这个段的虚拟地址的规模内
                    if (addressWSlide >= segCmd->vmaddr && addressWSlide < segCmd->vmaddr + segCmd->vmsize) {
                        // 假如在的话直接回来此 Image 的索引
                        return iImg;
                    }
                } else if (loadCmd->cmd == LC_SEGMENT_64) {
                    // 强转为 struct segment_command_64 * 指针
                    const struct segment_command_64* segCmd = (struct segment_command_64*)cmdPtr;
                    // 然后判别 addressWSlide 是否在这个段的虚拟地址的规模内
                    if (addressWSlide >= segCmd->vmaddr && addressWSlide < segCmd->vmaddr + segCmd->vmsize) {
                        // 假如在的话直接回来此 Image 的索引
                        return iImg;
                    }
                }
                // 偏移当时 cmd 的宽度,到下一个 Load Command
                cmdPtr += loadCmd->cmdsize;
            }
        }
    }
    // 假如未找到的话,就回来无符号 Int 的最大值
    return UINT_MAX;
}
/// 获得指定索引的 Image 的 __LINKEDIT 段的虚拟地址减去 fileoff(file offset of this segment),得出此 Image 的虚拟基地址,
/// 这儿为什么必定要用 __LINKEDIT 段没看理解,我运用 MachOView 查看了一下可执行文件,如下,看到运用其它几个段的 VM Address 减去 File Offset 得到的值是相同的,都是 4294967296
///
/// __TEXT: VM Address: 4294967296,File Offset: 0
/// __DATA_CONST: VM Address: 4295000064,File Offset: 32768 => 4295000064 - 32768 = 4294967296
/// __DATA: VM Address: 4295016448,File Offset: 49152 => 4295016448 - 49152 = 4294967296
/// __LINKEDIT: VM Address: 4295032832,File Offset: 65536 => 4295032832 - 65536 = 4294967296
///
/// @param idx image 索引
uintptr_t bs_segmentBaseOfImageIndex(const uint32_t idx) {
    const struct mach_header* header = _dyld_get_image_header(idx);
    // Look for a segment command and return the file image address.
    // typedef unsigned long uintptr_t;
    // 获得 image 的第一个段(撇掉 __PAGEZERO 段)的地址,并把 struct mach_header_64 * 指针强转为了 uintptr_t(无符号 long)
    uintptr_t cmdPtr = bs_firstCmdAfterHeader(header);
    if (cmdPtr == 0) {
        return 0;
    }
    // 遍历一切的 Load Command
    for (uint32_t i = 0;i < header->ncmds; i++) {
        const struct load_command* loadCmd = (struct load_command*)cmdPtr;
        // 仅排查类型是 LC_SEGMENT 和 LC_SEGMENT_64 类型的 Load Command,并找到 __LINKEDIT 姓名的段,计算出虚拟基地址并回来
        if (loadCmd->cmd == LC_SEGMENT) {
            // #define SEG_LINKEDIT "__LINKEDIT"
            // 获得段名是 __LINKEDIT 的段的虚拟基地址
            // 把地址强转为 struct segment_command * 指针
            const struct segment_command* segmentCmd = (struct segment_command*)cmdPtr;
            // the segment containing all structs created and maintained by the link editor.
            // Created with -seglinkedit option to ld(1) for MH_EXECUTE and FVMLIB file types only
            // #define SEG_LINKEDIT "__LINKEDIT"
            if (strcmp(segmentCmd->segname, SEG_LINKEDIT) == 0) {
                return segmentCmd->vmaddr - segmentCmd->fileoff;
            }
        } else if(loadCmd->cmd == LC_SEGMENT_64) {
            // 64 位的状况
            const struct segment_command_64* segmentCmd = (struct segment_command_64*)cmdPtr;
            // 获得段名是 __LINKEDIT 的段的虚拟基地址
            if (strcmp(segmentCmd->segname, SEG_LINKEDIT) == 0) {
                return (uintptr_t)(segmentCmd->vmaddr - segmentCmd->fileoff);
            }
        }
        // 依据当时段的大小宽度:cmdsize 偏移到下一个段
        cmdPtr += loadCmd->cmdsize;
    }
    // 未找到的话就回来 0
    return 0;
}

bs_logBacktraceEntry(GenerateBacbsrackEnrty)

 传入函数地址和此地址对应的符号信息,然后生成对应的 Image 和 函数姓名的字符串,相似这种格局:BsBacktraceLogger 0x10fa4359c -[ViewController bar] + 12

NSString* bs_logBacktraceEntry(const int entryNum,
                               const uintptr_t address,
                               const Dl_info* const dlInfo) {
    char faddrBuff[20];
    char saddrBuff[20];
    // 首要获得 Image 的姓姓名符串,例如:BsBacktraceLogger、UIKitCore、CoreFoundation 等等
    const char* fname = bs_lastPathEntry(dlInfo->dli_fname);
    // 假如 fname 为 NULL,则把 dlInfo->dli_fbase 作为 fname 运用
    if (fname == NULL) {
        // #define POINTER_FMT "0x%016lx"
        // 按指针格局把 (uintptr_t)dlInfo->dli_fbase 写入 faddrBuff 中
        sprintf(faddrBuff, POINTER_FMT, (uintptr_t)dlInfo->dli_fbase);
        // 把地址作为 fname 的姓名
        fname = faddrBuff;
    }
    // address 减去最近符号的地址,即得到 address 对应符号的偏移地址
    uintptr_t offset = address - (uintptr_t)dlInfo->dli_saddr;
    // dlInfo->dli_sname 获得最近符号的称号
    const char* sname = dlInfo->dli_sname;
    // 假如符号姓名为 NULL 的话,则相同把 dlInfo->dli_fbase 作为 sname 运用
    if (sname == NULL) {
        // #define POINTER_SHORT_FMT "0x%lx"
        // 这儿运用的是 0x%lx 格局
        sprintf(saddrBuff, POINTER_SHORT_FMT, (uintptr_t)dlInfo->dli_fbase);
        sname = saddrBuff;
        offset = address - (uintptr_t)dlInfo->dli_fbase;
    }
    // 拼装栈帧字符串,例如:UIKitCore                       0x7fff2489d02c -[UIViewController view] + 27 这样的格局
    // 首要是 Image 的姓名,然后是符号的地址,然后是函数符号的姓名,最终是函数符号的偏移长度也便是函数内容的宽度吧好像
    return [NSString stringWithFormat:@"%-30s  0x%08" PRIxPTR " %s + %lu\n", fname, (uintptr_t)address, sname, offset];
}
// 获得指定 path 的最终一段,
// 例如:path 是:"xxx/Application/E3358B42-7325-4EA4-BD81-2210A0F4AC8F/BsBacktraceLogger.app/BsBacktraceLogger"
// 然后回来 BsBacktraceLogger
const char* bs_lastPathEntry(const char* const path) {
    if (path == NULL) {
        return NULL;
    }
    // char *strrchr(const char *__s, int __c);
    // 查找一个字符 __c 在另一个字符串 __s 中末次呈现的方位(也便是从 __s 的右侧开端查找字符 __c 首次呈现的方位),并回来这个方位的地址。
    // 假如未能找到指定字符,那么函数将回来 NULL。运用这个地址回来从最终一个字符 __c 到 __s 结尾的字符串。
    char* lastFile = strrchr(path, '/');
//    printf(" %s \n", lastFile);
    // +1 是去掉 lastFile 最前面的 / 符号
    return lastFile == NULL ? path : lastFile + 1;
}

 至此 BSBacktraceLogger 的源码咱们就悉数看完了,注释超具体,这儿就不再总结了!

参阅链接

参阅链接:

  • iOS开发–探究iOS线程调用栈及符号化
  • iOS内存扫描东西完成
  • 获取任意线程的调用栈
  • iOS中符号的那些事儿
  • bestswifter/BSBacktraceLogger
  • iOS——CPU监控
  • 深入解析Mac OS X & iOS 操作系统 学习笔记(十二)
  • 一个“反诘”引发的内存反思
  • BSBackTracelogger学习笔记
  • Swift仓库信息获取
  • MAC OS 的 mach_port_t 和 pthread_self()
  • iOS功能监控
  • syscall(SyS_gettid)是什么
  • pthread_self() VS syscall(SYS_gettid)