BSBacktraceLogger源码解析

17 阅读37分钟

借助AI辅助。

源码地址

github.com/bestswifter…

逐行注释

//
//  BSBacktraceLogger.m
//  BSBacktraceLogger
//
//  Created by 张星宇 on 16/8/27.
//  Copyright © 2016年 bestswifter. All rights reserved.
//

// ==================== 头文件引入说明 ====================

// 导入自定义头文件,包含对外公开的接口声明
#import "BSBacktraceLogger.h"

// 【Mach内核】导入Mach内核相关头文件
// Mach是XNU内核的核心部分,提供了底层的线程、任务、内存管理等功能
// 这个头文件包含了thread_t(线程类型)、task_t(任务/进程类型)、kern_return_t(内核返回值类型)等
#import <mach/mach.h>

// 【动态链接】导入动态链接库相关函数
// 主要包含dladdr()函数,用于将内存地址转换为符号信息(本代码自己实现了类似功能)
// Dl_info结构体定义也在这里,用于存储符号信息(文件名、函数名、地址等)
#include <dlfcn.h>

// 【POSIX线程】导入POSIX线程库(pthread)
// pthread是跨平台的线程标准,Mach线程是macOS/iOS特有的底层线程
// 本代码需要在NSThread、pthread和Mach线程之间进行转换
#include <pthread.h>

// 【系统类型】导入系统基础类型定义
// 如size_t、ssize_t等基础类型
#include <sys/types.h>

// 【限制值】导入系统限制值定义
// 如ULONG_MAX(无符号长整型最大值)、UINT_MAX(无符号整型最大值)等
#include <limits.h>

// 【字符串操作】导入C标准库字符串操作函数
// 如strcmp(字符串比较)、strrchr(查找字符)等
#include <string.h>

// 【动态链接器】导入dyld(dynamic linker)相关函数
// dyld负责加载和链接动态库,提供了获取已加载镜像(动态库/可执行文件)信息的函数
// 如_dyld_image_count()、_dyld_get_image_header()等
#include <mach-o/dyld.h>

// 【符号表】导入Mach-O文件格式中符号表相关的结构体定义
// nlist/nlist_64是符号表项的结构体,包含符号名、地址、类型等信息
#include <mach-o/nlist.h>

// ==================== CPU架构适配宏定义 ====================
// 不同CPU架构的寄存器名称和结构不同,需要通过条件编译来适配

#pragma -mark DEFINE MACRO FOR DIFFERENT CPU ARCHITECTURE

// 【ARM64架构】适用于iPhone 5s及以后的iOS设备、Apple Silicon Mac
#if defined(__arm64__)

// ARM64指令地址去标签宏
// 背景知识:ARM64架构中,指针的低位可能被用来存储额外信息(称为"标签位")
// 低2位通常用于标识指针类型或其他元数据,需要清除这些位才能得到真实地址
// ~(3UL) 等于 11111...11100(二进制),与操作后会清除低2位
#define DETAG_INSTRUCTION_ADDRESS(A) ((A) & ~(3UL))

// 线程状态结构体的元素数量
// ARM_THREAD_STATE64_COUNT 是系统定义的常量,表示ARM64架构下线程状态结构体包含多少个字段
#define BS_THREAD_STATE_COUNT ARM_THREAD_STATE64_COUNT

// 线程状态类型标识符
// 用于告诉thread_get_state()函数我们想要获取哪种类型的线程状态
#define BS_THREAD_STATE ARM_THREAD_STATE64

// 帧指针寄存器(Frame Pointer Register)
// __fp 是ARM64架构中的x29寄存器,专门用作帧指针
// 帧指针指向当前函数的栈帧起始位置,通过它可以遍历整个调用栈
#define BS_FRAME_POINTER __fp

// 栈指针寄存器(Stack Pointer Register)
// __sp 是ARM64架构中的SP寄存器,指向栈顶
// 栈是向下增长的,SP总是指向最后一个压入栈的数据
#define BS_STACK_POINTER __sp

// 程序计数器/指令指针寄存器(Program Counter)
// __pc 是ARM64架构中的PC寄存器,指向当前正在执行的指令地址
// 这是我们获取调用栈的起点
#define BS_INSTRUCTION_ADDRESS __pc

// 【ARM32架构】适用于iPhone 5及之前的旧设备(已基本淘汰)
#elif defined(__arm__)

// ARM32指令地址去标签宏
// ARM32的低1位用于标识Thumb模式(ARM有两种指令集:ARM和Thumb)
// 如果低位是1,表示Thumb模式;如果是0,表示ARM模式
// ~(1UL) 等于 11111...11110(二进制),与操作后会清除低1位
#define DETAG_INSTRUCTION_ADDRESS(A) ((A) & ~(1UL))

// 线程状态结构体的元素数量(32位ARM)
#define BS_THREAD_STATE_COUNT ARM_THREAD_STATE_COUNT

// 线程状态类型标识符(32位ARM)
#define BS_THREAD_STATE ARM_THREAD_STATE

// 帧指针寄存器(32位ARM使用r7寄存器作为帧指针)
// ARM32有16个通用寄存器r0-r15,其中r7在iOS中被约定用作帧指针
#define BS_FRAME_POINTER __r[7]

// 栈指针寄存器(r13,也称为SP)
#define BS_STACK_POINTER __sp

// 程序计数器寄存器(r15,也称为PC)
#define BS_INSTRUCTION_ADDRESS __pc

// 【x86_64架构】适用于64位Intel Mac、iOS模拟器
#elif defined(__x86_64__)

// x86架构的指令地址不需要去标签,直接返回原值
// x86架构不像ARM那样在指针中嵌入额外信息
#define DETAG_INSTRUCTION_ADDRESS(A) (A)

// 线程状态结构体的元素数量(x86_64)
#define BS_THREAD_STATE_COUNT x86_THREAD_STATE64_COUNT

// 线程状态类型标识符(x86_64)
#define BS_THREAD_STATE x86_THREAD_STATE64

// 帧指针寄存器(x86_64使用rbp寄存器)
// rbp是Base Pointer的缩写,专门用作帧指针
#define BS_FRAME_POINTER __rbp

// 栈指针寄存器(rsp = Stack Pointer)
#define BS_STACK_POINTER __rsp

// 指令指针寄存器(rip = Instruction Pointer)
// x86_64使用rip而不是pc来表示指令指针
#define BS_INSTRUCTION_ADDRESS __rip

// 【i386架构】适用于32位Intel Mac、旧的iOS模拟器(已基本淘汰)
#elif defined(__i386__)

// x86架构不需要去标签
#define DETAG_INSTRUCTION_ADDRESS(A) (A)

// 线程状态结构体的元素数量(i386)
#define BS_THREAD_STATE_COUNT x86_THREAD_STATE32_COUNT

// 线程状态类型标识符(i386)
#define BS_THREAD_STATE x86_THREAD_STATE32

// 帧指针寄存器(32位x86使用ebp)
// ebp是Extended Base Pointer的缩写
#define BS_FRAME_POINTER __ebp

// 栈指针寄存器(esp = Extended Stack Pointer)
#define BS_STACK_POINTER __esp

// 指令指针寄存器(eip = Extended Instruction Pointer)
#define BS_INSTRUCTION_ADDRESS __eip

#endif

// ==================== 通用宏定义 ====================

// 【返回地址转调用指令地址】
// 背景知识:当函数A调用函数B时:
//   1. CPU会将"返回地址"(return address)压入栈,这个地址指向函数A中CALL指令的下一条指令
//   2. 如果我们想知道是在哪里调用的函数B,需要找到CALL指令本身的地址
//   3. CALL指令的地址 = 返回地址 - 1(或更多,取决于指令长度)
// 这个宏就是做这个转换:先去标签,然后减1
#define CALL_INSTRUCTION_FROM_RETURN_ADDRESS(A) (DETAG_INSTRUCTION_ADDRESS((A)) - 1)

// 【根据指针大小定义格式化字符串】
// __LP64__ 表示"Long and Pointer are 64-bit",用于区分32位和64位系统
#if defined(__LP64__)

// 64位系统的格式定义
// 堆栈跟踪输出格式示例:
// 0   MyApp                           0x0000000100001234 main + 52
// %-4d:序号,左对齐,占4个字符
// %-31s:模块名,左对齐,占31个字符
// 0x%016lx:地址,16位十六进制(前面补0)
// %s:符号名(函数名)
// %lu:偏移量(无符号长整型)
#define TRACE_FMT         "%-4d%-31s 0x%016lx %s + %lu"

// 指针的完整格式:0x0000000100001234(16位十六进制,前面补0)
#define POINTER_FMT       "0x%016lx"

// 指针的短格式:0x100001234(不补0)
#define POINTER_SHORT_FMT "0x%lx"

// 符号表结构体类型(64位)
// nlist_64包含:符号名索引、符号类型、段索引、描述符、符号地址(64位)
#define BS_NLIST struct nlist_64

#else

// 32位系统的格式定义
// 输出格式示例:
// 0   MyApp                           0x00001234 main + 52
#define TRACE_FMT         "%-4d%-31s 0x%08lx %s + %lu"

// 指针的完整格式:0x00001234(8位十六进制)
#define POINTER_FMT       "0x%08lx"

// 指针的短格式
#define POINTER_SHORT_FMT "0x%lx"

// 符号表结构体类型(32位)
// nlist包含:符号名索引、符号类型、段索引、描述符、符号地址(32位)
#define BS_NLIST struct nlist

#endif

// ==================== 数据结构定义 ====================

// 【栈帧结构体】
// 背景知识:什么是栈帧(Stack Frame)?
// 每次函数调用时,系统会在栈上分配一块内存区域,称为"栈帧",用于存储:
//   - 函数的局部变量
//   - 函数参数
//   - 返回地址(调用者的下一条指令)
//   - 前一个栈帧的地址(用于回溯)
//
// 栈帧通过帧指针(Frame Pointer)连接成链表结构:
//   main的栈帧 ← funcA的栈帧 ← funcB的栈帧 ← 当前函数栈帧
//
// BSStackFrameEntry就是这个链表节点的简化版本,只包含:
//   - previous: 指向调用者(上一个函数)的栈帧
//   - return_address: 返回到调用者的地址
typedef struct BSStackFrameEntry{
    // 指向前一个栈帧的指针(const表示这个指针本身不可修改)
    // 通过这个指针可以一层层往回遍历,直到找到main函数甚至更底层
    const struct BSStackFrameEntry *const previous;
    
    // 返回地址:当前函数执行完毕后,应该返回到哪里继续执行
    // 这个地址指向调用者的代码中,CALL指令的下一条指令
    // uintptr_t是无符号整型,其大小与指针相同(32位系统是32位,64位系统是64位)
    const uintptr_t return_address;
} BSStackFrameEntry;

// ==================== 静态全局变量 ====================

// 【主线程ID】
// mach_port_t 是Mach内核中的"端口"类型,用于进程间通信
// 在Mach中,每个线程都有一个唯一的端口ID(实际上就是一个整数)
// 这个变量在+load方法中初始化,保存主线程的Mach端口ID,用于后续快速识别主线程
static mach_port_t main_thread_id;

// ==================== 类实现 ====================

@implementation BSBacktraceLogger

// 【类加载方法】
// +load方法的特点:
//   1. 在程序启动时,类被加载到内存时自动调用(比main函数还早)
//   2. 每个类只调用一次
//   3. 即使类没有被使用也会调用
//   4. 调用顺序:父类 → 子类 → 分类
+ (void)load {
    // mach_thread_self() 返回当前线程的Mach端口ID
    // 因为+load在主线程中执行,所以这里获取的就是主线程ID
    // 保存这个ID是因为后续判断主线程时比较方便(直接比较ID,而不需要通过名称匹配)
    main_thread_id = mach_thread_self();
}

#pragma -mark Implementation of interface
// ==================== 公共接口实现 ====================

// 【获取指定NSThread的调用栈】
// 参数:thread - NSThread对象(OC层的线程抽象)
// 返回:格式化的调用栈字符串
+ (NSString *)bs_backtraceOfNSThread:(NSThread *)thread {
    // 分两步:
    // 1. bs_machThreadFromNSThread: 将OC的NSThread转换为底层的Mach线程ID
    // 2. _bs_backtraceOfThread: 根据Mach线程ID获取调用栈
    // 为什么要转换?因为真正的线程操作需要使用Mach内核API,它只认Mach线程ID
    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]];
}

// 【获取所有线程的调用栈】
// 应用场景:
//   1. 死锁检测 - 查看所有线程状态,分析是否存在相互等待
//   2. 性能分析 - 定期采样所有线程,统计CPU热点
//   3. 崩溃日志 - 在崩溃时记录所有线程状态
+ (NSString *)bs_backtraceOfAllThread {
    // thread_act_array_t 是线程ID数组的类型(实际上是 thread_t* 指针)
    thread_act_array_t threads;
    
    // mach_msg_type_number_t 是Mach消息类型的数量类型(实际上是unsigned int)
    mach_msg_type_number_t thread_count = 0;
    
    // task_t 是Mach中"任务"的类型,任务就是进程的Mach术语
    // mach_task_self() 返回当前进程的任务端口
    const task_t this_task = mach_task_self();
    
    // 【task_threads函数】获取指定任务(进程)的所有线程
    // 参数1:任务端口(进程)
    // 参数2:输出参数,返回线程ID数组
    // 参数3:输出参数,返回线程数量
    // 返回值:kern_return_t 是内核函数的返回值类型,KERN_SUCCESS表示成功
    kern_return_t kr = task_threads(this_task, &threads, &thread_count);
    
    // 检查是否成功获取线程列表
    if(kr != KERN_SUCCESS) {
        return @"Fail to get information of all threads";
    }
    
    // 创建可变字符串,用于拼接所有线程的回溯信息
    // %u 是无符号整型格式符
    NSMutableString *resultString = [NSMutableString stringWithFormat:@"Call Backtrace of %u threads:\n", thread_count];
    
    // 遍历所有线程
    for(int i = 0; i < thread_count; i++) {
        // threads[i] 是第i个线程的Mach端口ID
        // 获取该线程的调用栈并追加到结果字符串
        [resultString appendString:_bs_backtraceOfThread(threads[i])];
    }
    
    // 返回不可变副本(防止外部修改)
    return [resultString copy];
}

#pragma -mark Get call backtrace of a mach_thread
// ==================== 核心:获取Mach线程的调用栈回溯 ====================

// 【核心函数】根据Mach线程ID获取调用栈
// 参数:thread - thread_t类型,即Mach线程的端口ID(实际上是一个整数)
// 返回:格式化的调用栈字符串
NSString *_bs_backtraceOfThread(thread_t thread) {
    // 【步骤1:准备缓冲区】
    // 创建一个数组,用于存储最多50层的函数调用地址
    // uintptr_t 是"unsigned integer pointer type"的缩写,保证能容纳一个指针
    // 为什么是50?这是一个经验值,通常调用栈不会超过50层
    uintptr_t backtraceBuffer[50];
    
    // 当前处理到第几层调用栈
    int i = 0;
    
    // 创建结果字符串,包含线程ID(用于识别是哪个线程)
    NSMutableString *resultString = [[NSMutableString alloc] initWithFormat:@"Backtrace of Thread %u:\n", thread];
    
    // 【步骤2:获取线程的寄存器状态】
    // _STRUCT_MCONTEXT 是机器上下文结构体,包含所有寄存器的当前值
    // mcontext = machine context(机器上下文)
    // 为什么需要它?因为调用栈的起点是当前寄存器的值(PC、FP、SP等)
    _STRUCT_MCONTEXT machineContext;
    
    // 调用函数获取线程状态并填充到machineContext中
    // 如果失败(比如线程已销毁、权限不足等),返回错误信息
    if(!bs_fillThreadStateIntoMachineContext(thread, &machineContext)) {
        return [NSString stringWithFormat:@"Fail to get information about thread: %u", thread];
    }
    
    // 【步骤3:提取当前指令地址(PC寄存器)】
    // PC(Program Counter)= 程序计数器,指向当前正在执行的指令
    // 这是调用栈的第0层(最内层,即当前函数)
    const uintptr_t instructionAddress = bs_mach_instructionAddress(&machineContext);
    backtraceBuffer[i] = instructionAddress;
    ++i;
    
    // 【步骤4:提取链接寄存器(LR,仅ARM架构)】
    // LR(Link Register)= 链接寄存器,ARM架构特有
    // 背景知识:ARM架构中,当函数A调用函数B时:
    //   - 返回地址不是压入栈,而是保存在LR寄存器中(为了提高性能)
    //   - 如果函数B还要调用函数C,那么会先把LR的值压入栈,再更新LR
    // 所以LR通常包含直接调用者的地址(调用栈的第1层)
    uintptr_t linkRegister = bs_mach_linkRegister(&machineContext);
    if (linkRegister) {
        backtraceBuffer[i] = linkRegister;
        i++;
    }
    
    // 【安全检查】确保指令地址有效
    if(instructionAddress == 0) {
        return @"Fail to get instruction address";
    }
    
    // 【步骤5:开始遍历栈帧链表】
    // 初始化栈帧结构体(全部清零)
    BSStackFrameEntry frame = {0};
    
    // 获取帧指针(FP寄存器的值)
    // FP指向当前函数的栈帧,栈帧的开头就是BSStackFrameEntry结构体
    const uintptr_t framePtr = bs_mach_framePointer(&machineContext);
    
    // 检查帧指针是否有效,并尝试读取第一个栈帧
    // bs_mach_copyMem是安全的内存读取函数(使用内核API,可以读取其他线程的内存)
    if(framePtr == 0 ||
       bs_mach_copyMem((void *)framePtr, &frame, sizeof(frame)) != KERN_SUCCESS) {
        return @"Fail to get frame pointer";
    }
    
    // 【步骤6:循环遍历栈帧链表】
    // 栈帧链表:当前栈帧 → 调用者栈帧 → 调用者的调用者栈帧 → ... → main → _start
    for(; i < 50; i++) {
        // 保存当前栈帧的返回地址(即"从哪里调用过来的")
        backtraceBuffer[i] = frame.return_address;
        
        // 终止条件(满足任一条件就停止遍历):
        // 1. 返回地址为0 - 已经到达栈底
        // 2. 前一个栈帧指针为0 - 没有更上层的调用者了
        // 3. 无法读取前一个栈帧 - 内存访问失败(可能栈帧已损坏)
        if(backtraceBuffer[i] == 0 ||
           frame.previous == 0 ||
           bs_mach_copyMem(frame.previous, &frame, sizeof(frame)) != KERN_SUCCESS) {
            break;
        }
    }
    
    // 【步骤7:符号化所有地址】
    // 记录实际获取到多少层调用栈
    int backtraceLength = i;
    
    // Dl_info 是动态链接信息结构体(定义在<dlfcn.h>),包含:
    //   - dli_fname: 文件名(如"MyApp"、"libSystem.dylib")
    //   - dli_fbase: 文件加载的基地址
    //   - dli_sname: 符号名(函数名,如"main"、"-[ViewController viewDidLoad]")
    //   - dli_saddr: 符号地址(函数的起始地址)
    Dl_info symbolicated[backtraceLength];
    
    // bs_symbolicate函数将每个地址转换为符号信息
    // 参数1:地址数组
    // 参数2:输出符号信息数组
    // 参数3:数组长度
    // 参数4:跳过的条目数(0表示不跳过)
    bs_symbolicate(backtraceBuffer, symbolicated, backtraceLength, 0);
    
    // 【步骤8:格式化输出】
    // 遍历所有调用栈层级,格式化成可读字符串
    for (int i = 0; i < backtraceLength; ++i) {
        // bs_logBacktraceEntry格式化单条调用栈信息
        // 输出格式类似:0   MyApp    0x0000000100001234 main + 52
        [resultString appendFormat:@"%@", bs_logBacktraceEntry(i, backtraceBuffer[i], &symbolicated[i])];
    }
    
    // 添加结尾换行
    [resultString appendFormat:@"\n"];
    
    // 返回不可变副本
    return [resultString copy];
}

#pragma -mark Convert NSThread to Mach thread
// ==================== NSThread转Mach线程 ====================

// 【线程转换函数】将NSThread对象转换为Mach线程ID
// 背景知识:iOS/macOS中有三种线程抽象:
//   1. NSThread - OC层的线程封装,提供面向对象的接口
//   2. pthread - POSIX标准的线程,跨平台(Unix/Linux/macOS都支持)
//   3. Mach thread - Mach内核的原生线程,macOS/iOS特有,性能最高
// NSThread内部实际上封装了pthread,而pthread底层又是Mach thread
// 但它们之间没有直接的API可以互相转换,所以需要用一些技巧
//
// 参数:nsthread - NSThread对象
// 返回:thread_t - Mach线程ID(实际上是mach_port_t类型,即端口号)
thread_t bs_machThreadFromNSThread(NSThread *nsthread) {
    // 【准备工作】
    // 用于存储pthread线程名称的缓冲区(最多256字符)
    char name[256];
    
    // 线程数量
    mach_msg_type_number_t count;
    
    // 线程列表数组
    thread_act_array_t list;
    
    // 获取当前进程的所有Mach线程
    // mach_task_self() 返回当前进程的任务端口
    task_threads(mach_task_self(), &list, &count);
    
    // 【转换策略:通过线程名称匹配】
    // 因为没有直接的API,所以采用"临时修改线程名称"的方法:
    // 1. 给NSThread设置一个唯一的名称(时间戳)
    // 2. 遍历所有Mach线程,将其转换为pthread
    // 3. 通过pthread的名称找到匹配的线程
    // 4. 恢复原始名称
    
    // 获取当前时间戳(精确到微秒),确保唯一性
    // timeIntervalSince1970 返回自1970-01-01 00:00:00到现在的秒数(浮点数)
    NSTimeInterval currentTimestamp = [[NSDate date] timeIntervalSince1970];
    
    // 保存NSThread的原始名称(稍后需要恢复)
    NSString *originName = [nsthread name];
    
    // 将线程名称临时设置为时间戳字符串
    // 例如:"1609459200.123456"
    [nsthread setName:[NSString stringWithFormat:@"%f", currentTimestamp]];
    
    // 【特殊处理:主线程】
    // 主线程ID在+load方法中已经保存,可以直接返回,无需遍历
    if ([nsthread isMainThread]) {
        return (thread_t)main_thread_id;
    }
    
    // 【遍历所有Mach线程进行匹配】
    for (int i = 0; i < count; ++i) {
        // pthread_from_mach_thread_np: 将Mach线程转换为pthread
        // _np后缀表示"non-portable"(非可移植),即macOS/iOS特有的扩展函数
        pthread_t pt = pthread_from_mach_thread_np(list[i]);
        
        // 【冗余检查】这段代码实际上永远不会执行
        // 因为如果isMainThread为true,前面已经return了
        // 这可能是代码重构时遗留的冗余逻辑
        if ([nsthread isMainThread]) {
            if (list[i] == main_thread_id) {
                return list[i];
            }
        }
        
        // 如果pthread转换成功(有些Mach线程可能没有对应的pthread)
        if (pt) {
            // 清空名称缓冲区
            name[0] = '\0';
            
            // pthread_getname_np: 获取pthread的线程名称
            // 参数1:pthread
            // 参数2:输出缓冲区
            // 参数3:缓冲区大小
            pthread_getname_np(pt, name, sizeof name);
            
            // strcmp: 字符串比较函数,相等返回0
            // 如果pthread的名称与我们设置的时间戳匹配,说明找到了对应的Mach线程
            if (!strcmp(name, [nsthread name].UTF8String)) {
                // 【找到匹配】恢复原始线程名称
                [nsthread setName:originName];
                
                // 返回Mach线程ID
                return list[i];
            }
        }
    }
    
    // 【未找到匹配】恢复原始名称
    [nsthread setName:originName];
    
    // 返回当前线程ID作为后备方案
    // mach_thread_self() 返回当前正在执行的线程ID
    // 这种情况通常不应该发生,除非传入的NSThread对象有问题
    return mach_thread_self();
}

#pragma -mark GenerateBacbsrackEnrty
// ==================== 格式化调用栈条目 ====================

// 【格式化单条调用栈】将地址和符号信息格式化为可读字符串
// 输出示例:0   MyApp       0x0000000100001234 main + 52
//          ^   ^            ^                  ^    ^ ^
//          |   |            |                  |    | |
//        序号 模块名        地址              函数名 + 偏移量
//
// 参数:
//   entryNum - 调用栈序号(0表示最内层,数字越大越外层)
//   address - 内存地址
//   dlInfo - 符号信息(包含文件名、函数名、地址等)
// 返回:格式化的字符串
NSString* bs_logBacktraceEntry(const int entryNum,
                               const uintptr_t address,
                               const Dl_info* const dlInfo) {
    // 文件地址缓冲区(当文件名为空时用于存储地址的字符串形式)
    char faddrBuff[20];
    
    // 符号地址缓冲区(当符号名为空时用于存储地址的字符串形式)
    char saddrBuff[20];
    
    // 【提取文件名】
    // dlInfo->dli_fname 是完整路径,如"/System/Library/Frameworks/UIKit.framework/UIKit"
    // bs_lastPathEntry 提取最后一部分,如"UIKit"
    const char* fname = bs_lastPathEntry(dlInfo->dli_fname);
    
    // 如果文件名为空(符号信息不完整)
    if(fname == NULL) {
        // sprintf: 格式化输出到字符串
        // POINTER_FMT 是根据32/64位定义的格式(如"0x%08lx"或"0x%016lx")
        // dlInfo->dli_fbase 是文件的基地址(加载到内存的起始地址)
        sprintf(faddrBuff, POINTER_FMT, (uintptr_t)dlInfo->dli_fbase);
        
        // 使用地址字符串作为文件名
        fname = faddrBuff;
    }
    
    // 【计算偏移量】
    // 偏移量 = 当前地址 - 符号起始地址
    // 例如:main函数起始地址是0x100001200,当前地址是0x100001234,偏移量就是52字节
    uintptr_t offset = address - (uintptr_t)dlInfo->dli_saddr;
    
    // 【提取符号名】
    // dlInfo->dli_sname 是函数名,如"main"、"-[ViewController viewDidLoad]"
    const char* sname = dlInfo->dli_sname;
    
    // 如果符号名为空(可能是被strip剥离了符号,或者是动态生成的代码)
    if(sname == NULL) {
        // 使用文件基地址的字符串形式作为符号名
        sprintf(saddrBuff, POINTER_SHORT_FMT, (uintptr_t)dlInfo->dli_fbase);
        sname = saddrBuff;
        
        // 重新计算偏移量(相对于文件基地址而不是符号地址)
        offset = address - (uintptr_t)dlInfo->dli_fbase;
    }
    
    // 【格式化输出】
    // PRIxPTR 是可移植的指针格式符(定义在<inttypes.h>),根据平台自动选择正确的格式
    // 输出格式:
    //   %-30s: 文件名,左对齐,占30个字符
    //   0x%08" PRIxPTR ": 地址,十六进制,至少8位(32位)或16位(64位)
    //   %s: 符号名
    //   + %lu: 偏移量(无符号长整型)
    //   \n: 换行符
    return [NSString stringWithFormat:@"%-30s  0x%08" PRIxPTR " %s + %lu\n" ,fname, (uintptr_t)address, sname, offset];
}

// 【路径提取函数】从完整路径中提取文件名
// 例如:"/System/Library/Frameworks/UIKit.framework/UIKit" → "UIKit"
//       "MyApp" → "MyApp"(如果没有路径分隔符,返回原字符串)
//
// 参数:path - 完整路径字符串
// 返回:文件名字符串(指向原字符串的某个位置,不是新分配的内存)
const char* bs_lastPathEntry(const char* const path) {
    // 空指针检查
    if(path == NULL) {
        return NULL;
    }
    
    // strrchr: 从右往左查找指定字符
    // 查找最后一个'/'字符的位置
    char* lastFile = strrchr(path, '/');
    
    // 三元运算符:
    // 如果找到'/',返回'/'后面的部分(lastFile + 1跳过'/'字符)
    // 如果没找到'/',说明path本身就是文件名,直接返回
    return lastFile == NULL ? path : lastFile + 1;
}

#pragma -mark HandleMachineContext
// ==================== 机器上下文处理 ====================

// 【获取线程状态】将线程的寄存器状态填充到机器上下文结构体
// 这是获取调用栈的第一步:需要知道线程当前的寄存器值(PC、FP、SP等)
//
// 参数:
//   thread - Mach线程ID
//   machineContext - 输出参数,_STRUCT_MCONTEXT结构体指针,用于存储寄存器状态
// 返回:bool - true表示成功,false表示失败
bool bs_fillThreadStateIntoMachineContext(thread_t thread, _STRUCT_MCONTEXT *machineContext) {
    // 【mach_msg_type_number_t】
    // 状态结构体的大小(以"自然单位"为单位,通常是4字节)
    // BS_THREAD_STATE_COUNT 是根据CPU架构定义的宏,表示状态结构体包含多少个字段
    mach_msg_type_number_t state_count = BS_THREAD_STATE_COUNT;
    
    // 【thread_get_state】Mach内核函数,获取线程状态
    // 这是一个非常底层的系统调用,可以获取线程的完整寄存器快照
    //
    // 参数:
    //   thread - 线程ID
    //   BS_THREAD_STATE - 状态类型(ARM_THREAD_STATE64、x86_THREAD_STATE64等)
    //   (thread_state_t)&machineContext->__ss - 输出缓冲区
    //       __ss是"saved state"的缩写,表示保存的CPU状态
    //       thread_state_t 是void*的别名,所以需要类型转换
    //   &state_count - 输入/输出参数,输入时是缓冲区大小,输出时是实际写入的大小
    //
    // 返回值:kern_return_t - 内核函数的返回值
    //   KERN_SUCCESS (0) - 成功
    //   KERN_INVALID_ARGUMENT - 参数无效
    //   KERN_FAILURE - 其他失败
    kern_return_t kr = thread_get_state(thread, BS_THREAD_STATE, (thread_state_t)&machineContext->__ss, &state_count);
    
    // 检查是否成功
    return (kr == KERN_SUCCESS);
}

// 【提取帧指针】从机器上下文中读取帧指针寄存器的值
// 帧指针(Frame Pointer)指向当前函数的栈帧起始位置
// 通过帧指针可以遍历整个调用栈链表
//
// 参数:machineContext - 机器上下文(mcontext_t是_STRUCT_MCONTEXT的typedef别名)
// 返回:uintptr_t - 帧指针的值(一个内存地址)
uintptr_t bs_mach_framePointer(mcontext_t const machineContext){
    // machineContext->__ss 是"saved state",包含所有寄存器的值
    // BS_FRAME_POINTER 是根据CPU架构定义的宏:
    //   ARM64: __fp(x29寄存器)
    //   ARM32: __r[7](r7寄存器)
    //   x86_64: __rbp
    //   i386: __ebp
    return machineContext->__ss.BS_FRAME_POINTER;
}

// 【提取栈指针】从机器上下文中读取栈指针寄存器的值
// 栈指针(Stack Pointer)指向栈顶(最后压入的数据)
// 注意:栈是向下增长的,所以栈顶的地址比栈底小
//
// 参数:machineContext - 机器上下文
// 返回:uintptr_t - 栈指针的值
uintptr_t bs_mach_stackPointer(mcontext_t const machineContext){
    // BS_STACK_POINTER 根据CPU架构定义:
    //   ARM: __sp(Stack Pointer)
    //   x86: __rsp(x86_64)或__esp(i386)
    return machineContext->__ss.BS_STACK_POINTER;
}

// 【提取指令地址】从机器上下文中读取程序计数器/指令指针的值
// 程序计数器(PC)指向当前正在执行的指令地址
// 这是调用栈回溯的起点
//
// 参数:machineContext - 机器上下文
// 返回:uintptr_t - 当前指令地址
uintptr_t bs_mach_instructionAddress(mcontext_t const machineContext){
    // BS_INSTRUCTION_ADDRESS 根据CPU架构定义:
    //   ARM: __pc(Program Counter)
    //   x86_64: __rip(Instruction Pointer)
    //   i386: __eip
    return machineContext->__ss.BS_INSTRUCTION_ADDRESS;
}

// 【提取链接寄存器】从机器上下文中读取链接寄存器的值(仅ARM架构有效)
// 链接寄存器(LR, Link Register)是ARM架构特有的寄存器
// 当函数A调用函数B时,返回地址会保存在LR中(而不是压入栈)
// 这样做的好处:
//   1. 性能更好(寄存器访问比内存访问快得多)
//   2. 简化了叶子函数(不调用其他函数的函数)的栈帧管理
//
// 参数:machineContext - 机器上下文
// 返回:uintptr_t - 链接寄存器的值(x86架构返回0,因为没有LR)
uintptr_t bs_mach_linkRegister(mcontext_t const machineContext){
#if defined(__i386__) || defined(__x86_64__)
    // x86架构没有链接寄存器
    // 函数调用时,返回地址直接压入栈
    return 0;
#else
    // ARM架构:返回LR寄存器的值
    // __lr 是ARM架构中的x30寄存器(ARM64)或r14寄存器(ARM32)
    return machineContext->__ss.__lr;
#endif
}

// 【安全的内存复制】从指定内存地址复制数据到目标地址
// 为什么需要这个函数?
//   1. 直接使用指针访问其他线程的内存可能会崩溃(权限问题、野指针等)
//   2. vm_read_overwrite是内核级别的内存读取,更安全、更可靠
//   3. 可以跨线程读取内存(读取其他线程的栈内存)
//
// 参数:
//   src - 源地址(要读取的内存地址)
//   dst - 目标地址(复制到哪里)
//   numBytes - 要复制的字节数
// 返回:kern_return_t - 内核函数返回值(KERN_SUCCESS表示成功)
kern_return_t bs_mach_copyMem(const void *const src, void *const dst, const size_t numBytes){
    // vm_size_t 是虚拟内存大小类型(实际上就是unsigned long)
    // 用于记录实际复制了多少字节
    vm_size_t bytesCopied = 0;
    
    // 【vm_read_overwrite】虚拟内存读取覆写函数(Mach内核API)
    // 这是一个非常底层的系统调用,可以读取任意进程的内存
    //
    // 参数:
    //   mach_task_self() - 目标任务(进程),这里是当前进程
    //   (vm_address_t)src - 源地址,要读取的内存位置
    //   (vm_size_t)numBytes - 要读取的字节数
    //   (vm_address_t)dst - 目标地址,读取的数据写入到哪里
    //   &bytesCopied - 输出参数,实际复制的字节数
    //
    // 返回值:
    //   KERN_SUCCESS - 成功
    //   KERN_INVALID_ADDRESS - 地址无效(如野指针、空指针)
    //   KERN_PROTECTION_FAILURE - 权限不足
    //   KERN_NO_SPACE - 目标空间不足
    return vm_read_overwrite(mach_task_self(), (vm_address_t)src, (vm_size_t)numBytes, (vm_address_t)dst, &bytesCopied);
}

#pragma -mark Symbolicate
// ==================== 符号化:地址转换为可读符号 ====================

// 【符号化】将内存地址数组转换为符号信息数组
// 符号化就是"地址 → 可读信息"的过程:
//   0x0000000100001234 → "MyApp: main + 52"
//
// 为什么需要符号化?
//   - 内存地址对人类来说毫无意义
//   - 符号化后可以知道:哪个模块、哪个函数、距离函数起始地址多少字节
//   - 方便调试、性能分析、崩溃分析
//
// 参数:
//   backtraceBuffer - 输入,内存地址数组
//   symbolsBuffer - 输出,符号信息数组(Dl_info结构体数组)
//   numEntries - 数组长度
//   skippedEntries - 跳过的条目数(通常为0)
void bs_symbolicate(const uintptr_t* const backtraceBuffer,
                    Dl_info* const symbolsBuffer,
                    const int numEntries,
                    const int skippedEntries){
    // 当前处理的索引
    int i = 0;
    
    // 【特殊处理第一个地址】
    // 如果没有跳过条目,且索引有效
    if(!skippedEntries && i < numEntries) {
        // 第一个地址是当前指令地址(PC寄存器),直接符号化,不需要调整
        // 为什么不需要调整?因为PC指向的就是当前正在执行的指令
        bs_dladdr(backtraceBuffer[i], &symbolsBuffer[i]);
        i++;
    }
    
    // 【处理剩余的返回地址】
    // 从第二个地址开始,都是返回地址(return address)
    for(; i < numEntries; i++) {
        // 【关键点】返回地址需要-1才能得到调用指令的地址
        // 背景知识:
        //   当函数A调用函数B时,CPU执行CALL指令:
        //     1. 将"CALL的下一条指令地址"压入栈(这就是返回地址)
        //     2. 跳转到函数B
        //   所以返回地址指向的是CALL之后的指令,不是CALL本身
        //   为了知道是在哪里调用的,需要返回地址-1得到CALL指令的地址
        //
        // CALL_INSTRUCTION_FROM_RETURN_ADDRESS宏做两件事:
        //   1. DETAG_INSTRUCTION_ADDRESS: 去除ARM架构的标签位
        //   2. -1: 得到CALL指令地址
        bs_dladdr(CALL_INSTRUCTION_FROM_RETURN_ADDRESS(backtraceBuffer[i]), &symbolsBuffer[i]);
    }
}

// 【自定义dladdr】将内存地址转换为符号信息
// dladdr是系统函数(定义在<dlfcn.h>),但这里自己实现了一遍
// 为什么要自己实现?
//   1. 学习Mach-O文件格式和符号表结构
//   2. 可以添加自定义逻辑(如特殊符号处理)
//   3. 更好地理解符号化过程
//
// 参数:
//   address - 要查询的内存地址
//   info - 输出参数,Dl_info结构体指针,用于存储符号信息
// 返回:bool - true表示成功找到符号,false表示失败
bool bs_dladdr(const uintptr_t address, Dl_info* const info) {
    // 【步骤0:初始化输出结构体】
    // Dl_info结构体定义(来自<dlfcn.h>):
    //   typedef struct dl_info {
    //       const char *dli_fname;  // 文件名(模块名)
    //       void *dli_fbase;        // 文件基地址
    //       const char *dli_sname;  // 符号名(函数名)
    //       void *dli_saddr;        // 符号地址(函数起始地址)
    //   } Dl_info;
    info->dli_fname = NULL; // 文件名
    info->dli_fbase = NULL; // 文件基地址
    info->dli_sname = NULL; // 符号名(函数名)
    info->dli_saddr = NULL; // 符号地址
    
    // 【步骤1:查找包含该地址的镜像(动态库/可执行文件)】
    // 背景知识:iOS/macOS程序运行时会加载多个镜像:
    //   - 可执行文件本身(如MyApp)
    //   - 系统库(如UIKit.framework、libSystem.dylib)
    //   - 第三方库
    // bs_imageIndexContainingAddress 遍历所有镜像,找到包含该地址的那一个
    const uint32_t idx = bs_imageIndexContainingAddress(address);
    
    // 如果没找到(UINT_MAX表示失败),返回false
    // 这种情况很少发生,除非传入了无效地址
    if(idx == UINT_MAX) {
        return false;
    }
    
    // 【步骤2:获取镜像的Mach-O头部】
    // _dyld_get_image_header 是dyld提供的函数,返回指定索引的镜像头部
    // mach_header是Mach-O文件的头部结构体,包含:
    //   - magic: 魔数(标识文件类型和字节序)
    //   - cputype: CPU类型(ARM、x86等)
    //   - cpusubtype: CPU子类型
    //   - filetype: 文件类型(可执行文件、动态库等)
    //   - ncmds: 加载命令的数量
    //   - sizeofcmds: 所有加载命令的总大小
    const struct mach_header* header = _dyld_get_image_header(idx);
    
    // 【步骤3:处理ASLR(地址空间布局随机化)】
    // ASLR(Address Space Layout Randomization)是一种安全机制:
    //   - 每次程序启动时,镜像加载到内存的地址是随机的
    //   - 这样可以防止黑客利用固定地址进行攻击
    //
    // 地址关系:
    //   实际地址 = 文件中的地址 + ASLR偏移(slide)
    //
    // _dyld_get_image_vmaddr_slide 返回ASLR偏移量
    const uintptr_t imageVMAddrSlide = (uintptr_t)_dyld_get_image_vmaddr_slide(idx);
    
    // 计算去除ASLR偏移后的地址(文件中的原始地址)
    const uintptr_t addressWithSlide = address - imageVMAddrSlide;
    
    // 【步骤4:获取段基地址】
    // 段基地址用于计算符号表在内存中的位置
    // 符号表存储在__LINKEDIT段中,通过段基地址可以将文件偏移转换为内存地址
    const uintptr_t segmentBase = bs_segmentBaseOfImageIndex(idx) + imageVMAddrSlide;
    
    // 如果段基地址为0,说明获取失败
    if(segmentBase == 0) {
        return false;
    }
    
    // 【步骤5:填充基本信息】
    // _dyld_get_image_name 返回镜像的完整路径
    // 例如:"/System/Library/Frameworks/UIKit.framework/UIKit"
    info->dli_fname = _dyld_get_image_name(idx);
    
    // 文件基地址就是Mach-O头部地址
    info->dli_fbase = (void*)header;
    
    // 【步骤6:查找符号表并匹配最接近的符号】
    // 最佳匹配的符号表项(初始化为NULL,表示还没找到)
    const BS_NLIST* bestMatch = NULL;
    
    // 最小距离(初始化为最大值)
    // 我们要找的是:地址 >= 符号地址,且距离最小的那个符号
    uintptr_t bestDistance = ULONG_MAX;
    
    // 【步骤7:获取第一个加载命令的地址】
    // Mach-O文件结构:
    //   [Mach Header] [Load Commands] [Segments/Sections] [Data]
    // 加载命令紧跟在头部之后
    uintptr_t cmdPtr = bs_firstCmdAfterHeader(header);
    
    // 如果命令指针为0,说明头部损坏
    if(cmdPtr == 0) {
        return false;
    }
    
    // 【步骤8:遍历所有加载命令,查找符号表命令】
    // header->ncmds 是加载命令的数量
    for(uint32_t iCmd = 0; iCmd < header->ncmds; iCmd++) {
        // 将指针转换为load_command结构体
        // load_command是所有加载命令的基类,包含:
        //   - cmd: 命令类型(LC_SEGMENT、LC_SYMTAB等)
        //   - cmdsize: 命令大小
        const struct load_command* loadCmd = (struct load_command*)cmdPtr;
        
        // 【查找符号表命令】
        // LC_SYMTAB表示这是一个符号表命令
        if(loadCmd->cmd == LC_SYMTAB) {
            // 转换为symtab_command结构体
            // symtab_command包含:
            //   - symoff: 符号表在文件中的偏移
            //   - nsyms: 符号数量
            //   - stroff: 字符串表在文件中的偏移
            //   - strsize: 字符串表大小
            const struct symtab_command* symtabCmd = (struct symtab_command*)cmdPtr;
            
            // 【计算符号表在内存中的地址】
            // 符号表地址 = 段基地址 + 文件偏移
            // BS_NLIST是根据32/64位定义的符号表项类型(nlist或nlist_64)
            const BS_NLIST* symbolTable = (BS_NLIST*)(segmentBase + symtabCmd->symoff);
            
            // 【计算字符串表在内存中的地址】
            // 字符串表存储所有符号的名称字符串
            // 符号表项中只存储字符串的索引,真正的字符串在字符串表中
            const uintptr_t stringTable = segmentBase + symtabCmd->stroff;
            
            // 【遍历符号表,查找最佳匹配】
            for(uint32_t iSym = 0; iSym < symtabCmd->nsyms; iSym++) {
                // nlist/nlist_64结构体:
                //   - n_un.n_strx: 符号名在字符串表中的索引
                //   - n_type: 符号类型(函数、变量、调试符号等)
                //   - n_sect: 符号所在的段索引
                //   - n_desc: 描述符(引用类型、可见性等)
                //   - n_value: 符号地址(函数/变量的地址)
                
                // 【过滤外部符号】
                // 如果n_value为0,说明这是一个外部符号(引用其他库的符号)
                // 外部符号的地址在当前镜像中不存在,需要跳过
                if(symbolTable[iSym].n_value != 0) {
                    // 符号的基地址(函数/变量的起始地址)
                    uintptr_t symbolBase = symbolTable[iSym].n_value;
                    
                    // 计算地址与符号基地址的距离
                    uintptr_t currentDistance = addressWithSlide - symbolBase;
                    
                    // 【最佳匹配算法】
                    // 条件1:地址必须 >= 符号基地址(即地址在符号的范围内)
                    // 条件2:距离必须 <= 当前最佳距离(找最接近的符号)
                    //
                    // 为什么这样做?
                    // 例如:
                    //   main函数起始地址:0x100001200
                    //   其他函数起始地址:0x100001300
                    //   查询地址:0x100001234
                    // 那么最佳匹配是main(距离0x34),而不是其他函数(距离为负)
                    if((addressWithSlide >= symbolBase) &&
                       (currentDistance <= bestDistance)) {
                        // 更新最佳匹配
                        bestMatch = symbolTable + iSym;
                        // 更新最小距离
                        bestDistance = currentDistance;
                    }
                }
            }
            
            // 【步骤9:提取符号信息】
            // 如果找到了匹配的符号
            if(bestMatch != NULL) {
                // 符号地址 = 文件中的地址 + ASLR偏移
                info->dli_saddr = (void*)(bestMatch->n_value + imageVMAddrSlide);
                
                // 【提取符号名】
                // 符号名地址 = 字符串表基地址 + 符号名索引
                // (intptr_t)强制转换是为了正确处理指针算术
                info->dli_sname = (char*)((intptr_t)stringTable + (intptr_t)bestMatch->n_un.n_strx);
                
                // 【处理C/C++符号前缀】
                // C/C++编译器会在符号名前加下划线(_)
                // 例如:main → _main,printf → _printf
                // 为了输出更友好,跳过开头的下划线
                if(*info->dli_sname == '_') {
                    info->dli_sname++;
                }
                
                // 【处理stripped符号】
                // 如果符号地址等于文件基地址,且类型为3(N_SECT)
                // 说明这是一个section符号(不是真正的函数/变量符号)
                // 这种情况通常发生在符号被strip剥离后
                // 剥离符号可以减小文件大小,但会导致调试困难
                if(info->dli_saddr == info->dli_fbase && bestMatch->n_type == 3) {
                    info->dli_sname = NULL;
                }
                
                // 找到符号后跳出循环(不需要继续遍历其他加载命令)
                break;
            }
        }
        
        // 移动到下一个加载命令
        // cmdPtr += loadCmd->cmdsize 等价于 cmdPtr = cmdPtr + cmdsize
        cmdPtr += loadCmd->cmdsize;
    }
    
    // 返回成功(即使没找到符号,也返回true,因为至少找到了文件名)
    return true;
}

// ==================== 辅助函数:Mach-O文件解析 ====================

// 【获取第一个加载命令地址】
// Mach-O文件结构:[Header][Load Commands][Segments][Data]
// 这个函数计算加载命令的起始地址
//
// 参数:header - Mach-O头部指针
// 返回:uintptr_t - 第一个加载命令的地址(0表示头部损坏)
uintptr_t bs_firstCmdAfterHeader(const struct mach_header* const header) {
    // 根据魔数(magic number)判断文件类型和字节序
    switch(header->magic) {
        // 【32位Mach-O】
        case MH_MAGIC:    // 0xfeedface - 32位小端序
        case MH_CIGAM:    // 0xcefaedfe - 32位大端序(字节序相反)
            // 32位Mach-O头部大小是sizeof(struct mach_header)
            // header + 1 会跳过整个头部,指向加载命令起始位置
            return (uintptr_t)(header + 1);
            
        // 【64位Mach-O】
        case MH_MAGIC_64: // 0xfeedfacf - 64位小端序
        case MH_CIGAM_64: // 0xcffaedfe - 64位大端序
            // 64位Mach-O头部大小是sizeof(struct mach_header_64)
            // 需要先转换为mach_header_64*,然后+1跳过头部
            return (uintptr_t)(((struct mach_header_64*)header) + 1);
            
        default:
            // 未知魔数,说明文件损坏或不是Mach-O文件
            return 0;  // Header is corrupt
    }
}

