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

引言

上篇文章講到了dyldobjc的連接,在_objc_init函數(shù)中,通過_dyld_objc_notify_register注冊三個回調(diào)函數(shù):map_images,load_images,unmap_image,如圖所示。我們在011-iOS底層原理-_objc_init中已經(jīng)探索了load_images,unmap_image的作用與流程,本文將探索map_images。

工程:LGProject

map_images

對以 headerList開頭的鏈表中的 headers 進(jìn)行初始處理
011-iOS底層原理-_objc_init中已經(jīng)探索了map_images函數(shù)內(nèi)部返回的是map_images_nolock()的結(jié)果,進(jìn)入map_images_nolock找到了_read_images()這個函數(shù)。而此函數(shù)是本文所探索的入口。

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) {內(nèi)設(shè)置為doneOnce = YES;因此只走一次。
1)disableTaggedPointers()為禁用所有TaggedPointers,其內(nèi)部實現(xiàn)為:

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()隨機(jī)初始化 objc_debug_taggedpointer_obfuscator。標(biāo)記指針混淆器旨在使攻擊者更難將特定對象構(gòu)造為標(biāo)記指針,在存在緩沖區(qū)溢出或其他寫入控制的情況下記憶?;煜髟谠O(shè)置時與標(biāo)記指針異或或檢索有效載荷值。他們首先充滿了隨機(jī)性采用。
總而言之,這個函數(shù)就是為了小對象類型的一些處理,初始化小對象類型(NSNumber、NSString都是有小對象組成的對象,存放在常量區(qū),并且占用空間非常的小。),主要對小對象通過mask做一些混淆
參考文章
3)gdb_objc_realized_classes實際上是NXMapTable類型的哈希表,包含了不在 dyld 共享緩存中的被命名的類,這些類不管是否被實現(xiàn)。此表不包括 必須使用 getClass查找的 被懶加載命名的類。
換句話說,gdb_objc_realized_classes相當(dāng)于一個總表。而在_objc_init函數(shù)中,runtime_init里初始化的allocatedClasses表,是一張已經(jīng)初始化好的類和元類的表。
也就是說:gdb_objc_realized_classes包含allocatedClasses。
這張總表所開辟的內(nèi)存大小,是在總類數(shù)量的4/3倍4/3NXMapTable的加載因子。這是為了配合前面cache_t擴(kuò)容的3/4負(fù)載因子。

2.2、修復(fù)預(yù)編譯階段的@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代碼塊打上幾個斷點,如圖所示。運(yùn)行后用lldb調(diào)試,結(jié)果如下:

lldb調(diào)試結(jié)果

1、sel來自于sel_registerNameNoLock() -> __sel_registerName() ->search_builtins() -> _dyld_get_objc_selector()。換句話說就是sel來自于dyld加載出來的。
2、sels來自于Mach-O文件里的__objc_selrefs,即:_getObjc2SelectorRefs -> __objc_selrefs。
兩個sel來源不同,會導(dǎo)致同名不同地址的情況。因此,需要對這些selectors進(jìn)行fix up。

2.3、錯誤混亂的類處理

1、從MachO文件中字段__objc_classlist獲取所有類列表,然后 通過readClass得到相應(yīng)的類。
2、走完for循環(huán),發(fā)現(xiàn)if (newCls != cls && newCls) {}并未進(jìn)入。原因是:如果readClass的結(jié)果newClas與列表中的cls不同,則進(jìn)行修復(fù)操作,但這一般不會出現(xiàn),只有類被移動并且沒有被刪除才會出現(xiàn)。
3、lldb調(diào)試

lldb調(diào)試
由圖可知,從MachO中獲取的類,未通過readClass時,只有一個地址,并未關(guān)聯(lián)到相應(yīng)的類名。通過readClass之后,關(guān)聯(lián)上了相應(yīng)的類名。并且得到的newCls與原始的cls名字+地址都一致。

