如何正确的hook方法objc_msgSend
前言
如果希望对 Objective-C 的方法调用进行 log, 一个很好的解决方法就是 hook 方法 objc_msgSend
, 当然想到的就是利用 InlinkHook 直接 hook 完事, 然而 objc_msgSend
是一个可变参数函数, 这就有点蛋疼了.
objc4-680
, 和目前的 objc4-709
没有有很大出入.
以下 在举例 arm 相关时使用 objc4-680
, 说明 x64 时使用 objc4-709
整个代码使用 c++, 所以有些地方需要参考 objc 的源码去造一个轮子, 比如 object_getClass
等.
这篇文章假设读者对以下有了解.
- OC 类的内存布局模型
- OC 实例的内存布局模型
- OC 函数调用与消息传递机制
- macho 文件格式
- 基本 x64, arm 汇编指令
- 函数调用与参数传递的 x64, arm 汇编实现机制(函数调用约定)
- inlinehook 机制
Hook 思路
这里首先明确, objc_msgSend 的第三个参数是不定参数, 无法确定 objc_msgSend
的具体函数签名, 也就无法通过传参来调用原函数, 所以只能上暴力的方法, 通过保存/恢复栈和寄存器方法调用原函数, 之后在汇编指令中实现原函数的跳转调用.
objc_msgSend
的函数声明
1 2
| // runtime/message.h OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)
|
objc_msgSend
的函数实现, 这里以 x64 和 arm64 举例.
ARM64 下 ojbc_msgSend 实现机制
_objc_msgSend
首先会取得基本的参数, 比如 isa, class, 之后会使用宏 CacheLookup
先进行缓存查找, 如果没有命中, 会触发 JumpMiss
宏处理, 跳转到 __objc_msgSend_uncached_impcache
, 这个方法是了解如何正确 hook 的关键.
1 2 3 4 5 6 7 8 9 10
| // objc4-680/runtime/Messengers.subproj/objc-msg-arm64.s ENTRY _objc_msgSend MESSENGER_START cmp x0, #0 // nil check and tagged pointer check b.le LNilOrTagged // (MSB tagged pointer looks negative) ldr x13, [x0] // x13 = isa and x9, x13, #ISA_MASK // x9 = class LGetIsaDone: CacheLookup NORMAL // calls imp or objc_msgSend_uncached
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| // objc4-680/runtime/Messengers.subproj/objc-msg-arm64.s .macro JumpMiss .if $0 == NORMAL b __objc_msgSend_uncached_impcache .else b LGetImpMiss .endif .endmacro .macro CacheLookup // x1 = SEL, x9 = isa ldp x10, x11, [x9, #CACHE] // x10 = buckets, x11 = occupied|mask and w12, w1, w11 // x12 = _cmd & mask add x12, x10, x12, LSL #4 // x12 = buckets + ((_cmd & mask)<<4) ldp x16, x17, [x12] // {x16, x17} = *bucket 1: cmp x16, x1 // if (bucket->sel != _cmd) b.ne 2f // scan more CacheHit $0 // call or return imp 2: // not hit: x12 = not-hit bucket CheckMiss $0 // miss if bucket->cls == 0 cmp x12, x10 // wrap if bucket == buckets b.eq 3f ldp x16, x17, [x12, #-16]! // {x16, x17} = *--bucket b 1b // loop 3: // wrap: x12 = first bucket, w11 = mask add x12, x12, w11, UXTW #4 // x12 = buckets+(mask<<4) // Clone scanning loop to miss instead of hang when cache is corrupt. // The slow path may detect any corruption and halt later. ldp x16, x17, [x12] // {x16, x17} = *bucket 1: cmp x16, x1 // if (bucket->sel != _cmd) b.ne 2f // scan more CacheHit $0 // call or return imp 2: // not hit: x12 = not-hit bucket CheckMiss $0 // miss if bucket->cls == 0 cmp x12, x10 // wrap if bucket == buckets b.eq 3f ldp x16, x17, [x12, #-16]! // {x16, x17} = *--bucket b 1b // loop 3: // double wrap JumpMiss $0 .endmacro
|
具体解析下 __objc_msgSend_uncached_impcache
这个函数, 在函数入口保存寄存器和返回地址, 在调用 __class_lookupMethodAndLoadCache3
之后进行恢复. __class_lookupMethodAndLoadCache3
函数返回需要调用函数的地址.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| // objc4-680/runtime/Messengers.subproj/objc-msg-arm64.s STATIC_ENTRY __objc_msgSend_uncached_impcache // THIS IS NOT A CALLABLE C FUNCTION // Out-of-band x9 is the class to search MESSENGER_START // push frame stp fp, lr, [sp, #-16]! mov fp, sp MESSENGER_END_SLOW // save parameter registers: x0..x8, q0..q7 sub sp, sp, #(10*8 + 8*16) stp q0, q1, [sp, #(0*16)] stp q2, q3, [sp, #(2*16)] stp q4, q5, [sp, #(4*16)] stp q6, q7, [sp, #(6*16)] stp x0, x1, [sp, #(8*16+0*8)] stp x2, x3, [sp, #(8*16+2*8)] stp x4, x5, [sp, #(8*16+4*8)] stp x6, x7, [sp, #(8*16+6*8)] str x8, [sp, #(8*16+8*8)] // receiver and selector already in x0 and x1 mov x2, x9 bl __class_lookupMethodAndLoadCache3 // imp in x0 mov x17, x0 // restore registers and return ldp q0, q1, [sp, #(0*16)] ldp q2, q3, [sp, #(2*16)] ldp q4, q5, [sp, #(4*16)] ldp q6, q7, [sp, #(6*16)] ldp x0, x1, [sp, #(8*16+0*8)] ldp x2, x3, [sp, #(8*16+2*8)] ldp x4, x5, [sp, #(8*16+4*8)] ldp x6, x7, [sp, #(8*16+6*8)] ldr x8, [sp, #(8*16+8*8)] mov sp, fp ldp fp, lr, [sp], #16 br x17 END_ENTRY __objc_msgSend_uncached_impcache
|
举个例子演示下, 这里可以仔细观察 lldb 的输出.

x64 下 ojbc_msgSend 实现机制
整体思路与 arm64 类似, 但几个关键部分不同, 这里主要关注 GetIsaFast
和 MethodTableLookup
, MethodTableLookup
是在缓存中没有命中时进行查找.
1 2 3 4 5 6 7 8 9
| objc4-709/runtime/Messengers.subproj/objc-msg-x86_64.s ENTRY _objc_msgSend UNWIND _objc_msgSend, NoFrame MESSENGER_START NilTest NORMAL GetIsaFast NORMAL // r10 = self->isa CacheLookup NORMAL, CALL // calls IMP on success
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| objc4-709/runtime/Messengers.subproj/objc-msg-x86_64.s .macro GetIsaFast .if $0 != STRET testb $$1, %a1b PN jnz LGetIsaSlow_f movq $$0x00007ffffffffff8, %r10 andq (%a1), %r10 .else testb $$1, %a2b PN jnz LGetIsaSlow_f movq $$0x00007ffffffffff8, %r10 andq (%a2), %r10 .endif LGetIsaDone: .endmacro
|
这里需要关注 x64 保存和恢复参数的操作, 特别是这里的栈对齐.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| objc4-709/runtime/Messengers.subproj/objc-msg-x86_64.s .macro MethodTableLookup push %rbp mov %rsp, %rbp sub $$0x80+8, %rsp // +8 for alignment movdqa %xmm0, -0x80(%rbp) push %rax // might be xmm parameter count movdqa %xmm1, -0x70(%rbp) push %a1 movdqa %xmm2, -0x60(%rbp) push %a2 movdqa %xmm3, -0x50(%rbp) push %a3 movdqa %xmm4, -0x40(%rbp) push %a4 movdqa %xmm5, -0x30(%rbp) push %a5 movdqa %xmm6, -0x20(%rbp) push %a6 movdqa %xmm7, -0x10(%rbp) // _class_lookupMethodAndLoadCache3(receiver, selector, class) .if $0 == NORMAL // receiver already in a1 // selector already in a2 .else movq %a2, %a1 movq %a3, %a2 .endif movq %r10, %a3 call __class_lookupMethodAndLoadCache3 // IMP is now in %rax movq %rax, %r11 movdqa -0x80(%rbp), %xmm0 pop %a6 movdqa -0x70(%rbp), %xmm1 pop %a5 movdqa -0x60(%rbp), %xmm2 pop %a4 movdqa -0x50(%rbp), %xmm3 pop %a3 movdqa -0x40(%rbp), %xmm4 pop %a2 movdqa -0x30(%rbp), %xmm5 pop %a1 movdqa -0x20(%rbp), %xmm6 pop %rax movdqa -0x10(%rbp), %xmm7 .if $0 == NORMAL cmp %r11, %r11 // set eq for nonstret forwarding .else test %r11, %r11 // set ne for stret forwarding .endif leave .endmacro
|
构建 fake_objc_msgSend
在了解了 objc_msgSend 的流程, 接下来构建 fake_objc_msgSend
, 需要保存保存寄存器状态, 以正确调用原 objc_msgSend
, 这里解释下为什么需要保存寄存器状态, 因为参数个数不定, 所以可能会同时用到多个寄存器和栈来同时传参, 所以这里需要将可能会用到的所有寄存器以及 sp 都保存, 以确保多参数不被修改. 其实把这里的保护寄存器实现在 hook 层可能会更好一些. 最终通过把寄存器参数保存在栈中, 并以栈指针作为参数, 传给 hookBefore
, 实现函数参数的完全访问.
ok, 说完了函数的参数(寄存器)保存和恢复部分, 接下来谈一谈函数调用栈的问题.
- 限制函数调用栈深度
- 记录函数调用的返回值
问题 1, 比较好理解. 问题 2, 如果需要记录函数返回值, 并且过滤了部分的函数记录, 必须要保证 hookBefore
和 hookAfter
是对于同一个 函数
做的处理, 所以这里需要自建函数调用栈以及标记 函数
, 也就是说必须要保证自建的调用栈的 push 和 pop 操作必须对应, 这一点 InspectiveC 不同, 它是直接跟踪所有函数入栈(依赖系统标准函数栈的准确). 这里使用 sp
寄存器做标记区分函数(关于为何使用 sp
寄存器做标记区分, 希望读者能自己仔细思考函数调用本质), 以确保 push 和 pop 的对应正确.
这里总结一下 fake_objc_msgSend_safe 的工作
- 保存寄存器/参数
- hookBefore 工作(过滤/判断
调用的函数
)
- 恢复寄存器/参数
- 如果经过判断不需要追踪, 则直接调用原 ojbc_msgSend
- 如果需要追踪, 则修改默认栈内保存的返回地址后, 继续调用 objc_msgSend (这里涉及到 trick)
- 保存寄存器/参数
- hookAfter 工作(解析返回值, 函数调用出栈, 确保两次
sp
值相同)
- 恢复寄存器/参数
- ret
下面列出 fake_objc_msgSend_safe 的核心代码.
这里关于调用栈的操作主要放在 hookBefore
和 hookAfter
, 关于在内联汇编中调用 C 函数这里也使用了两种方法, 一种使用 nm
导出符号, 一种使用内联汇编传参.
这里关于寄存器的使用与污染问题, 请查阅相关资料或参考下方参考资料, 以了解寄存器使用约定.
这里关于如何获取到汇编的下一条指令地址的 trick, 可以参考 <程序员自我修养>
中地址无关代码中使用到的 get_pc_thunk
.
使用 ‘横线’ 分割了 hookBefore
和 hookAfter
相关处理代码, 大部分代码我都已经加了注释.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
| __attribute__((__naked__)) static void fake_objc_msgSend_safe() { // test for direct jmp // __asm__ volatile( // "jmpq *%0n": // : "r" (orig_objc_msgSend) // :); // backup registers __asm__ volatile( "subq $(16*8+8), %%rspn" // +8 for alignment "movdqa %%xmm0, (%%rsp)n" "movdqa %%xmm1, 0x10(%%rsp)n" "movdqa %%xmm2, 0x20(%%rsp)n" "movdqa %%xmm3, 0x30(%%rsp)n" "movdqa %%xmm4, 0x40(%%rsp)n" "movdqa %%xmm5, 0x50(%%rsp)n" "movdqa %%xmm6, 0x60(%%rsp)n" "movdqa %%xmm7, 0x70(%%rsp)n" "pushq %%raxn" // stack align "pushq %%r9n" // might be xmm parameter count "pushq %%r8n" "pushq %%rcxn" "pushq %%rdxn" "pushq %%rsin" "pushq %%rdin" "pushq %%raxn" // origin rsp, contain `ret address`, how to use leaq, always wrong. "movq %%rsp, %%raxn" "addq $(16*8+8+8+7*8), %%raxn" "pushq (%%rax)n" "pushq %%raxn" :: :); // prepare args for func __asm__ volatile( "movq %%rsp, %%rdin" "callq __Z10hookBeforeP9RegState_n" :: :); // get value from `ReturnPayload` __asm__ volatile( "movq (%%rax), %%r10n" "movq 8(%%rax), %%r11n" :: :); // restore registers __asm__ volatile( "popq %%raxn" "popq (%%rax)n" "popq %%raxn" "popq %%rdin" "popq %%rsin" "popq %%rdxn" "popq %%rcxn" "popq %%r8n" "popq %%r9n" "popq %%raxn" // stack align "movdqa (%%rsp), %%xmm0n" "movdqa 0x10(%%rsp), %%xmm1n" "movdqa 0x20(%%rsp), %%xmm2n" "movdqa 0x30(%%rsp), %%xmm3n" "movdqa 0x40(%%rsp), %%xmm4n" "movdqa 0x50(%%rsp), %%xmm5n" "movdqa 0x60(%%rsp), %%xmm6n" "movdqa 0x70(%%rsp), %%xmm7n" "addq $(16*8+8), %%rspn" :: :); // go to the original objc_msgSend __asm__ volatile( // "br x9n" "cmpq $0, %%r11n" "jne Lthroughxn" "jmpq *%%r10n" "Lthroughx:n" // trick to jmp "jmp NextInstructionn" "Begin:n"< 大专栏 如何正确的hook方法objc_msgSend · jmpews/div> "popq %%r11n" "movq %%r11, (%%rsp)n" "jmpq *%%r10n" "NextInstruction:n" "call Begin" :: :); //----------------------------------------------------------------------------- // after objc_msgSend we parse the result. // backup registers __asm__ volatile( "pushq %%r10n" // stack align "push %%rbpn" "movq %%rsp, %%rbpn" "subq $(16*8), %%rspn" // +8 for alignment "movdqa %%xmm0, -0x80(%%rbp)n" "push %%r9n" // might be xmm parameter count "movdqa %%xmm1, -0x70(%%rbp)n" "push %%r8n" "movdqa %%xmm2, -0x60(%%rbp)n" "push %%rcxn" "movdqa %%xmm3, -0x50(%%rbp)n" "push %%rdxn" "movdqa %%xmm4, -0x40(%%rbp)n" "push %%rsin" "movdqa %%xmm5, -0x30(%%rbp)n" "push %%rdin" "movdqa %%xmm6, -0x20(%%rbp)n" "push %%raxn" "movdqa %%xmm7, -0x10(%%rbp)n" "pushq 0x8(%%rbp)n" "movq %%rbp, %%raxn" "addq $8, %%raxn" "pushq %%raxn" :: :); // prepare args for func __asm__ volatile( "movq %%rsp, %%rdin" // "callq __Z9hookAfterP9RegState_n" "callq *%0n" "movq %%rax, %%r10n" : : "r"(func_ptr) : "%rax"); // restore registers __asm__ volatile( "pop %%raxn" "pop 8(%%rbp)n" "movdqa -0x80(%%rbp), %%xmm0n" "pop %%raxn" "movdqa -0x70(%%rbp), %%xmm1n" "pop %%rdin" "movdqa -0x60(%%rbp), %%xmm2n" "pop %%rsin" "movdqa -0x50(%%rbp), %%xmm3n" "pop %%rdxn" "movdqa -0x40(%%rbp), %%xmm4n" "pop %%rcxn" "movdqa -0x30(%%rbp), %%xmm5n" "pop %%r8n" "movdqa -0x20(%%rbp), %%xmm6n" "pop %%r9n" "movdqa -0x10(%%rbp), %%xmm7n" "leaven" // go to the original objc_msgSend "movq %%r10, (%%rsp)n" "retn" :: :); }
|
虽然现在已经可以 hook 到 objc_msgSend.
利用下面的数据结构获取之前保存在栈中的参数, ps: 这个结构是参考 InspectiveC, 实现的很精巧, 在此基础上做了一些修改.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| -ARM64 http://infocenter.arm.com/help/topic/com.arm.doc.den0024a/DEN0024A_v8_architecture_PG.pdf (7.2.1 Floating-point) (4.6.1 Floating-point register organization in AArch64) use struct and union to describe diagram in the above link, nice! -X86 https://en.wikipedia.org/wiki/X86_calling_conventions RDI, RSI, RDX, RCX, R8, R9, XMM0–7 */ typedef union FPMReg_ { __int128_t q; struct { double d1; double d2; } d; struct { float f1; float f2; float f3; float f4; } f; } FPReg; struct RegState_ { uint64_t bp; uint64_t ret; union { uint64_t arr[7]; struct { uint64_t rax; uint64_t rdi; uint64_t rsi; uint64_t rdx; uint64_t rcx; uint64_t r8; uint64_t r9; } regs; } general; uint64_t _; union { FPReg arr[8]; struct { FPReg xmm0; FPReg xmm1; FPReg xmm2; FPReg xmm3; FPReg xmm4; FPReg xmm5; FPReg xmm6; FPReg xmm7; } regs; } floating; }; typedef struct pa_list_ { struct RegState_ *regs; unsigned char *stack; int ngrn; int nsrn; } pa_list;
|
构建 hookBefore
hookBefore
实现了函数调用前的 trace 工作, 过滤 函数
, 将 函数
压进调用栈, 解析类实例对象, 解析不定参数.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| arg1: object-address(need to parse >> class) arg2: method string address arg3: method signature */ vm_address_t hookBefore(struct RegState_ *rs) { gettimeofday(&start,NULL); pa_list args = (pa_list){ rs, reinterpret_cast<unsigned char *>(rs->bp), 2, 0}; ThreadCallStack *cs = getThreadCallStack(threadKey); ReturnPayload *rp = cs->rp; vm_address_t xaddr = (uint64_t)orig_objc_msgSend; rp->addr = xaddr; rp->ret = rs->ret; rp->bp = rs->bp; rp->isTrace = 0; rp->key = rs->ret & rs->bp; if (1 || pthread_main_np()) { pay attention!!! as `objc_msgSend` invoked so often, the `filter` code snippet that use `c` to write it must be fast!!!. */ do { char *methodSelector = reinterpret_cast<char *>(rs->general.regs.rsi); if(!(methodSelector && checkAddressRange((vm_address_t)methodSelector, macho_load_addr, macho_load_end_addr))) break; vm_address_t class_addr = macho_objc_getClass(rs->general.regs.rdi); if (!(class_addr && checkAddressRange(class_addr, macho_load_addr, macho_load_end_addr))) break; if(check_freq_filter((unsigned long)methodSelector)) break; if (!checkLogFilters_MethodName(NULL, methodSelector, FT_EXINLUDE)) break; objc_class_info_t *xobjc_class_info; xobjc_class_info = mem->parse_OBJECT(rs->general.regs.rdi); if(!xobjc_class_info) break; if (!checkLogFilters_ClassName(xobjc_class_info->class_name, NULL, FT_EXINLUDE)) break; objc_method_info_t * objc_method_info; objc_method_info = search_method_name(&(xobjc_class_info->methods), methodSelector); if (!objc_method_info) break; if(check_thread_filter(cs->thread)) break; add_freq_filter((unsigned long)methodSelector, objc_method_info); CallRecord *cr = pushCallRecord(xobjc_class_info->class_name, methodSelector, reinterpret_cast<void *>(rs->bp), reinterpret_cast<void *>(rs->ret), cs); if(!cr) break; printCallRcord(cr, cs); rp->isTrace = 1; } while(0); } gettimeofday(&end,NULL); time_cost += (end.tv_sec-start.tv_sec)+(end.tv_usec-start.tv_usec)/1000000.0; return reinterpret_cast<unsigned long>(rp); }
|
hookBefore 的参数其实之前备份寄存器后的 sp 地址, 再通过 RegState_
格式, 就可以取得所有寄存器的值. rs->general.regs.x0
存放的是实例地址, 但是按理说应该通过 object_getClass(rs->general.regs.x0)
应该取得类地址, 但是这里避免使用 Objective-C以及 parser 内代码格式统一 就直接使用了 c++ 去实现源码中对应的函数. rs->general.regs.x1
存放的是函数签名字符串.
Macho 的文件解析
这里用到了个人之前实现的 macho 的 parser 模块, 大致介绍下这个模块, 可以通过三种状态对 macho 格式进行解析: 1. 文件 2. pid 3. 自身进程, 这里使用第三种, 对自身进程进行解析. (这个模块实现也挺有意思, 需要考虑到 ‘文件偏移’, ‘虚拟地址’, ‘内存地址’ 三种地址的处理)

对于类实例的解析, 比较复杂, 这里简单介绍下, 具体可以参照 macho-ABI 文档.
构建 hookAfter
hookAfter
实现了 objc_msgSend
执行后, 函数的出栈工作, 并解析函数返回值.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| vm_address_t hookAfter(struct RegState_ *rs) { pa_list args = (pa_list){ rs, reinterpret_cast<unsigned char *>(rs->bp), 2, 0}; ThreadCallStack *cs = getThreadCallStack(threadKey); CallRecord *cr = popCallRecordSafe(cs, (void *)rs->bp); if (cr) { return reinterpret_cast<unsigned long>(cr->ret); } else { cr = popCallRecord(cs); return reinterpret_cast<unsigned long>(cr->ret); } }
|
hookBefore 与 hookAfter 的数据传递.
x64 层面
关键在于函数调用栈中 函数返回地址
的传递, 也就是 (%%rsp)
, 由于需要获取 objc_msgSend
的返回值, 所以需要修改栈内默认的函数返回值为下一条指令的地址(请读者思考为什么不直接 push 内存地址? ). 这涉及到如何恢复栈中函数返回地址, 采用线程私有变量的方式, 为每一个 CallRecord
添加 ret
成员.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| typedef struct { vm_address_t addr; unsigned long isTrace; vm_address_t ret; vm_address_t bp; vm_address_t key; } ReturnPayload; // Shared structures. typedef struct CallRecord_ { void *objc; void *method; void *bp; void *ret; } CallRecord;
|
arm64 层面
其实关键在于 lr 寄存器值的传递, 由于需要获取 objc_msgSend
的返回值, 所以必须以 blr objc_msgSend
的方式调用, 此时之前 lr 寄存器的值被覆盖, 此时也不能进行 push 操作, 因为栈中可能已经保存了可变参数的值, 需要维持 sp 和 栈中内容不变, 那这里的 lr 如何保存恢复成了问题.
最后采用了线程私有变量的方法解决, 为每一个 CallRecord
添加 lr
成员, 在 hookAfter
是进行安全 pop, 也就是说同时需要校验 rs->fp
的值, 是否一致
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| typedef struct { vm_address_t addr; unsigned long isTrace; vm_address_t lr; vm_address_t fp; vm_address_t key; } ReturnPayload; // Shared structures. typedef struct CallRecord_ { void *objc; void *method; void *fp; void *lr; } CallRecord;
|
如何正确过滤 objc_msgSend
目前我是通过过滤来避免 log 太复杂.
1. 过滤函数地址
默认会过滤解析地址在 self 内存区内, 因为 MachoParser 本身就是一个耗时操作.
2.1 设置不解析类/方法
需要使用 hash 表进行快速 cache 和 search.
2.2 设置只解析的类/方法
需要使用 hash 表进行快速 cache 和 search.
3. 限制函数调用栈深度
通过自建模拟函数栈, 限制函数栈调用深度.
4. 过滤频繁调用函数 (log, monitor 之类)
对于一定时间段内频繁调用的函数添加到 hash-map 中.
应用场景
目前主要关注逆向方面, 当然 APM 方向也是可以玩的.
Thunder 逆向
参考资料

1 2
| // 函数调用约定, 寄存器使用约定 http://abcdxyzk.github.io/blog/2012/11/23/assembly-args/
|