13

从汇编角度分析objc_msgSend的hook过程

 3 years ago
source link: https://chipengliu.github.io/2019/07/13/hook-objc-MsgSend/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

从汇编角度分析objc_msgSend的hook过程

2019-07-132020-10-24计算机基础

objc_msgSend 是基于汇编实现的,hook objc_msgSend 和我们平时 hook OC 方法不一样,在 github 上有开源的项目通过 hook objc_msgSend 来监控每个函数的耗时情况。这篇文章对其 hook 逻辑的主要代码进行分析记录。阅读前建议先了解开源库 fishhook 的源码。

先看开源 项目 主要代码

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
#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()
}

对以上代码我们分步骤来看

  1. save() 保存函数入参(x0-x8)到栈内存,因为接下来你的函数调用修改原有参数。这里源码里面看到 x9 的值也被保存了,这里的原因是因为栈指针移动必须满足 SP Mod 16 = 0 的条件,而在 x8 寄存器只占用8个字节,剩余8个字节控件由 x9 来填充

    1
    2
    3
    4
    5
    6
    7
    #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");
  2. 保存 lr 到 x2,以便 call(blr, &before_objc_msgSend) 的调用,保存到 x2 是因为 before_objc_msgSend 函数第三个参数需要传入 lr,方便后续返回;blr 指令会改变 lr 寄存器的值,所以调用前先保存 lr

    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
    #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;
    }
    }
    }

    __asm volatile ("mov x3, x4\n"); 目前个人认为是冗余代码,在整个流程中貌似并没有实际作用。

  3. 通过 blr 指令 跳转执行 before_objc_msgSend 函数。这里会先保存 x8、x9 寄存器的值,原因是__asm volatile ("mov x12, %0\n" :: "r"(value)) 执行命令过程中会通过 x8 来保存函数地址,再进行跳转,所以这里会先要保存 x8,和步骤1相同,栈指针移动必须满足 SP Mod 16 = 0 的条件,所以 x9 也被保存。执行完之后 x8、x9 恢复。

    1
    2
    3
    4
    5
    #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");

    __asm volatile ("mov x12, %0\n" :: "r"(value)) 下断点可以看到 cpu 是通过 adrp + add 2个指令结合寻址到函数的地址并执行,过程中改变了 x8 的值

    image-20190713185417531
  1. Step 4 到 Step 6,恢复原有入参,执行原函数,然后保存入参

  2. call(blr, &after_objc_msgSend) 和步骤3相似,执行 hook 收尾的函数,主要是通过 TSD 返回步骤3保存的原来 lr 寄存器保存的内容,也就是hook前的 lr 寄存器值

    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
    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;
    }
  3. __asm volatile ("mov lr, x0\n"); 将步骤5返回的值(原来lr的初始值)到lr寄存器

  4. Step 9 - Step 10 恢复寄存器值,并返回。主要目的是还原原始函数的执行之后的状态。

遗留问题:

以上就是整个汇编 hook objc_msgSend 的主要过程,目前遗留一个问题是:

  1. __asm volatile ("mov x3, x4\n"); 这行代码是否属于冗余代码呢?

参考文章:

arm64程序调用规则


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK