A method of hook static initializers

  • 这个section的用途是什么呢?
  • 有哪些方法可以产生Initializer?
  • 为什么要关注这些
  • 再看backtrace
  • 编译器合并规律
  • 如何 Hook,先找调用来源
  • 想想如何hook
  • 如何修改mod_init_func数据
  • 怎么调用原来的Initializer?
  • ASLR
  • 浮动的日志出来了,怎么再定位到文件?
  • 代码
  • 总结
  • 先补充:标题中 static initializers 其实应该叫做 C++ static initializers and C/C++ __attribute__(constructor) functions

    使用 MachOView 打开一个MachO文件,多数情况下会看到这个section __mod_init_func

    这个section的用途是什么呢?

    从名字大概猜测,module initializer functions,模块初始化函数,大概就是这个意思。

    从dyld的源码中可以找到mod_init_func相关字样:

    typedef void (*Initializer)(int argc, const char* argv[], const char* envp[], const char* apple[]);
    
    extern const Initializer  inits_start  __asm("section$start$__DATA$__mod_init_func");
    extern const Initializer  inits_end    __asm("section$end$__DATA$__mod_init_func");
    
    
    static void runDyldInitializers(const struct macho_header* mh, intptr_t slide, int argc, const char* argv[], const char* envp[], const char* apple[])
    {
    	for (const Initializer* p = &inits_start; p < &inits_end; ++p) {
    		(*p)(argc, argv, envp, apple);
    	}
    }
    

    注意注意:调试时会发现,dyld并没有通过调用 runDyldInitializers 来执行所有Initializer,而是通过 void ImageLoaderMachO::doModInitFunctions(const LinkContext& context) 来执行的。但上面的代码在首次搜索时,可以让我们对mod_init_func有个大概的印象。

    通过其他资料,可以知道有很多途径可以让代码产生对应的一个Initializer。

    有哪些方法可以产生Initializer?

    1. __attribute((constructor))

    __attribute__((constructor)) void myentry(){
        NSLog(@"constructor");
    }
    

    2. 全局变量的初始化需要执行代码

    这里主要是对于C++来说(或者Objective C++)源文件扩展名是.cpp .cxx 或.mm 。这里说的全局变量包括static修饰的作用域仅在当前文件的,也包括不被static修饰的。

    全局变量的初始化如果涉及以下情况,则会在mod_init_func中产生对应的条目:

    (1)需要执行C函数

    bool initBar(){
        int i = 0;
        ++i;
        return i == 1;
    }
    
    static bool globalBar = initBar();
    bool globalBar2 = initBar();
    

    (2)需要执行C++类的构造函数

    class FooObject{
    public:
        FooObject(){
            // do somthing
            NSLog(@"in fooobject");
        }
        
    };
    
    static FooObject globalObj = FooObject();
    FooObject globalObj2 = FooObject();
    

    (3)需要构造Objective-C 类

    static NSDictionary * dictObject = @{@"one":@"1"};
    NSDictionary * dictObject2 = @{@"one":@"1", @"two":@"2"};
    

    (4)struct对于C++来说也可以说是一种类

    这种代码其实就执行了CGRect的构造函数,很隐蔽呀~防不胜防啊~

    CGRect globalRect = CGRectZero;
    

    (5)间接导致运行函数

    下面的代码间接导致了初始化globalArray时运行了description方法。

    NSString *description(const char *str){
        return [NSString stringWithFormat:@"hello %s",str];
    }
    
    #define E(str) description(str)
    
    
    NSString* globalArray[] = {
        E("hello"),
        E("hello"),
        E("hello"),
        E("hello"),
        E("hello"),
        E("hello"),
    };
    
    NSString *globalString = E("world");
    

    (6)其他

    还有各式各样其他的姿势。

    为什么要关注这些

    由于目前iOS App多数都只是在使用静态库,大量第三方或内部C++写的代码需要静态链接,上面这些代码间接增加了主程序在main函数之前的执行时间。

    如果是动态库,且是启动阶段加载,那这些代码依然对启动性能有影响。

    再看backtrace

    加断点后backtrace,可以看到dyld的调用堆栈:

    编译器合并规律

    同一个文件中的所有initializer会自动产生一个Initializer,类似于把一个文件中的所有初始化工作交给一个新创建的函数。

    如果在一个文件中初始化大量的全局变量,可以发现:最终在mod_init_func段中只产生了一项。而且这一项的符号是下面这样:

    frame #3: 0x00000001000b8854 ModFuncInitApp`_GLOBAL__sub_I_TestClass.mm + 24 at TestClass.mm:0
    

    类似于生成了一个 名称是 _GLOBAL__sub_I_TestClass.mm 的函数。

    如何 Hook,先找调用来源

    先看调用来源,

    (lldb) bt
    * thread #1: tid = 0x47250, 0x00000001000b87c8 ModFuncInitApp`FooObject::FooObject(this=0x00000001000bd2d8) + 20 at TestClass.mm:15, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
      * frame #0: 0x00000001000b87c8 ModFuncInitApp`FooObject::FooObject(this=0x00000001000bd2d8) + 20 at TestClass.mm:15
        frame #1: 0x00000001000b879c ModFuncInitApp`FooObject::FooObject(this=0x00000001000bd2d8) + 28 at TestClass.mm:13
        frame #2: 0x00000001000b8804 ModFuncInitApp`::__cxx_global_var_init() + 24 at TestClass.mm:20
        frame #3: 0x00000001000b8854 ModFuncInitApp`_GLOBAL__sub_I_TestClass.mm + 24 at TestClass.mm:0
        frame #4: 0x00000001000b93e8 ModFuncInitApp`myInitFunc_Initializer(argc=1, argv=0x000000016fd4bab8, envp=0x000000016fd4bac8, apple=0x000000016fd4bb48, vars=0x00000001001d9918) + 140 at hook_cpp_init.mm:64
        frame #5: 0x00000001001bd95c dyld`ImageLoaderMachO::doModInitFunctions(ImageLoader::LinkContext const&) + 372
        frame #6: 0x00000001001bdb84 dyld`ImageLoaderMachO::doInitialization(ImageLoader::LinkContext const&) + 36
        frame #7: 0x00000001001b8f2c dyld`ImageLoader::recursiveInitialization(ImageLoader::LinkContext const&, unsigned int, char const*, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 368
        frame #8: 0x00000001001b7f50 dyld`ImageLoader::processInitializers(ImageLoader::LinkContext const&, unsigned int, ImageLoader::InitializerTimingList&, ImageLoader::UninitedUpwards&) + 140
        frame #9: 0x00000001001b8004 dyld`ImageLoader::runInitializers(ImageLoader::LinkContext const&, ImageLoader::InitializerTimingList&) + 84
        frame #10: 0x00000001001aa488 dyld`dyld::initializeMainExecutable() + 220
        frame #11: 0x00000001001ae8f4 dyld`dyld::_main(macho_header const*, unsigned long, int, char const**, char const**, char const**, unsigned long*) + 3892
        frame #12: 0x00000001001a9044 dyld`_dyld_start + 68
    

    通过堆栈,能看到 dyld 的 doModInitFunctions 会调用每个文件中的Initializer。从dyld的源码中找到这个函数:

    void ImageLoaderMachO::doModInitFunctions(const LinkContext& context)
    {
    	if ( fHasInitializers ) {
    		const uint32_t cmd_count = ((macho_header*)fMachOData)->ncmds;
    		const struct load_command* const cmds = (struct load_command*)&fMachOData[sizeof(macho_header)];
    		const struct load_command* cmd = cmds;
    		for (uint32_t i = 0; i < cmd_count; ++i) {
    			if ( cmd->cmd == LC_SEGMENT_COMMAND ) {
    				const struct macho_segment_command* seg = (struct macho_segment_command*)cmd;
    				const struct macho_section* const sectionsStart = (struct macho_section*)((char*)seg + sizeof(struct macho_segment_command));
    				const struct macho_section* const sectionsEnd = &sectionsStart[seg->nsects];
    				for (const struct macho_section* sect=sectionsStart; sect < sectionsEnd; ++sect) {
    					const uint8_t type = sect->flags & SECTION_TYPE;
    					if ( type == S_MOD_INIT_FUNC_POINTERS ) {
    						Initializer* inits = (Initializer*)(sect->addr + fSlide);
    						const size_t count = sect->size / sizeof(uintptr_t);
    						for (size_t i=0; i < count; ++i) {
    							Initializer func = inits[i];
    							// <rdar://problem/8543820&9228031> verify initializers are in image
    							if ( ! this->containsAddress((void*)func) ) {
    								dyld::throwf("initializer function %p not in mapped image for %s\n", func, this->getPath());
    							}
    							if ( context.verboseInit )
    								dyld::log("dyld: calling initializer function %p in %s\n", func, this->getPath());
    							func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
    						}
    					}
    				}
    			}
    			cmd = (const struct load_command*)(((char*)cmd)+cmd->cmdsize);
    		}
    	}
    }
    

    注意看这三句:

    Initializer* inits = (Initializer*)(sect->addr + fSlide);
    Initializer func = inits[i];
    func(context.argc, context.argv, context.envp, context.apple, &context.programVars);
    
    

    可以看到mod_init_func中的每一项,都作为一个函数地址调用,函数类型是 Initializer。那我们找到 Initializer 的原型:

    	typedef void (*Initializer)(int argc, const char* argv[], const char* envp[], const char* apple[], const ProgramVars* vars);
    

    想想如何hook

    既然mod_init_func中的每个地址都是一个函数地址,且原型也都是一样的。那我们就想办法把mod_init_func中的所有地址都替换为我们自己的函数地址。

    先定义一个自己的函数:

    void myInitFunc_Initializer(int argc, const char* argv[], const char* envp[], const char* apple[], const struct MyProgramVars* vars){
        printf("my init func\n");
    
    }
    

    那么问题来了,如何让dyld在读取mod_init_func中的数据时,读到的是我们自己的myInitFunc_Initializer呢?

    (1)首先,注意到 __mod_init_func section 位于 __DATA segment。__DATA segment是数据段,是可以在运行时修改的。

    (2)其次,就是找个时机,要早于dyld读取这些Initializer。

    平时在使用Objective C的+load方法时,注意到文档这么写:

    The order of initialization is as follows:
    
    - All initializers in any framework you link to.
    - All +load methods in your image.
    - All C++ static initializers and C/C++ __attribute__(constructor) functions in your image.
    - All initializers in frameworks that link to you.
    

    +load methods 竟然要更早。那就好办了。在任意一个+load方法中找到进程加载后,mod_init_func段在内存中的地址,把数据都改为 myInitFunc_Initializer 的地址。

    如何修改mod_init_func数据

    使用 getsectiondata 函数可以获取mod_init_func段的内存地址,直接修改就行了。

    代码如下:

    #ifndef __LP64__
        typedef uint32_t MemoryType;
    #else /* defined(__LP64__) */
        typedef uint64_t MemoryType;
    #endif /* defined(__LP64__) */
    
        Dl_info info;
        dladdr((const void *)hookModInitFunc, &info);
        
    #ifndef __LP64__
        const struct mach_header *mhp = (struct mach_header*)info.dli_fbase;
        unsigned long size = 0;
        MemoryType *memory = (uint32_t*)getsectiondata(mhp, "__DATA", "__mod_init_func", & size);
    #else /* defined(__LP64__) */
        const struct mach_header_64 *mhp = (struct mach_header_64*)info.dli_fbase;
        unsigned long size = 0;
        MemoryType *memory = (uint64_t*)getsectiondata(mhp, "__DATA", "__mod_init_func", & size);
    #endif /* defined(__LP64__) */
        for(int idx = 0; idx < size/sizeof(void*); ++idx){
            MemoryType original_ptr = memory[idx];
            // 这里可以保存原来的地址
            
            memory[idx] = (MemoryType)myInitFunc_Initializer; // 替换为我们自己的Initializer
        }
    

    怎么调用原来的Initializer?

    想来想去,没想到办法给myInitFunc_Initializer增加记录对应的原函数地址的方法。突然一想,不用管调用顺序,把所有的原函数地址记录下来,然后每调用一次 myInitFunc_Initializer 就逐个调用原函数就行了。

    还是看代码吧。

    static std::vector<MemoryType> *g_initializer; // 记录每一个原函数地址
    static int g_cur_index;
    

    然后,在自己的Initializer中逐个获取每一个原函数地址,调用并计算耗时。

    
    typedef void (*OriginalInitializer)(int argc, const char* argv[], const char* envp[], const char* apple[], const MyProgramVars* vars);
    
    void myInitFunc_Initializer(int argc, const char* argv[], const char* envp[], const char* apple[], const struct MyProgramVars* vars){
        printf("my init func\n");
        
        ++g_cur_index;
        OriginalInitializer func = (OriginalInitializer)g_initializer->at(g_cur_index);
        
        CFTimeInterval start = CFAbsoluteTimeGetCurrent();
        
        func(argc,argv,envp,apple,vars);
        
        CFTimeInterval end = CFAbsoluteTimeGetCurrent();
    }
    

    ASLR

    由于ASLR的存在,不能只记录函数的地址,还需要记录ASLR的地址。用于后续通过符号文件定位出函数地址。

    ASLR偏移(准确的说是,ASLR偏移后的基地址,感谢 Joy__指出)就是上面代码中的变量 mhp(也就是 mach_header_64 的dli_fbase)。

    浮动的日志出来了,怎么再定位到文件?

    有了符号文件,把app文件和dsym放在同一个目录下,就可以定位到文件啦。

    atos -o Demo.app/Demo 0x100a1a47c -l 0x100018000
    
    _GLOBAL__sub_I_XXXXX.cpp (in Demo) + 1
    
    

    详细参考这篇文章

    • http://www.jamiegrove.com/software/fixing-bugs-using-os-x-crash-logs-and-atos-to-symbolicate-and-find-line-numbers 或者
    • https://everettjf.github.io/2015/09/09/ios-plcrashreporter#dsym

    代码

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

    总结

    定位起来确实麻烦,但使用这个方法能从日志中定位到真实使用App的过程中那些耗时浮动较大的Initializer。

    每个Initializer都耗时很少,但长期以来,各种不需要在App启动阶段执行的Initializer都悄无声息的进来了。群众的力量大啊。