從匯編角度分析objc_msgSend的hook過(guò)程

objc_msgSend 是基于匯編實(shí)現(xiàn)的钥屈,hook objc_msgSend 和我們平時(shí) hook OC 方法不一樣歪沃,在 github 上有開源的項(xiàng)目通過(guò) hook objc_msgSend 來(lái)監(jiān)控每個(gè)函數(shù)的耗時(shí)情況撒汉。這篇文章對(duì)其 hook 邏輯的主要代碼進(jìn)行分析記錄。閱讀前建議先了解開源庫(kù) fishhook 的源碼霜旧。

主流程

先看開源 項(xiàng)目 主要代碼

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

對(duì)以上代碼我們分步驟來(lái)看

  1. save() 保存函數(shù)入?yún)?x0-x8)到棧內(nèi)存棚潦,因?yàn)榻酉聛?lái)你的函數(shù)調(diào)用修改原有參數(shù)。這里源碼里面看到 x9 的值也被保存了软啼,這里的原因是因?yàn)闂V羔樢苿?dòng)必須滿足 SP Mod 16 = 0 的條件桑谍,而在 x8 寄存器只占用8個(gè)字節(jié),剩余8個(gè)字節(jié)控件由 x9 來(lái)填充

    #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) 的調(diào)用,保存到 x2 是因?yàn)?before_objc_msgSend 函數(shù)第三個(gè)參數(shù)需要傳入 lr贞间,方便后續(xù)返回贿条;blr 指令會(huì)改變 lr 寄存器的值,所以調(diào)用前先保存 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"); 目前個(gè)人認(rèn)為是冗余代碼增热,在整個(gè)流程中貌似并沒(méi)有實(shí)際作用整以。

  3. 通過(guò) blr 指令 跳轉(zhuǎn)執(zhí)行 before_objc_msgSend 函數(shù)。這里會(huì)先保存 x8峻仇、x9 寄存器的值公黑,原因是__asm volatile ("mov x12, %0\n" :: "r"(value)) 執(zhí)行命令過(guò)程中會(huì)通過(guò) x8 來(lái)保存函數(shù)地址,再進(jìn)行跳轉(zhuǎn)摄咆,所以這里會(huì)先要保存 x8凡蚜,和步驟1相同,棧指針移動(dòng)必須滿足 SP Mod 16 = 0 的條件吭从,所以 x9 也被保存朝蜘。執(zhí)行完之后 x8、x9 恢復(fù)涩金。

    #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)) 下斷點(diǎn)可以看到 cpu 是通過(guò) adrp + add 2個(gè)指令結(jié)合尋址到函數(shù)的地址并執(zhí)行谱醇,過(guò)程中改變了 x8 的值

    image-20190713185417531
  4. Step 4 到 Step 6,恢復(fù)原有入?yún)⒉阶觯瑘?zhí)行原函數(shù)副渴,然后保存入?yún)?/p>

  5. call(blr, &after_objc_msgSend) 和步驟3相似,執(zhí)行 hook 收尾的函數(shù)全度,主要是通過(guò) TSD 返回步驟3保存的原來(lái) lr 寄存器保存的內(nèi)容煮剧,也就是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返回的值(原來(lái)lr的初始值)到lr寄存器

  7. Step 9 - Step 10 恢復(fù)寄存器值,并返回。主要目的是還原原始函數(shù)的執(zhí)行之后的狀態(tài)轿秧。

遺留問(wèn)題:

以上就是整個(gè)匯編 hook objc_msgSend 的主要過(guò)程中跌,目前遺留一個(gè)問(wèn)題是:

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

