自研 iOS 熱更新機(jī)制——OCPack技術(shù)方案總結(jié)

https://juejin.im/post/6884833291740905480?utm_source=gold_browser_extension

I. 方案簡(jiǎn)介

OCPack是一種 iOS 平臺(tái)上 App 動(dòng)態(tài)化技術(shù)方案,用戶可以使用 Objective-C 語(yǔ)言編寫待動(dòng)態(tài)化的功能邏輯(生成.m文件),然后通過OCPack提供的工具鏈生成 patch 文件(.bin格式)??蛻舳藙t內(nèi)置了一個(gè)基于 Native 環(huán)境的的虛擬棧機(jī),它可以動(dòng)態(tài)加載并執(zhí)行存儲(chǔ)在客戶端的 patch 文件中的方法。Patch 文件可根據(jù)業(yè)務(wù)需要隨時(shí)下載、更新并由虛擬機(jī)重新加載、運(yùn)行。

此方案的主要優(yōu)點(diǎn):

  • 開發(fā)效率

    可以使開發(fā)者像寫普通功能代碼一樣,使用熟悉的 Xcode IDE 和 Objective-C 語(yǔ)言進(jìn)行開發(fā)、調(diào)試,在開發(fā)完成后使用工具鏈即可方便地生成 patch 文件,提高開發(fā) patch 的效率。

  • 語(yǔ)法覆蓋

    考慮到使用者的方便性和開發(fā)周期的平衡,目前OCPack的實(shí)現(xiàn)覆蓋了c語(yǔ)言的基本語(yǔ)法和 Objective-C 中比較常用的語(yǔ)法,保證開發(fā)者在使用中大部分常用的寫法都能直接支持,而部分不能支持的語(yǔ)法也有相應(yīng)的替代實(shí)現(xiàn)方式。

  • 問題定位

    • 對(duì)于暫不支持的語(yǔ)法,OCPack的工具鏈能夠明確地給出錯(cuò)誤原因提示及錯(cuò)誤代碼位置,方便定位開發(fā)中遇到的問題。

    • 上線后也可以調(diào)用相應(yīng)接口獲取虛擬機(jī)在各線程的調(diào)用棧信息,結(jié)合編譯過程中生成的符號(hào)文件,開發(fā)就能夠準(zhǔn)確定位到當(dāng)時(shí)虛擬機(jī)調(diào)用到的源文件代碼行數(shù),方便定位、解決線上問題。符號(hào)解析工具也包含在工具鏈中。

  • 性能

    由于是基于 Native 環(huán)境,且自定義的棧機(jī)指令集意義明確、設(shè)計(jì)精簡(jiǎn),并且大部分與 Native 環(huán)境之間的交互都是直接操作內(nèi)存地址,省去了像 JSPatch 一樣頻繁字符串解析及 Box/Unbox 的開銷。其中 OC 橋接調(diào)用效率近似原生。

  • 內(nèi)存及穩(wěn)定性

    • JavaScript 的 GC 的內(nèi)存管理機(jī)制會(huì)導(dǎo)致內(nèi)存不能及時(shí)釋放,而如果強(qiáng)行釋放掉 JSContext 會(huì)導(dǎo)致線上出現(xiàn)一些詭異的崩潰,難以定位和解決。

    • 此方案支持 ARC 內(nèi)存管理,能夠與客戶端的 ARC/MRC 內(nèi)存管理機(jī)制正確配合,消除了GC時(shí)機(jī)不可控的問題。

    • 而且結(jié)合項(xiàng)目本身的特點(diǎn),將主要使用到的內(nèi)存都放到文件映射(mmap)中,盡量不占用應(yīng)用程序的內(nèi)存配額,提高應(yīng)用的穩(wěn)定性。

II. 技術(shù)方案

OCPack復(fù)用 clang 前端來分析目標(biāo) Objective-C 代碼的語(yǔ)法樹,通過自定義 ASTFrontendAction 來遍歷語(yǔ)法樹,生成自定義指令集的匯編程序。在客戶端,由自研的虛擬棧機(jī)來解釋執(zhí)行匯編程序中的二進(jìn)制指令。

生成 patch 文件的基本數(shù)據(jù)流程是:

  • Objective-C 源碼會(huì)首先通過OCPack編譯模塊轉(zhuǎn)換為自定義指令集的匯編程序(.s)。此過程主要是通過解析 LLVM 生成的 Objective-C 代碼語(yǔ)法樹(AST)實(shí)現(xiàn)的。

  • 匯編程序(.s)隨后會(huì)通過一個(gè)自研匯編工具(smc)被轉(zhuǎn)換為二進(jìn)制的 patch 文件(.bin)。

注:OCPack自定義的棧機(jī)匯編指令主要有67條,除基本指令以外,主要是依據(jù)語(yǔ)法樹結(jié)點(diǎn)類型設(shè)計(jì)。

在運(yùn)行時(shí),客戶端內(nèi)置的虛擬棧機(jī)能夠根據(jù)用戶需求加載指定的 patch 文件,然后就可以執(zhí)行其中任意方法了。

以下分模塊來介紹主要技術(shù)點(diǎn):

編譯模塊:

功能:Objective-C程序(.m) -> 語(yǔ)法樹 -> 匯編程序(.s)

1. 獨(dú)立的編譯程序

主要使用 clang 的 libTooling 接口,實(shí)現(xiàn)了 AST FrontendAction,通過實(shí)現(xiàn)自定義的 ASTConsumer 遞歸遍歷語(yǔ)法樹,對(duì)不同的節(jié)點(diǎn)類型作相應(yīng)處理,生成可執(zhí)行的匯編指令程序。

  • 編譯選項(xiàng)

    要調(diào)用 clang 的模塊為目標(biāo) Objective-C 源文件(.m)生成語(yǔ)法樹,需要先提供編譯此 .m 所需的編譯選項(xiàng)。對(duì)于集成方來說,目標(biāo)文件可能需要依賴很多相關(guān)的.h或有其他編譯開關(guān),為了方便集成方得到完整的編譯選項(xiàng)列表,我們制作了相應(yīng)的工具,可以方便地從集成方工程的編譯日志中得到目標(biāo)文件所需的編譯選項(xiàng)的完整列表。

  • 編譯錯(cuò)誤和警告

    OCPack支持 Objective-C 語(yǔ)言中常用的語(yǔ)法類型,對(duì)于不支持的語(yǔ)法,在編譯期間會(huì)生成相應(yīng)的日志文件,具體標(biāo)明了錯(cuò)誤類型和錯(cuò)誤位置 ,方便開發(fā)定位問題。

:為了進(jìn)一步提高開發(fā)效率,OCPack還實(shí)現(xiàn)了一個(gè)獨(dú)立的 clang plugin,可以通過給工程添加一個(gè) .xcconfig 文件(替換默認(rèn)的 clang),實(shí)現(xiàn)在 Xcode 中顯示相關(guān)的編譯錯(cuò)誤,并能一鍵生成 .bin文件,省去了獲取編譯選項(xiàng)和手工查看錯(cuò)誤日志的步驟,簡(jiǎn)化了開發(fā)流程。

2. 棧機(jī)匯編指令集

為了連接包含有 Objective-C 代碼邏輯的語(yǔ)法樹和客戶端運(yùn)行的虛擬機(jī),OCPack需要定義一套比較完整的匯編指令集。該指令集應(yīng)該滿足以下兩個(gè)條件:

  • 提供足夠的功能支持,用以實(shí)現(xiàn)預(yù)定義的 Objective-C 語(yǔ)法范圍。具體地,對(duì)于指定的語(yǔ)法樹節(jié)點(diǎn)類型集合,能夠通過編譯邏輯生成相應(yīng)的匯編指令組合,等價(jià)地完成原 Objective-C 代碼所要實(shí)現(xiàn)的邏輯功能。

  • 盡量減少指令集的復(fù)雜性:一方面應(yīng)盡量減少指令的條數(shù),以降低虛擬機(jī)實(shí)現(xiàn)的復(fù)雜性;另一方面應(yīng)盡量降低單條指令本身的語(yǔ)義復(fù)雜性,每條指令應(yīng)完成明確而有限的功能。

以下簡(jiǎn)要介紹幾個(gè)比較典型的指令的設(shè)計(jì)方案:

2.1 push 和 pop 指令

棧機(jī)中最基本的部件是操作棧,用于存放正在進(jìn)行中的操作數(shù)和操作結(jié)果。如:要計(jì)算 1 + 2,棧機(jī)需要執(zhí)行類似以下指令:

    push instant 1
    push instant 2
    add
復(fù)制代碼

先將兩個(gè)操作數(shù)12依次 push 進(jìn)操作棧,再執(zhí)行 add 操作。add 操作負(fù)責(zé)先 pop 對(duì)應(yīng)的操作數(shù),經(jīng)過加法計(jì)算后再將結(jié)果 push 進(jìn)棧。以上指令執(zhí)行完后,操作棧頂存放的就是操作結(jié)果3

但只有操作棧是不夠的,程序邏輯的復(fù)雜性要求像局部變量、方法參數(shù)等數(shù)據(jù)擁有確定不變的內(nèi)存地址,因此OCPack將局部變量、靜態(tài)變量、常量、指針、立即數(shù)等分別對(duì)應(yīng)一個(gè)段,每種類型的變量對(duì)應(yīng)于所屬段中的一個(gè)序號(hào)(index)。

  • 段:用于存放各種非臨時(shí)數(shù)據(jù)(可取到地址的數(shù)據(jù)),這些數(shù)據(jù)又分線程相關(guān)與線程無關(guān),其中:
    線程相關(guān)的數(shù)據(jù)段主要包括:

            local    //局部變量
            arg      //方法實(shí)參
            this     //存放self(用于super的實(shí)現(xiàn))
            that     //用于實(shí)現(xiàn)struct的成員變量
            pointer  //用于輔助實(shí)現(xiàn)this, that 
            //注: 線程相關(guān)的段數(shù)據(jù)存放在thread_context(線程局部存儲(chǔ))中,只對(duì)本線程可見
    復(fù)制代碼
    

    線程無關(guān)的數(shù)據(jù)段主要包括:

            const    //常量字符串
            static   //靜態(tài)變量
            instant  //立即數(shù)
            //注:線程無關(guān)的段數(shù)據(jù)存放在machine中,各線程都可見
    復(fù)制代碼
    

在對(duì)語(yǔ)法樹進(jìn)行遍歷的過程中,OCPack編譯器會(huì)維護(hù)一個(gè)符號(hào)表,對(duì)每個(gè)變量聲明(VarDecl)建立相應(yīng)的符號(hào)表項(xiàng),存放其段名和序號(hào)(index)。

對(duì)語(yǔ)法樹中的變量引用(VarDeclRef 結(jié)點(diǎn)),OCPack編譯器會(huì)找到其相應(yīng)的 VarDecl 的符號(hào)表項(xiàng),生成相應(yīng)的 push、pop 指令。

push和pop指令的參數(shù)就是段名和序號(hào)(index):

  • push segment index —— 將 segment 段中的 index 處數(shù)據(jù) push 到操作棧頂
  • pop segment index —— 將操作棧頂?shù)臄?shù)據(jù) pop 到 segment 段中的 index 處
2.2 prolog 指令
  • prolog 指令是每個(gè)虛擬機(jī)中方法的第一條指令,它會(huì)根據(jù)其指令參數(shù)為當(dāng)前方法棧幀中的local段開辟相應(yīng)大小的段空間,并記錄當(dāng)前棧幀的返回地址,然后計(jì)算并記錄參數(shù)列表(arg 段)起始地址,再將調(diào)用者的棧頂指針指向參數(shù)表之前,最后切換到被調(diào)用者的棧幀。

  • 格式:prolog arg_size local_size

    • 其中 arg_size 表示所有參數(shù)的總長(zhǎng)度,用于計(jì)算參數(shù)列表的起始地址

    • local_size 表示局部變量段的長(zhǎng)度

方法調(diào)用和傳參這塊的設(shè)計(jì)需要一些特別的考慮,主要需要滿足幾個(gè)要求:

  • 調(diào)用者只需將參數(shù)和返回值按要求 push 到操作棧上,然后直接跳轉(zhuǎn)到被調(diào)方法的起始地址,程序就可順利執(zhí)行,調(diào)用方不應(yīng)承擔(dān)其他不必要的責(zé)任

  • 根據(jù)棧機(jī)的一般調(diào)用邏輯,被調(diào)函數(shù)返回時(shí),剛才push進(jìn)來的參數(shù)和返回值應(yīng)該已由被調(diào)者pop,此時(shí)棧頂應(yīng)該只有返回值,棧頂以下應(yīng)該是跟這次調(diào)用無關(guān)的其他數(shù)據(jù)

  • 被調(diào)方需要知道參數(shù)和返回地址的具體位置,參數(shù)需要有固定地址,支持隨機(jī)訪問,不能是只靠 pop 得到的暫存值

  • 局部變量也需要隨機(jī)訪問,其大小需要在函數(shù)執(zhí)行一開始就分配好

為了滿足這些需求,OCPack中設(shè)計(jì)了 prolog 這一指令:

  • 在每個(gè)方法頭部加此指令,調(diào)用者一跳到當(dāng)前方法就執(zhí)行此指令,相關(guān)設(shè)置都在此指令中執(zhí)行,盡量減少對(duì)調(diào)用者的要求。

  • 根據(jù)約定,執(zhí)行到 prolog 時(shí),棧頂存放的是返回地址,棧頂以下是倒排的參數(shù)表, prolog 指令先 pop 返回地址并保存下來,然后將調(diào)用者的棧楨的 sp 往回調(diào)整參數(shù)表的長(zhǎng)度(此長(zhǎng)度作為指令參數(shù)由編譯時(shí)確定,調(diào)用者無需通過棧來傳遞此信息),也即指向第一個(gè)參數(shù)的位置。注意此時(shí)還沒有生成被調(diào)用者的棧楨,所有的操作都還在調(diào)用者的棧楨上下文內(nèi)。這樣就能夠保證被調(diào)者返回時(shí)調(diào)用者的 sp 是在合適的位置,到時(shí)候直接 push 返回值就可以。

  • 此時(shí) prolog 根據(jù)返回地址及調(diào)用者的棧楨信息生成新棧楨,新棧幀中建立的 arg 段直接指向參數(shù)表的起始位置,之后訪問參數(shù)即可使用 push arg i 或 pop arg i 等指令。

  • 同時(shí),局部變量的 local 段也需要建立,其大小也由編譯時(shí)確定,即是 prolog 指令的 local_size 參數(shù),在建好棧楨并切換當(dāng)前棧幀后,即完成了方法調(diào)用的過渡階段,程序流程便可繼續(xù)進(jìn)行。

2.3 ret 指令
  • ret 指令是虛擬機(jī)中方法的最后一條指令,與 prolog 相對(duì)應(yīng),用于回退棧幀(unwind frame),并將返回值數(shù)據(jù)由被調(diào)方的棧頂拷貝到 unwind 以后調(diào)用方的操作棧頂,以實(shí)現(xiàn)調(diào)用完成后返回值位于當(dāng)前棧頂?shù)恼{(diào)用約定。

  • 格式:ret retSize

此處有一次數(shù)據(jù)拷貝,拷貝大小即為返回值的大小。為盡量減少對(duì)調(diào)用者的影響,在編譯期給 ret 方法增加 retSize 參數(shù),以便在執(zhí)行 ret 的時(shí)候就能完成數(shù)據(jù)拷貝,棧幀回退到調(diào)用者后,調(diào)用者可以預(yù)期返回值就在自己操作棧的棧頂,后續(xù)邏輯不受當(dāng)前棧頂值是由方法調(diào)用返回還是自行 push 得到的影響,邏輯較清晰。

2.4 跳轉(zhuǎn)指令

為了實(shí)現(xiàn)條件判斷 if/else 和 for 循環(huán)等流程控制語(yǔ)法,OCPack指令集定義了jmpjmp_if指令,根據(jù)語(yǔ)法樹中對(duì)應(yīng)類型的節(jié)點(diǎn)具體情況,生成相應(yīng)的跳轉(zhuǎn)指令和跳轉(zhuǎn) label。這些文本跳轉(zhuǎn) label 會(huì)被存儲(chǔ)在 .s 文件中,然后在下一階段(Assembler 將 .s 轉(zhuǎn)換為 .bin時(shí))被替換成相應(yīng)的偏移地址。

2.5 switch指令
1) switch跳轉(zhuǎn)表

switch需要運(yùn)行時(shí)決定跳到哪個(gè) case label 對(duì)應(yīng)的地址,只用jmp_if需要在 case列表代碼尾部插入多條比較語(yǔ)句,而棧機(jī)又需要每次比較前都push相應(yīng)的參數(shù),實(shí)現(xiàn)比較繁瑣而且性能較差,因此OCPack在指令集中增加了cmp_n、resolve_labeljmp_tos指令。

  • 首先,OCPack編譯器在生成指令時(shí)會(huì)先將 switch 要比較的目標(biāo) push 進(jìn)操作棧,然后再將各個(gè) case 的值 push 進(jìn)棧,然后添加cmp_n n指令。在運(yùn)行時(shí),cmp_n n指令會(huì)從棧上 pop 出n個(gè)數(shù)據(jù),與棧頂?shù)臄?shù)據(jù)(即 switch 的目標(biāo))進(jìn)行比較,如果與第i個(gè)相等,則將ipush到棧頂。

  • 后面再添加指令resolve_label label_prefix。此指令在運(yùn)行時(shí)會(huì)將 label_prefix 與棧頂?shù)?code>i進(jìn)行字符串拼接,生成目標(biāo) label 名,并在 machine 中進(jìn)行查找,找到對(duì)應(yīng)的 label 地址,push 到棧頂。其中 label_prefix 是每個(gè) switch 語(yǔ)句唯一的,可以支持 switch 嵌套。

  • 然后再添加指令jmp_tos。此指令在運(yùn)行時(shí)會(huì)跳轉(zhuǎn)到棧頂?shù)牡刂?,從而?shí)現(xiàn) switch 的功能。

2) continue和break的支持:

分別維護(hù)一個(gè) break和 continue 的 label 棧,棧頂元素為當(dāng)前 break 或 continue 調(diào)用時(shí)應(yīng)該 jmp 到的目標(biāo) label,在目標(biāo)表達(dá)式開始和結(jié)束時(shí)進(jìn)行入棧和出棧操作。在遇到語(yǔ)法樹上結(jié)點(diǎn)為 break 或 continue 時(shí),取出當(dāng)前棧頂?shù)哪繕?biāo) label,生成jmp 目標(biāo)label指令。

2.6 call指令
  • 使用 libffi 實(shí)現(xiàn)動(dòng)態(tài) c 方法調(diào)用。對(duì)于每個(gè)被調(diào)用的 c 方法,在 .s 會(huì)有一項(xiàng)DECL_C_FUNC 的聲明,聲明包含此方法的名稱、簽名(包括參數(shù)個(gè)數(shù)和類型)等信息。

  • .s 中的參數(shù)類型是OCPack自定義的字符串,一一對(duì)應(yīng)到 libffi 的類型。對(duì)于 struct 來說,生成指令時(shí)需要遞歸找到 struct 所有成員的類型,拼成相應(yīng)的字符串,然后在運(yùn)行期反解字符串,構(gòu)造出 libffi 所需的數(shù)據(jù)類型。

  • 對(duì)于變參的方法,方法名相同而參數(shù)個(gè)數(shù)或類型不同的,在 DECL_C_FUNC 時(shí)會(huì)對(duì)應(yīng)不同的條目,虛擬機(jī)在運(yùn)行時(shí)會(huì)根據(jù)對(duì)應(yīng)的條目去構(gòu)造相應(yīng)的 libffi 參數(shù)數(shù)據(jù)。

2.7 基本一元、二元運(yùn)算符指令
  • 指令集中對(duì)算術(shù)、邏輯、移位等等基本運(yùn)算符都有對(duì)應(yīng)的指令,指令參數(shù)包括返回值類型、操作數(shù)類型等。

  • 在虛擬機(jī)的實(shí)現(xiàn)代碼中將各種運(yùn)算、數(shù)據(jù)類型的組合分配到相應(yīng)的c語(yǔ)言實(shí)現(xiàn),運(yùn)行時(shí)就根據(jù)傳入的指令和參數(shù)調(diào)用其相應(yīng)實(shí)現(xiàn)。

注: 此指令只支持整型、浮點(diǎn)型等基本數(shù)據(jù)類型的運(yùn)算,不支持自定義類型重載的運(yùn)算符

2.8 左右值轉(zhuǎn)換
  • 指令集有左值轉(zhuǎn)右值的指令,其參數(shù)為右值的 size。此指令的作用為:先 pop 操作棧頂存儲(chǔ)的地址(addr),然后取地址為 addr 的大小為 size 的內(nèi)存數(shù)據(jù),push 到操作棧頂。

  • 在 clang 生成的 AST 中,所有 VarDeclRef 其實(shí)對(duì)應(yīng)的是變量的地址,對(duì)于訪問變量?jī)?nèi)容(變量右值)的代碼,AST 中 VarDeclRef 的父節(jié)點(diǎn)都是左右值轉(zhuǎn)換節(jié)點(diǎn)。因此OCPack中 push 指令,類似push seg index都是將 seg 段 index 處的地址 push 進(jìn)操作棧,而取對(duì)應(yīng)地址處的具體內(nèi)容由左右值轉(zhuǎn)換指令來完成。

注: 在實(shí)現(xiàn)初期,OCPack的 push 指令是直接將 seg 段 index 處變量的右值 push 進(jìn)操作棧(即這種情況下忽略左右值轉(zhuǎn)換的結(jié)點(diǎn)),但后來發(fā)現(xiàn)在類似賦值操作中的左值變量的情況下,AST 中沒有左右值轉(zhuǎn)換結(jié)點(diǎn),如果對(duì)這些情況特殊處理,邏輯會(huì)變得較為復(fù)雜且難以保證覆蓋完全,后來決定完全依照 AST 中結(jié)點(diǎn)的排布邏輯,將 push 操作的對(duì)象改成了對(duì)應(yīng)變量的左值,犧牲部分性能換取程序的可靠性。

2.9 Objective-C 方法調(diào)用指令
  • 指令集中有專用于調(diào)用 Objective-C 方法的指令 OBJC_MSG_CLASS/OBJC_MSG_INST。

  • 虛擬機(jī)在解釋執(zhí)行此指令時(shí),先取得存儲(chǔ)在棧上的所有參數(shù),然后構(gòu)造相應(yīng)的 NSInvocation,通過 invoke 來實(shí)現(xiàn)對(duì) Objective-C runtime 的調(diào)用。

  • 指令實(shí)現(xiàn)中,對(duì)于 target 和參數(shù)都采用 __unsafe_unretained 方式進(jìn)行引用,即不改變其生命周期。返回值則一律使用 autorelease 方式,確保返回值在返回給直接調(diào)用者時(shí)是有效的。

注: 實(shí)現(xiàn)過程中,Objective-C 調(diào)用指令所需的輸入數(shù)據(jù)的內(nèi)存排布順序也經(jīng)歷了一番修改。因?yàn)閷?duì)于 Objective-C 方法來說,只有拿到 selector 才能知道具體有多少個(gè)參數(shù),所以之前設(shè)計(jì)是參數(shù)表倒著放,即第一個(gè)參數(shù)放棧頂,第二個(gè)參數(shù)依次往下排。這樣可以穩(wěn)定地 pop 兩次就得到 selector 的聲明,然后再根據(jù) selector 中指明的參數(shù)個(gè)數(shù)及大小 pop 所有的參數(shù)。但這種方法在參數(shù)大小大于 64 bit 的情況下(如 struct)就比較難處理了,因?yàn)橐玫秸_的 struct 數(shù)據(jù),程序需要 pop 對(duì)應(yīng)個(gè)數(shù)的64 bit,然后做拼接,煩瑣而且容易出錯(cuò)。經(jīng)權(quán)衡,還是在指令參數(shù)中增加了參數(shù)表長(zhǎng)度(編譯期得到),在調(diào)用 OBJC_MSG_CLASS/OBJC_MSG_INST 指令前,參數(shù)還是按順序push(即第一個(gè)參數(shù)先 push,棧頂是最后一個(gè)參數(shù)),在指令的實(shí)現(xiàn)中,根據(jù)指令參數(shù)中提供的參數(shù)表長(zhǎng)度,直接從 sp 算出第一個(gè)參數(shù)的起始位置,這樣所有的參數(shù)都可以用指針訪問,而不用關(guān)心其大小了。原先需要的多次 pop 指令,變成只需在指令退出前,將 sp 回退參數(shù)表長(zhǎng)度即可。

匯編模塊

功能:匯編程序(.s) -> 二進(jìn)制補(bǔ)丁程序(.bin)

解析整個(gè) .s 文本,將文本 token 轉(zhuǎn)換為對(duì)應(yīng)的二進(jìn)制數(shù)據(jù),主要包括:

  • 立即數(shù)從文本到二進(jìn)制數(shù)據(jù)的轉(zhuǎn)換

  • 跳轉(zhuǎn) label 到地址的轉(zhuǎn)換

  • 常量字符串的轉(zhuǎn)換,此處一開始直接在 .s 中存儲(chǔ)了字符串,后來遇到 '\n\t' 等情況不能很好地支持,就改成了直接存儲(chǔ)字節(jié)碼

  • 生成導(dǎo)出函數(shù)表,表中記錄了虛擬機(jī)中定義的方法名和地址的對(duì)應(yīng)關(guān)系

  • 生成導(dǎo)入函數(shù)表,包含了所有調(diào)用到的c方法聲明及其 index,代碼段中調(diào)用 c 方法時(shí)直接調(diào)用此處的 index

  • static 數(shù)據(jù)段大小和全局區(qū)的總大小,因?yàn)樘摂M機(jī)初始化時(shí)需要將全局區(qū)放到一段 shared & anonymous mmap 內(nèi)存上,故需要此 size

  • 存儲(chǔ) GUID 值

  • 存儲(chǔ) Target arch,此值用于驗(yàn)證 32bit 和 64bit,確保平臺(tái)和 .bin 文件的匹配

  • 文本指令轉(zhuǎn)化為二進(jìn)制指令

轉(zhuǎn)換完成后將各數(shù)據(jù)存入內(nèi)存中相應(yīng)的數(shù)據(jù)段,再將整個(gè)內(nèi)存 dump 成一個(gè)二進(jìn)制文件。

注: 二進(jìn)程文件在運(yùn)行時(shí)所需的大部分?jǐn)?shù)據(jù)其排布都與 .bin 文件里的排布完全相同,這樣能方便地使用內(nèi)存映射來實(shí)現(xiàn) .bin 文件的加載,從而可以減少私有內(nèi)存的占用量。

加載模塊

功能:二進(jìn)制補(bǔ)丁程序(.bin)加載

在調(diào)用 load_image 時(shí),虛擬機(jī)會(huì)先將 .bin 文件 mmap 到一段內(nèi)存中,檢測(cè) magic number, bin version 及 arch 是否匹配。

然后按全局區(qū)的大小申請(qǐng)一段 shared anonymous mmap 內(nèi)存。

然后分別加載各個(gè)數(shù)據(jù)段,建立必要的運(yùn)行時(shí)內(nèi)存數(shù)據(jù),主要的數(shù)據(jù)段包括:

  • 常量字符串段,將全局區(qū)對(duì)應(yīng)大小的內(nèi)存分配給常量段,并將對(duì)應(yīng)的 index 指向?qū)?yīng)的字符串起始地址

  • 靜態(tài)數(shù)據(jù)段,將全局區(qū)對(duì)應(yīng)大小的內(nèi)存分配給靜態(tài)段

  • 導(dǎo)出符號(hào)表

  • 導(dǎo)入符號(hào)表

  • 代碼段

  • GUID數(shù)據(jù)

注:bin文件具體格式如下: [圖片上傳中...(image-1b9c27-1603349931409-1)]

執(zhí)行模塊

功能:二進(jìn)制補(bǔ)丁程序(.bin)的執(zhí)行

1) 虛擬機(jī)基本信息

  • 棧和各個(gè)段都以 64bit 為單位

  • 調(diào)用方法前,需要將對(duì)應(yīng)的參數(shù) push 到操作棧上

  • 調(diào)用完成后,棧頂放的就是返回值

  • 運(yùn)行時(shí)上下文(thread_context)和棧幀

    • thread_context 維護(hù)一個(gè)棧幀的鏈表,用以存放調(diào)用關(guān)系

    • 棧幀用于存放當(dāng)前方法的運(yùn)行時(shí)信息,主要包括:

      • 棧幀基地址
      • 返回地址
      • 指向調(diào)用方棧幀基地址的指針
      • 段表基地址
      • 操作棧基址
      • 操作棧指針 sp
      • 程序計(jì)數(shù)器 ip

注:VM 函數(shù)棧幀具體內(nèi)存布局如下:

[圖片上傳中...(image-370312-1603349931409-0)]

2) Objective-C 調(diào)用虛擬機(jī)方法

  • Objective-C 代碼通過向虛擬機(jī) 的 callFunctionWithArgs: 方法傳入要調(diào)用函數(shù)名及其參數(shù)(此處的參數(shù)為真正參數(shù)的地址)并得到返回值來與虛擬機(jī)進(jìn)行交互。

  • callFunctionWithArgs: 方法內(nèi)部會(huì)通過函數(shù)名查找導(dǎo)出表,找到其方法簽名和地址。然后根據(jù)方法簽名中指定的參數(shù)大小,將傳入的參數(shù)地址處對(duì)應(yīng)大小的數(shù)據(jù) push 到操作棧上,然后再跳轉(zhuǎn)到被調(diào)方法的開始地址處,開始執(zhí)行匯編指令。

  • 虛擬機(jī)執(zhí)行完所有匯編指令后返回到 callFunctionWithArgs: 中,該方法再負(fù)責(zé)把棧頂?shù)姆祷刂禂?shù)據(jù)拷貝到調(diào)用方傳入的返回值地址處。

  • 調(diào)用完成后,虛擬機(jī)的棧頂?shù)刂窇?yīng)該保持與調(diào)用前完全一致。

  • 如果虛擬機(jī)方法的返回值是 NSObject* 類型,OCPack會(huì)根據(jù)存儲(chǔ)返回值的變量是否是強(qiáng)引用而決定是否需要對(duì)返回的對(duì)象做__brige_retained操作,用以中和調(diào)用方對(duì) strong 類型變量的 release 操作。其他情況下因?yàn)榉祷氐亩际?autorelease 的對(duì)象,返回時(shí)不做特殊處理(詳見編譯模塊小節(jié)中 2.9 段Objective-C方法調(diào)用指令)。

3) 虛擬機(jī)調(diào)用 OC 方法(f1),f1 又調(diào)用到了虛擬機(jī)的方法(f2)

要支持此流程,需保證 f2 調(diào)用完成后虛擬機(jī)當(dāng)前棧幀的 sp 與調(diào)用前完全一致,以保證 f1 的執(zhí)行不受影響。

4) 虛擬機(jī)方法間互相調(diào)用

在調(diào)用OC方法時(shí),會(huì)先檢測(cè)對(duì)應(yīng)的方法是否在導(dǎo)出函數(shù)表中,如果在,則走此流程。這也要求調(diào)用虛擬機(jī)方法時(shí)的參數(shù)表應(yīng)該與直接調(diào)用 OC 方法是一致的,否則還需要重新拷貝參數(shù)做適配,降低虛擬機(jī)性能。

5) 多線程支持

  • 運(yùn)行時(shí)上下文(thread_context)指針放在線程局部存儲(chǔ)中,每個(gè)線程在讀、寫上下文中數(shù)據(jù)時(shí)都是操作當(dāng)前線程自己的數(shù)據(jù),這樣就能保證各個(gè)線程之間運(yùn)行狀態(tài)相互隔離,從而支持多線程的調(diào)用場(chǎng)景。

  • OCPack注冊(cè)了線程退出的回調(diào)函數(shù),當(dāng)一個(gè)線程退出時(shí)OCPack會(huì)刪除所有虛擬機(jī)在此線程中的上下文相關(guān)數(shù)據(jù)。

6) 內(nèi)存占用

  • 二進(jìn)制文件加載使用 mmap,全局?jǐn)?shù)據(jù)區(qū)使用shared & anonymous mmap,常量字符串?dāng)?shù)據(jù)直接指向 .bin 中的地址。

  • 運(yùn)行時(shí)上下文(thread_context)是每個(gè)線程一份,都使用shared & anonymous mmap。

  • 運(yùn)行時(shí)的虛擬機(jī)類本身只維護(hù)導(dǎo)入函數(shù)和導(dǎo)出函數(shù)數(shù)據(jù)以及少量指針數(shù)據(jù)。

7) 崩潰時(shí)的?;厮?/h4>
  • 運(yùn)行時(shí)上下文(thread_context)中維護(hù)有一個(gè)棧幀結(jié)構(gòu)(thread_frame)的鏈表,對(duì)應(yīng)的就是虛擬機(jī)中方法的調(diào)用關(guān)系。

  • 崩潰時(shí)遍歷所有線程中所有虛擬機(jī)的 thread_context,遍歷其棧幀結(jié)構(gòu)鏈表,取出每個(gè)棧幀中存儲(chǔ)的 ip 地址寫到崩潰日志中。

  • 崩潰日志中按照線程組織崩潰棧,還會(huì)記錄每個(gè)虛擬機(jī)的地址和其加載的 .bin 文件的 GUID,用以正確區(qū)分線程、虛擬機(jī)的實(shí)例和 bin 文件。

8) 崩潰符號(hào)解析

OCPack編譯器生成或指定一個(gè) GUID,后續(xù)生成的所有相關(guān)文件(包括.s、.sym、.bin以及運(yùn)行時(shí)生成的崩潰 log)中都存有此 GUID。線上的崩潰日志發(fā)送回來后,崩潰解析服務(wù)器能夠根據(jù)日志中的 GUID 查找到相應(yīng)的符號(hào)文件進(jìn)行符號(hào)解析。同時(shí)構(gòu)建服務(wù)器數(shù)據(jù)庫(kù)中存儲(chǔ)了對(duì)應(yīng) GUID 的 bin 文件打包時(shí)所有依賴項(xiàng)的源信息(包括對(duì)應(yīng)的 .s 文件、bin 代碼對(duì)應(yīng)的源代碼版本、OCPack工具鏈的版本等),方便開發(fā)重現(xiàn)、定位相關(guān)問題。

