引言
上篇文章講到了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
:管理文件中和動態(tài)庫中所有的符號:class凤藏,protocal,selector堕伪,category
1揖庄、map_images_nolock
2、_read_images
_read_images
的源碼共有360行
(行行出狀元欠雌?)
由我們之前探索dyld
加載流程的思路:掌握主線蹄梢。將if else
等分支代碼全部折疊起來,可以看到富俄,共有的特性:ts.log()
打印沒段代碼的作用禁炒,如圖所示:
因此,我們得到如下過程霍比,我們將逐步探索這10個過程:
2.1 幕袱、doneOnce條件控制執(zhí)行一次的加載
doneOnce
的定義是static bool doneOnce;
,靜態(tài)變量悠瞬,在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
浅妆。標記指針混淆器旨在使攻擊者更難將特定對象構造為標記指針玛痊,在存在緩沖區(qū)溢出或其他寫入控制的情況下記憶】翊颍混淆器在設置時與標記指針異或或檢索有效載荷值。他們首先充滿了隨機性采用混弥。
總而言之趴乡,這個函數就是為了小對象類型的一些處理对省,初始化小對象類型(NSNumber、NSString都是有小對象組成的對象晾捏,存放在常量區(qū),并且占用空間非常的小蒿涎。),主要對小對象通過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循環(huán)长捧,發(fā)現if (newCls != cls && newCls) {}
并未進入嚣鄙。原因是:如果readClass
的結果newClas
與列表中的cls
不同,則進行修復操作串结,但這一般不會出現哑子,只有類被移動并且沒有被刪除才會出現。
3肌割、lldb調試
MachO
中獲取的類,未通過readClass
時声功,只有一個地址烦却,并未關聯到相應的類名。通過readClass
之后先巴,關聯上了相應的類名其爵。并且得到的newCls
與原始的cls
名字+地址都一致。
2.4伸蚯、修復重映射一些沒有被鏡像文件加載進來的類
將未映射的類和父類重映射摩渺,其中被重映射的類都是非懶加載的類。此代碼塊一般情況下是不會被執(zhí)行剂邮。
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()
標記死陆,如果選擇某些非共享緩存二進制文件作為規(guī)范定義,則可能不是這樣唧瘾。
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 返回共享緩存協議菌羽,則規(guī)范定義已經在共享緩存中掠械,我們不需要做任何事情。
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 {
未預優(yōu)化鏡像的新協議是晨。將其固定到位肚菠。修復可卸載包中的重復協議
newproto->initIsa(protocol_class); // fixme pinned
insertFn(protocol_map, newproto->mangledName, newproto);
}
}
2.7、修復沒有被加載的協議
如圖所示:remapProtocolRef()
未執(zhí)行
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、分類處理
僅在完成初始化
分類后才執(zhí)行此操作檬寂。對于啟動時
出現的分類终抽,被推遲到_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
執(zhí)行第一次初始化吏廉,包括分配其讀寫(r w)數據,因為前面的readClass
只讀取了類的名字和地址
惰许,并未讀取r w數據席覆,因此在此讀取。不執(zhí)行任何 Swift 端初始化汹买,最終返回類的真實類的結構佩伤。
2.10 聊倔、沒有被處理的類 優(yōu)化那些被侵犯的類
實現新解析的未來類,以防 CF 操作這些類生巡。
在2.3中耙蔑,resolvedFutureClasses
被賦值,但我們通過調試孤荣,可知前面的賦值并未執(zhí)行甸陌。因此,此處的resolvedFutureClasses
為空盐股。只有第2.3步的resolvedFutureClasses
執(zhí)行賦值操作后钱豁,此處才會在這步處理這些未來類。
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
創(chuàng)建的表中私爷,該表存放已經創(chuàng)建的類)。由于
readClass
是在for循環(huán)中調用的膊夹,即從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
(未執(zhí)行)撵颊,此時的Class只有一個地址
宇攻。
2、斷點執(zhí)行
addNamedClass
(執(zhí)行完畢)倡勇。3逞刷、斷點執(zhí)行到
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)方法:
打印方法發(fā)現打印不出來粘室。繼續(xù)往下走。
【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
蚣常。兩者之間的差異,如圖所示: