借助AI辅助。
源码地址
逐行注释
//
// 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