012-iOS底层原理-类的加载

引言

上篇文章讲到了dyldobjc的连接,在_objc_init函数中,通过_dyld_objc_notify_register注册三个回调函数:map_imagesload_imagesunmap_image,如图所示。我们在011-iOS底层原理-_objc_init中已经探索了load_imagesunmap_image的作用与流程,本文将探索map_images

工程:LGProject

map_images

对以 headerList开头的链表中的 headers 进行初始处理
在011-iOS底层原理-_objc_init中已经探索了map_images函数内部返回的是map_images_nolock()的结果,进入map_images_nolock找到了_read_images()这个函数。而此函数是本文所探索的入口。

map_images管理文件中和动态库中所有的符号:class,protocal,selector,category

1、map_images_nolock
map_images_nolock
2、_read_images

_read_images的源码共有360行(行行出状元?)
由我们之前探索dyld加载流程的思路:掌握主线。将if else等分支代码全部折叠起来,可以看到,共有的特性:ts.log()打印没段代码的作用,如图所示:

_read_images

因此,我们得到如下过程,我们将逐步探索这10个过程:
_read_images代码块作用

2.1 、doneOnce条件控制执行一次的加载

doneOnce的定义是static bool doneOnce;,静态变量,在if (!doneOnce) {内设置为doneOnce = YES;因此只走一次。
1)disableTaggedPointers()为禁用所有TaggedPointers,其内部实现为:

static void disableTaggedPointers()
{
    objc_debug_taggedpointer_mask = 0;
    objc_debug_taggedpointer_slot_shift = 0;
    objc_debug_taggedpointer_slot_mask = 0;
    objc_debug_taggedpointer_payload_lshift = 0;
    objc_debug_taggedpointer_payload_rshift = 0;

    objc_debug_taggedpointer_ext_mask = 0;
    objc_debug_taggedpointer_ext_slot_shift = 0;
    objc_debug_taggedpointer_ext_slot_mask = 0;
    objc_debug_taggedpointer_ext_payload_lshift = 0;
    objc_debug_taggedpointer_ext_payload_rshift = 0;
}

2)initializeTaggedPointerObfuscator()随机初始化 objc_debug_taggedpointer_obfuscator。标记指针混淆器旨在使攻击者更难将特定对象构造为标记指针,在存在缓冲区溢出或其他写入控制的情况下记忆。混淆器在设置时与标记指针异或或检索有效载荷值。他们首先充满了随机性采用。
总而言之,这个函数就是为了小对象类型的一些处理,初始化小对象类型(NSNumber、NSString都是有小对象组成的对象,存放在常量区,并且占用空间非常的小。),主要对小对象通过mask做一些混淆
参考文章
3)gdb_objc_realized_classes实际上是NXMapTable类型的哈希表,包含了不在 dyld 共享缓存中的被命名的类,这些类不管是否被实现。此表不包括 必须使用 getClass查找的 被懒加载命名的类。
换句话说,gdb_objc_realized_classes相当于一个总表。而在_objc_init函数中,runtime_init里初始化的allocatedClasses表,是一张已经初始化好的类和元类的表。
也就是说:gdb_objc_realized_classes包含allocatedClasses
这张总表所开辟的内存大小,是在总类数量的4/3倍4/3NXMapTable的加载因子。这是为了配合前面cache_t扩容的3/4负载因子。

2.2、修复预编译阶段的@selector混乱问题

我们知道SEL是由名字+地址组成的,因此匹配两个SEL,需要对比名字+地址。否则可判定为不相等。
源码如下:

// Fix up @selector references
    static size_t UnfixedSelectors;
   {
        mutex_locker_t lock(selLock);
        for (EACH_HEADER) {
            if (hi->hasPreoptimizedSelectors()) continue;
            bool isBundle = hi->isBundle();
            SEL *sels = _getObjc2SelectorRefs(hi, &count);
            UnfixedSelectors += count;
            for (i = 0; i < count; i++) {
                const char *name = sel_cname(sels[i]);
                SEL sel = sel_registerNameNoLock(name, isBundle);
                if (sels[i] != sel) {
                    sels[i] = sel;
                }
            }
        }
    }
    ts.log("IMAGE TIMES: fix up selector references");

