上一次介绍了函数调用栈的原理,由此我们可以得知,每一次LR寄存器和FP寄存器压入栈中,那么我们就可以拿到栈帧层层递归获取整个函数调用栈的情况
首先先要获取到_STRUCT_MCONTEXT machineContext ,其结构里面有__ss,__ss当中能够拿到LR、FP、SP等寄存器
// 首先初始化
_STRUCT_MCONTEXT machineContext;
bool fillThradStateContext(thread_t thread, _STRUCT_MCONTEXT *machineContext){
mach_msg_type_number_t state_count = JY_THREAD_STATE_COUNT;
kern_return_t kr = thread_get_state(thread, JY_THREAD_STATE, (thread_state_t)&machineContext->__ss, &state_count);
return (kr == KERN_SUCCESS);
}
拿到对应的寄存器LR、PC、FP
// pc寄存器
const uintptr_t pcRegister = machineContext.__ss.JY_INSTRUCTION_ADDRESS;
if (pcRegister == 0) {
return @"Fail to get pc address";
}
// LR寄存器,函数返回地址.用于递归符号化堆栈
uintptr_t lrRegister;
#if defined(__i386__) || defined(__x86_64__)
lrRegister = 0;
#else
lrRegister = machineContext.__ss.__lr;
#endif
// 拿到FP栈帧指针,指向函数的起始地址
const uintptr_t fpRegister = machineContext.__ss.JY_FRAME_POINTER;
紧接着开始递归拿到函数堆栈
// 初始化一个长度为StackMaxDepth的buffer
uintptr_t backtraceBuffer[StackMaxDepth];
int i = 0;
// 首先把pc寄存器放进去,知道当前地址在哪
backtraceBuffer[i++] = pcRegister;
// 接着开始初始化结构体 构建栈帧
JYStackFrame frame = {(void *)fpRegister, lrRegister};
vm_size_t len = sizeof(frame);
// 开始递归
while (frame.fp && i < StackMaxDepth) {
backtraceBuffer[i++] = frame.lr;
bool flag = readFPMemory(frame.fp, &frame, len);
if (!flag || frame.fp==0 || frame.lr==0) {
break;
}
}
readFPMemory:读取fp开始,len(16)字节长度的内存。sp fp, lr... , fp占8字节,然后紧接着上面8字节是lr
bool readFPMemory(const void *fp, const void *dst, const vm_size_t len)
{
vm_size_t bytesCopied = 0;
kern_return_t kr = vm_read_overwrite(mach_task_self(), (vm_address_t)fp, len, (vm_address_t)dst, &bytesCopied);
return KERN_SUCCESS == kr;
}
经过递归已经收集好函数堆栈信息了,现在就要开始拿到指令集开始恢复符号了
// 收集好所有的lr 开始恢复符号表
restoreSymbol(backtraceBuffer,i,thread).copy;
定义struct,用于记录符号信息等
typedef struct{
uint64_t address; // 基础地址
uint64_t offset; // 偏移地址
const char * symbol; // 符号
const char * machOName; // 对应的二进制Macho名字
} JYFuncInfo;
typedef struct{
JYFuncInfo *stacks;
int allocLenght;
int length;
} JYCallStackInfo;
现在开始来还原,一开始做一下初始化操作
//还原符号表
NSString * restoreSymbol(uintptr_t *backtraceBuffer, int length ,thread_t thread){
JYCallStackInfo * csInfo = malloc(sizeof(JYCallStackInfo));
if (csInfo == NULL) {
return @"fail to malloc";
}
csInfo->length = 0;
csInfo->allocLenght = length;
csInfo->stacks = (JYFuncInfo *)malloc(sizeof(JYFuncInfo) * csInfo ->allocLenght);
if (csInfo->stacks == NULL) {
return @"error";
}
callStackOfSymbol(backtraceBuffer, length, csInfo);
NSMutableString *strM = [NSMutableString stringWithFormat:@"\n 🔥🔥🔥JYCallStack of thread: %u 🔥🔥🔥\n", thread];
for (int j = 0; j < csInfo->length; j++) {
[strM appendFormat:@"%@", formatFuncInfo(csInfo->stacks[j])];
}
freeMemory(csInfo);
return strM.copy;
}
来到关键代码 callStackOfSymbol(backtraceBuffer, length, csInfo);
void callStackOfSymbol(uintptr_t *backtraceBuffer, int length ,JYCallStackInfo *csInfo){
// 循环之前我们拿到的堆栈数据 开始恢复每一条指令
for (int i = 0; i<length; i++) {
// 获取当前lr地址 在程序加载的all image当中找到 image
JYMachHeader * machHeader = getLrInMach(backtraceBuffer[i]);
if (machHeader) {
//在image中找到这个LR符号
findSymbolInMach(backtraceBuffer[i],machHeader,csInfo);
}
}
}
首先我们backtraceBuffer[i]当中除了第一条是pc指令,其实都是lr寄存器指令的值
那么我们首先要知道,在运行时,有很多个image,我们需要获取到所有的image,因为image当中包含了ASLR,名称,头等
void getMachHeader(void){
// 开辟空间
machHeaderArr = (JYMachHeaderArr *)malloc(sizeof(JYMachHeaderArr));
//_dyld_image_count 获取所有的image的数量
machHeaderArr->allocLength = _dyld_image_count();
// 获取第一个image的基址
// intptr_t base_addr = _dyld_get_image_vmaddr_slide(0);
//image当中的
machHeaderArr->array = (JYMachHeader *)malloc(sizeof(JYMachHeader) * machHeaderArr->allocLength);
for (uint32_t i = 0; i < machHeaderArr->allocLength; i++) {
JYMachHeader *machHeader = &machHeaderArr->array[i];
//获取image的头
machHeader->header = _dyld_get_image_header(i);
//获取image的名称
machHeader->name = _dyld_get_image_name(i);
//获取进程中单个image加载的Slide值
// Slide 代表默认在内存中加载的基地址
machHeader->slide = _dyld_get_image_vmaddr_slide(i);
}
}
拿到了所有的image存到machHeaderArr当中,我们现在就可以开始查找指令是在哪一个image当中了
// 在machO中找到header
JYMachHeader *getLrInMach(uintptr_t lr)
{
if (!machHeaderArr) {
// 获取所有的image镜像文件加入到machHeaderArr,machHeaderArrm里面放的都是header
getMachHeader();
}
// 开始循环所有的image,确定当前的指令是在哪个image当中
for (uint32_t i = 0; i < machHeaderArr->allocLength; i++) {
// 拿到每个image的header
JYMachHeader *machHeader = &machHeaderArr->array[i];
// 开始查找lr寄存器中的指令是在哪一个image当中
if (backtraceBufferItemInMach(lr-machHeader->slide, machHeader->header)) {
// 找到在哪个image当中 然后返回 对应的machHeader
return machHeader;
}
}
return NULL;
}
来到backtraceBufferItemInMach这个函数,我们首先通过header拿到当前的Load Commands的地址,Load Commands的结构如下图,开始遍历Load Commands,
bool backtraceBufferItemInMach(uintptr_t slideLR, const struct mach_header *header)
{
// 向下偏移1个mach_header长度也就是 Load Commands的位置
// cur 也就是 Load Commands的位置
uintptr_t cur = (uintptr_t)(((struct mach_header_64*)header) + 1);
// 遍历loadCommands,确认lr是否落在当前image的某个segment中.
// 开始循环 ncmds: loadcommands的数量.
for (uint32_t i = 0; i < header->ncmds; i++) {
// 将Load Commands的起始位置开始赋值给command
struct load_command *command = (struct load_command *)cur;
// 判断command 类型是否为 LC_SEGMENT_64, 使用结构体segment_command_64
if (command->cmd == LC_SEGMENT_64) {
// 将command换成结构体 segment_command_64 的格式
struct segment_command_64 *segmentCommand = (struct segment_command_64 *)command;
// command的起始位置
uintptr_t start = segmentCommand->vmaddr;
// command 的 起始位置 + command的大小 得到 开始start 和 结束end 的位置
uintptr_t end = segmentCommand->vmaddr + segmentCommand->vmsize;
// 然后开始判断我们存在数组中的数据 是否存在于 这个区间 存在则返回
if (slideLR >= start && slideLR <= end) {
// 如果LR的地址落在这个模块里,则返回映像索引号
return true;
}
}
#warning TODO
// 如果command 类型为 LC_SEGMENT,则需要使用结构体segment_command
// command地址是连续的,移动到下一个command的位置
cur = cur + command->cmdsize;
}
return false;
}
我们找到这条指令在哪一个image当中,回到callStackOfSymbol,我们现在该去当前的这个image当中找到我们的符号findSymbolInMach(backtraceBuffer[i],machHeader,csInfo);这也是最关键的一步
首先我们需要了解一下MachO的结构,Symbol Table与String Table之间的关系,以及LC_SYMTAB段与__LINKEDIT段的作用,网上有很多讲解原理,可以自行了解下
__LINKEDIT段包含动态链接器使用的原始数据,如符号、字符串和重定位表项。
LC_SYMTAB 描述了字符串表和符号表在__LINKEDIT中的位置
首先我们还是通过image的header拿到Load Commands,然后开始循环,找到LC_SYMTAB段与__LINKEDIT段
我们backtraceBuffer当中lr的地址
seg_linkedit->vmaddr=LINKEDIT虚拟地址seg_linkedit->fileoff=LINKEDIT的文件地址(uintptr_t)machHeader->slide=ASLR
lr的偏移地址=lr真实地址-ASLR
拿到__LINKEDIT的基地址
segment加载进内存的基地址 = ASLR + LINKEDIT虚拟地址 - LINKEDIT的文件地址
符号表真实地址 = 符号表的虚拟地址 + symoff偏移地址
因为我们的lr真实地址 只是一条指令地址,它应该大于等于这个函数的入口地址,也就是对应符号的值,我们应该遍历所有符号表条目 找到距离lr最近的那个函数入口地址 才是最准确的,遍历所有的 Symbol Tabel获取所有的 symbol.n_value 然后与 lr的偏移地址做比较得到一个最小的值
stringTable字符串表 + symtab[best].n_un.n_strx(获取符号名在字符表中的偏移地址,best 代表符号表中的栏目第几个) ,两者的结果也就获取了符号的地址,然后读取值
得到符号名
void findSymbolInMach(uintptr_t lr, JYMachHeader * machHeader, JYCallStackInfo * csInfo){
if (!machHeader) {
return;
}
// 用于保存__LINKEDIT段的结构体 __LINKEDIT段包含动态链接器使用的原始数据,如符号、字符串和重定位表项。
struct segment_command_64 * seg_linkedit = NULL;
// 用于保存LC_SYMTAB Command的信息 里面有符号表的信息
struct symtab_command * sym_command = NULL;
// machO的header
const struct mach_header * header = machHeader->header;
// 向下偏移1个mach_header长度也就是 Load Commands的位置
// cur 也就是 Load Commands的位置
uintptr_t cur = (uintptr_t)(((struct mach_header_64*)header) + 1);
// 遍历Load Commands,找到 LC_SYMTAB 段
for (uint32_t i = 0; i<header->ncmds; i++) {
// 将Load Commands的起始位置开始赋值给command
struct load_command * command = (struct load_command*)cur;
if (command->cmd == LC_SEGMENT_64) {
struct segment_command_64 * segmentCommand = (struct segment_command_64 *)command;
// 我们需要找到__LINKEDIT段 也就是 SEG_LINKEDIT
if (strcmp(segmentCommand->segname, SEG_LINKEDIT) == 0) {
seg_linkedit = segmentCommand;
}
}else if (command->cmd == LC_SYMTAB){
/*
LC_SYMTAB 描述了string表和symbol表在__LINKEDIT中的位置
而symbol表描述了符号的地址信息,以及符号对应的字符串(函数名)在string表中 的位置
*/
sym_command = (struct symtab_command*)command;
}
// command地址是连续的,移动到下一个command的位置
cur = cur + command->cmdsize;
}
// 非空判断
if (!seg_linkedit || !sym_command) {
return;
}
// segment加载进内存的基地址 = ASLR + LINKEDIT虚拟地址 - LINKEDIT的文件地址
uintptr_t linkedit_base = (uintptr_t)machHeader->slide + seg_linkedit->vmaddr - seg_linkedit->fileoff;
// 符号表真实地址 = 符号表的虚拟地址 + symoff偏移地址
struct nlist_64 *symbolTable = (struct nlist_64 *)(linkedit_base + sym_command->symoff);
// 字符串表对应的位置
const uintptr_t stringTable = linkedit_base + sym_command->stroff;
uintptr_t slideLR = lr - machHeader->slide;
uint64_t offset = UINT64_MAX;
int best = -1;
// 遍历所有符号,找到与LR最近的那个. symtabCmd->nsyms指示了符号表的条目
for (uint32_t i = 0; i < sym_command->nsyms; i++) {
// 找到距离最近的那一个符号 lr偏移地址 - 符号的地址 = 得到两者之间的距离 distance
uint64_t distance = slideLR - symbolTable[i].n_value;
if (slideLR >= symbolTable[i].n_value && distance <= offset) {
offset = distance;
best = i;
}
}
if (best >= 0) {
JYFuncInfo *funcInfo = &csInfo->stacks[csInfo->length++];
funcInfo->machOName = machHeader->name;
funcInfo->address = symbolTable[best].n_value;
funcInfo->offset = offset;
// 去字符串表中寻找对应的符号名称,
// symtab[best].n_un.n_strx 获取符号名在字符表中的偏移地址
funcInfo->symbol = (char *)(stringTable + symbolTable[best].n_un.n_strx);
// 去掉下划线
if (*funcInfo->symbol == '_')
{
// char里储存的是0~255的数,然后呢,显示出来是字符(按照Ascii表)。
// ++ --是对数字来运算的 所以 这里就是去掉下划线而已
funcInfo->symbol++;
}
if (funcInfo->machOName == NULL) {
funcInfo->machOName = "";
}
}
}
最后就是一些收尾工作了,将所有收集到的指令都找到函数入口并且成功恢复符号,这也是大佬所写的BSBacktraceLogger的全部原理