- 011-iOS底层原理-_objc_init中已经探索了
load_images,unmap_image的作用与流程,本文将探索map_images。
">引言
上篇文章讲到了dyld与objc的连接,在_objc_init函数中,通过_dyld_objc_notify_register注册三个回调函数:map_images,load_images,unmap_image,如图所示。我们在011-iOS底层原理-_objc_init中已经探索了load_images,unmap_image的作用与流程,本文将探索map_images。
- 011-iOS底层原理-_objc_init中已经探索了
map_images函数内部返回的是map_images_nolock()的结果,进入map_images_nolock找到了_read_images()这个函数。而此函数是本文所探索的入口。">map_images
对以headerList开头的链表中的headers进行初始处理
在011-iOS底层原理-_objc_init中已经探索了map_images函数内部返回的是map_images_nolock()的结果,进入map_images_nolock找到了_read_images()这个函数。而此函数是本文所探索的入口。
#####2、_read_images_read_images的源码共有360行(行行出状元?)
由我们之前探索dyld加载流程的思路:掌握主线。将if else等分支代码全部折叠起来,可以看到,共有的特性:ts.log()打印没段代码的作用,如图所示:
因此,我们得到如下过程,我们将逐步探索这10个过程:
######2.1 、doneOnce条件控制执行一次的加载doneOnce的定义是static bool doneOnce;,静态变量,在if (!doneOnce) {内设置为doneOnce = YES;因此只走一次。
1)disableTaggedPointers()为禁用所有TaggedPointers,其内部实现为:">1、map_images_nolock
#####2、_read_images_read_images的源码共有360行(行行出状元?)
由我们之前探索dyld加载流程的思路:掌握主线。将if else等分支代码全部折叠起来,可以看到,共有的特性:ts.log()打印没段代码的作用,如图所示:
因此,我们得到如下过程,我们将逐步探索这10个过程:
######2.1 、doneOnce条件控制执行一次的加载doneOnce的定义是static bool doneOnce;,静态变量,在if (!doneOnce) {内设置为doneOnce = YES;因此只走一次。
1)disableTaggedPointers()为禁用所有TaggedPointers,其内部实现为:readProtocol()源码如下:">2.6、修复protocol引用,并 readProtocol
通过读取MachO的__objc_protolist字段,将得到的protolist存入到protocol_map哈希表中。
如果这是来自共享缓存的image镜像,则跳过读取协议。请注意,启动后我们确实需要遍历协议,因为共享缓存中的协议用isCanonical()标记,如果选择某些非共享缓存二进制文件作为规范定义,则可能不是这样。
readProtocol()源码如下:remapProtocolRef()函数如下,通过remapProtocol()函数,重新映射得到新的newproto,再与protoref比较,将newproto赋值给*protoref。">2.7、修复没有被加载的协议
如图所示:remapProtocolRef()未执行
remapProtocolRef()函数如下,通过remapProtocol()函数,重新映射得到新的newproto,再与protoref比较,将newproto赋值给*protoref。- 2.8、分类处理
仅在完成初始化分类后才执行此操作。对于启动时出现的分类,被推迟到_dyld_objc_notify_register调用完成后的第一个load_images调用。即loadAllCategories();
源码如下: - 2.9、类的加载处理 (重点)
主要是实现类的加载处理,加载非懒加载类。流程如下:
1、通过nlclslist()函数从MachO文件中的__objc_nlclslist字段获取classlist类表。
即:nlclslist()—>_getObjc2NonlazyClassList()—>MachO的__objc_nlclslist
######【4.3.1】预处理方法列表:prepareMethodListsprepareMethodLists源码中,最主要的是对方法列表的修复,遍历addedLists,调用fixupMethodList函数。
######【4.3.2】修复方法列表:fixupMethodList
此函数是遍历方法列表,把方法名设置后,对方法进行排序:
a)meth.setName(sel_registerNameNoLock(name, bundleCopy));实际上是调用了__sel_registerName(),也就是我们前面的_read_images第2.5步,修复objc_msgSend重定向的时候提到的地方。
调试结果如下:
由此可见,方法的排序,并非以名字排序,而是以地址排序。">【4.3】配置类的方法:methodizeClass
在上面的4.1步骤中,我们未能打印method,methodizeClass函数即为配置类的方法。
######【4.3.1】预处理方法列表:prepareMethodListsprepareMethodLists源码中,最主要的是对方法列表的修复,遍历addedLists,调用fixupMethodList函数。
######【4.3.2】修复方法列表:fixupMethodList
此函数是遍历方法列表,把方法名设置后,对方法进行排序:
a)meth.setName(sel_registerNameNoLock(name, bundleCopy));实际上是调用了__sel_registerName(),也就是我们前面的_read_images第2.5步,修复objc_msgSend重定向的时候提到的地方。
调试结果如下:
由此可见,方法的排序,并非以名字排序,而是以地址排序。
【5.2】分类(category)的加载将在下一篇讲解
【5.3】此流程为非懒加载类的流程,即在测试类QLPerson中实现了+load方法,在map_images中加载所有类的数据。
若是未实现+load方法,则在实现类的函数realizeClassWithoutSwift的流程如下:lookUpImpOrForward->realizeClassMaybeSwiftMaybeRelock->realizeClassWithoutSwift->methodizeClass。
两者之间的差异,如图所示:
">5、总结
【5.1】类的加载(本类)流程图如下:
【5.2】分类(category)的加载将在下一篇讲解
【5.3】此流程为非懒加载类的流程,即在测试类QLPerson中实现了+load方法,在map_images中加载所有类的数据。
若是未实现+load方法,则在实现类的函数realizeClassWithoutSwift的流程如下:lookUpImpOrForward->realizeClassMaybeSwiftMaybeRelock->realizeClassWithoutSwift->methodizeClass。
两者之间的差异,如图所示:
引言
上篇文章讲到了dyld与objc的连接,在_objc_init函数中,通过_dyld_objc_notify_register注册三个回调函数:map_images,load_images,unmap_image,如图所示。我们在011-iOS底层原理-_objc_init中已经探索了load_images,unmap_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

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

因此,我们得到如下过程,我们将逐步探索这10个过程:

######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/3是NXMapTable的加载因子。这是为了配合前面cache_t扩容的3/4负载因子。
######2.2、修复预编译阶段的@selector混乱问题
我们知道SEL是由名字+地址组成的,因此匹配两个SEL,需要对比名字+地址。否则可判定为不相等。
源码如下:
// Fix up @selector referencesstatic 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调试,结果如下:
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调试
由图可知,从MachO中获取的类,未通过readClass时,只有一个地址,并未关联到相应的类名。通过readClass之后,关联上了相应的类名。并且得到的newCls与原始的cls名字+地址都一致。
######2.4、修复重映射一些没有被镜像文件加载进来的类
将未映射的类和父类重映射,其中被重映射的类都是非懒加载的类。此代码块一般情况下是不会被执行。
######2.5、修复一些消息
通过读取MachO文件的__objc_msgrefs字段,通过fixupMessageRef函数进行修复,如如alloc -> objc_alloc、allocWithZone -> objc_allocWithZone 等,内部如下:
__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插入方法表namedSelectorsauto 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()源码如下:
static voidreadProtocol(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 pinnedinsertFn(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);

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);---------省略-后面代码--------}

由上图打印结果可以看到,我们自定义的类名出现在了打印的最后。我们只需要知道类的加载过程,系统类太复杂,不利于我们添加断点停下,因此并非我们的首选。我们的思路是通过我们自定义的类的加载来探索,因此,我们只需要判断mangledName与QLPerson相等的时候,停下来。即可查看变量的值以及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 remappingcls->setSuperclass(supercls);cls->initClassIsa(metacls);....省略代码......此处要用递归的视角去看待,将继承链完善。if (supercls) {addSubclass(supercls, cls);} else {addRootClass(cls);}