我们在objc工程中UnfixedSelectors代码块打上几个断点,如图所示。运行后用lldb调试,结果如下:

lldb调试结果

1、sel来自于sel_registerNameNoLock() -> __sel_registerName() ->search_builtins() -> _dyld_get_objc_selector()。换句话说就是sel来自于dyld加载出来的。
2、sels来自于Mach-O文件里的__objc_selrefs,即:_getObjc2SelectorRefs -> __objc_selrefs
两个sel来源不同,会导致同名不同地址的情况。因此,需要对这些selectors进行fix up。

2.3、错误混乱的类处理

1、从MachO文件中字段__objc_classlist获取所有类列表,然后 通过readClass得到相应的类。
2、走完for循环,发现if (newCls != cls && newCls) {}并未进入。原因是:如果readClass的结果newClas与列表中的cls不同,则进行修复操作,但这一般不会出现,只有类被移动并且没有被删除才会出现。
3、lldb调试

lldb调试
由图可知,从MachO中获取的类,未通过readClass时,只有一个地址,并未关联到相应的类名。通过readClass之后,关联上了相应的类名。并且得到的newCls与原始的cls名字+地址都一致。

2.4、修复重映射一些没有被镜像文件加载进来的类

将未映射的类和父类重映射,其中被重映射的类都是非懒加载的类。此代码块一般情况下是不会被执行。


image.png
2.5、修复一些消息

通过读取MachO文件的__objc_msgrefs字段,通过fixupMessageRef函数进行修复,如如alloc -> objc_alloc、allocWithZone -> objc_allocWithZone 等,内部如下:

image.png

__sel_registerName注册方法名,内部源码如下:

static SEL __sel_registerName(const char *name, bool shouldLock, bool copy) 
{
    SEL result = 0;

    if (shouldLock) selLock.assertUnlocked();
    else selLock.assertLocked();

    if (!name) return (SEL)0;
    // 从dyld里查找,有该name就返回
    result = search_builtins(name);
    if (result) return result;
    
    conditional_mutex_locker_t lock(selLock, shouldLock);
    // 将name插入方法表namedSelectors
    auto it = namedSelectors.get().insert(name);
    if (it.second) {
        // No match. Insert.
        *it.first = (const char *)sel_alloc(name, copy);
    }
    return (SEL)*it.first;
}
2.6、修复protocol引用,并 readProtocol

通过读取MachO__objc_protolist字段,将得到的protolist存入到protocol_map哈希表中。
如果这是来自共享缓存的image镜像,则跳过读取协议。请注意,启动后我们确实需要遍历协议,因为共享缓存中的协议用 isCanonical()标记,如果选择某些非共享缓存二进制文件作为规范定义,则可能不是这样。

readProtocol

readProtocol()源码如下:

static void
readProtocol(protocol_t *newproto, Class protocol_class,
             NXMapTable *protocol_map, 
             bool headerIsPreoptimized, bool headerIsBundle)
{
    // This is not enough to make protocols in unloaded bundles safe, 
    // but it does prevent crashes when looking up unrelated protocols.
    auto insertFn = headerIsBundle ? NXMapKeyCopyingInsert : NXMapInsert;

    protocol_t *oldproto = (protocol_t *)getProtocol(newproto->mangledName);

    if (oldproto) {
        if (oldproto != newproto) {
            如果我们是一个共享缓存二进制文件,那么我们就有了这个协议的定义,但是如果选择了另一个,那么我们需要清除我们的 isCanonical 位,以便没有人信任它。
如果 getProtocol 返回共享缓存协议,则规范定义已经在共享缓存中,我们不需要做任何事情。
            if (headerIsPreoptimized && !oldproto->isCanonical()) {
                // Note newproto is an entry in our __objc_protolist section which
                // for shared cache binaries points to the original protocol in
                // that binary, not the shared cache uniqued one.
                auto cacheproto = (protocol_t *)
                    getSharedCachePreoptimizedProtocol(newproto->mangledName);
                if (cacheproto && cacheproto->isCanonical())
                    cacheproto->clearIsCanonical();// 清除isCanonical 位
            }
            
        }
    }
    else if (headerIsPreoptimized) { 
        共享缓存初始化了协议对象本身,但为了允许缓存外替换,需要将其添加到协议表中。

        protocol_t *cacheproto = (protocol_t *)
            getPreoptimizedProtocol(newproto->mangledName);
        protocol_t *installedproto;
        if (cacheproto  &&  cacheproto != newproto) {
            // Another definition in the shared cache wins (because 
            // everything in the cache was fixed up to point to it).
            installedproto = cacheproto;
        }
        else {
            // This definition wins.
            installedproto = newproto;
        }
        ......省略代码......
        insertFn(protocol_map, installedproto->mangledName, 
                 installedproto);
    }
    else {
        未预优化镜像的新协议。将其固定到位。修复可卸载包中的重复协议
        newproto->initIsa(protocol_class);  // fixme pinned
        insertFn(protocol_map, newproto->mangledName, newproto);
    }
}
2.7、修复没有被加载的协议

