一种延迟 premain code 的方法
下面三种方法可以让代码在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能力。