大量的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函數之前執行:
-
All +load methods
-
All C++ static initializers
-
All C/C++ attribute(constructor) functions
main函數之前執行的問題
-
無法Patch
-
不能審計耗時
-
調用UIKit相關方法會導致部分類提早初始化
-
主線程執行,完全阻塞式執行
如何解決這些問題
能否提供一種便捷的方法把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中可以存任意地址,可以更靈活。