引言
上篇文章讲到了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
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/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调试,结果如下:
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);
---------省略-后面代码--------
}
打印结果
由上图打印结果可以看到,我们自定义的类名出现在了打印的最后。我们只需要知道类的加载过程,系统类太复杂,不利于我们添加断点停下,因此并非我们的首选。我们的思路是通过我们自定义的类的加载来探索,因此,我们只需要判断
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】类的加载(本类)流程图如下:
类的加载.png
【5.2】分类(category)的加载将在下一篇讲解
【5.3】此流程为
非懒加载类的流程,即在测试类QLPerson中实现了+load方法,在map_images中加载所有类的数据。若是未实现
+load方法,则在实现类的函数realizeClassWithoutSwift的流程如下:lookUpImpOrForward->realizeClassMaybeSwiftMaybeRelock->realizeClassWithoutSwift->methodizeClass。两者之间的差异,如图所示:













网友评论