012-iOS底層原理-類的加載

引言

上篇文章講到了dyldobjc的連接柬姚,在_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
map_images_nolock
2、_read_images

_read_images的源碼共有360行(行行出狀元欠雌?)
由我們之前探索dyld加載流程的思路:掌握主線蹄梢。將if else等分支代碼全部折疊起來,可以看到富俄,共有的特性:ts.log()打印沒段代碼的作用禁炒,如圖所示:

_read_images

因此,我們得到如下過程霍比,我們將逐步探索這10個過程:
_read_images代碼塊作用

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/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循環(huán)长捧,發(fā)現if (newCls != cls && newCls) {}并未進入嚣鄙。原因是:如果readClass的結果newClas與列表中的cls不同,則進行修復操作串结,但這一般不會出現哑子,只有類被移動并且沒有被刪除才會出現。
3肌割、lldb調試

lldb調試
由圖可知卧蜓,從MachO中獲取的類,未通過readClass時声功,只有一個地址烦却,并未關聯到相應的類名。通過readClass之后先巴,關聯上了相應的類名其爵。并且得到的newCls與原始的cls名字+地址都一致。

2.4伸蚯、修復重映射一些沒有被鏡像文件加載進來的類

將未映射的類和父類重映射摩渺,其中被重映射的類都是非懶加載的類。此代碼塊一般情況下是不會被執(zhí)行剂邮。


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()標記死陆,如果選擇某些非共享緩存二進制文件作為規(guī)范定義,則可能不是這樣唧瘾。

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 返回共享緩存協議菌羽,則規(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);
addClassTableEntry

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

打印結果

由上圖打印結果可以看到,我們自定義的類名出現在了打印的最后。我們只需要知道類的加載過程助币,系統類太復雜浪听,不利于我們添加斷點停下,因此并非我們的首選眉菱。我們的思路是通過我們自定義的類的加載來探索迹栓,因此,我們只需要判斷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(未執(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】類的加載(本類)流程圖如下:

類的加載.png

【5.2】分類(category)的加載將在下一篇講解
【5.3】此流程為非懶加載類的流程良价,即在測試類QLPerson中實現了+load方法寝殴,在map_images中加載所有類的數據。
若是未實現+load方法明垢,則在實現類的函數realizeClassWithoutSwift的流程如下:lookUpImpOrForward->realizeClassMaybeSwiftMaybeRelock->realizeClassWithoutSwift->methodizeClass蚣常。
兩者之間的差異,如圖所示:

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末痊银,一起剝皮案震驚了整個濱河市抵蚊,隨后出現的幾起案子,更是在濱河造成了極大的恐慌溯革,老刑警劉巖贞绳,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異致稀,居然都是意外死亡冈闭,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進店門抖单,熙熙樓的掌柜王于貴愁眉苦臉地迎上來拒秘,“玉大人,你說我怎么就攤上這事臭猜。” “怎么了押蚤?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵蔑歌,是天一觀的道長。 經常有香客問我揽碘,道長次屠,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任雳刺,我火速辦了婚禮劫灶,結果婚禮上,老公的妹妹穿的比我還像新娘掖桦。我一直安慰自己本昏,他們只是感情好,可當我...
    茶點故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布枪汪。 她就那樣靜靜地躺著涌穆,像睡著了一般。 火紅的嫁衣襯著肌膚如雪雀久。 梳的紋絲不亂的頭發(fā)上宿稀,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天,我揣著相機與錄音赖捌,去河邊找鬼祝沸。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的罩锐。 我是一名探鬼主播奉狈,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼唯欣!你這毒婦竟也來了嘹吨?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤境氢,失蹤者是張志新(化名)和其女友劉穎蟀拷,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體萍聊,經...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡问芬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了寿桨。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片此衅。...
    茶點故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖亭螟,靈堂內的尸體忽然破棺而出挡鞍,到底是詐尸還是另有隱情,我是刑警寧澤预烙,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布墨微,位于F島的核電站,受9級特大地震影響扁掸,放射性物質發(fā)生泄漏翘县。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一谴分、第九天 我趴在偏房一處隱蔽的房頂上張望锈麸。 院中可真熱鬧,春花似錦牺蹄、人聲如沸忘伞。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽虑省。三九已至,卻和暖如春僧凰,著一層夾襖步出監(jiān)牢的瞬間探颈,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工训措, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留伪节,地道東北人光羞。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像怀大,于是被迫代替她去往敵國和親纱兑。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,092評論 2 355

推薦閱讀更多精彩內容