一種延遲 premain code 的方法

大量的premain代碼,不可控,在線上隨時都是炸彈。爲了讓開發者過渡的更“透明“,有了下面的方法。

 

想法來源仍然是兩年前的三篇分析Facebook客戶端的文章:

 

1- 探索 facebook iOS 客戶端 - section fbsessiongks

https://everettjf.github.io/2016/08/21/facebook-explore-section-fbsessiongks/
2- 探索 facebook iOS 客戶端 - section FBInjectable

https://everettjf.github.io/2016/08/20/facebook-explore-section-fbinjectable/

3- 探索 Facebook iOS 客戶端 - Section RODATA

https://everettjf.github.io/2016/08/19/facebook-explore-section-rodata/

 

下面三種方法可以讓代碼在main函數之前執行:

  1. All +load methods

  2. All C++ static initializers

  3. All C/C++ attribute(constructor) functions

main函數之前執行的問題

  1. 無法Patch

  2. 不能審計耗時

  3. 調用UIKit相關方法會導致部分類提早初始化

  4. 主線程執行,完全阻塞式執行

如何解決這些問題

能否提供一種便捷的方法把main函數之前的代碼移植到main函數之後。

想法來源

發現 Facebook 有個新增的段 FBInjectable ,學習這個段的含義可以知道:可以在編譯及鏈接時期把一些數據放到自定義段中,然後程序中獲取段的數據。

如果這個數據是字符串,我們可以通過字符串獲取類名;如果是函數地址,我們可以直接調用。

(關於 Facebook 的段 FBInjectable 的含義,可以參考文章 https://everettjf.github.io/2016/08/20/facebook-explore-section-fbinjectable )

那麼如何創建FBInjectable段呢?

可以使用 __attribute((used,section("segmentname,sectionname"))) 關鍵字把某個變量的放入特殊的section中。

(attribute 參考 http://gcc.gnu.org/onlinedocs/gcc-3.2/gcc/Variable-Attributes.html )

例如:

char * kString1 __attribute((used,section("__DATA,FBInjectable"))) = "string 1";
char * kString2 __attribute((used,section("__DATA,FBInjectable"))) = "string 2";
char * kString3 __attribute((used,section("__DATA,FBInjectable"))) = "string 3";

編譯後,可以在程序的 DATA segment下新建 FBInjectable section,並把kString1,kString2,kString3 三個變量的地址作爲 FBInjectable section 內容。

如何應用

模仿Facebook的代碼,下面這段代碼可以把函數地址(varSampleObject的值)的地址放到QWLoadable段中。

typedef void (*QWLoadableFunctionTemplate)();
static void QWLoadableSampleFunction(){
    // Do something
}
static QWLoadableFunctionTemplate varSampleObject __attribute((used, section("__DATA,QWLoadable"))) = QWLoadableSampleFunction;

然後主程序在啓動時通過getsectiondata獲取到QWLoadable的內容,並逐個調用。

進一步完善

爲了能標記每個函數的名字,可以讓函數內部傳出,如下:

typedef int (*QWLoadableFunctionCallback)(const char *);
typedef void (*QWLoadableFunctionTemplate)(QWLoadableFunctionCallback);

static void QWLoadableSampleFunction(QWLoadableFunctionCallback QWLoadableCallback){
    if(0 != QWLoadableCallback("SampleObject")) return;

    // Do something
}

static QWLoadableFunctionTemplate varSampleObject __attribute((used, section("__DATA,QWLoadable"))) = QWLoadableSampleFunction;

這樣函數通過 QWLoadableCallback 告訴外部自己的“標識”,並給予外部過濾自己(不調用)的能力。

啓動時調用

static int QWLoadableFunctionCallbackImpl(const char *name){
    // filter by name
    return 0;
}

static void QWLoadableRun(){
    CFTimeInterval loadStart = CFAbsoluteTimeGetCurrent();

    Dl_info info;
    int ret = dladdr(QWLoadableRun, &info);
    if(ret == 0){
        // fatal error
    }

#ifndef __LP64__
    const struct mach_header *mhp = (struct mach_header*)info.dli_fbase;
    unsigned long size = 0;
    uint32_t *memory = (uint32_t*)getsectiondata(mhp, QWLoadableSegmentName, QWLoadableSectionName, & size);
#else /* defined(__LP64__) */
    const struct mach_header_64 *mhp = (struct mach_header_64*)info.dli_fbase;
    unsigned long size = 0;
    uint64_t *memory = (uint64_t*)getsectiondata(mhp, QWLoadableSegmentName, QWLoadableSectionName, & size);
#endif /* defined(__LP64__) */

    CFTimeInterval loadComplete = CFAbsoluteTimeGetCurrent();
    NSLog(@"QWLoadable:loadcost:%@ms",@(1000.0*(loadComplete-loadStart)));
    if(size == 0){
        NSLog(@"QWLoadable:empty");
        return;
    }

    for(int idx = 0; idx < size/sizeof(void*); ++idx){
        QWLoadableFunctionTemplate func = (QWLoadableFunctionTemplate)memory[idx];
        func(QWLoadableFunctionCallbackImpl);
    }

    NSLog(@"QWLoadable:callcost:%@ms",@(1000.0*(CFAbsoluteTimeGetCurrent()-loadComplete)));
}

改造

調用方可以像下面這樣,把原來在+load中的代碼移植到兩個宏(QWLoadableFunctionBegin 和 QWLoadableFunctionEnd)之間。

QWLoadableFunctionBegin(FooObject)
[BarObject userDefinedLoad];
// anything here
QWLoadableFunctionEnd(FooObject)

動態庫

動態庫是獨立的個體,所以需要單獨處理動態庫中的QWLoadable的段。

性能

把+load等main函數之前的代碼移植到了main函數之後,但也新增了一個讀取section的耗時。

經過測試,100個函數地址的讀取,在iPhone5的設備上讀取不到1ms。新增了這不到1ms的耗時(這1ms也是可審計的),帶來了所有啓動階段行爲的可審計,以及最重要的Patch能力。

參考代碼

https://github.com/everettjf/Yolo/tree/master/LoadableMacro

總結

這只是一個最簡單的例子,section中可以存任意地址,可以更靈活。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章