引言
上篇文章讲到了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 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
调试,结果如下:
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插入方法表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()
源码如下:
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);
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 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】类的加载(本类)流程图如下:
【5.2】分类(category)的加载将在下一篇讲解
【5.3】此流程为
非懒加载
类的流程,即在测试类QLPerson
中实现了+load
方法,在map_images中加载所有类的数据。
若是未实现
+load
方法,则在实现类的函数realizeClassWithoutSwift
的流程如下:lookUpImpOrForward
->realizeClassMaybeSwiftMaybeRelock
->realizeClassWithoutSwift
->methodizeClass
。
两者之间的差异,如图所示: