從彙編角度分析objc_msgSend的hook過程

objc_msgSend 是基於彙編實現的,hook objc_msgSend 和咱們平時 hook OC 方法不同,在 github 上有開源的項目經過 hook objc_msgSend 來監控每一個函數的耗時狀況。這篇文章對其 hook 邏輯的主要代碼進行分析記錄。閱讀前建議先了解開源庫 fishhook 的源碼。git

主流程

先看開源 項目 主要代碼github

#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 來填充post

    #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 寄存器的值,因此調用前先保存 lrui

    #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"); 目前我的認爲是冗餘代碼,在整個流程中貌似並無實際做用。spa

  3. 經過 blr 指令 跳轉執行 before_objc_msgSend 函數。這裏會先保存 x八、x9 寄存器的值,緣由是__asm volatile ("mov x12, %0\n" :: "r"(value)) 執行命令過程當中會經過 x8 來保存函數地址,再進行跳轉,因此這裏會先要保存 x8,和步驟1相同,棧指針移動必須知足 SP Mod 16 = 0 的條件,因此 x9 也被保存。執行完以後 x八、x9 恢復。指針

    #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 的值 rest

    image-20190713185417531

  4. Step 4 到 Step 6,恢復原有入參,執行原函數,而後保存入參code

  5. call(blr, &after_objc_msgSend) 和步驟3類似,執行 hook 收尾的函數,主要是經過 TSD 返回步驟3保存的原來 lr 寄存器保存的內容,也就是hook前的 lr 寄存器值cdn

    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;
    }
    複製代碼
  6. __asm volatile ("mov lr, x0\n"); 將步驟5返回的值(原來lr的初始值)到lr寄存器

  7. Step 9 - Step 10 恢復寄存器值,並返回。主要目的是還原原始函數的執行以後的狀態。

遺留問題:

以上就是整個彙編 hook objc_msgSend 的主要過程,目前遺留一個問題是:

  1. __asm volatile ("mov x3, x4\n"); 這行代碼是否屬於冗餘代碼呢?

參考文章:

arm64程序調用規則

相關文章
相關標籤/搜索