重學(xué)iOS系列之APP啟動(dòng)(二)dyld

導(dǎo)讀

? ? 本文承接自APP啟動(dòng)-流程(一)陨溅,有疑惑的同學(xué)可以先閱讀上一篇的內(nèi)容发钝。本文會(huì)帶大家詳細(xì)的解讀dyld-852.2源碼中關(guān)于APP啟動(dòng)最重要的一個(gè)函數(shù)_main()偶洋。我們從上一節(jié)了解到_main函數(shù)的實(shí)現(xiàn)代碼有900行版保,所以不可能一行一行的來進(jìn)行解讀洲愤,本文只會(huì)針對(duì)重點(diǎn)函數(shù)進(jìn)行跟進(jìn)解讀。有需要的同學(xué)自行下載源碼解讀乐埠。源碼解讀并非跟著文章看一遍就能記住學(xué)會(huì)抗斤,這個(gè)過程需要反復(fù)的跟讀囚企,所以建議讀者將源碼下載下來丈咐,跟著筆者的進(jìn)度同時(shí)對(duì)照著源碼學(xué)習(xí)效果才會(huì)最佳,也不至于看得云里霧里龙宏。

dyld下載地址:http://opensource.apple.com/tarballs/dyld

在分析具體的源碼之前棵逊,我們必須要了解一些前置知識(shí)點(diǎn):

dyld的全稱是dynamic loader。dyld 是 iOS 上的二進(jìn)制加載器银酗,用于加載 Image辆影。有不少人認(rèn)為 dyld 只負(fù)責(zé)加載應(yīng)用依賴的所有動(dòng)態(tài)鏈接庫,這個(gè)理解是錯(cuò)誤的黍特。dyld 工作的具體流程如下:


dyld2與dyld3

在?iOS 13?之前蛙讥,所有的第三方?App?都是通過?dyld 2?來啟動(dòng)?App?的,主要過程如下:

1灭衷、Parse mach-o headers?????解析mach-o頭文件

2次慢、find dependencels 根據(jù)頭文件的信息查找依賴項(xiàng)

3、Map mach-o files ?將mach-o文件映射到內(nèi)存翔曲,簡(jiǎn)單來講就是加載到內(nèi)存中

4迫像、Perform symbol lookups這個(gè)步驟表示執(zhí)行符號(hào)查找。(例如:如果你使用了printf函數(shù)瞳遍,就會(huì)查找printf是否在庫系統(tǒng)中闻妓,找到它的地址,將它賦值到你的程序中的函數(shù)指針)

5掠械、Bind and rebase ?符號(hào)綁定和地址重定位(由于ASLR的原因)

6由缆、Run initializers ?dyld會(huì)運(yùn)行初始化函數(shù)注祖,初始化動(dòng)態(tài)庫,初始化主程序均唉,然后進(jìn)入運(yùn)行時(shí)runtime初始化氓轰,注冊(cè)類,分類浸卦,方法唯一性檢查等(后續(xù)的文章會(huì)詳細(xì)分析runtime的初始化過程)

在iOS 13之后署鸡,dyld3開放給第三方APP使用了,也就是說限嫌,在iOS 13以上的系統(tǒng)里靴庆,APP是通過dyld3來啟動(dòng)的。

從上圖來看dyld3是由2個(gè)部分組成怒医,上半部分在程序啟動(dòng)進(jìn)程外執(zhí)行的炉抒,這一步會(huì)在App下載安裝和版本更新的時(shí)候會(huì)去執(zhí)行。下半部分才是在程序啟動(dòng)進(jìn)程內(nèi)執(zhí)行的稚叹。

原本在dyld2中執(zhí)行的1焰薄、2、4步被放到了程序啟動(dòng)進(jìn)程外執(zhí)行扒袖,然后向磁盤寫入閉包處理 “Write closure to disk”(啟動(dòng)閉包(launch closure):這是一個(gè)新引入的概念塞茅,指的是 app 在啟動(dòng)期間所需要的所有信息。比如這個(gè) app 使用了哪些動(dòng)態(tài)鏈接庫季率,其中各個(gè)符號(hào)的偏移量野瘦,代碼簽名在哪里等等)。這樣飒泻,啟動(dòng)閉包處理就成了啟動(dòng)程序的重要環(huán)節(jié)鞭光。稍后可以在APP的進(jìn)程中使用 dyld 3包含的這三個(gè)部分,

啟動(dòng)閉包比mach-o更簡(jiǎn)單泞遗。它們是內(nèi)存映射文件惰许,不需要用復(fù)雜的方法進(jìn)行分析。

我們可以簡(jiǎn)單的驗(yàn)證它們史辙,這樣可以提高速度汹买,也就是說在不需要修改代碼的情況下官方幫我們做了啟動(dòng)優(yōu)化。

dyld3的主要過程如下:

主程序進(jìn)程外

1髓霞、Parse mach-o headers?????解析mach-o頭文件

2卦睹、find dependencels 根據(jù)頭文件的信息查找依賴項(xiàng)

3、Perform symbol lookups ?這個(gè)步驟表示執(zhí)行符號(hào)查找方库。(例如:如果你使用了printf函數(shù)结序,就會(huì)查找printf是否在庫系統(tǒng)中,找到它的地址纵潦,將它賦值到你的程序中的函數(shù)指針)

4徐鹤、Write closure to disk ?將1垃环、2、3步做完的事情組裝成一個(gè)啟動(dòng)閉包返敬,并且寫入磁盤

主程序進(jìn)程內(nèi)

5遂庄、Read in closure ? ?從啟動(dòng)閉包中讀取必要的信息數(shù)據(jù)

6、Validata closure ? ? 驗(yàn)證啟動(dòng)閉包

7劲赠、Map mach-o files ?將mach-o文件映射到內(nèi)存涛目,簡(jiǎn)單來講就是加載到內(nèi)存中

8、Bind and rebase ?符號(hào)綁定和地址重定位(由于ASLR的原因)

9凛澎、Run initializers ?dyld會(huì)運(yùn)行初始化函數(shù)霹肝,初始化動(dòng)態(tài)庫,初始化主程序塑煎,然后進(jìn)入運(yùn)行時(shí)runtime初始化沫换,注冊(cè)類,分類最铁,方法唯一性檢查等(后續(xù)的文章會(huì)詳細(xì)分析runtime的初始化過程)

dyld3 被分為了三個(gè)組件:

一讯赏、一個(gè)進(jìn)程外的 MachO 解析器

1、預(yù)先處理了所有可能影響啟動(dòng)速度的 search path冷尉、@rpaths 和環(huán)境變量

2漱挎、然后分析 Mach-O 的 Header 和依賴,并完成了所有符號(hào)查找的工作

3网严、最后將這些結(jié)果創(chuàng)建成了一個(gè)啟動(dòng)閉包

4识樱、這是一個(gè)普通的 daemon 進(jìn)程,可以使用通常的測(cè)試架構(gòu)

二震束、一個(gè)進(jìn)程內(nèi)的引擎,用來運(yùn)行啟動(dòng)閉包

1当犯、這部分在進(jìn)程中處理

2垢村、驗(yàn)證啟動(dòng)閉包的安全性,然后映射到 dylib 之中嚎卫,再跳轉(zhuǎn)到 main 函數(shù)

3嘉栓、不需要解析 Mach-O 的 Header 和依賴,也不需要符號(hào)查找拓诸。

三侵佃、一個(gè)啟動(dòng)閉包緩存服務(wù)

1、系統(tǒng) App 的啟動(dòng)閉包被構(gòu)建在一個(gè) Shared Cache 中奠支, 我們甚至不需要打開一個(gè)單獨(dú)的文件

2馋辈、對(duì)于第三方的 App,我們會(huì)在 App 安裝或者升級(jí)的時(shí)候構(gòu)建這個(gè)啟動(dòng)閉包倍谜。

3迈螟、在 iOS叉抡、tvOS、watchOS中答毫,這這一切都是 App 啟動(dòng)之前完成的褥民。在 macOS 上,由于有 Side Load App洗搂,進(jìn)程內(nèi)引擎會(huì)在首次啟動(dòng)的時(shí)候啟動(dòng)一個(gè) daemon 進(jìn)程消返,之后就可以使用啟動(dòng)閉包啟動(dòng)了。

dyld 3 把很多耗時(shí)的查找耘拇、計(jì)算和 I/O 的事前都預(yù)先處理好了侦副,這使得啟動(dòng)速度有了很大的提升。



_main函數(shù)

函數(shù)的每個(gè)參數(shù)意義如下:

// dyld的main函數(shù) dyld的入口方法kernel加載dyld并設(shè)置設(shè)置一些寄存器并調(diào)用此函數(shù),之后跳轉(zhuǎn)到__dyld_start

// mainExecutableSlide 主程序的slider,用于做重定向 會(huì)在main方法中被賦值

// mainExecutableMH 主程序MachO的header

// argc 表示main函數(shù)參數(shù)個(gè)數(shù)

// argv 表示main函數(shù)的參數(shù)值 argv[argc] 可以獲取到參數(shù)值

// envp[] 表示以設(shè)置好的環(huán)境變量

// apple 是從envp開始獲取到第一個(gè)值為NULL的指針地址

uintptr_t

_main(constmacho_header* mainExecutableMH,uintptr_tmainExecutableSlide,?

intargc,constchar* argv[],constchar* envp[],constchar* apple[],?

uintptr_t* startGlue)

源碼分析_main()函數(shù):

配置運(yùn)行環(huán)境

檢查是否開啟debug追蹤驼鞭,追蹤的是啟動(dòng)可執(zhí)行文件的過程秦驯。

檢查是否有內(nèi)核相關(guān)的標(biāo)記,setFlags內(nèi)部會(huì)檢查dyld3是否已經(jīng)初始化挣棕,沒有初始化則不設(shè)置flags

檢查并查看內(nèi)核是否禁用了JOP(Jump-Oriented Programming)指針簽名译隘,指針簽名是用于防范內(nèi)核攻擊的一個(gè)手段。

從環(huán)境變量中獲取主要可執(zhí)行文件的?cdHash?值洛心。這個(gè)哈希值?mainExecutableCDHash?在后面用來校驗(yàn)?dyld3?的啟動(dòng)閉包

獲取當(dāng)前設(shè)備的一些信息固耘,比如:cpu架構(gòu)類型,基本信息词身,mach進(jìn)程通信端口等


通知內(nèi)核開始加載dyld和主程序的可執(zhí)行文件了

獲取主程序的macho_header結(jié)構(gòu)

獲取主程序的slide值厅目,slide其實(shí)就是mach-o映射到內(nèi)存的偏移量

查找可執(zhí)行文件支持的平臺(tái)(看綠色高亮的FIXME這行,其實(shí)這部分代碼可以刪除法严,因?yàn)樵趦?nèi)核中已經(jīng)處理過了)

接著沒截出來的那一段代碼是基于OS系統(tǒng)的判斷邏輯损敷,本文不做分析。


設(shè)置上下文信息深啤,保存回調(diào)函數(shù)的地址拗馒,以便后續(xù)直接調(diào)用∷萁郑可以看看setContext的源碼如下:


根據(jù)環(huán)境變量從內(nèi)核拿到可執(zhí)行文件的路徑诱桂。

移除過渡代碼(修復(fù)了rdar://problem/13868260這個(gè)bug)


由于是多平臺(tái)共用一套代碼,內(nèi)核傳遞出來的exec路徑是不全的呈昔,如果是iphone真機(jī)環(huán)境挥等,則會(huì)拼接一段路徑的前綴,大家肯定打印過很多Document的路徑堤尾,真機(jī)都是/var/開頭的和模擬器的不一樣肝劲。

判斷?exec?路徑是否為絕對(duì)路徑,如果為相對(duì)路徑,使用?cwd?轉(zhuǎn)化為絕對(duì)路徑

為了后續(xù)的日志打印從?exec?路徑中取出進(jìn)程的名稱 (strrchr?函數(shù)是獲取第二個(gè)參數(shù)出現(xiàn)的最后的一個(gè)位置涡相,然后返回從這個(gè)位置開始到結(jié)束的內(nèi)容)


配置進(jìn)程的受限模式哲泊,設(shè)置gLinkContext的環(huán)境變量,跟簽名以及代碼注入有關(guān)催蝗。查看詳細(xì)的configureProcessRestrictions代碼實(shí)現(xiàn)切威,會(huì)發(fā)現(xiàn)一個(gè)新系統(tǒng)AMFI (AppleMobileFileIntegrity)。

簡(jiǎn)單介紹下AMFI是一個(gè)內(nèi)核擴(kuò)展丙号,最初是在iOS中引入的先朦。在版本10.10中,它也被添加到macOS中犬缨。它擴(kuò)展了MACF(強(qiáng)制訪問控制框架)喳魏,就像沙盒一樣,它在實(shí)施SIP和代碼簽名方面起著關(guān)鍵作用(這一塊涉及的內(nèi)容非常多怀薛,不做詳細(xì)分析)

dyld3::internalInstall()這個(gè)函數(shù)在xcode默認(rèn)設(shè)置下是返回yes的刺彩。

ClosureMode是個(gè)calss類型的枚舉,有4種模式:

Unset????表示我們沒有提供env變量或boot arg來顯式選擇模式

On????表示我們將DYLD_USE_CLOSURES設(shè)置為1枝恋,或者我們沒有將DYLD_USE_CLOSURES設(shè)置為0创倔,但在iOS上設(shè)置了-force_dyld3=1環(huán)境變量或者一個(gè)外部緩存(啟動(dòng)閉包)

Off????意味著我們?cè)O(shè)置了DYLD_USE_CLOSURES=0,或者我們沒有設(shè)置DYLD_USE_CLOSURES=1焚碌,但在iOS上設(shè)置了-force\u dyld2=1環(huán)境變量或者一個(gè)內(nèi)部緩存

PreBuiltOnly????意味著只使用共享緩存閉包畦攘,而不嘗試構(gòu)建新的緩存閉包

ClosureKind 也是個(gè)class類型的枚舉,有3種狀態(tài):unset, full, minimal

默認(rèn)值都是unset

檢查是否強(qiáng)制使用共享緩存十电,在iOS上為了節(jié)省內(nèi)存知押,是要求強(qiáng)制使用共享緩存的。



創(chuàng)建了緩存閉包路徑的數(shù)組變量鹃骂,如果出錯(cuò)台盯,則直接退出進(jìn)程。

成功則將閉包模式設(shè)置為on偎漫。

到此爷恳,運(yùn)行環(huán)境全部設(shè)置完成。細(xì)心的同學(xué)應(yīng)該會(huì)注意到象踊,整個(gè)過程中有很多DYLD_****開頭的環(huán)境變量。其實(shí)這些都是可以在Xcode中配置使其在上面的流程中生效的棚壁,我們打開工程然后依次點(diǎn)擊“Product”->“Scheme”->“Edit Scheme…”杯矩,如下圖所示。


然后運(yùn)行Xcode即可看到控制臺(tái)打印的詳細(xì)信息袖外。有很多這樣的DYLD_*開頭的環(huán)境變量史隆,感興趣的同學(xué)可以自行測(cè)試。

加載共享緩存

這里補(bǔ)充一下共享緩存的知識(shí)點(diǎn):

在iOS系統(tǒng)中曼验,每個(gè)程序依賴的動(dòng)態(tài)庫都需要通過dyld(位于/usr/lib/dyld)一個(gè)一個(gè)加載到內(nèi)存泌射,然而如果在每個(gè)程序運(yùn)行的時(shí)候都重復(fù)的去加載一次粘姜,勢(shì)必造成運(yùn)行緩慢,為了優(yōu)化啟動(dòng)速度和提高程序性能熔酷,共享緩存機(jī)制就應(yīng)運(yùn)而生孤紧。所有默認(rèn)的動(dòng)態(tài)鏈接庫被合并成一個(gè)大的緩存文件,放到/System/Library/Caches/com.apple.dyld/目錄下拒秘,按不同的架構(gòu)保存分別保存著号显。

如果沒有緩存庫存在的話,那么我們手機(jī)上的每一個(gè)App躺酒,如果要用到系統(tǒng)動(dòng)態(tài)庫的話押蚤,是需要每一個(gè)App都要去加載一次的,一樣的資源被加載多次(加載幾次就需要消耗幾份內(nèi)存)羹应,無論是空間還是執(zhí)行效率揽碘,都是造成了浪費(fèi)。

如果有共享緩存庫(系統(tǒng)會(huì)提前將一些常用的庫加載到內(nèi)存中)存在的話园匹,那么我們手機(jī)上的每一個(gè)App雳刺,如果要用到系統(tǒng)動(dòng)態(tài)庫的話,只需要先去內(nèi)存中找偎肃,找到了就直接鏈接就行煞烫,沒有找到的花,加載一份到共享緩存的內(nèi)存空間中累颂,然后再鏈接這個(gè)庫滞详。節(jié)省了內(nèi)存,還提高了運(yùn)行速度紊馏。

前面說過料饥,iOS系統(tǒng)是強(qiáng)制使用共享緩存的,所以checkSharedRegionDisable里面什么都不會(huì)做朱监,然后將共享緩存映射到當(dāng)前進(jìn)程的內(nèi)存空間內(nèi)岸啡。

真正加載共享緩存的是這句代碼:mapSharedCache(); 里面調(diào)用了loadDyldCache(),從if else代碼可以看出赫编,共享緩存加載又分為三種情況:

僅加載到當(dāng)前進(jìn)程巡蘸,調(diào)用mapCachePrivate()。

共享緩存已加載擂送,不做任何處理悦荒。

當(dāng)前進(jìn)程首次加載共享緩存,調(diào)用mapCacheSystemWide()嘹吨。

mapCachePrivate()搬味、mapCacheSystemWide()里面就是具體的共享緩存解析邏輯,感興趣的同學(xué)可以自己深入詳細(xì)分析。

dyld3

查找啟動(dòng)閉包

如果沒有設(shè)置閉包模式碰纬,則檢查環(huán)境變量里的字段以及緩存類型來判斷設(shè)置哪種模式萍聊。

4種模式在前文有詳細(xì)介紹。


宏檢查是否是真機(jī)悦析,創(chuàng)建一些零時(shí)變量存儲(chǔ)啟動(dòng)閉包的信息寿桨,檢查共享緩存中是否有啟動(dòng)閉包,前文有提到她按,系統(tǒng)app會(huì)將啟動(dòng)閉包保存到共享緩存中牛隅,所以是有必要做這項(xiàng)檢查的。

從 findClosure() 實(shí)現(xiàn)里面也可以看出:

(anything in /System/ should have a closure)任何在/System/ 路徑下的可執(zhí)行文件都有一個(gè)啟動(dòng)閉包酌泰。

重新回到_main()函數(shù)

接下來是一系列重新構(gòu)建閉包的判斷媒佣。所有的if else 都是在為allowClosureRebuilds服務(wù)的。

需要特別注意的是6997行用紅色框框出來的closureValid()函數(shù)陵刹,這個(gè)函數(shù)里面做了大量的判斷默伍,來判斷緩存里是否存在閉包。注意衰琐,這里的緩存和上文的共享緩存是有區(qū)別的也糊!

下面是具體的實(shí)現(xiàn),由于實(shí)現(xiàn)代碼太長(zhǎng)羡宙,把很多分支都隱藏還是無法展示全部代碼狸剃,所以這里只截取了前半部分代碼,不過我會(huì)把有注釋的判斷都列出來:


如上圖狗热,在_main()函數(shù)里獲取的CDHash在這里發(fā)揮了作用钞馁,代碼能執(zhí)行到這說明是有緩存閉包的,后續(xù)的判斷都是針對(duì)這個(gè)存在的閉包匿刮,判斷是否有效僧凰。

下面列出剩余的注釋,一行注釋就是一個(gè)判斷:

//If we found cd hashes, but they were all invalid, then print them out - 如果我們發(fā)現(xiàn)了cdHash熟丸,但它們都是無效的训措,那么就把它們打印出來

// verify UUID of main executable is same as recorded in closure - 驗(yàn)證主可執(zhí)行文件的UUID是否與閉包中記錄的相同

// verify DYLD_* env vars are same as when closure was built -?驗(yàn)證DYLD_*env變量是否與構(gòu)建閉包時(shí)相同

// verify files that are supposed to be missing actually are missing -?驗(yàn)證應(yīng)該丟失的文件是否確實(shí)丟失

// verify files that are supposed to exist are there with the -?驗(yàn)證假定存在的文件是否與 (這里的注釋不全,根據(jù)打印log光羞,應(yīng)該是驗(yàn)證文件完整性的)

// verify closure did not require anything unavailable -?驗(yàn)證閉包是否有依賴任何不可用的內(nèi)容


分析完closureValid()绩鸣,我們回到_main()函數(shù)中,繼續(xù)閉包相關(guān)的源碼閱讀:

這里驗(yàn)證了上文提到的只有第三方app的啟動(dòng)閉包會(huì)保存在磁盤中纱兑。

主要邏輯是 獲取主程序啟動(dòng)閉包CDHash全闷,然后做如下判斷:

1、如果CDHash不為空萍启,則再判斷是否啟動(dòng)了dyld3,如果啟動(dòng)了則什么都不做,從打印我們知道勘纯,在啟動(dòng)dyld3的情況下內(nèi)部系統(tǒng)是允許磁盤上的啟動(dòng)閉包的局服。如果沒有啟動(dòng)dyld3,則設(shè)置canUseClosureFromDisk = false驳遵,也就是說不允許使用磁盤上的啟動(dòng)閉包淫奔。

2、如果CDHash為空堤结,則判斷主程序啟動(dòng)閉包是否為空唆迁,如果不為空則將mainClosure設(shè)置為空指針。結(jié)合注釋我們可以了解到cdHash和啟動(dòng)閉包是必須同時(shí)存在的竞穷,如果CDHash為空唐责,則找到的緩存閉包也不能使用。

構(gòu)建啟動(dòng)閉包

rdar的bug先不管(其實(shí)是筆者水平不夠沒弄懂)瘾带。

然后如果發(fā)現(xiàn)沒有找到一個(gè)有效的啟動(dòng)閉包鼠哥,則嘗試自己構(gòu)建一個(gè)。

buildLaunchClosure()這個(gè)函數(shù)就是構(gòu)建閉包的函數(shù)看政,內(nèi)部實(shí)現(xiàn)比較長(zhǎng)比較復(fù)雜朴恳,在本文不做詳細(xì)的分析。后續(xù)有空的話會(huì)新開一篇專門分析啟動(dòng)閉包構(gòu)建和加載允蚣。

接下來判斷sJustBuildClosure是否為yes于颖,如果為yes意思就是只構(gòu)建閉包,不做加載動(dòng)作嚷兔,上文提到APP在下載完成以及升級(jí)完成后會(huì)重新構(gòu)建啟動(dòng)閉包森渐,在這種情況下dyld構(gòu)建完閉包后就直接退出了。

加載閉包

這里的邏輯比較簡(jiǎn)單谴垫,launchWithClosure()加載啟動(dòng)閉包章母,判斷是否成功,如果失敗翩剪,判斷是否啟動(dòng)閉包是否過期乳怎,過期了重新構(gòu)建一個(gè),構(gòu)建完成后再重新加載閉包前弯。

launchWithClosure()函數(shù)內(nèi)部實(shí)現(xiàn)很長(zhǎng)蚪缀,粗略的看了下具體的實(shí)現(xiàn),簡(jiǎn)單說一下:

拿到所有的image數(shù)組(最多三個(gè):緩存動(dòng)態(tài)庫恕出、其他操作系統(tǒng)動(dòng)態(tài)庫和主程序)询枚,

然后調(diào)用了dyld3 的一個(gè)Load的loader()方法,然后遞歸的加載依賴庫浙巫,并且將加載的庫添加到allImages數(shù)組中保存金蜀。

找到dyld的入口刷后,把所有鏡像的信息傳遞給dyld,然后run?initializers渊抄。

加載完閉包后返回一個(gè)result尝胆,這是一個(gè)假main()函數(shù)的入口,內(nèi)部實(shí)現(xiàn)就是return 0护桦;

到此含衔,dyld3的啟動(dòng)過程就完成了。

但是_main()函數(shù)還未結(jié)束二庵,后續(xù)的代碼是使用dyld2的方式加載流程贪染。模擬器以及iOS13以下是沒有dyld3的,所以是走的dyld2啟動(dòng)流程催享。

dyld2

后面我們繼續(xù)分析dyld2的流程:

實(shí)例化主程序

// add dyld itself to UUID list

addDyldImageToUUIDList();

這里會(huì)添加?dyld?的鏡像文件到?UUID?列表中杭隙,主要的目的是啟用堆棧的符號(hào)化。

這里有個(gè)SUPPORT_ACCELERATE_TABLES宏睡陪,判斷是否支持加速器表寺渗。從注釋我們可以得出arm64架構(gòu)的情況下是不使用加速器表的。(后面還會(huì)有個(gè)有意思的判斷)

然后接著就是reloadAllImages:的一個(gè)標(biāo)簽兰迫⌒攀猓可以使用goto語句直接跳轉(zhuǎn)到這個(gè)標(biāo)簽,然后從這開始執(zhí)行代碼汁果。

這里插一個(gè)知識(shí)點(diǎn):ImageLoader

ImageLoader 是一個(gè)用于加載可執(zhí)行文件的基類涡拘,它負(fù)責(zé)鏈接鏡像,但不關(guān)心具體文件格式据德,因?yàn)檫@些都交給子類去實(shí)現(xiàn)鳄乏。每個(gè)可執(zhí)行文件都會(huì)對(duì)應(yīng)一個(gè) ImageLoader實(shí)例。ImageLoaderMachO 是用于加載 Mach-O 格式文件的 ImageLoader 子類棘利,而 ImageLoaderMachOClassic 和 ImageLoaderMachOCompressed 都繼承于 ImageLoaderMachO橱野,分別用于加載那些 __LINKEDIT 段為傳統(tǒng)格式和壓縮格式的 Mach-O 文件。


這一步將主程序的Mach-O加載進(jìn)內(nèi)存善玫,并實(shí)例化一個(gè)ImageLoader水援。instantiateFromLoadedImage()首先調(diào)用isCompatibleMachO()檢測(cè)Mach-O頭部的magic、cputype茅郎、cpusubtype等相關(guān)屬性蜗元,判斷Mach-O文件的兼容性,如果兼容性滿足系冗,則調(diào)用ImageLoaderMachO::instantiateMainExecutable()實(shí)例化主程序的ImageLoader奕扣。

ImageLoaderMachO::instantiateMainExecutable()函數(shù)里面首先會(huì)調(diào)用sniffLoadCommands()函數(shù)來獲取一些數(shù)據(jù),包括:

compressed:若Mach-O存在LC_DYLD_INFO和LC_DYLD_INFO_ONLY加載命令掌敬,則說明是壓縮類型的Mach-O

segCount:根據(jù) LC_SEGMENT_COMMAND 加載命令來統(tǒng)計(jì)段數(shù)量惯豆,這里拋出的錯(cuò)誤日志也說明了段的數(shù)量是不能超過255個(gè)

libCount:根據(jù) LC_LOAD_DYLIB池磁、LC_LOAD_WEAK_DYLIB、LC_REEXPORT_DYLIB循帐、LC_LOAD_UPWARD_DYLIB 這幾個(gè)加載命令來統(tǒng)計(jì)庫的數(shù)量娇跟,庫的數(shù)量不能超過4095個(gè)

當(dāng)sniffLoadCommands()解析完以后仲墨,根據(jù)compressed的值來決定調(diào)用哪個(gè)子類進(jìn)行實(shí)例化基括。

這里總結(jié)為4步:

ImageLoaderMachOCompressed::instantiateStart()創(chuàng)建ImageLoaderMachOCompressed對(duì)象簿姨。

image->disableCoverageCheck()禁用段覆蓋檢測(cè)沈撞。

image->instantiateFinish()首先調(diào)用parseLoadCmds()解析加載命令值骇,然后調(diào)用this->setDyldInfo()設(shè)置動(dòng)態(tài)庫鏈接信息萨赁,最后調(diào)用this->setSymbolTableInfo() 設(shè)置符號(hào)表相關(guān)信息透绩,代碼片段如下:

image->setMapped()函數(shù)注冊(cè)通知回調(diào)寻馏、計(jì)算執(zhí)行時(shí)間等等棋弥。

在調(diào)用完ImageLoaderMachO::instantiateMainExecutable()后繼續(xù)調(diào)用addImage(),將image加入到sAllImages全局鏡像列表诚欠,并將image映射到申請(qǐng)的內(nèi)存中顽染。

加載插入的動(dòng)態(tài)庫

這一步是加載環(huán)境變量DYLD_INSERT_LIBRARIES中配置的動(dòng)態(tài)庫,先判斷環(huán)境變量DYLD_INSERT_LIBRARIES中是否存在要加載的動(dòng)態(tài)庫轰绵,如果存在則調(diào)用loadInsertedDylib()依次加載粉寞。

loadInsertedDylib()內(nèi)部設(shè)置了一個(gè)LoadContext參數(shù)后,調(diào)用了load()函數(shù)左腔,

load()函數(shù)的實(shí)現(xiàn)為一系列的loadPhase*()函數(shù)唧垦,loadPhase0()~loadPhase1()函數(shù)會(huì)按照下面所示順序搜索動(dòng)態(tài)庫,并調(diào)用不同的函數(shù)來繼續(xù)處理液样。

DYLD_ROOT_PATH ? --> ?LD_LIBRARY_PATH ?--> ?DYLD_FRAMEWORK_PATH ||?DYLD_LIBRARY_PATH ?--> ?raw path ?--> ?DYLD_FALLBACK_LIBRARY_PATH

當(dāng)內(nèi)部調(diào)用到loadPhase5load()函數(shù)的時(shí)候振亮,會(huì)先在共享緩存中搜尋,如果存在則使用ImageLoaderMachO::instantiateFromCache()來實(shí)例化ImageLoader鞭莽,否則通過loadPhase5open()打開文件并讀取數(shù)據(jù)到內(nèi)存后坊秸,再調(diào)用loadPhase6(),通過ImageLoaderMachO::instantiateFromFile()實(shí)例化ImageLoader澎怒,最后調(diào)用checkandAddImage()驗(yàn)證鏡像并將其加入到全局鏡像列表中褒搔。


鏈接主程序

在調(diào)用link()進(jìn)行鏈接主程序之前,會(huì)有一個(gè)宏判斷丹拯,如果支持加速器表并且主程序已經(jīng)rebase了站超,則重新rebase一次,用于ASLR的工作乖酬。

這一步調(diào)用link()函數(shù)先將傳入的image添加進(jìn)sAllImages全局靜態(tài)數(shù)組死相,接著將image添加進(jìn)sImageRoots(后續(xù)遞歸初始化所有image的時(shí)候有用到)數(shù)組,然后將實(shí)例化后的主程序進(jìn)行動(dòng)態(tài)修正咬像,讓二進(jìn)制變?yōu)榭烧?zhí)行的狀態(tài)算撮。link()函數(shù)內(nèi)部調(diào)用了ImageLoader::link()函數(shù)

從源代碼可以看到生宛,這一步主要做了以下幾個(gè)事情:

recursiveLoadLibraries() 根據(jù)LC_LOAD_DYLIB加載命令把所有依賴庫加載進(jìn)內(nèi)存。

recursiveUpdateDepth() 遞歸刷新依賴庫的層級(jí)肮柜。

recursiveRebase() 由于ASLR的存在陷舅,必須遞歸對(duì)主程序以及依賴庫進(jìn)行重定位操作。

recursiveBind() 把主程序二進(jìn)制和依賴進(jìn)來的動(dòng)態(tài)庫全部執(zhí)行符號(hào)表綁定审洞。

weakBind() 如果鏈接的不是主程序二進(jìn)制的話莱睁,會(huì)在此時(shí)執(zhí)行弱符號(hào)綁定,主程序二進(jìn)制則在link()完后再執(zhí)行弱符號(hào)綁定芒澜,后面會(huì)進(jìn)行分析仰剿。

recursiveGetDOFSections()、context.registerDOFs() 注冊(cè)DOF(DTrace Object Format)節(jié)痴晦。

鏈接插入的動(dòng)態(tài)庫

這一步與鏈接主程序一樣南吮,將前面調(diào)用addImage()函數(shù)保存在sAllImages中的動(dòng)態(tài)庫列表循環(huán)取出并調(diào)用link()進(jìn)行鏈接,需要注意的是誊酌,sAllImages中保存的第一項(xiàng)是主程序的鏡像部凑,所以要從i+1的位置開始,取到的才是動(dòng)態(tài)庫的ImageLoader:

ImageLoader* image = sAllImages[i+1];

接下來循環(huán)調(diào)用每個(gè)鏡像的registerInterposing()函數(shù)碧浊,該函數(shù)會(huì)遍歷Mach-O的LC_SEGMENT_COMMAND加載命令涂邀,讀取__DATA,__interpose,并將讀取到的信息保存到fgInterposingTuples中辉词,為后續(xù)的bind做準(zhǔn)備工作必孤。


這里宏判斷加速器表,然后如果加速器表和隱試插入庫同時(shí)存在瑞躺,則禁用加速器表敷搪,并且使用goto 語句跳轉(zhuǎn)到reloadAllImages重新加載。

遞歸Bind

先調(diào)用applyInterposingToDyldCache()申請(qǐng)將所有鏈接的庫插入共享緩存幢哨,內(nèi)部將會(huì)找到鏈接庫的symbol符號(hào)表赡勘,為后面的Rebase做準(zhǔn)備。

接著執(zhí)行主程序的recursiveBindWithAccounting()函數(shù)捞镰,該函數(shù)內(nèi)部其實(shí)就是調(diào)用了recursiveBind()闸与,然后for循環(huán)對(duì)插入的庫執(zhí)行recursiveBind(),recursiveBind內(nèi)部先進(jìn)行遞歸所有依賴庫調(diào)用recursiveBind岸售,然后調(diào)用doBind()函數(shù)践樱,該函數(shù)有2個(gè)不一樣的實(shí)現(xiàn),分別在ImageLoaderMachOClassic.cpp 和?ImageLoaderMachOCompressed.cpp文件中凸丸。

ImageLoaderMachOClassic.cpp文件的實(shí)現(xiàn):調(diào)用doBindExternalRelocations 以及?bindIndirectSymbolPointers執(zhí)行真正的符號(hào)綁定拷邢。

ImageLoaderMachOCompressed.cpp文件的實(shí)現(xiàn):調(diào)用eachBind() 和eachLazyBind(),具體處理函數(shù)是bindAt()屎慢。

執(zhí)行弱符號(hào)綁定

weakBind()首先通過getCoalescedImages()合并所有動(dòng)態(tài)庫的弱符號(hào)到一個(gè)列表里瞭稼,然后調(diào)用initializeCoalIterator()對(duì)需要綁定的弱符號(hào)進(jìn)行排序忽洛,接著調(diào)用incrementCoalIterator()讀取dyld_info_command結(jié)構(gòu)的weak_bind_off和weak_bind_size字段,確定弱符號(hào)的數(shù)據(jù)偏移與大小环肘,最終進(jìn)行弱符號(hào)綁定欲虚。


執(zhí)行初始化方法(重點(diǎn))

這一步由initializeMainExecutable()完成。我們看看內(nèi)部的具體實(shí)現(xiàn):

先看注釋?run initialzers for any inserted dylibs 將已經(jīng)插入的動(dòng)態(tài)庫執(zhí)行初始化操作悔雹。