如图所示:remapProtocolRef()未执行


remapProtocolRef()函数如下,通过remapProtocol()函数,重新映射得到新的newproto,再与protoref比较,将newproto赋值给*protoref

static void remapProtocolRef(protocol_t **protoref)
{
    runtimeLock.assertLocked();

    protocol_t *newproto = remapProtocol((protocol_ref_t)*protoref);
    if (*protoref != newproto) {
        *protoref = newproto;
        UnfixedProtocolReferences++;
    }
}
2.8、分类处理

仅在完成初始化分类后才执行此操作。对于启动时出现的分类,被推迟到_dyld_objc_notify_register 调用完成后的第一个load_images 调用。即loadAllCategories();
源码如下:

if (didInitialAttachCategories) {
        for (EACH_HEADER) {
            load_categories_nolock(hi);
        }
    }
2.9、类的加载处理 (重点)

主要是实现类的加载处理,加载非懒加载类。流程如下:
1、通过nlclslist()函数从MachO文件中的__objc_nlclslist字段获取classlist类表。
即:nlclslist()-->_getObjc2NonlazyClassList()-->MachO的__objc_nlclslist

classref_t const *classlist = hi->nlclslist(&count);

2、遍历classlist将class重新映射,得到的新class和metaClass插入类表中。

addClassTableEntry(cls);
addClassTableEntry

3、通过realizeClassWithoutSwift(cls, nil);实现类。
cls 执行第一次初始化,包括分配其读写(r w)数据,因为前面的readClass只读取了类的名字和地址,并未读取r w数据,因此在此读取。不执行任何 Swift 端初始化,最终返回类的真实类的结构。

2.10 、没有被处理的类 优化那些被侵犯的类

实现新解析的未来类,以防 CF 操作这些类。
在2.3中,resolvedFutureClasses被赋值,但我们通过调试,可知前面的赋值并未执行。因此,此处的resolvedFutureClasses为空。只有第2.3步的resolvedFutureClasses执行赋值操作后,此处才会在这步处理这些未来类。

3、(核心重点分析) readClass

在2.3步骤中,从Macho读取__objc_classlist字段的类表后,遍历此classlist,通过readClass()读取类并加入到类表、内存中。其中readClass得到的是类的名称和地址,类的内容在此时并没有配置。
进入readClass内部,源码如下:

由上图的红色字体和方框注释,将readClass简化后的代码如下:

1、从ro中读取到类名;
2、addNamedClass()类名插入到哈希表中(gdb_objc_realized_classes,前面提到的,该表存放所有类);
3、addClassTableEntry()类和元类插入到哈希表中(allocatedClasses,前面提到的,该表在_objc_init中的runtime_init创建的表中,该表存放已经创建的类)。
由于readClass是在for循环中调用的,即从MachO中读取到的classlist遍历操作readClass,因此除了我们自定义的类之外,还会有很多系统的类。我们将其打印出来。源码以及打印结果如下:

Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized)
{
    const char *mangledName = cls->nonlazyMangledName();
    
    printf("---- %s----%s\n",__func__,mangledName);
    ---------省略-后面代码--------
}

打印结果

由上图打印结果可以看到,我们自定义的类名出现在了打印的最后。我们只需要知道类的加载过程,系统类太复杂,不利于我们添加断点停下,因此并非我们的首选。我们的思路是通过我们自定义的类的加载来探索,因此,我们只需要判断mangledNameQLPerson相等的时候,停下来。即可查看变量的值以及lldb调试。代码设计如下:加入了strcmp函数,将断点添加进来,并在每一个if处打上断点。

Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized)
{
    const char *mangledName = cls->nonlazyMangledName();
    const char *customClsName = "QLPerson";
    int cmpResult = strcmp(mangledName, customClsName);
    if (cmpResult == 0) {
        printf("---- %s----%s\n",__func__,mangledName);
    }
---------省略-后面代码--------
}

断点停下后,Xcode点击Step over,再一次验证了不在此处设置类的rw 、ro。
1、断点来到addNamedClass(未执行),此时的Class只有一个地址


2、断点执行addNamedClass(执行完毕)。

3、断点执行到addClassTableEntry,将cls和元类插入表中。

4、(核心重点分析) realizeClassWithoutSwift

上面第3步read_class加载的是类名+地址。realizeClassWithoutSwift则是加载类的data,配置ro,rw等内容。我们将通过断点调试,来探索这其中的流程。

【4.1】、加载本类data,设置ro,rw

由于我们只需要探索我们自定义的类,因此在realizeClassWithoutSwift()函数内,我们加入了判断mangledName = QLPerson,让断点停在此处。进一步lldb调试ro,rw,等内容。我们所要探索的类的内容,请参考006--iOS底层 - 类的结构(属性、成员变量、方法的探索)。包括属性,成员变量,方法,cache等。


调试结果如下:
1)属性/成员变量:

2)方法:

打印方法发现打印不出来。继续往下走。

【4.2】递归实现父类,元类完善继承链和isa走向

如果父类和元类还没有被实现,则递归调用realizeClassWithoutSwift()去实现父类和元类。

    supercls = realizeClassWithoutSwift(remapClass(cls->getSuperclass()), nil);
    metacls = realizeClassWithoutSwift(remapClass(cls->ISA()), nil);

实现了父类和元类后,并设置是否支持Non-pointer isa ,将他们保存。

// Update superclass and metaclass in case of remapping
    cls->setSuperclass(supercls);
    cls->initClassIsa(metacls);

....省略代码......
      此处要用递归的视角去看待,将继承链完善。
    if (supercls) {
        addSubclass(supercls, cls);
    } else {
        addRootClass(cls);
    }
【4.3】配置类的方法:methodizeClass

在上面的4.1步骤中,我们未能打印method,methodizeClass函数即为配置类的方法。

【4.3.1】预处理方法列表:prepareMethodLists

prepareMethodLists源码中,最主要的是对方法列表的修复,遍历addedLists,调用fixupMethodList函数

【4.3.2】修复方法列表:fixupMethodList

此函数是遍历方法列表,把方法名设置后,对方法进行排序:
a)meth.setName(sel_registerNameNoLock(name, bundleCopy));实际上是调用了__sel_registerName(),也就是我们前面的_read_images第2.5步,修复objc_msgSend重定向的时候提到的地方。


调试结果如下:
方法排序前后

由此可见,方法的排序,并非以名字排序,而是以地址排序。

5、总结

【5.1】类的加载(本类)流程图如下:

类的加载.png

【5.2】分类(category)的加载将在下一篇讲解
【5.3】此流程为非懒加载类的流程,即在测试类QLPerson中实现了+load方法,在map_images中加载所有类的数据。
若是未实现+load方法,则在实现类的函数realizeClassWithoutSwift的流程如下:lookUpImpOrForward->realizeClassMaybeSwiftMaybeRelock->realizeClassWithoutSwift->methodizeClass
两者之间的差异,如图所示:

你可能感兴趣的:(012-iOS底层原理-类的加载)