A method of delay premain code

  • main函数之前执行的问题
  • 如何解决这些问题
  • 动态库
  • 性能
  • 参考代码
  • 下面三种方法可以让代码在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 的含义,可以参考文章 http://everettjf.com/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