2.4、修復(fù)重映射一些沒有被鏡像文件加載進(jìn)來的類

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


image.png
2.5、修復(fù)一些消息

通過讀取MachO文件的__objc_msgrefs字段,通過fixupMessageRef函數(shù)進(jìn)行修復(fù),如如alloc -> objc_alloc、allocWithZone -> objc_allocWithZone 等,內(nèi)部如下:

image.png

__sel_registerName注冊方法名,內(nèi)部源碼如下:

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、修復(fù)protocol引用,并 readProtocol

通過讀取MachO__objc_protolist字段,將得到的protolist存入到protocol_map哈希表中。
如果這是來自共享緩存的image鏡像,則跳過讀取協(xié)議。請注意,啟動后我們確實需要遍歷協(xié)議,因為共享緩存中的協(xié)議用 isCanonical()標(biāo)記,如果選擇某些非共享緩存二進(jìn)制文件作為規(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) {
            如果我們是一個共享緩存二進(jìn)制文件,那么我們就有了這個協(xié)議的定義,但是如果選擇了另一個,那么我們需要清除我們的 isCanonical 位,以便沒有人信任它。
如果 getProtocol 返回共享緩存協(xié)議,則規(guī)范定義已經(jīng)在共享緩存中,我們不需要做任何事情。
            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) { 
        共享緩存初始化了協(xié)議對象本身,但為了允許緩存外替換,需要將其添加到協(xié)議表中。

        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ù)優(yōu)化鏡像的新協(xié)議。將其固定到位。修復(fù)可卸載包中的重復(fù)協(xié)議
        newproto->initIsa(protocol_class);  // fixme pinned
        insertFn(protocol_map, newproto->mangledName, newproto);
    }
}
2.7、修復(fù)沒有被加載的協(xié)議

如圖所示:remapProtocolRef()未執(zhí)行


remapProtocolRef()函數(shù)如下,通過remapProtocol()函數(shù),重新映射得到新的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í)行此操作。對于啟動時出現(xiàn)的分類,被推遲到_dyld_objc_notify_register 調(diào)用完成后的第一個load_images 調(diào)用。即loadAllCategories();
源碼如下:

if (didInitialAttachCategories) {
        for (EACH_HEADER) {
            load_categories_nolock(hi);
        }
    }
2.9、類的加載處理 (重點)

主要是實現(xiàn)類的加載處理,加載非懶加載類。流程如下:
1、通過nlclslist()函數(shù)從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);實現(xiàn)類。
cls 執(zhí)行第一次初始化,包括分配其讀寫(r w)數(shù)據(jù),因為前面的readClass只讀取了類的名字和地址,并未讀取r w數(shù)據(jù),因此在此讀取。不執(zhí)行任何 Swift 端初始化,最終返回類的真實類的結(jié)構(gòu)。

2.10 、沒有被處理的類 優(yōu)化那些被侵犯的類

實現(xiàn)新解析的未來類,以防 CF 操作這些類。
在2.3中,resolvedFutureClasses被賦值,但我們通過調(diào)試,可知前面的賦值并未執(zhí)行。因此,此處的resolvedFutureClasses為空。只有第2.3步的resolvedFutureClasses執(zhí)行賦值操作后,此處才會在這步處理這些未來類。

3、(核心重點分析) readClass

在2.3步驟中,從Macho讀取__objc_classlist字段的類表后,遍歷此classlist,通過readClass()讀取類并加入到類表、內(nèi)存中。其中readClass得到的是類的名稱和地址,類的內(nèi)容在此時并沒有配置。
進(jìn)入readClass內(nèi)部,源碼如下:

由上圖的紅色字體和方框注釋,將readClass簡化后的代碼如下:

1、從ro中讀取到類名;
2、addNamedClass()類名插入到哈希表中(gdb_objc_realized_classes,前面提到的,該表存放所有類);
3、addClassTableEntry()類和元類插入到哈希表中(allocatedClasses,前面提到的,該表在_objc_init中的runtime_init創(chuàng)建的表中,該表存放已經(jīng)創(chuàng)建的類)。
由于readClass是在for循環(huán)中調(diào)用的,即從MachO中讀取到的classlist遍歷操作readClass,因此除了我們自定義的類之外,還會有很多系統(tǒng)的類。我們將其打印出來。源碼以及打印結(jié)果如下:

Class readClass(Class cls, bool headerIsBundle, bool headerIsPreoptimized)
{
    const char *mangledName = cls->nonlazyMangledName();
    
    printf("---- %s----%s\n",__func__,mangledName);
    ---------省略-后面代碼--------
}

打印結(jié)果

由上圖打印結(jié)果可以看到,我們自定義的類名出現(xiàn)在了打印的最后。我們只需要知道類的加載過程,系統(tǒng)類太復(fù)雜,不利于我們添加斷點停下,因此并非我們的首選。我們的思路是通過我們自定義的類的加載來探索,因此,我們只需要判斷mangledNameQLPerson相等的時候,停下來。即可查看變量的值以及lldb調(diào)試。代碼設(shè)計如下:加入了strcmp函數(shù),將斷點添加進(jìn)來,并在每一個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,再一次驗證了不在此處設(shè)置類的rw 、ro。
1、斷點來到addNamedClass(未執(zhí)行),此時的Class只有一個地址。


2、斷點執(zhí)行addNamedClass(執(zhí)行完畢)。

3、斷點執(zhí)行到addClassTableEntry,將cls和元類插入表中。

4、(核心重點分析) realizeClassWithoutSwift

上面第3步read_class加載的是類名+地址。realizeClassWithoutSwift則是加載類的data,配置ro,rw等內(nèi)容。我們將通過斷點調(diào)試,來探索這其中的流程。

【4.1】、加載本類data,設(shè)置ro,rw

由于我們只需要探索我們自定義的類,因此在realizeClassWithoutSwift()函數(shù)內(nèi),我們加入了判斷mangledName = QLPerson,讓斷點停在此處。進(jìn)一步lldb調(diào)試ro,rw,等內(nèi)容。我們所要探索的類的內(nèi)容,請參考006--iOS底層 - 類的結(jié)構(gòu)(屬性、成員變量、方法的探索)。包括屬性,成員變量,方法,cache等。


調(diào)試結(jié)果如下:
1)屬性/成員變量:

2)方法:

打印方法發(fā)現(xiàn)打印不出來。繼續(xù)往下走。

【4.2】遞歸實現(xiàn)父類,元類完善繼承鏈和isa走向

如果父類和元類還沒有被實現(xiàn),則遞歸調(diào)用realizeClassWithoutSwift()去實現(xiàn)父類和元類。

    supercls = realizeClassWithoutSwift(remapClass(cls->getSuperclass()), nil);
    metacls = realizeClassWithoutSwift(remapClass(cls->ISA()), nil);

實現(xiàn)了父類和元類后,并設(shè)置是否支持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函數(shù)即為配置類的方法。

【4.3.1】預(yù)處理方法列表:prepareMethodLists

prepareMethodLists源碼中,最主要的是對方法列表的修復(fù),遍歷addedLists,調(diào)用fixupMethodList函數(shù)。

【4.3.2】修復(fù)方法列表:fixupMethodList

此函數(shù)是遍歷方法列表,把方法名設(shè)置后,對方法進(jìn)行排序:
a)meth.setName(sel_registerNameNoLock(name, bundleCopy));實際上是調(diào)用了__sel_registerName(),也就是我們前面的_read_images第2.5步,修復(fù)objc_msgSend重定向的時候提到的地方。


調(diào)試結(jié)果如下:
方法排序前后

由此可見,方法的排序,并非以名字排序,而是以地址排序。

5、總結(jié)

【5.1】類的加載(本類)流程圖如下:

類的加載.png

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容