參考文章:

  1. arm64程序調(diào)用規(guī)則
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末菇篡,一起剝皮案震驚了整個(gè)濱河市漩符,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌驱还,老刑警劉巖嗜暴,帶你破解...
    沈念sama閱讀 222,252評(píng)論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異议蟆,居然都是意外死亡闷沥,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,886評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門咐容,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)舆逃,“玉大人,你說(shuō)我怎么就攤上這事戳粒÷肥ǎ” “怎么了?”我有些...
    開封第一講書人閱讀 168,814評(píng)論 0 361
  • 文/不壞的土叔 我叫張陵蔚约,是天一觀的道長(zhǎng)奄妨。 經(jīng)常有香客問(wèn)我,道長(zhǎng)苹祟,這世上最難降的妖魔是什么砸抛? 我笑而不...
    開封第一講書人閱讀 59,869評(píng)論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮树枫,結(jié)果婚禮上直焙,老公的妹妹穿的比我還像新娘。我一直安慰自己团赏,他們只是感情好箕般,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,888評(píng)論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著舔清,像睡著了一般丝里。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上体谒,一...
    開封第一講書人閱讀 52,475評(píng)論 1 312
  • 那天杯聚,我揣著相機(jī)與錄音,去河邊找鬼抒痒。 笑死幌绍,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播傀广,決...
    沈念sama閱讀 41,010評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼颁独,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了伪冰?” 一聲冷哼從身側(cè)響起誓酒,我...
    開封第一講書人閱讀 39,924評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎贮聂,沒(méi)想到半個(gè)月后靠柑,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,469評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡吓懈,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,552評(píng)論 3 342
  • 正文 我和宋清朗相戀三年歼冰,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片耻警。...
    茶點(diǎn)故事閱讀 40,680評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡隔嫡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出甘穿,到底是詐尸還是另有隱情畔勤,我是刑警寧澤,帶...
    沈念sama閱讀 36,362評(píng)論 5 351
  • 正文 年R本政府宣布扒磁,位于F島的核電站,受9級(jí)特大地震影響式曲,放射性物質(zhì)發(fā)生泄漏妨托。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,037評(píng)論 3 335
  • 文/蒙蒙 一吝羞、第九天 我趴在偏房一處隱蔽的房頂上張望兰伤。 院中可真熱鬧,春花似錦钧排、人聲如沸敦腔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,519評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)符衔。三九已至,卻和暖如春糟袁,著一層夾襖步出監(jiān)牢的瞬間判族,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,621評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工项戴, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留形帮,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,099評(píng)論 3 378
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像辩撑,于是被迫代替她去往敵國(guó)和親界斜。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,691評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容

  • 關(guān)鍵時(shí)刻合冀,第一時(shí)間送達(dá)各薇! 問(wèn)題種類 時(shí)間復(fù)雜度 在集合里數(shù)據(jù)量小的情況下時(shí)間復(fù)雜度對(duì)于性能的影響看起來(lái)微乎其微。但...
    C9090閱讀 895評(píng)論 0 1
  • 在平時(shí)開發(fā)和調(diào)試中水慨,經(jīng)常遇到C調(diào)用棧和匯編得糜,所以這里來(lái)統(tǒng)一的了解下這部分內(nèi)容,本章需要一定的匯編基礎(chǔ)才能更好的理解...
    碼農(nóng)蒼耳閱讀 3,210評(píng)論 1 3
  • 引言 最近工作比較忙晰洒,沒(méi)怎么去研究匯編的內(nèi)容朝抖,這么多天,感覺(jué)有點(diǎn)生疏谍珊,有的時(shí)候累死累活很晚才回來(lái)治宣,還要在學(xué)習(xí)別的東...
    struggle3g閱讀 2,800評(píng)論 0 0
  • 一贝润、詞匯概念辨析 查了一下維基百科上關(guān)于這兩個(gè)詞匯的解釋绊茧,分別如下: 1. 登錄 登錄(英語(yǔ):login),計(jì)算機(jī)...
    shenxiaoma閱讀 5,249評(píng)論 1 9
  • 今天下午媽媽帶我和姐姐去看書了打掘,我邊看邊讀华畏,慢慢體會(huì)書中的意思。我看的有:《植物大戰(zhàn)僵尸二》還有好多好多的書呢尊蚁,姐...
    王文哲同學(xué)閱讀 238評(píng)論 0 0