然后用一個(gè)for循環(huán)拿到每一個(gè)動(dòng)態(tài)庫的指針調(diào)用runInitializers()函數(shù)复哆。

繼續(xù)看注釋?run initializers for main executable and everything it brings up ,執(zhí)行主程序的初始化荠商。

所以dyld會(huì)優(yōu)先初始化鏈接的動(dòng)態(tài)庫寂恬,然后再初始化主程序。

這點(diǎn)很重要莱没,后面我會(huì)從源碼分析為什么dyld要這樣做!


runInitializers()內(nèi)部調(diào)用了processInitializers()

函數(shù)的注釋解釋:向上動(dòng)態(tài)庫初始化的執(zhí)行為時(shí)過早酷鸦。為了處理向上鏈接而不是向下鏈接的懸空動(dòng)態(tài)庫饰躲,所有向上鏈接的動(dòng)態(tài)庫都將其初始化延遲到通過向下動(dòng)態(tài)庫的遞歸完成之后。

什么意思呢臼隔,動(dòng)態(tài)庫直接也是有依賴關(guān)系的嘹裂,舉個(gè)例子:現(xiàn)在要初始化動(dòng)態(tài)庫A,但是動(dòng)態(tài)庫A依賴了動(dòng)態(tài)庫B摔握,動(dòng)態(tài)庫B又依賴了動(dòng)態(tài)庫C,D等寄狼,那么這里就會(huì)遞歸的先初始化C、D氨淌,然后再初始化B泊愧,最后才初始化A。

processInitializers內(nèi)部調(diào)用了recursiveInitialization()來進(jìn)行遞歸初始化盛正,我們?cè)賮砜纯磖ecursiveInitialization的實(shí)現(xiàn)

為了方便截圖删咱,筆著把for循環(huán)遞歸調(diào)用邏輯給折疊了,從注釋也可以知道低級(jí)的庫優(yōu)先初始化豪筝。這跟我們上面的分析是一樣的痰滋。然后我們重點(diǎn)看看紅框里的代碼,context上下文我們之前分析過了续崖,里面保存了很多函數(shù)的地址敲街,參數(shù)等。

紅框內(nèi)先調(diào)用了notifySingle严望,注意第一個(gè)參數(shù)dyld_image_state_dependents_initialized多艇,說明這次是調(diào)用的依賴庫的初始化,然后接著調(diào)用了doInitialization()著蟹,從函數(shù)名稱完全可以猜到這個(gè)函數(shù)才是真正去執(zhí)行初始化操作的墩蔓。接著再次調(diào)用notifySingle梢莽,看第一個(gè)參數(shù)dyld_image_state_initialized,這次是當(dāng)前庫的初始化奸披。我們先分析notifySingle的內(nèi)部實(shí)現(xiàn)(非常重要;杳!U竺妗)轻局,后面再分析doInitialization。

notifySingle函數(shù)里面唯一跟init有關(guān)的能調(diào)用的函數(shù)就是上圖紅色框里的sNotifyObjCInit样刷。全局搜索sNotifyObjCInit仑扑。只發(fā)現(xiàn)registerObjCNotifiers函數(shù)內(nèi)部的賦值操作。

// record functions to call

sNotifyObjCMapped = mapped;

sNotifyObjCInit = init;

sNotifyObjCUnmapped = unmapped;

看注釋:記錄函數(shù)用于調(diào)用置鼻。這里賦值了3個(gè)函數(shù)镇饮,一個(gè)mapped,一個(gè)init箕母,一個(gè)unmapped储藐。

接著全局搜索registerObjCNotifiers,發(fā)現(xiàn)是_dyld_objc_notify_register函數(shù)調(diào)用的嘶是。

全局搜索一下钙勃,發(fā)現(xiàn)在dyldAPIsInLibSystem.cpp文件中還有一個(gè)具體的實(shí)現(xiàn)。但是沒有發(fā)現(xiàn)有任何函數(shù)調(diào)用過_dyld_objc_notify_register聂喇。

上圖說明這個(gè)函數(shù)可能跟LibSystem.dylib有關(guān)辖源。

那么到底誰調(diào)用了_dyld_objc_notify_register()呢?靜態(tài)分析已經(jīng)無法得知希太,只能對(duì)_dyld_objc_notify_register()下個(gè)符號(hào)斷點(diǎn)觀察一下了克饶,

點(diǎn)擊Xcode的“Debug”菜單,然后點(diǎn)擊“Breakpoints”跛十,接著選擇“Create Symbolic Breakpoint...”彤路。然后添加_dyld_objc_notify_register,運(yùn)行工程芥映,如下圖所示

請(qǐng)看打印信息洲尊,調(diào)用順序如下:


最后發(fā)現(xiàn)是_objc_init()方法調(diào)用的,而且也驗(yàn)證了上面真正做初始化的函數(shù)是doInitialization()

我們把上面的幾個(gè)庫(libSystem奈偏、libdispatch坞嘀、libobjc)的源碼都下載下來驗(yàn)證一下!

從libSystem_init()到_objc_init()惊来,內(nèi)部調(diào)用順序和我們打印的確實(shí)一樣丽涩,最終調(diào)用了_dyld_objc_notify_register(),然后_objc_init函數(shù)就結(jié)束了。

那么問題來了矢渊,libSystem_initializer()什么時(shí)候調(diào)用的呢继准?

其實(shí)這個(gè)答案在前文分析initializeMainExecutable()源碼的時(shí)候就已經(jīng)講過了。dyld會(huì)優(yōu)先初始化動(dòng)態(tài)庫矮男,然后初始化主程序移必。libSystem_initializer()就是在這個(gè)時(shí)候調(diào)用的。

我們回到_dyld_objc_notify_register函數(shù)來毡鉴,看看這3個(gè)參數(shù)崔泵,都是函數(shù)指針

_dyld_objc_notify_register(&map_images, load_images, unmap_image);

最后得到

sNotifyObjCMapped = map_images;

sNotifyObjCInit = load_images;

sNotifyObjCUnmapped = unmap_image;

也就是說sNotifyObjCInit的調(diào)用其實(shí)就是調(diào)用load_images();

由于篇幅的原因,load_images的分析我們放到下一節(jié)猪瞬。

前文提到doInitialization()才是真正執(zhí)行初始化的函數(shù)憎瘸,我們回過頭來繼續(xù)分析doInitialization()的具體實(shí)現(xiàn):

內(nèi)部調(diào)用doImageInit() 和?doModInitFunctions()

這2個(gè)函數(shù)內(nèi)部都有l(wèi)ibSystemInitialized的判斷,要求libSystem這個(gè)動(dòng)態(tài)庫必須先初始化陈瘦,這也驗(yàn)證了我們前面的結(jié)論是對(duì)的幌甘。

然后調(diào)用func執(zhí)行初始化方法。假設(shè)當(dāng)前是libSystem庫痊项,那么這個(gè)func就是libSystem_initializer()

到此含潘,所有的初始化方法調(diào)用已經(jīng)閉環(huán)。


查找入口點(diǎn)并返回

這一步調(diào)用主程序鏡像的getEntryFromLC_MAIN()线婚,從加載命令讀取LC_MAIN入口,如果沒有LC_MAIN就調(diào)用getEntryFromLC_UNIXTHREAD()讀取LC_UNIXTHREAD盆均,找到后就跳到入口點(diǎn)指定的地址并返回塞弊。

至此,整個(gè)dyld的加載過程就分析完成了泪姨。

總結(jié)下:

dyld3的加載流程如下:

第一步:設(shè)置運(yùn)行環(huán)境游沿。

第二步:加載共享緩存。

第三步:檢查啟動(dòng)閉包肮砾。

第四步:構(gòu)建啟動(dòng)閉包诀黍。

第五步:加載啟動(dòng)閉包。(這一步包括dyld2中的3仗处、4眯勾、5、6婆誓、7吃环、8)

第六步:找到入口點(diǎn)并返回。

dyld2的加載流程如下:

第一步:設(shè)置運(yùn)行環(huán)境洋幻。

第二步:加載共享緩存郁轻。

第三步:實(shí)例化主程序。

第四步:加載插入的動(dòng)態(tài)庫。

第五步:鏈接主程序好唯。

第六步:鏈接插入的動(dòng)態(tài)庫竭沫。

第七步:執(zhí)行弱符號(hào)綁定

第八步:執(zhí)行初始化方法。

第九步:查找入口點(diǎn)并返回骑篙。

那么我們現(xiàn)在既然了解了dyld的加載流程蜕提,那么在這個(gè)階段我們能做什么具體的優(yōu)化呢?

1替蛉、避免鏈接無用的 frameworks贯溅,在 Xcode 中檢查一下項(xiàng)目中的「Linked Frameworks and Librares」部分是否有無用的鏈接

2、避免在啟動(dòng)時(shí)加載動(dòng)態(tài)庫躲查,將項(xiàng)目的 Pods 以靜態(tài)編譯的方式打包它浅,尤其是 Swift 項(xiàng)目,這地方的時(shí)間損耗是很大的

3镣煮、硬鏈接你的依賴項(xiàng)姐霍,這里做了緩存優(yōu)化

也許有人會(huì)有疑惑,現(xiàn)在都使用了 dyld3 了典唇,我們就不需要做 Static Link 了镊折,其實(shí)還是需要的,這里放2張對(duì)比圖給大家看看介衔。

Dyld 2 和 Dyld 3啟動(dòng)時(shí)間的對(duì)比:

Static linking 和 Dyld 3啟動(dòng)時(shí)間對(duì)比:

結(jié)果很明顯恨胚,可以看出,在冷啟動(dòng)時(shí)Dyld3 比 Dyld2快20%炎咖,靜態(tài)鏈接依舊是比dyld3要快赃泡,提升了23%左右。

dyld2加載流程圖如下:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末乘盼,一起剝皮案震驚了整個(gè)濱河市升熊,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌绸栅,老刑警劉巖级野,帶你破解...
    沈念sama閱讀 216,651評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異粹胯,居然都是意外死亡蓖柔,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門矛双,熙熙樓的掌柜王于貴愁眉苦臉地迎上來渊抽,“玉大人,你說我怎么就攤上這事议忽±撩疲” “怎么了?”我有些...
    開封第一講書人閱讀 162,931評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)愤估。 經(jīng)常有香客問我帮辟,道長(zhǎng),這世上最難降的妖魔是什么玩焰? 我笑而不...
    開封第一講書人閱讀 58,218評(píng)論 1 292
  • 正文 為了忘掉前任由驹,我火速辦了婚禮,結(jié)果婚禮上昔园,老公的妹妹穿的比我還像新娘蔓榄。我一直安慰自己,他們只是感情好默刚,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,234評(píng)論 6 388
  • 文/花漫 我一把揭開白布甥郑。 她就那樣靜靜地躺著,像睡著了一般荤西。 火紅的嫁衣襯著肌膚如雪澜搅。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,198評(píng)論 1 299
  • 那天邪锌,我揣著相機(jī)與錄音勉躺,去河邊找鬼。 笑死觅丰,一個(gè)胖子當(dāng)著我的面吹牛饵溅,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播妇萄,決...
    沈念sama閱讀 40,084評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼概说,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了嚣伐?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,926評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤萍丐,失蹤者是張志新(化名)和其女友劉穎轩端,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體逝变,經(jīng)...
    沈念sama閱讀 45,341評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡基茵,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,563評(píng)論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了壳影。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拱层。...
    茶點(diǎn)故事閱讀 39,731評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖宴咧,靈堂內(nèi)的尸體忽然破棺而出根灯,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 35,430評(píng)論 5 343
  • 正文 年R本政府宣布烙肺,位于F島的核電站纳猪,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏桃笙。R本人自食惡果不足惜氏堤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,036評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望搏明。 院中可真熱鬧鼠锈,春花似錦、人聲如沸星著。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽强饮。三九已至由桌,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間邮丰,已是汗流浹背行您。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留剪廉,地道東北人娃循。 一個(gè)月前我還...
    沈念sama閱讀 47,743評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像斗蒋,于是被迫代替她去往敵國和親捌斧。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,629評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容