9) Hook Objective-C 方法

  • 原理與 JSPatch 類似,通過將目標(biāo)類的目標(biāo)方法替換為 objc_msgForward,同時(shí)將 forwardInvocation 替換為自定義的 forward 方法,實(shí)現(xiàn)當(dāng)目標(biāo)方法被調(diào)用時(shí),轉(zhuǎn)向 forwardInvocation 的自定義實(shí)現(xiàn)。在自定義的 forward 的實(shí)現(xiàn)中,將 NSInvocation 傳給內(nèi)置的虛擬機(jī),虛擬機(jī)會(huì)取出其參數(shù)并調(diào)用相應(yīng)的虛擬機(jī)方法,最后將返回值設(shè)回給 NSInvocation,即完成了 Hook 的功能。

  • 與 JSPatch 相比,用OCPack的方法調(diào)用省去了大量的字符串解析操作,參數(shù)大都可直接傳入虛擬機(jī)進(jìn)行處理,方法調(diào)用的整體開銷比 JSPatch 小。

性能優(yōu)化

  1. 二進(jìn)制程序的大小優(yōu)化
  • OCPack在實(shí)現(xiàn)初期,采用了模板類的方式實(shí)現(xiàn)一、二元操作符的對(duì)不同操作數(shù)和返回值類型的支持,這樣調(diào)試起來比較方便。但后來發(fā)現(xiàn)這種方案會(huì)導(dǎo)致代碼體積會(huì)暴增。模板方法會(huì)根據(jù)輸入、輸出參數(shù)的類型生成大量的方法,而其中大部分方法都只有很短的幾條指令,從最后的二進(jìn)制內(nèi)容分析看,光方法名就占用了大量?jī)?nèi)存。

    • 在功能基本穩(wěn)定后,在單元測(cè)試的保證下,將模板改為了宏實(shí)現(xiàn),大幅地減小代碼段和數(shù)據(jù)段的體積,framework 文件的大小從3.5M減到了不到150k。
  1. 性能優(yōu)化
  • 匯編代碼優(yōu)化

    • 由語(yǔ)法樹直接生成的匯編代碼里面會(huì)有很多無用的 push 操作,主要原因是某些表達(dá)式的返回值沒有被用到,此時(shí) push 是多余的,而且在比較大的循環(huán)還可能會(huì)使棧長(zhǎng)度暴增,影響性能和穩(wěn)定性

    • 優(yōu)化方法是盡量去掉無用的 push 指令。主要是通過定義一些規(guī)則來判別某指令的返回值是否被用到,對(duì)于無用的表達(dá)式返回值不進(jìn)行 push 操作。

  • 虛擬機(jī)性能優(yōu)化

    • 將頻繁用到的數(shù)據(jù)緩存下來:取運(yùn)行時(shí)上下文和讀寫棧幀是頻繁操作,涉及線程局部存儲(chǔ)數(shù)據(jù)的讀取,運(yùn)行時(shí)上下文結(jié)構(gòu)的指針只會(huì)在一個(gè)線程中訪問,通過對(duì)代碼進(jìn)行重構(gòu),將它緩存到 executor 類中,提高了運(yùn)行時(shí)效率。

    • 盡量減少核心循環(huán)處代碼的內(nèi)存訪問次數(shù):將棧幀中的部分?jǐn)?shù)據(jù)(如:ip)提出來放到 executor 類中,減少頻繁讀寫ip時(shí)導(dǎo)致的不必要的內(nèi)存操作次數(shù)。

    • 盡量提高核心循環(huán)處代碼效率:用數(shù)組代替 map 實(shí)現(xiàn)指令到指令處理器的映射關(guān)系,提高運(yùn)行時(shí)查詢效率。

    • 優(yōu)化后比優(yōu)化前提高了將近一倍

III. 未來計(jì)劃

鏈接器

  • 支持多個(gè) .m 對(duì)應(yīng)的多個(gè) .bin 鏈接成一個(gè) .bin 文件,且各個(gè) .bin 文件之間能夠互相調(diào)用

其他語(yǔ)法支持

  • 支持聲明 block 的語(yǔ)法

性能優(yōu)化

  • 去掉虛函數(shù)調(diào)用
  • 指令長(zhǎng)度對(duì)齊
  • 保證 sp、ip 等頻繁操作的數(shù)據(jù)都放入寄存器——匯編實(shí)現(xiàn)相關(guān)核心方法、或者改變代碼結(jié)構(gòu)
  • 生成匯編代碼的優(yōu)化(窺孔優(yōu)化等)
  • 其他

Debug工具

  • 因?yàn)樘摂M機(jī)執(zhí)行時(shí)是按指令執(zhí)行的,不容易直接對(duì)應(yīng)到 Objective-C 代碼,調(diào)試起來有點(diǎn)麻煩,后續(xù)計(jì)劃做一些功能,調(diào)試時(shí)方便地顯示指令地址到 Objective-C 源代碼的對(duì)應(yīng)關(guān)系。

作者:zhangjiezhi_
鏈接:https://juejin.im/post/6884833291740905480
來源:掘金
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請(qǐng)注明出處。

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

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