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

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

主流程

先看开源 项目 主要代码

#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 来填充

    #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

    #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 恢复。

    #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
  4. Step 4 到 Step 6,恢复原有入参,执行原函数,然后保存入参

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

    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"); 这行代码是否属于冗余代码呢?

参考文章:

  1. arm64程序调用规则
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容

  • 关键时刻,第一时间送达! 问题种类 时间复杂度 在集合里数据量小的情况下时间复杂度对于性能的影响看起来微乎其微。但...
    C9090阅读 889评论 0 1
  • 在平时开发和调试中,经常遇到C调用栈和汇编,所以这里来统一的了解下这部分内容,本章需要一定的汇编基础才能更好的理解...
    码农苍耳阅读 3,171评论 1 3
  • 引言 最近工作比较忙,没怎么去研究汇编的内容,这么多天,感觉有点生疏,有的时候累死累活很晚才回来,还要在学习别的东...
    struggle3g阅读 2,763评论 0 0
  • 一、词汇概念辨析 查了一下维基百科上关于这两个词汇的解释,分别如下: 1. 登录 登录(英语:login),计算机...
    shenxiaoma阅读 5,230评论 1 9
  • 今天下午妈妈带我和姐姐去看书了,我边看边读,慢慢体会书中的意思。我看的有:《植物大战僵尸二》还有好多好多的书呢,姐...
    王文哲同学阅读 232评论 0 0