Objective-C runtime 拾遺 (二)——Log message send

canopus4u發表於2015-12-25

原因

最近在考慮對App中所有的message進行Log,資料不少,前人也有一些實現,做些記錄。

對Objc_msgSend進行Hook

OC的Messaging都是通過改函式的呼叫的。如[foo bar],會被轉化成:Objc_msgSend(foo,@selector(bar))。這個大家應該都知道,不多說。具體參見Apple文件:這裡這裡

如果能夠對其進行Hook,那就可以進行Log了。Facebook出品了fishhook原理介紹很多,在載入符號表時進行替換,不多說。但最後問題是,Hook之後怎麼將原來的引數傳遞給原來的實現呢?好像也不是很straightforward,這個後面再寫一篇文章來討論這個問題。但目前想要簡單的通過HookObjc_msgSend來對message進行Log並不方便

系統提供的方法

因為其實最開始並不想做Hook,所以先看看系統是否提供Log所有Message的方法。果然,Simulator上是有方法的。

開啟instrumentObjcMessageSends

方法並不唯一

在lldb中

(lldb)p (void)instrumentObjcMessageSends(YES)

開啟之後Log日誌放在/private/tmp/msgSends-%d%d是程式pid

在code中

instrumentObjcMessageSends(YES);

要注意的是,使用前記得宣告一下:

extern void instrumentObjcMessageSends(BOOL);

當然可以稍微geek一點來呼叫:

通過dlsym

typedef void functype(BOOL);
void *libobjc = dlopen("/usr/lib/libobjc.dylib", RTLD_LAZY);
functype *instrumentObjcMessageSends = dlsym(libobjc, "instrumentObjcMessageSends");
instrumentObjcMessageSends(YES);

日誌還是放在同樣的地方。

logMessageSend

上述只是開啟instrumentObjcMessageSends,但是,具體Log的實現是系統的。如果開發者想要對Log的message進行過濾,似乎還要一些手段(當然可以通過處理msgSends-%d檔案來實現,不過那樣就不動態了)。
看看最新的原始碼,發現一段程式碼:

bool logMessageSend(bool isClassMethod,
                    const char *objectsClass,
                    const char *implementingClass,
                    SEL selector)
{
    ...
}

這個就是系統實現Log方法,具體不展開解釋了。

那麼只需要對logMessageSend進行Hook一下就好了,現在可以愉快的使用fishhook了。

#import "fishhook.h"
#import <dlfcn.h>

void (*orig_logMessageSend)(bool,const char *,const char *,SEL);
void my_logMessageSend(bool,const char *,const char *,SEL);

void my_logMessageSend(bool isClassMethod,
                    const char *objectsClass,
                    const char *implementingClass,
                    SEL selector)
{
    //實現自己的Log
    ......
    //也可以不呼叫
    orig_logMessageSend(isClassMethod,objectsClass,implementingClass,selector);
}

rebind_symbols((struct rebinding[1]){{"logMessageSend", my_logMessageSend, (void *)&orig_logMessageSend}}, 1);

使用DTrace

sudo dtrace -q -n `objc1234:::entry { printf("%s %s
", probemod, probefunc); }` 

沒試過,參考:1 2

總述&討論

  • 上述方法只能在模擬器上,不支援iOS device。可以參見原始碼。

  • 該方法只能Log,其他也不能幹什麼。似乎還是得打objc_msgSend的主意。

已經有人這樣幹過了,Mike Ash 翻譯在此。這是個很好的參考。不過稍微有點年久失修,下次再講講原理和實踐。

  • 在上面進行logMessageSendHook時還有點其他東西。原本有工程師發現以前的原始碼是支援從外部注入Log函式的。這是系統的實現,原始碼

typedef int    (*ObjCLogProc)(BOOL, const char *, const char *, SEL);
__private_extern__ void    logObjcMessageSends      (ObjCLogProc    logProc)
{
    if (logProc)
    {
        objcMsgLogProc = logProc;
        objcMsgLogEnabled = 1;
    }
    else
    {
        objcMsgLogProc = logProc;
        objcMsgLogEnabled = 0;
    }

    if (objcMsgLogFD != (-1))
        fsync (objcMsgLogFD);
}

只要找到logObjcMessageSends符號,呼叫即可。雖然被staic隱藏了,但不妨礙。除了上述dlsym。還有下面一種方法:

#import <mach-o/nlist.h>
typedef int    (*ObjCLogProc)(BOOL, const char *, const char *, SEL);
typedef int (*LogObjcMessageSendsFunc)(ObjCLogProc);
extern bool logMessageSend(bool isClassMethod,
                    const char *objectsClass,
                    const char *implementingClass,
                           SEL selector);
{
    LogObjcMessageSendsFunc fcn;
    struct nlist nl[2];
    bzero(&nl, sizeof(struct nlist) * 2);
    nl[0].n_un.n_name = "_instrumentObjcMessageSends";
    nl[1].n_un.n_name = "_logObjcMessageSends";

    typedef int (*LogObjcMessageSendsFunc)(ObjCLogProc);
    fcn = (LogObjcMessageSendsFunc)( (long) (&instrumentObjcMessageSends) + (nl[1].n_value-nl[0].n_value));
    fcn(&my_logMessageSend);
}                           

不太實用。因為新的實現已經沒有這個方法。只是覺得比較有趣,記錄一下。

原作寫於segmentfault 連結

相關文章