// 【查找包含指定地址的镜像索引】
// 背景知识:程序运行时会加载多个镜像(可执行文件、动态库)
// 每个镜像被加载到内存的不同区域,这个函数找出地址属于哪个镜像
//
// 参数:address - 要查询的内存地址
// 返回:uint32_t - 镜像索引(UINT_MAX表示未找到)
uint32_t bs_imageIndexContainingAddress(const uintptr_t address) {
    // 【获取镜像总数】
    // _dyld_image_count() 返回当前进程加载的镜像数量
    // 镜像(image)包括:
    //   - 主程序可执行文件(如MyApp)
    //   - 动态库(如UIKit.framework、libSystem.dylib)
    //   - 插件、bundle等
    const uint32_t imageCount = _dyld_image_count();
    
    // Mach-O头部指针
    const struct mach_header* header = 0;
    
    // 【遍历所有镜像】
    for(uint32_t iImg = 0; iImg < imageCount; iImg++) {
        // _dyld_get_image_header(i) 返回第i个镜像的Mach-O头部
        header = _dyld_get_image_header(iImg);
        
        // 检查头部是否有效
        if(header != NULL) {
            // 【处理ASLR偏移】
            // 计算去除ASLR偏移后的地址(文件中的原始虚拟地址)
            uintptr_t addressWSlide = address - (uintptr_t)_dyld_get_image_vmaddr_slide(iImg);
            
            // 获取第一个加载命令的地址
            uintptr_t cmdPtr = bs_firstCmdAfterHeader(header);
            
            // 如果命令指针无效,跳过这个镜像
            if(cmdPtr == 0) {
                continue;
            }
            
            // 【遍历该镜像的所有加载命令,查找段命令】
            // 段(segment)是内存中的一块连续区域,包含多个节(section)
            // 常见的段:
            //   - __TEXT: 代码段(只读、可执行)
            //   - __DATA: 数据段(可读写)
            //   - __LINKEDIT: 链接信息段(符号表、字符串表等)
            for(uint32_t iCmd = 0; iCmd < header->ncmds; iCmd++) {
                // 当前加载命令
                const struct load_command* loadCmd = (struct load_command*)cmdPtr;
                
                // 【处理32位段命令】
                if(loadCmd->cmd == LC_SEGMENT) {
                    // 转换为segment_command结构体
                    // segment_command包含:
                    //   - segname: 段名(如"__TEXT"、"__DATA")
                    //   - vmaddr: 虚拟内存地址
                    //   - vmsize: 虚拟内存大小
                    //   - fileoff: 文件偏移
                    //   - filesize: 文件大小
                    const struct segment_command* segCmd = (struct segment_command*)cmdPtr;
                    
                    // 【检查地址是否在该段的虚拟地址范围内】
                    // 条件:vmaddr <= address < vmaddr + vmsize
                    if(addressWSlide >= segCmd->vmaddr &&
                       addressWSlide < segCmd->vmaddr + segCmd->vmsize) {
                        // 找到了!返回镜像索引
                        return iImg;
                    }
                }
                // 【处理64位段命令】
                else if(loadCmd->cmd == LC_SEGMENT_64) {
                    // segment_command_64与segment_command结构类似
                    // 只是地址和大小字段是64位的
                    const struct segment_command_64* segCmd = (struct segment_command_64*)cmdPtr;
                    
                    // 检查地址是否在该段的范围内
                    if(addressWSlide >= segCmd->vmaddr &&
                       addressWSlide < segCmd->vmaddr + segCmd->vmsize) {
                        // 找到了!返回镜像索引
                        return iImg;
                    }
                }
                
                // 移动到下一个加载命令
                cmdPtr += loadCmd->cmdsize;
            }
        }
    }
    
    // 【没有找到】
    // 返回UINT_MAX表示失败
    // 这种情况很少发生,除非:
    //   1. 传入了无效地址(野指针、栈溢出等)
    //   2. 地址指向动态分配的内存(malloc分配的堆内存)
    //   3. 地址指向系统保留区域
    return UINT_MAX;
}

// 【获取镜像的段基地址】
// 段基地址用于将文件偏移转换为虚拟内存地址
// 公式:虚拟内存地址 = 段基地址 + 文件偏移
//
// 为什么需要段基地址?
// - 符号表、字符串表等信息存储在文件中,用文件偏移表示
// - 程序运行时需要访问这些信息,必须转换为内存地址
// - 段基地址 = __LINKEDIT段的虚拟地址 - 文件偏移
//
// 参数:idx - 镜像索引
// 返回:uintptr_t - 段基地址(0表示失败)
uintptr_t bs_segmentBaseOfImageIndex(const uint32_t idx) {
    // 获取镜像的Mach-O头部
    const struct mach_header* header = _dyld_get_image_header(idx);
    
    // 获取第一个加载命令的地址
    uintptr_t cmdPtr = bs_firstCmdAfterHeader(header);
    
    // 如果命令指针无效,返回0
    if(cmdPtr == 0) {
        return 0;
    }
    
    // 【遍历所有加载命令,查找__LINKEDIT段】
    // __LINKEDIT段(Link Edit Segment)包含:
    //   - 符号表(Symbol Table)
    //   - 字符串表(String Table)
    //   - 重定位信息(Relocation Info)
    //   - 代码签名(Code Signature)
    // 这些信息在运行时需要被dyld访问
    for(uint32_t i = 0; i < header->ncmds; i++) {
        // 当前加载命令
        const struct load_command* loadCmd = (struct load_command*)cmdPtr;
        
        // 【处理32位段命令】
        if(loadCmd->cmd == LC_SEGMENT) {
            // 转换为segment_command结构体
            const struct segment_command* segmentCmd = (struct segment_command*)cmdPtr;
            
            // strcmp: 字符串比较,相等返回0
            // SEG_LINKEDIT是系统定义的常量:"__LINKEDIT"
            if(strcmp(segmentCmd->segname, SEG_LINKEDIT) == 0) {
                // 【计算段基地址】
                // 段基地址 = 虚拟内存地址 - 文件偏移
                //
                // 为什么这样计算?
                // 假设__LINKEDIT段:
                //   - 文件中的偏移:0x4000
                //   - 加载到内存的地址:0x100004000
                //   - 段基地址 = 0x100004000 - 0x4000 = 0x100000000
                //
                // 那么,符号表在文件中的偏移是0x5000,其内存地址就是:
                //   0x100000000 + 0x5000 = 0x100005000
                return segmentCmd->vmaddr - segmentCmd->fileoff;
            }
        }
        // 【处理64位段命令】
        else if(loadCmd->cmd == LC_SEGMENT_64) {
            // segment_command_64与segment_command结构类似
            const struct segment_command_64* segmentCmd = (struct segment_command_64*)cmdPtr;
            
            // 查找__LINKEDIT段
            if(strcmp(segmentCmd->segname, SEG_LINKEDIT) == 0) {
                // 计算段基地址(64位版本,需要类型转换)
                return (uintptr_t)(segmentCmd->vmaddr - segmentCmd->fileoff);
            }
        }
        
        // 移动到下一个加载命令
        cmdPtr += loadCmd->cmdsize;
    }
    
    // 【没有找到__LINKEDIT段】
    // 返回0表示失败
    // 这种情况不应该发生,因为所有Mach-O文件都有__LINKEDIT段
    return 0;
}

@end