Objc_msgSend is implemented based on assembly. Hook objc_msgSend is different from our normal hook OC method. There are open source projects on Github using Hook objc_msgSend to monitor the time of each function. This article analyzes and records the main code of its hook logic. It is recommended to read the source code of fishhook before reading.

Main process

Take a look at the main code of the open source project

#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 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 link(b, value) \ __asm volatile ("stp x8, lr, [sp, #-16]! \n"); \ __asm volatile ("sub sp, sp, #16\n"); \ call(b, value); \ __asm volatile ("add sp, sp, #16\n"); \ __asm volatile ("ldp x8, lr, [sp], #16\n"); #define ret() __asm volatile ("ret\n"); __attribute__((__naked__)) static void hook_Objc_msgSend() { // Save parameters. /// Step 1 save() /// Step 2 __asm volatile ("mov x2, lr\n"); __asm volatile ("mov x3, x4\n"); // Call our before_objc_msgSend. /// Step 3 call(blr, &before_objc_msgSend) // Load parameters. /// Step 4 load() // Call through to the original objc_msgSend. /// Step 5 call(blr, orig_objc_msgSend) // Save original objc_msgSend return value. /// Step 6 save() // Call our after_objc_msgSend. /// Step 7 call(blr, &after_objc_msgSend) // restore lr /// Step 8 __asm volatile ("mov lr, x0\n"); // Load original objc_msgSend return value. /// Step 9 load() // return /// Step 10 ret() }Copy the code

Let’s look at the above code step by step

  1. Save () saves the function entry arguments (x0-x8) to stack memory, as your subsequent function call modifies the original arguments. The value of x9 is also saved, because the stack pointer movement must meet the SP Mod 16 = 0 condition, and the X8 register only occupies 8 bytes, the remaining 8 bytes control is filled by X9

    #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");Copy the code
  2. Save LR to X2 for call(BLR, &before_objc_msgSend) calls. Save to X2 because before_objc_msgSend needs to pass LR as the third argument to facilitate subsequent returns; The BLR instruction changes the VALUE of the LR register, so save lr before calling

    #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"); void before_objc_msgSend(id self, SEL _cmd, uintptr_t lr) { push_call_record(self, object_getClass(self), _cmd, lr); } 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; }}}Copy the code

    __asm volatile (“mov x3, x4\n”); At present, I think it is redundant code, which seems to have no practical effect in the whole process.

  3. Jump to execute before_objc_msgSend with BLR instruction. __asm Volatile (“mov x12, %0\n” :: “R “(value)) will save the function address through x8 before jumping, so x8 will be saved first. Same as step 1, stack pointer movement must meet the condition SP Mod 16 = 0, so x9 will be saved as well. X8 and x9 resume after execution.

    #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");Copy the code

    The breakpoint under __asm Volatile (“mov x12, %0\n” :: “r”(value)) shows that the CPU addresses the function with adRP + add, changing the value of x8

  4. Step 4 through Step 6, restore the original input parameter, execute the original function, and save the input parameter

  5. Call (BLR, &after_objc_msgSend), similar to Step 3, performs the hook closure function, mainly through TSD to return the original LR register saved in Step 3, that is, the LR register value before hook

    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; 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; }Copy the code
  6. __asm volatile (“mov lr, x0\n”); Add the value returned in Step 5 (the original LR initial value) to the LR register

  7. Step 9 – Step 10 Restore the register value and return. The main purpose is to restore the state after the execution of the original function.

Remaining issues:

This is the main process of compiling Hook objc_msgSend.

  1. __asm volatile ("mov x3, x4\n");Is this line of code redundant?

Reference article:

Arm64 program calls rules