本篇是iOS开发高手课读书笔记第一篇
fishoook
fishoook是Facebook 开源的一个库,可以动态地重新绑定在 iOS 上运行的 Mach-O 二进制文件的符号
fishhook 实现的大致思路是,通过重新绑定符号,可以实现对 c 方法的 hook。dyld 是通过更新 Mach-O 二进制的 __DATA segment 特定的部分中的指针来绑定 lazy 和 non-lazy 符号,通过确认传递给 rebind_symbol 里每个符号名称更新的位置,就可以找出对应替换来重新绑定这些符号。
简单实例
官方给的使用例子
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#import <dlfcn.h>
#import <fishhook/fishhook.h>
static int (*orig_close)(int);
static int (*orig_open)(const char *, int, ...);
int my_close(int fd) {
printf("Calling real close(%d)\n", fd);
return orig_close(fd);
}
int my_open(const char *path, int oflag, ...) {
va_list ap = {0};
mode_t mode = 0;
if ((oflag & O_CREAT) != 0) {
// mode only applies to O_CREAT
va_start(ap, oflag);
mode = va_arg(ap, int);
va_end(ap);
printf("Calling real open('%s', %d, %d)\n", path, oflag, mode);
return orig_open(path, oflag, mode);
} else {
printf("Calling real open('%s', %d)\n", path, oflag);
return orig_open(path, oflag, mode);
}
}
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// hook close 和 open 函数
rebind_symbols((struct rebinding[2]){{"close", my_close, (void *)&orig_close}, {"open", my_open, (void *)&orig_open}}, 2);
// 打开我们自己的二进制文件并打印出前4个字节(这对于给定架构上的所有Mach-O二进制文件都是相同的)
int fd = open(argv[0], O_RDONLY);
uint32_t magic_number = 0;
read(fd, &magic_number, 4);
printf("Mach-O Magic Number: %x \n", magic_number);
close(fd);
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
运行效果
Calling real open('/Users/geneqiao/Library/Developer/CoreSimulator/Devices/B1724970-4E12-4FA1-961F-5987B32AD8C7/data/Containers/Bundle/Application/6BCC65C2-A74F-406D-BC15-4CB90FD67864/hook_msgsend.app/hook_msgsend', 0)
Calling real close(5)
hook NSLog 函数
- (void)viewDidLoad {
[super viewDidLoad];
struct rebinding nslog;
nslog.name = "NSLog";
nslog.replacement = nNSLog;
nslog.replaced = (void *)&oNSLog;
struct rebinding rebinds[1] = {nslog};
rebind_symbols(rebinds, 1);
}
static void (* oNSLog)(NSString *format, ...);
void nNSLog(NSString *format, ...) {
oNSLog(@"%@",[format stringByAppendingString:@"被HOOK了"]);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"测试");
}
打印结果
2020-12-18 16:56:05.908524+0800 hook_msgsend[57695:13206866] 测试被HOOK了
对于我们自己的C函数,fishHook并没有hook成功,还是执行原来的方法。
fishHook原理
1.共享缓存库
在iOS or Mac系统中,几乎所有的程序都会用到动态库,而动态库在加载的时候都需要用dyld(位于/usr/lib/dyld)程序进行链接。
很多系统库几乎每个程序都要用到,如果在程序运行的时候一个一个将这些动态库加载进来,不仅耗费内存,而且耗时。为了降低内存,提高性能,苹果引入了共享缓存库,用来存储系统的库。
Mac下的共享缓存库位置:/private/var/db/dyld/
iOS下的共享缓存库位置:/System/Library/Caches/com.apple.dyld/
文件名都是以“dyld_shared_cache_”开头,再加上这个dyld缓存文件所支持的指令集。
2.PIC技术(位置独立代码)
C语言是静态的,也就是说,在编译的时候就已经确定了函数的地址。而系统的函数由于共享缓存库的存在,必须是dyld加载的时候(运行时)才能确定,这明显存在矛盾。
为了解决这个问题,苹果针对Mach-O文件提供了一种PIC技术,即在Match-O的_Data段中添加懒加载表(Lazy Symbol Pointers)和非懒加载表(Non-Lazy Symbol Pointers)两个表,让系统的函数在编译的时候先指向懒加载表(Lazy Symbol Pointers)或非懒加载表(Non-Lazy Symbol Pointers)中的符号地址,
这两个表中符号地址的指向在编译的时候并没有指向任何地方,app启动,被dyld加载到内存,就进行链接, 给这2个表赋值动态缓存库的地址进行符号绑定。
使用 lldb 调试
使用 image list
lldb 命令,可以得到 ASLR 地址偏移
(lldb) image list
[ 0] FEC53598-931B-3F55-A692-AB7462FC5755 0x000000010260d000 /Users/geneqiao/Library/Developer/Xcode/DerivedData/hook_msgsend-cdykumyojctkvuabnasqgaskibpl/Build/Products/Debug-iphonesimulator/hook_msgsend.app/hook_msgsend
使用MatchOView查看MatchO懒加载表中的NSLog符号在文件中的偏移值
调用NSLog前
(lldb) x 0x00000001025ec000+0x8000
x 0x00000001025ec000+0x8000
0x1025f4000: aa e3 5e 02 01 00 00 00 44 17 7f 20 ff 7f 00 00 ..^.....D.. ....
0x1025f4010: 57 5c 69 24 ff 7f 00 00 0e e4 5e 02 01 00 00 00 W\i$......^.....
0x1025f4000: aa e3 5e 02 01 00 00 00 44 17 7f 20 ff 7f 00 00 ..^.....D.. ....
0x1025f4010: 57 5c 69 24 ff 7f 00 00 0e e4 5e 02 01 00 00 00 W\i$......^.....
(lldb) dis -s 01025ee3aa
0x1025ee3aa: pushq $0x0
0x1025ee3af: jmp 0x1025ee390
0x1025ee3b4: pushq $0xd
0x1025ee3b9: jmp 0x1025ee390
0x1025ee3be: pushq $0x65
0x1025ee3c3: jmp 0x1025ee390
调用NSLog后
(lldb) x 0x00000001025ec000+0x8000
0x1025f4000: 0d 4d 80 20 ff 7f 00 00 44 17 7f 20 ff 7f 00 00 .M. ....D.. ....
0x1025f4010: 57 5c 69 24 ff 7f 00 00 0e e4 5e 02 01 00 00 00 W\i$......^.....
(lldb) dis -s 7fff20804d0d
Foundation`NSLog:
0x7fff20804d0d <+0>: pushq %rbp
0x7fff20804d0e <+1>: movq %rsp, %rbp
0x7fff20804d11 <+4>: subq $0xd0, %rsp
0x7fff20804d18 <+11>: testb %al, %al
0x7fff20804d1a <+13>: je 0x7fff20804d42 ; <+53>
0x7fff20804d1c <+15>: movaps %xmm0, -0xa0(%rbp)
0x7fff20804d23 <+22>: movaps %xmm1, -0x90(%rbp)
Hook NSLog后
(lldb) x 0x00000001025ec000+0x8000
0x1025f4000: c0 d9 5e 02 01 00 00 00 44 17 7f 20 ff 7f 00 00 ..^.....D.. ....
0x1025f4010: 57 5c 69 24 ff 7f 00 00 0e e4 5e 02 01 00 00 00 W\i$......^.....
(lldb) dis -s 01025ed9c0
hook_msgsend`nNSLog:
0x1025ed9c0 <+0>: pushq %rbp
0x1025ed9c1 <+1>: movq %rsp, %rbp
0x1025ed9c4 <+4>: subq $0x30, %rsp
0x1025ed9c8 <+8>: movq $0x0, -0x8(%rbp)
0x1025ed9d0 <+16>: leaq -0x8(%rbp), %rax
0x1025ed9d4 <+20>: movq %rdi, -0x10(%rbp)
0x1025ed9d8 <+24>: movq %rax, %rdi
0x1025ed9db <+27>: movq -0x10(%rbp), %rsi
查看NSLog执行之前,执行了NSLog,被Hook以后这三个时刻,懒加载表中NSLog符号指向的地址。得到结论:
fishHook其实就是修改懒加载表(Lazy Symbol Pointers)、非懒加载表(Non-Lazy Symbol Pointers)中的符号地址的指向,从而达到hook的目的。
将指向系统方法(外部函数)的指针重新进行绑定指向内部函数/自定义 C 函数。
将内部函数的指针在动态链接时指向系统方法的地址。
也就是说内部/自定义的 C 函数 fishhook 也 HOOK 不了,它只能HOOK Mach-O 外部(共享缓存库中)的函数
fishhook 是如何根据字符串对应在符号表中的指针,找到其在共享库的函数实现的?
这张图在描述如何由一个字符串(比如 "NSLog"),跟着它在 MachO 文件的懒加载表中对应的指针,一步步的找到该指针指向的函数实现地址,大致步骤如下:
1.根据 LoadCommand 字段,获取 lazy_symbol_ptr Section地址。
遍历 lazy_symbol_ptr Section,根据当前是第几项,加上LoadCommand上,Indirect SymIndex的值,查找当前符号在间接表( Dynamic Symbols Table -> Indirect Symbols)中的信息。
从间接表中获取到符号表下标。
从符号表中获取到字符串表下标。
得到符号字符串,与待替换函数名称进行比较。若相等,则更新 got 表中的值。
fishhook 使用场景
防止第三方注入
基本思路:
- 在基础的动态调试逆向中,最常见的就是定位到目标方法后,通过 runtime 中的几种方法交换方式,实现代码注入的目的。iOS代码注入+HOOK微信登录
- 既然 fishhook 可以拦截系统外部的 C 函数,那自然就可以 HOOK 到 runtime 库中的所有方法。
-
那我们就将所有可能用来篡改我们 OC 方法实现的 runtime API,都用 fishhook 拦截掉,使其无法用代码注入的方式成功 HOOK。
这时我们程序跑起来就可以看到如下输出:
为了阻止其完成方法交换,我们要 hook method_exchangeImplementations 方法,拖入 fishhook 源文件,再添加一个分类并写好 hook method_exchangeImplementations 的代码
你会发现 method_exchangeImplementations 并没有 HOOK 成功, viewDidAppear: 依然被篡改了实现.
项目里参与编译的文件顺序就是其编译后被加载时的载入顺序,即此时 ViewController+HOOKTest 的 load 方法会早于 ViewController+FishHook 的 load 调用,所以 method_exchangeImplementations 的实现被我们 HOOK 发生在 viewDidAppear: 被别人交换之后,从而导致防护的失败:
如上图所示,在调整了编译文件的顺序之后成功 HOOK 到了 method_exchangeImplementations
的调用,但实际开发中我们不可能采用这么笨的方法,也不可能通过这种方式决定文件的载入顺序,因此我们要想办法保证 fishhook 的代码必须最先执行才行。
那如何做到呢?由此前的 dyld背后的故事&源码分析 可以得知,本地的Framework中的类一定会早于后注入的库(动态库例外,非越狱设备是没有插入动态库的权限的)和可执行文件中的类进行初始化。所以我们将 fishhook 的 HOOK 操作代码移到自建的 Framework 中即可:
至此,我们已经知道了 fishhook 反调试的基本思路,实际开发中,像method_getImplementation、method_setImplementation等函数都需要用同样的方式HOOK,同时,如果自己的项目中已经用到了这些函数,还需要设计相应的白名单方案,并且在检测到是被三方非法 HOOK 时通常直接调用 exit(0) 这类接口终止掉进程。
fishhook 源码分析
在使用 fishhook 时,需要声明一个 rebinding 类型的结构体变量.
/*
一种结构,表示从符号名到它的替换物之间重新绑定的特定意图
*/
struct rebinding {
const char *name;// 需要hook的函数名
void *replacement;//新函数的地址
void **replaced;//指向原函数实现的指针
};
创建结构体后, 再把这些结构体放到一个数组中,然后调用重绑定符号函数 int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel);
(如果绑定成功返回 0,否则返回 -1),并将结构体数组长度作为参数传入:
rebind_symbols
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel) {
int retval = prepend_rebindings(&_rebindings_head, rebindings, rebindings_nel);
if (retval < 0) {
return retval;
}
// 第一次调用,添加回调,对库的装载完成进行监听和回调:
if (!_rebindings_head->next) {
_dyld_register_func_for_add_image(_rebind_symbols_for_image);
} else {
// 遍历所有 image
uint32_t c = _dyld_image_count();
for (uint32_t i = 0; i < c; i++) {
//已经载入的镜像文件(也就是库)逐一查找目标符号进行 hook。
_rebind_symbols_for_image(_dyld_get_image_header(i), _dyld_get_image_vmaddr_slide(i));
}
}
return retval;
}
prepend_rebindings
struct rebindings_entry {
struct rebinding *rebindings;
size_t rebindings_nel;
struct rebindings_entry *next;
};
static struct rebindings_entry *_rebindings_head;
static int prepend_rebindings(struct rebindings_entry **rebindings_head,
struct rebinding rebindings[],
size_t nel) {
// 创建结构体
struct rebindings_entry *new_entry = malloc(sizeof(struct rebindings_entry));
if (!new_entry) {
return -1;
}
// 赋值结构体
new_entry->rebindings = malloc(sizeof(struct rebinding) * nel);
// 失败
if (!new_entry->rebindings) {
free(new_entry);
return -1;
}
// 内存拷贝
memcpy(new_entry->rebindings, rebindings, sizeof(struct rebinding) * nel);
//
new_entry->rebindings_nel = nel;
new_entry->next = *rebindings_head;
*rebindings_head = new_entry;
return 0;
}
看源码:_rebindings_head
被声明为一个指向 rebindings_entry
结构体的静态指针变量,那 &_rebindings_head 就是取出这个指针的地址.
而 ** rebindings_entry ** 是一个链表,存储有 rebinding
数组,数组元素的数量(用于开辟对应大小的空间),和下一个rebindings_entry
结构体
_rebindings_head 就是指向该链表的指针。
为了加深理解,我为你画了一张 prepend_rebindings 函数的作用示意图:
prepend_rebindings 函数的目的:将新加入的 rebindings 数组不断的添加到 _rebindings_head 这个链表的头部成为新的头节点。
extern void _dyld_register_func_for_add_image(void (*func)(const struct mach_header* mh, intptr_t vmaddr_slide)) __OSX_AVAILABLE_STARTING(__MAC_10_1, __IPHONE_2_0);
对已经载入的镜像文件(也就是库)逐一查找目标符号进行 hook。但 fishhook 的代码执行时间非常早,所以第一次执行时,要 hook 的库可能还没完成装载,因此如果是第一次调用,会通过一个函数对库的装载完成进行监听和回调:
static void _rebind_symbols_for_image(const struct mach_header *header,
intptr_t slide) {
// 当回调到 _rebind_symbols_for_image 时,会将存着待绑定函数信息的链表作为参数传入,用于符号查找和函数指针的交换,第二个参数 header是 当前 image 的头信息,第三个参数 slide是 ASLR 的偏移
rebind_symbols_for_image(_rebindings_head, header, slide);
}
// 无非就是按照规则计算各种表的地址和指针在表中的偏移量。
static void rebind_symbols_for_image(struct rebindings_entry *rebindings,
const struct mach_header *header,
intptr_t slide) {
Dl_info info;
// 加载当前 mach-O 的所有header
if (dladdr(header, &info) == 0) {
return;
}
segment_command_t *cur_seg_cmd;
segment_command_t *linkedit_segment = NULL;
struct symtab_command* symtab_cmd = NULL;
struct dysymtab_command* dysymtab_cmd = NULL;
// 找到符号表相关的 command,包括 linkedit segment command、symtab command 和 dysymtab command
// 跳过header的大小,直接寻找loadcommand
uintptr_t cur = (uintptr_t)header + sizeof(mach_header_t);
for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
cur_seg_cmd = (segment_command_t *)cur;
if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
if (strcmp(cur_seg_cmd->segname, SEG_LINKEDIT) == 0) {
linkedit_segment = cur_seg_cmd;
}
} else if (cur_seg_cmd->cmd == LC_SYMTAB) {
symtab_cmd = (struct symtab_command*)cur_seg_cmd;
} else if (cur_seg_cmd->cmd == LC_DYSYMTAB) {
dysymtab_cmd = (struct dysymtab_command*)cur_seg_cmd;
}
}
if (!symtab_cmd || !dysymtab_cmd || !linkedit_segment ||
!dysymtab_cmd->nindirectsyms) {
return;
}
//获得 base symbol 和 indirect (string table) 符号表
// 链接时程序的基础地址 = slide的偏移值 + _ _LINKEDIT.VM_Address - _ _LINKEDIT.File_Offset
uintptr_t linkedit_base = (uintptr_t)slide + linkedit_segment->vmaddr - linkedit_segment->fileoff;
// Symbol Table Offset 符号表的地址 = 基址 + 符号表偏移量
nlist_t *symtab = (nlist_t *)(linkedit_base + symtab_cmd->symoff);
// String Table 字符串表的地址 = 基址 + 字符串偏移量
char *strtab = (char *)(linkedit_base + symtab_cmd->stroff);
// Dynamic Symbol Table 动态符号表的地址 = 基址 + 动态符号表偏移量
uint32_t *indirect_symtab = (uint32_t *)(linkedit_base + dysymtab_cmd->indirectsymoff);
cur = (uintptr_t)header + sizeof(mach_header_t);
// 重新遍历
for (uint i = 0; i < header->ncmds; i++, cur += cur_seg_cmd->cmdsize) {
cur_seg_cmd = (segment_command_t *)cur;
if (cur_seg_cmd->cmd == LC_SEGMENT_ARCH_DEPENDENT) {
//找到DATA和DATA_CONST segment
if (strcmp(cur_seg_cmd->segname, SEG_DATA) != 0 &&
strcmp(cur_seg_cmd->segname, SEG_DATA_CONST) != 0) {
continue;
}
// 遍历 number of sections in segment
for (uint j = 0; j < cur_seg_cmd->nsects; j++) {
section_t *sect =
(section_t *)(cur + sizeof(segment_command_t)) + j;
// 找懒加载表
if ((sect->flags & SECTION_TYPE) == S_LAZY_SYMBOL_POINTERS) {
perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
}
// 非懒加载表
if ((sect->flags & SECTION_TYPE) == S_NON_LAZY_SYMBOL_POINTERS) {
perform_rebinding_with_section(rebindings, sect, slide, symtab, strtab, indirect_symtab);
}
}
}
}
}
最后根据算好的符号表地址和偏移量,找到共享库目标函数的指针,然后将该指针的值(即目标函数的地址)赋值给 *replaced,之后修改该指针的值为我们的 replacement(新的函数地址),perform_rebinding_with_section
static void perform_rebinding_with_section(struct rebindings_entry *rebindings,
section_t *section,
intptr_t slide,
nlist_t *symtab,
char *strtab,
uint32_t *indirect_symtab) {
// section的reserved1 + Dynamic Symbol Table 动态符号表的地址 = indirect_symbol 的地址
uint32_t *indirect_symbol_indices = indirect_symtab + section->reserved1;
// slider + section->addr = 符号对应存储函数实现的数组,用来寻找到函数的地址
void **indirect_symbol_bindings = (void **)((uintptr_t)slide + section->addr);
// 遍历section 的每一个符号
for (uint i = 0; i < section->size / sizeof(void *); i++) {
// 找到符号在Symbol Table表中的索引
uint32_t symtab_index = indirect_symbol_indices[i];
if (symtab_index == INDIRECT_SYMBOL_ABS || symtab_index == INDIRECT_SYMBOL_LOCAL ||
symtab_index == (INDIRECT_SYMBOL_LOCAL | INDIRECT_SYMBOL_ABS)) {
continue;
}
// 以symtab_index 作为下标,访问symbol table中对应的函数信息
uint32_t strtab_offset = symtab[symtab_index].n_un.n_strx;
// 获取symbol_name
char *symbol_name = strtab + strtab_offset;
// 遍历 rebindings
struct rebindings_entry *cur = rebindings;
while (cur) {
for (uint j = 0; j < cur->rebindings_nel; j++) {
// 这里 strcmp是判断symbol_name与rebindings 中对应的函数名是否相等,相等即为目标hook函数
if (strlen(symbol_name) > 1 &&
strcmp(&symbol_name[1], cur->rebindings[j].name) == 0) {
// 判断replace.地址不为NULL以及还没有hook过,避免重复交换和空指针
if (cur->rebindings[j].replaced != NULL &&
indirect_symbol_bindings[i] != cur->rebindings[j].replacement) {
// 让rebindings[j].repalced 保存 indirect_symbol_bindings[i]的函数地址
*(cur->rebindings[j].replaced) = indirect_symbol_bindings[i];
}
// 将替换后的方法给原先的方法,也就是替换内容为自定义函数地址
indirect_symbol_bindings[i] = cur->rebindings[j].replacement;
//遍历下一个符号
goto symbol_loop;
}
}
cur = cur->next;
}
symbol_loop:;
}
}
hook msg_send
objc_msgSend底层使用汇编实现的: objc_msgSend 源码跟踪,
函数定义如下
objc_msgSend(id _Nullable self, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
看了函数定义,hook的难点是不定参数。既然是不定参数,其实我们可以根据不定参数的原理,在 va_list 来解析数组,就可以获取到所有参数了。
虽然使用 va_list 是行的通的,但是我们需要考虑 va_list 在不同平台上的数据结构差异。
但既然都已经对不同平台做差异化处理了,那么干脆就直接使用内联汇编来实现传参逻辑就可以了
因此需要使用一些汇编代码,
先实现两个方法 pushCallRecord 和 popCallRecord,来分别记录 objc_msgSend 方法调用前后的时间,然后相减就能够得到方法的执行耗时。
针对 arm64 架构: arm64 有 31 个 64 bit 的整数型寄存器,分别用 x0 到 x30 表示。
主要的实现思路是:
- 入栈参数,参数寄存器是 x0~ x7。对于 objc_msgSend 方法来说,x0 第一个参数是传入对象,x1 第二个参数是选择器 _cmd。syscall 的 number 会放到 x8 里。
- 交换寄存器中保存的参数,将用于返回的寄存器 lr 中的数据移到 x1 里。
- 使用 bl label 语法调用 pushCallRecord 函数。
- 执行原始的 objc_msgSend,保存返回值。
- 使用 bl label 语法调用 popCallRecord 函数。
开始hook,记得设置hook的深度
void smCallTraceStart() {
_call_record_enabled = true;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
/*
线程私有数据采用了一键多值的技术,即一个键对应多个值。访问数据时都是通过键值来访问,好像是对一个变量进行访问,其实是在访问不同的数据。
一键多值靠的是一个关键数据结构数组即TSD池,创建一个TSD就相当于将结构数组中的某一项设置为“in_use”,并将其索引返回给*key,然后设置清理函数。
第一个参数为指向一个键值的指针,key一旦被创建,所有线程都可以访问它,但各线程可根据自己的需要往key中填入不同的值,这就相当于提供了一个同名而不同值的全局变量,一键多值。
第二个参数指明了一个destructor函数,如果这个参数不为空,那么当每个线程结束时,系统将调用这个函数来释放绑定在这个键上的内存块。
*/
pthread_key_create(&_thread_key, &release_thread_call_stack);
// 使用fishhook hook `objc_msgSend`函数
fish_rebind_symbols((struct rebinding[6]){
{"objc_msgSend", (void *)hook_Objc_msgSend, (void **)&orig_objc_msgSend},
}, 1);
});
}
// 停止hook
void smCallTraceStop() {
_call_record_enabled = false;
}
// 最小时间
void smCallConfigMinTime(uint64_t us) {
_min_time_cost = us;
}
// 最大深度
void smCallConfigMaxDepth(int depth) {
_max_call_depth = depth;
}
// 获取?
smCallRecord *smGetCallRecords(int *num) {
if (num) {
*num = _smRecordNum;
}
return _smCallRecords;
}
// 清除
void smClearCallRecords() {
if (_smCallRecords) {
free(_smCallRecords);
_smCallRecords = NULL;
}
_smRecordNum = 0;
}
hook_Objc_msgSend
结构体的定义
typedef struct {
__unsafe_unretained Class cls;
SEL sel;
uint64_t time; // us (1/1000 ms)
int depth;
} smCallRecord;
// CallRecord 记录调用方法详细信息,包括 obj 和 SEL 等;
typedef struct {
id self; //通过 object_getClass 能够得到 Class 再通过 NSStringFromClass 能够得到类名
Class cls;
SEL cmd; //通过 NSStringFromSelector 方法能够得到方法名
uint64_t time; //us
uintptr_t lr; // link register
} thread_call_record;
// ThreadCallStack 里面,需要用 index 记录当前调用方法树的深度。
typedef struct {
thread_call_record *stack;
int allocated_length;
int index; //index 记录当前调用方法树的深度
bool is_main_thread;
} thread_call_stack;
// 获取 thread_call_stack
static inline thread_call_stack * get_thread_call_stack() {
thread_call_stack *cs = (thread_call_stack *)pthread_getspecific(_thread_key);
if (cs == NULL) {
cs = (thread_call_stack *)malloc(sizeof(thread_call_stack));
cs->stack = (thread_call_record *)calloc(128, sizeof(thread_call_record));
cs->allocated_length = 64;
cs->index = -1;
cs->is_main_thread = pthread_main_np();
pthread_setspecific(_thread_key, cs);
}
return cs;
}
// 释放 thread_call_stack
static void release_thread_call_stack(void *ptr) {
thread_call_stack *cs = (thread_call_stack *)ptr;
if (!cs) return;
if (cs->stack) free(cs->stack);
free(cs);
}
typedef struct {
__unsafe_unretained Class cls;
SEL sel;
uint64_t time; // us (1/1000 ms)
int depth;
} smCallRecord;
static smCallRecord *_smCallRecords;
/*
即所谓的“裸函数”,对于这种函数,编译器不会生成任何函数入口代码和退出代码。这种函数一般应用在与操作系统内核相关的代码中,如中断处理函数、钩子函数等。
因为编译器不会生成入口代码和退出代码,所以写naked函数的时候要分外小心。进入函数代码时,父函数仅仅会将参数和返回地址压栈,亦即只有esp寄存器和eip寄存器会发生变化。
一般来说,使用naked函数时需要注意以下问题
1、函数必须显式返回。
一般通过__asm ret的内嵌汇编指令返回,否则的话,就等着int 3吧^^
2、不可以通过任何方式使用局部变量。
若声明一个局部变量,并在代码中为其赋值,则会更改父函数中相应位置的局部函数的值。举例:
*/
__attribute__((__naked__))
static void hook_Objc_msgSend() {
// 保存参数
save()
// 交换寄存器中保存的参数,将用于返回的寄存器 lr 中的数据移到 x1 里。
__asm volatile ("mov x2, lr\n");
__asm volatile ("mov x3, x4\n");
// 调用 before_objc_msgSend函数
call(blr, &before_objc_msgSend)
// 加载参数
load()
// 调用原始的objc_msgSend方法
call(blr, orig_objc_msgSend)
// 保存原始的返回值
save()
// 调用 after_objc_msgSend 函数
call(blr, &after_objc_msgSend)
// 还原lr
__asm volatile ("mov lr, x0\n");
// 加载原始的 返回值
load()
// 返回
ret()
}
参数保存 save()
/*
入栈参数,参数寄存器是 x0~ x7。对于 objc_msgSend 方法来说,x0 第一个参数是传入对象,x1 第二个参数是选择器 _cmd。syscall 的 number 会放到 x8 里。
*/
#define save() \
__asm volatile ( \
"stp x8, x9, [sp, #-16]!\n" \
"stp x6, x7, [sp, #-16]!\n" \
"stp x4, x5, [sp, #-16]!\n" \
"stp x2, x3, [sp, #-16]!\n" \
"stp x0, x1, [sp, #-16]!\n");
// 参数还原
#define load() \
__asm volatile ( \
"ldp x0, x1, [sp], #16\n" \
"ldp x2, x3, [sp], #16\n" \
"ldp x4, x5, [sp], #16\n" \
"ldp x6, x7, [sp], #16\n" \
"ldp x8, x9, [sp], #16\n" );
调用
#define call(b, value) \
__asm volatile ("stp x8, x9, [sp, #-16]!\n"); \
__asm volatile ("mov x12, %0\n" :: "r"(value)); \
__asm volatile ("ldp x8, x9, [sp], #16\n"); \
__asm volatile (#b " x12\n");
返回
#define ret() __asm volatile ("ret\n");
before and after
void before_objc_msgSend(id self, SEL _cmd, uintptr_t lr) {
push_call_record(self, object_getClass(self), _cmd, lr);
}
uintptr_t after_objc_msgSend() {
return pop_call_record();
}
static inline void push_call_record(id _self, Class _cls, SEL _cmd, uintptr_t lr) {
thread_call_stack *cs = get_thread_call_stack();
if (cs) {
int nextIndex = (++cs->index);
if (nextIndex >= cs->allocated_length) {
cs->allocated_length += 64;
cs->stack = (thread_call_record *)realloc(cs->stack, cs->allocated_length * sizeof(thread_call_record));
}
thread_call_record *newRecord = &cs->stack[nextIndex];
newRecord->self = _self;
newRecord->cls = _cls;
newRecord->cmd = _cmd;
newRecord->lr = lr;
if (cs->is_main_thread && _call_record_enabled) {
记录当前时间
struct timeval now;
gettimeofday(&now, NULL);
newRecord->time = (now.tv_sec % 100) * 1000000 + now.tv_usec;
}
}
}
static int _smRecordAlloc;
static inline uintptr_t pop_call_record() {
thread_call_stack *cs = get_thread_call_stack();
int curIndex = cs->index;
int nextIndex = cs->index--;
thread_call_record *pRecord = &cs->stack[nextIndex];
if (cs->is_main_thread && _call_record_enabled) {
struct timeval now;
gettimeofday(&now, NULL);
// 计算耗时
uint64_t time = (now.tv_sec % 100) * 1000000 + now.tv_usec;
if (time < pRecord->time) {
time += 100 * 1000000;
}
// 耗时
uint64_t cost = time - pRecord->time;
// 大于最小耗时.且深度小于最大深度
//static int _smRecordNum;
if (cost > _min_time_cost && cs->index < _max_call_depth) {
if (!_smCallRecords) {
_smRecordAlloc = 1024;
_smCallRecords = malloc(sizeof(smCallRecord) * _smRecordAlloc);
}
_smRecordNum++;
if (_smRecordNum >= _smRecordAlloc) {
_smRecordAlloc += 1024;
_smCallRecords = realloc(_smCallRecords, sizeof(smCallRecord) * _smRecordAlloc);
}
// 存储相关信息
smCallRecord *log = &_smCallRecords[_smRecordNum - 1];
log->cls = pRecord->cls;
log->depth = curIndex;
log->sel = pRecord->cmd;
log->time = cost;
}
}
return pRecord->lr;
}
调用代码
#pragma mark - Trace
#pragma mark - OC Interface
+ (void)start {
smCallTraceStart();
}
+ (void)startWithMaxDepth:(int)depth {
smCallConfigMaxDepth(depth);
[SMCallTrace start];
}
+ (void)startWithMinCost:(double)ms {
smCallConfigMinTime(ms * 1000);
[SMCallTrace start];
}
+ (void)startWithMaxDepth:(int)depth minCost:(double)ms {
smCallConfigMaxDepth(depth);
smCallConfigMinTime(ms * 1000);
[SMCallTrace start];
}
+ (void)stop {
smCallTraceStop();
}
+ (void)save {
NSMutableString *mStr = [NSMutableString new];
NSArray<SMCallTraceTimeCostModel *> *arr = [self loadRecords];
for (SMCallTraceTimeCostModel *model in arr) {
//记录方法路径
model.path = [NSString stringWithFormat:@"[%@ %@]",model.className,model.methodName];
[self appendRecord:model to:mStr];
}
// NSLog(@"%@",mStr);
}
+ (void)stopSaveAndClean {
[SMCallTrace stop];
[SMCallTrace save];
smClearCallRecords();
}
+ (void)appendRecord:(SMCallTraceTimeCostModel *)cost to:(NSMutableString *)mStr {
// [mStr appendFormat:@"%@\n path%@\n",[cost des],cost.path];
if (cost.subCosts.count < 1) {
cost.lastCall = YES;
//记录到数据库中
[[SMLagDB shareInstance] addWithClsCallModel:cost];
} else {
for (SMCallTraceTimeCostModel *model in cost.subCosts) {
if ([model.className isEqualToString:@"SMCallTrace"]) {
break;
}
//记录方法的子方法的路径
model.path = [NSString stringWithFormat:@"%@ - [%@ %@]",cost.path,model.className,model.methodName];
[self appendRecord:model to:mStr];
}
}
}
+ (NSArray<SMCallTraceTimeCostModel *>*)loadRecords {
NSMutableArray<SMCallTraceTimeCostModel *> *arr = [NSMutableArray new];
int num = 0;
smCallRecord *records = smGetCallRecords(&num);
for (int i = 0; i < num; i++) {
smCallRecord *rd = &records[i];
SMCallTraceTimeCostModel *model = [SMCallTraceTimeCostModel new];
model.className = NSStringFromClass(rd->cls);
model.methodName = NSStringFromSelector(rd->sel);
model.isClassMethod = class_isMetaClass(rd->cls);
model.timeCost = (double)rd->time / 1000000.0;
model.callDepth = rd->depth;
[arr addObject:model];
}
NSUInteger count = arr.count;
for (NSUInteger i = 0; i < count; i++) {
SMCallTraceTimeCostModel *model = arr[i];
if (model.callDepth > 0) {
[arr removeObjectAtIndex:i];
//Todo:不需要循环,直接设置下一个,然后判断好边界就行
for (NSUInteger j = i; j < count - 1; j++) {
//下一个深度小的话就开始将后面的递归的往 sub array 里添加
if (arr[j].callDepth + 1 == model.callDepth) {
NSMutableArray *sub = (NSMutableArray *)arr[j].subCosts;
if (!sub) {
sub = [NSMutableArray new];
arr[j].subCosts = sub;
}
[sub insertObject:model atIndex:0];
}
}
i--;
count--;
}
}
return arr;
}