iOS:load方法能不能被hook?

作者: 笑出zhu声 | 来源:发表于2020-06-06 00:01 被阅读0次

    今天我们讨论的hook方式仅仅是指Method SwizzlefishhookCydia Substrate 等方式不在今天的讨论范畴。

    hook load方法我们主要面临以下问题:

    • 能不能hook:hook的原理是什么,load方法和普通方法有什么不同?

    • 什么时机hook:我们经常在load方法中hook其他方法,那hook load方法在什么时机呢?

    能不能hook?

    首先,我们看下Objc中方法交换的原理,下面是一段典型的实现方法交换的代码:

    Class class = [self class];
    
    // 原方法名和替换方法名
    SEL originalSelector = @selector(isEqualToString:);
    SEL swizzledSelector = @selector(swizzle_IsEqualToString:);
    
    // 原方法和替换方法
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    
    // 如果当前类没有原方法的实现IMP,先调用class_addMethod来给原方法添加实现
    BOOL didAddMethod = class_addMethod(class,
                                        originalSelector,
                                      method_getImplementation(swizzledMethod),
                                        method_getTypeEncoding(swizzledMethod));
    
    if (didAddMethod) {// 添加方法实现IMP成功后,替换方法实现
        class_replaceMethod(class,
                            swizzledSelector,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
    } else { // 有原方法,交换两个方法的实现
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
    

    其中实现交换的关键方法是class_replaceMethodmethod_exchangeImplementations,我们分别看下这两个方法的实现原理(源码来自:objc-runtime-new.mm)。

    • class_replaceMethod

      IMP 
      class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
      {
          if (!cls) return nil;
      
          mutex_locker_t lock(runtimeLock);
          return addMethod(cls, name, imp, types ?: "", YES);
      }
      

      class_replaceMethod实际上调用的addMethods,入参replace = YES,我们看下addMethods的实现:

      static IMP 
      addMethod(Class cls, SEL name, IMP imp, const char *types, bool replace)
      {
          IMP result = nil;
      
          runtimeLock.assertLocked();
      
          checkIsKnownClass(cls);
          
          ASSERT(types);
          ASSERT(cls->isRealized());
      
          method_t *m;
          if ((m = getMethodNoSuper_nolock(cls, name))) {
              // already exists
              if (!replace) {
                  result = m->imp;
              } else {
                  result = _method_setImplementation(cls, m, imp);
              }
          } else {
              // fixme optimize
              method_list_t *newlist;
              newlist = (method_list_t *)calloc(sizeof(*newlist), 1);
              newlist->entsizeAndFlags = 
                  (uint32_t)sizeof(method_t) | fixed_up_method_list;
              newlist->count = 1;
              newlist->first.name = name;
              newlist->first.types = strdupIfMutable(types);
              newlist->first.imp = imp;
      
              prepareMethodLists(cls, &newlist, 1, NO, NO);
              cls->data()->methods.attachLists(&newlist, 1);
              flushCaches(cls);
      
              result = nil;
          }
      
          return result;
      }
      

      addMethods 中最终调用 _method_setImplementation 来替换Method的imp来实现方法替换:

      static IMP 
      _method_setImplementation(Class cls, method_t *m, IMP imp)
      {
          runtimeLock.assertLocked();
      
          if (!m) return nil;
          if (!imp) return nil;
      
          IMP old = m->imp;
          m->imp = imp;
      
          // Cache updates are slow if cls is nil (i.e. unknown)
          // RR/AWZ updates are slow if cls is nil (i.e. unknown)
          // fixme build list of classes whose Methods are known externally?
      
          flushCaches(cls);
      
          adjustCustomFlagsForMethodChange(cls, m);
      
          return old;
      }
      
    • method_exchangeImplementations

      method_exchangeImplementations 的实现相对简单:

      void method_exchangeImplementations(Method m1, Method m2)
      {
       if (!m1 || !m2) return;
       mutex_locker_t lock(runtimeLock);
       //交换IMP
       IMP m1_imp = m1->imp;
       m1->imp = m2->imp;
       m2->imp = m1_imp;
       // RR/AWZ updates are slow because class is unknown
       // Cache updates are slow because class is unknown
       // fixme build list of classes whose Methods are known externally?
       flushCaches(nil);
       adjustCustomFlagsForMethodChange(nil, m1);
       adjustCustomFlagsForMethodChange(nil, m2);
      }
      

      方法交换的核心是通过交换Method的imp来实现交换方法,而method_exchangeImplementations入参是MethodMethod通过class_getClassMethod 或者 class_getInstanceMethodclass_getClassMethod最终还是调用的class_getInstanceMethod注意:这里为什么用cls->getMeta(),我们后面介绍):

      Method class_getClassMethod(Class cls, SEL sel)
      {
       if (!cls || !sel) return nil;
       return class_getInstanceMethod(cls->getMeta(), sel);
      }
      

      class_getInstanceMethod最终通过搜索方法列表找到对应方法(调用栈较长,这里就不提供源码了)。

    通过上面的分析:如果load方法在类的方法列表中,就能实现方法交换。我们新建个带个load和替换方法swizzle_load的类:

    @implementation ClassA
    + (void)load {
        NSLog(@"load");
    }
    
    + (void)swizzle_load {
        NSLog(@"swizzle_load");
    }
    @end
    

    然后在main函数中写一段测试代码,获取ClassA的方法列表:

    void runTests (Class c ) {
        unsigned int count;
        Method *methods = class_copyMethodList(c, &count);
        for (int i = 0; i < count; i++) {
            Method method = methods[i];
            SEL selector = method_getName(method);
            NSString *name = NSStringFromSelector(selector);
            NSLog(@"方法名:%@",name);
        }
    }
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            runTests(ClassA.class);
        }
        return 0;
    }
    

    我们发现:ClassA的方法列表是空的

    为了理解这个原因,我们需要了解下元类(Meta Class)。我们知道Objc中方法的调用是通过给对象发消息实现的,对于实例方法是可行的,但是类方法呢?类方法的调用没有“对象”可以发送消息。所以Objc的设计者引入了元类:元类对象是描述类对象的类,每个类都有自己的元类,也就是类的isa指向的类,调用类方法实际上是给类的元类对象发送消息上文中class_getClassMethod 的实现是调用class_getInstanceMethod并且入参 cls->getMeta()正是这个原因。

    对象,类,元类关系示意

    了解了元类后,既然load方法是类方法,那我们尝试获取下ClassA元类的方法列表,将main函数中的代码做下修改:

    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            Class metaClass = objc_getMetaClass("ClassA");
            runTests(metaClass);
        }
        return 0;
    }
    
     HookTest[10929:9866637] 方法名:swizzle_load
     HookTest[10929:9866637] 方法名:load
    

    现在我们成功获取到了ClassA的方法列表。方法列表中既然有load ,说明load方法是可以hook的了。我们新建ClassB,并且保证先调用ClassBload方法(Compile Sources的顺序:ClassBClassA前面),然后在ClassB中的load方法hook ClassAload方法(注意:这里交换的是ClassA的元类的方法,其它类方法的hook同理):

    + (void) load {
        Class class = NSClassFromString(@"ClassA");
    
        //交换的是ClassA的元类的方法
        Class mateClass = objc_getMetaClass("ClassA");
    
        // 原方法名和替换方法名
        SEL originalSelector = @selector(load);
        SEL swizzledSelector = @selector(swizzle_load);
    
        // 原方法和替换方法
        Method originalMethod = class_getClassMethod(class, originalSelector);
        Method swizzledMethod = class_getClassMethod(class, swizzledSelector);
    
    
        // 如果当前类没有原方法的实现IMP,先调用class_addMethod来给原方法添加实现
        BOOL didAddMethod = class_addMethod(mateClass,
                                            originalSelector,
                                          method_getImplementation(swizzledMethod),
                                            method_getTypeEncoding(swizzledMethod));
    
        if (didAddMethod) {// 添加方法实现IMP成功后,替换方法实现
            class_replaceMethod(mateClass,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else { // 有原方法,交换两个方法的实现
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    }
    @end
    

    运行后发现:ClassAswizzle_load方法并没有被调用,load方法hook失败。

    什么时机hook?

    上文中解决了元类、load 调用顺序的问题,为什么还是失败了呢?是不是hook的时机晚了呢?我们先来了解下load方法的调用原理,我们在ClassAload方法打个断点,看下调用栈:

    load方法调用栈

    动态链接器dyld完成对二进制文件(动态库,可执行文件)的初始化后通过回调函数_dyld_objc_notify_register调用load_imagescall_load_methods实现load方法的调用:

    void
    load_images(const char *path __unused, const struct mach_header *mh)
    {
        // Return without taking locks if there are no +load methods here.
        if (!hasLoadMethods((const headerType *)mh)) return;
    
        recursive_mutex_locker_t lock(loadMethodLock);
    
        // Discover load methods
        {
            mutex_locker_t lock2(runtimeLock);
            prepare_load_methods((const headerType *)mh);
        }
    
        // Call +load methods (without runtimeLock - re-entrant)
        call_load_methods();
    }
    

    load_images中先通过prepare_load_methods将所有类的load方法加入到list中:

    void prepare_load_methods(const headerType *mhdr)
    {
        size_t count, i;
    
        runtimeLock.assertLocked();
    
        classref_t const *classlist = 
            _getObjc2NonlazyClassList(mhdr, &count);
        for (i = 0; i < count; i++) {
            schedule_class_load(remapClass(classlist[i]));
        }
    
        category_t * const *categorylist = _getObjc2NonlazyCategoryList(mhdr, &count);
        for (i = 0; i < count; i++) {
            category_t *cat = categorylist[i];
            Class cls = remapClass(cat->cls);
            if (!cls) continue;  // category for ignored weak-linked class
            if (cls->isSwiftStable()) {
                _objc_fatal("Swift class extensions and categories on Swift "
                            "classes are not allowed to have +load methods");
            }
            realizeClassWithoutSwift(cls, nil);
            ASSERT(cls->ISA()->isRealized());
            add_category_to_loadable_list(cat);
        }
    }
    

    prepare_load_methods中调用schedule_class_load,的实现如下:

    static void schedule_class_load(Class cls)
    {
        if (!cls) return;
        ASSERT(cls->isRealized());  // _read_images should realize
    
        if (cls->data()->flags & RW_LOADED) return;
    
        // Ensure superclass-first ordering
        schedule_class_load(cls->superclass);
    
        add_class_to_loadable_list(cls);
        cls->setInfo(RW_LOADED); 
    }
    

    这里主要看下add_class_to_loadable_list的实现:

    void add_class_to_loadable_list(Class cls)
    {
        IMP method;
    
        loadMethodLock.assertLocked();
    
        method = cls->getLoadMethod();
        if (!method) return;  // Don't bother if cls has no +load method
    
        if (PrintLoading) {
            _objc_inform("LOAD: class '%s' scheduled for +load", 
                         cls->nameForLogging());
        }
    
        if (loadable_classes_used == loadable_classes_allocated) {
            loadable_classes_allocated = loadable_classes_allocated*2 + 16;
            loadable_classes = (struct loadable_class *)
                realloc(loadable_classes,
                                  loadable_classes_allocated *
                                  sizeof(struct loadable_class));
        }
    
        loadable_classes[loadable_classes_used].cls = cls;
        loadable_classes[loadable_classes_used].method = method;
        loadable_classes_used++;
    }
    

    注意这里的method只是load方法的imp,并不是Method结构体,看下getLoadMethod的实现:

    IMP 
    objc_class::getLoadMethod()
    {
        runtimeLock.assertLocked();
    
        const method_list_t *mlist;
    
        ASSERT(isRealized());
        ASSERT(ISA()->isRealized());
        ASSERT(!isMetaClass());
        ASSERT(ISA()->isMetaClass());
    
        mlist = ISA()->data()->ro->baseMethods();
        if (mlist) {
            for (const auto& meth : *mlist) {
                const char *name = sel_cname(meth.name);
                if (0 == strcmp(name, "load")) {
                    return meth.imp;
                }
            }
        }
    
        return nil;
    }
    

    loadable_classes 记录了所有有load方法的类和load方法的imp,prepare_load_methods结束后通过call_load_methods调用所有的load方法:

    void call_load_methods(void)
    {
        static bool loading = NO;
        bool more_categories;
    
        loadMethodLock.assertLocked();
    
        // Re-entrant calls do nothing; the outermost call will finish the job.
        if (loading) return;
        loading = YES;
    
        void *pool = objc_autoreleasePoolPush();
    
        do {
            // 1. Repeatedly call class +loads until there aren't any more
            while (loadable_classes_used > 0) {
                call_class_loads();
            }
    
            // 2. Call category +loads ONCE
            more_categories = call_category_loads();
    
            // 3. Run more +loads if there are classes OR more untried categories
        } while (loadable_classes_used > 0  ||  more_categories);
    
        objc_autoreleasePoolPop(pool);
    
        loading = NO;
    }
    

    call_class_loads的实现:

    static void call_class_loads(void)
    {
        int i;
    
        // Detach current loadable list.
        struct loadable_class *classes = loadable_classes;
        int used = loadable_classes_used;
        loadable_classes = nil;
        loadable_classes_allocated = 0;
        loadable_classes_used = 0;
    
        // Call all +loads for the detached list.
        for (i = 0; i < used; i++) {
            Class cls = classes[i].cls;
            load_method_t load_method = (load_method_t)classes[i].method;
            if (!cls) continue; 
    
            if (PrintLoading) {
                _objc_inform("LOAD: +[%s load]\n", cls->nameForLogging());
            }
            (*load_method)(cls, @selector(load));
        }
    
        // Destroy the detached list.
        if (classes) free(classes);
    }
    

    通过上面的分析,我们知道了load方法为什么hook失败的原因:在调用load方法之前,所有有load方法的类和load方法的imp已经被记录到loadable_classeslist中,所以后面再交换load方法的imp就没用了。 那么hook的操作还能提前吗,提前到prepare_load_methods之前呢?其实是可以的,了解dyld过程的可能知道:dyld对每个二进制文件(动态库,可执行文件)都会有一个load_images的回调,而这个回调的顺序也是二进制文件的加载顺序,二进制文件的加载顺序是先动态库,再可执行文件(从依赖关系的叶子节点开始加载)。如果我们在一个动态库中hook可执行文件中的某个load方法应该可以提前到可执行文件的prepare_load_methods之前。
    ClassB制作成动态库,然后加到工程中,结构如下:

    工程结构
    运行下后:
    HookTest[21670:10529108] swizzle_load
    

    如果要hook某个动态库中的load方法呢?原理是一样的,找到在它前面加载的动态库即可。

    综上,load方法是可以hook的,不过成本较高。在大型App中,load方法在整个App的启动耗时中可能会有比较大的占比。在做启动优化的过程中,需要计算load方法的耗时,如果使用日志的方式往往工作量巨大,甚至在多二进制文件的工程中变得不可能,上面的hook方式可以作为参考。

    相关文章

      网友评论

        本文标题:iOS:load方法能不能被hook?

        本文链接:https://www.haomeiwen.com/subject/niozmhtx.html