帶你走入蘋果的世界

帶你走入蘋果的世界

  • 1.從iOS-Beta說起
  • 2.背后隱藏的問題
    2.1 是否所有手機都能夠升級到最新的iOS系統(tǒng)
    2.2為什么我們的beta系統(tǒng)升級完成之后,手機里大部分軟件還是能正常運行?
    2.3 什么原因?qū)е履承┸浖荒苷_\行
  • 3.更深層次的問題-軟件的運行的本質(zhì)
    3.1 軟件運行的目的
    3.2 可執(zhí)行程序是(數(shù)據(jù)+CPU指令)
    3.3 操作系統(tǒng)讀懂可執(zhí)行程序
    3.4 文件格式到底長成什么樣?
    3.5 細(xì)看segment

#######1.從iOS-Beta說起
iOS-Beta版本一發(fā)布,開發(fā)者一般都會搶先下載體驗,這有三個主要目的:
1.看看自家已上架的App在新系統(tǒng)里會不會出現(xiàn)crash,好盡早修復(fù)問題.免得蘋果iOS版本發(fā)布后,用戶遭殃.
2.App工程和新iOS-SDK能否build Success.充分把握這個buffer時間,適配新的編譯環(huán)境.
3.基于新iOS-SDK開發(fā)新功能.


image.png

為了達(dá)到目的,我們首先需要把手機更新到beta版本,同時升級改造升級我們開發(fā)人員的IDE工具,以支持開發(fā).

image.png

好像,講到這里,我們要說的事情就結(jié)束了.... 這只是一次相對并不復(fù)雜的配置改動.
然鵝,聰明的你,肯定知道這才是剛剛開始.
#######2.背后隱藏的問題
你是否想過,是否所有手機都能夠升級到最新的iOS系統(tǒng).
為什么我們的beta系統(tǒng)升級完成之后,手機里大部分軟件還是能正常運行?
是什么原因?qū)е履承┸浖荒苷_\行?

今天我們將一步步剖析,圍繞beta版本的體驗問題,回顧歷史,不斷深入,讓你了解到軟件運行的本質(zhì).


image.png

########2.1 是否所有手機都能夠升級到最新的iOS系統(tǒng)
有時候蘋果推出新的iOS新系統(tǒng),有些老舊iOS設(shè)備出現(xiàn)掉隊,不支持運行新系統(tǒng).iOS11更是一個分水嶺,徹底和硬件層面是32位架構(gòu)的"SoC"說再見.(System on a Chip系統(tǒng)級芯片是CPU、GPU、音頻芯片、無線芯片、電池管理等等的集合體,CPU,GPU是它的重要組成部分)
具體可以參考 https://en.wikipedia.org/wiki/System_on_a_chip
下圖應(yīng)該是比較形象地展示了SoC,注意中間的 ARM Cortex M3, 這是Micro Computer Unit, 屬于一種微處理器類型.

image.png

32-bit架構(gòu)的"系統(tǒng)級芯片"如果想運行64-bit的操作系統(tǒng),這是不可能的.恩,你應(yīng)該會問為什么?這個問題如果要深究展開技術(shù)細(xì)節(jié),估計要展開好久.不過我覺得一句話可以比較形象地解釋:

應(yīng)用運行于系統(tǒng)之上,必然受到OS系統(tǒng)的限制,
而OS系統(tǒng)是對硬件資源的操作管理,那它暴露出去的能力也需要建立在硬件能力之上.

用一張圖來解釋,基于別人的能力,那你總不能比別人牛吧,硬件說我CPU和內(nèi)存之間只有32根線,你尋址只能32-bit,結(jié)果管理硬件資源的OS說,老哥我OS的尋址能力是64-bit,這就有點扯了......

image.png

但是如果硬件層面是64-bit,系統(tǒng)層面和軟件層面是32-bit倒是有可能.不過就有點浪費了自己的天賦了.
看起來能否影響到手機是否可以升級主要有倆個基礎(chǔ)概念:

  • 系統(tǒng)級芯片SoC的指令架構(gòu)是"32-bit"還是"64-bit"
  • iOS系統(tǒng)版本是"32-bit"還是"64-bit"

大邏輯就是:系統(tǒng)級芯片SoC架構(gòu)的指令集(32-bit or 64-bit) >= iOS系統(tǒng)版本(32-bit or 64-bit)

#########2.1.1 iPhone手機的SoC & iOS系統(tǒng)歷史
從上面我們可以看到,SoC和OS系統(tǒng)是緊密關(guān)聯(lián)的,我們下面以蘋果為案例,來看看他是如何處理倆者之間的關(guān)系.
##########2.1.1.1 蘋果的SoC歷史
時間回溯到2005年,當(dāng)時的Intel CEO 保羅·歐德寧把Intel帶向了輝煌.Mac也從PowerPC轉(zhuǎn)移到Intel x86,喬布斯干脆咔嚓掉早年給Mac搭建的芯片設(shè)計團隊,寄希望于Intel的Atom處理器.然而當(dāng)時因為iPod而成為蘋果二號人物的Tony Fadell卻大力反對,支持更簡單、更省電的ARM架構(gòu).蘋果說SoC不僅僅是一個處理器的事情,還要其他類型芯片來配合,Intel只擅長處理器,繪圖芯片做得爛,改進動作太慢;Intel則說錢沒給夠.總之這倆最后在移動端就這樣分手了.于是蘋果就轉(zhuǎn)頭三星設(shè)計的ARM架構(gòu)應(yīng)用處理器.


image.png

2007年,初代iPhone發(fā)布之后,喬布斯發(fā)現(xiàn)芯片是個基礎(chǔ)中的基礎(chǔ),需要完全自控.但是倆年前自個干掉了芯片團隊,這個坑,只能自己填上.一方面和三星簽訂SoC開發(fā)協(xié)議.另一方面招兵買馬,收購芯片公司.于是乎,2010年,誕生了自行設(shè)計的第一款SoC,Apple A4芯片.這個芯片后來被用在iPhone4上.蘋果在芯片之路越走越遠(yuǎn),從A4,A5一直到2017年發(fā)布的A11.其中一個重要的分水A7芯片,這個芯片宣布了64-bit SoC 時代的到來.這款產(chǎn)品被用在iPhone5s上.
當(dāng)然后來有一天,你會發(fā)現(xiàn)....

image.png

臺積電的故事也是相當(dāng)復(fù)雜,劇情跌宕起伏,總之感興趣的自己可以網(wǎng)上去看看,這里只提供關(guān)鍵詞:

臺積電  叛徒 三星 中芯國際 大陸  張忠謀

##########2.1.1.2 SoC的重要特征-指令集支持
A4,A5,A6芯片都是32-bit的指令架構(gòu)(Instruction Set Arch)
A7,A8,A9,A10是 32-bit/64-bit 都支持的指令架構(gòu)
到了A11 只支持64-bit的指令架構(gòu)(這里注意了, A11是64-bit only)

image.png

A7芯片讓蘋果邁入了一個新的時代,繼續(xù)領(lǐng)跑.在股價體現(xiàn)上,少不了A7的功勞.

image.png

#########2.1.2 iOS版本歷史
運行于硬件上的就是操作系統(tǒng).由于A7以前是只支持32-bit指令架構(gòu),運行在這些機器上對應(yīng)的系統(tǒng)要與之匹配.所以在iOS7的時候,是分倆個大版本,一個是32-bit,一個是64-bit.
當(dāng)時的iPhone5s采用64-bit的A7芯片,所以對應(yīng)的iOS7就是64-bit的系統(tǒng),但是iPhone5由于是32-bit的A6芯片,所以它運行的是32-bit的iOS7系統(tǒng).

image.png

從iOS7到iOS10,每一個系列,都有32-bit,也有64-bit對應(yīng)的系統(tǒng).
而到了iOS11的時候,蘋果一刀切,系統(tǒng)不再出32-bit的系統(tǒng)了.真正和那些只支持32-bit指令架構(gòu)的硬件Say GoodBye.
########2.2 為什么我們的beta系統(tǒng)升級完成之后,手機里大部分軟件還是能正常運行?
這個問題主要涉及到程序是如何在系統(tǒng)里運行的.不知道此刻,你還能不能想起來課本說的大概的原理.我們這里先講倆個關(guān)鍵點:動態(tài)庫鏈接和指令架構(gòu)兼容
#########2.2.1 動態(tài)庫鏈接
我們前面提到,從iOS7到iOS10,每一個系列,都有32-bit,也有64-bit對應(yīng)的系統(tǒng).比如說iPhone5由于他是32-bit的A6芯片,那運行的就是32-bit的iOS,iPhone5s是64-bit的A7芯片,那么就是64-bit的iOS系統(tǒng).

  • 第一種情況, 如果我們升級了iPhone5的操作系統(tǒng),手機上的軟件還是可以運行的,雖然內(nèi)置在系統(tǒng)中的動態(tài)庫發(fā)生了升級,可能增加了新的功能,但是由于動態(tài)鏈接技術(shù),所以實現(xiàn)了老APP在升級后的系統(tǒng)中,run起來之后,還是能夠鏈接到他需要的內(nèi)容.動態(tài)鏈接是怎么做到的呢.我先看看這篇文章會不會太長,再決定要不要在后面加入對應(yīng)的內(nèi)容.
    后面我們會講一下到底動態(tài)鏈接是怎么樣做到的.

  • 第二種情況,由于iPhone5s剛剛發(fā)布的時候,開發(fā)者都還沒做好適配工作,總不能讓用戶沒有APP可以下載吧.于是,蘋果在64-bit的操作系統(tǒng)中塞入了32-bit的動態(tài)鏈接庫,沒錯,這意味同一個動態(tài)鏈接庫,其實在64-bit的系統(tǒng)中是存在倆份的,一份是32-bit的,一份是64-bit,如果你的程序是32-bit的APP,那么OK,系統(tǒng)就加載對應(yīng)32-bit的庫,如果是64-bit的app就加載64-bit的動態(tài)鏈接庫.這里邊有個問題,不知道你發(fā)現(xiàn)了沒? 就是系統(tǒng)在內(nèi)存里加載了倆份動態(tài)庫,打個比方32-bit的APP-A加載了一份用于GPS定位的動態(tài)庫,64-bit的App-B則加載了另一份.這會拖慢系統(tǒng)運行.


    image.png

#########2.2.2 指令架構(gòu)兼容
假如說僅僅iOS系統(tǒng)中多了一份32-bit的動態(tài)庫,是不是真的就能夠運行,并不是這樣.如果指令集架構(gòu)不支持32-bit,那也是白折騰啊.我們的程序最后是機器碼的執(zhí)行.
所以前面 A7,A8,A9,A10是 32-bit/64-bit 都支持的指令架構(gòu),就是為了兼容老的32-bit軟件.讓他們能夠跑起來.


image.png

當(dāng)然這是一把雙刃劍,為了兼容,讓不同指令架構(gòu)的軟件能夠運行,我們丟失了設(shè)計上的優(yōu)雅和更高的性能.在那個時間點,蘋果選擇了這么一個平衡點,用三四年的時間,讓開發(fā)者過渡到64-bit軟件上來.從A6-A10的指令集都是支持32-bit和64-bit,向下兼容老的軟件.

image.png

到了A11,蘋果徹底甩掉歷史包袱,這個芯片只支持64-bit指令架構(gòu),除了本身自己硬件上的常規(guī)升級,單獨一款指令支持,不用考慮兼容,跑起來自然嗖嗖快.


image.png

我們平時在評價一個芯片的時候,總是看相關(guān)的硬件指標(biāo),特別的是頻率.但是其實一個簡潔的設(shè)計,沒有歷史包袱的芯片,在同樣頻率指標(biāo)下,其實是更加優(yōu)秀的.我們就拿喜歡曬參數(shù)的小米手機來看,單獨說一下snapdragon 845, 小米8采用的這款芯片.


image.png

當(dāng)你去官網(wǎng)找到他的spec之后,你會發(fā)現(xiàn)這個屬于 ARM Cortex-A75 微架構(gòu),結(jié)合最新發(fā)布的ARM Cortex-A76(這家伙比米8的 ARM Cortex-A75 更強)


image.png

即使是 ARM Cortex-A76 依然支持 32-bit的 A32&T32指令架構(gòu)(為了讓老舊的32-bit軟件運行),在內(nèi)核代碼則只支持64-bit, 那ARM Cortex-A75你可想而知.
總之,即使 ARM Cortex-A76 也是背負(fù)著一定的歷史債務(wù).這也和Android生態(tài)的自由生長有很大關(guān)系.
蘋果為了讓開發(fā)者支持64-bit指令架構(gòu),主要從三個方面下手

  • 不斷升級蘋果自家IDE Xcode,讓開發(fā)者很容易就升級支持64-bit指令架構(gòu).
  • 從提交上給了一個deadline,拒絕only 32-bit的APP提交審核
  • 在應(yīng)用上給出了很強的用戶提示,讓用戶知道這個APP拖慢系統(tǒng)運行
    于是,為了兼容老指令架構(gòu)同時支持新指令架構(gòu).蘋果搞了一個fat binary(也就是multiarchitecture binary)


    image.png

#########2.2.2.1 fat binary
fat binary 說白了把不同指令架構(gòu)下的可執(zhí)行文件,打包到一起,對應(yīng)平臺跑對應(yīng)的二進制文件.系統(tǒng)會從你APP里挑選最佳的可執(zhí)行文件進行運行,以最好地發(fā)揮性能. 比如說你是iPhone5,由于這個芯片是32-bit的SoC,那好,運行的時候系統(tǒng)就取出來是32-bit指令架構(gòu)的二進制程序進行運行;如果是iPhone5s,那就從里邊選64-bit指令架構(gòu)的二進制程序運行.
這導(dǎo)致Appstore中APP的size增大了不少.
關(guān)于fat binary 這個技法,其實老早前,蘋果已經(jīng)在PC時代用上了.
最開始蘋果采用的的架構(gòu)是 Motorola 68K, 94年后之后開始轉(zhuǎn) PowerPC,中間還出現(xiàn)了 PowerPC (G3, G4) version和 PowerPC 64 (G5) version,后面使用X86,具體可以看 http://hohle.net/scrap_post.php?post=197,有詳細(xì)的介紹.
#########2.2.2.2 bit code
考慮到這個問題,蘋果想,開發(fā)者老鐵們,要不你們提交給我審核的時候,別提交二進制文件了,你們提交一些中間形式的代碼(bitcode),我在后臺幫你們編譯,然后用戶從Appstore下載的時候,蘋果后臺跟進用戶手機的芯片型號,下發(fā)對應(yīng)的二進制安裝包.一來用戶下載的時候包不會那么大,二來我后臺機器牛啊,我可以幫你們做足編譯鏈接優(yōu)化,三來哪天我推出新的指令架構(gòu)硬件,我只要在云端再重新編譯一次,就可以完美支持新硬件.之前為了過渡用戶可是花了三四年的時間,才把你們這些開發(fā)者和用戶趕上架,真是心累.

image.png

注意圖中紅色部分,按照這種方式,以后完全是存在完美兼容的方案,開發(fā)者提交的是中間代碼,蘋果后臺編譯器進行重編,完美適配最新的系統(tǒng),同時在最新的設(shè)備上運行.

########2.3 什么原因?qū)е履承┸浖荒苷_\行
#########2.3.1 32-bit app run on only 64-bit OS
有了前面的鋪墊,這個問題就很好回答了, 前面我們說到從iOS7到iOS10,每一個系列,都有32-bit,也有64-bit對應(yīng)的系統(tǒng).在64bit系統(tǒng)里還多放了一份32-bit的動態(tài)鏈接庫,到了iOS11,32-bit的OS被干掉了,蘋果也不想維護32-bit的動態(tài)鏈接庫.于是運行iOS11的系統(tǒng)里,動態(tài)依賴庫只有64-bit版本.你手機上的老APP,如果是32-bit,自然就再也跑不起來了....


image.png

#########2.3.2API接口廢棄
某個API接口被徹底廢棄了...動態(tài)鏈接的時候沒調(diào)到.那肯定就掛了...
#######3.更深層次的問題-軟件運行的本質(zhì)
我感覺我應(yīng)該是講清楚這些問題背后隱藏的原理.但是還是不夠系統(tǒng).我只是從表象去追尋原理.接下來我們就正式開講:軟件運行的本質(zhì).App是如何運行起來的.


image.png

其實你看到這里,才是真正的開始.雖然前面說了那么多,不過沒關(guān)系,當(dāng)你看到這里的時候,我相信你已經(jīng)學(xué)到很多之前沒接觸過的知識.
雖然前面我們一直在以iPhone手機這個具體實例來闡述,但是當(dāng)你把他應(yīng)用到其他芯片或者操作系統(tǒng)上,他們也是類似的.所以當(dāng)你去看硬件(PC時代的CPU或者移動時代的芯片)或者是操作系統(tǒng)變遷(Windows或者Android),沿用上面的方式去思考追尋答案,最終也是大同小異.
在前面大知識基礎(chǔ)之上,我們來仔細(xì)分析一下運行于iOS操作系統(tǒng)之上的App.

image.png

########3.1 軟件運行的目的
我們都知道軟件存在的意義就是"幫人類解決問題".
如果美圖秀秀不能幫我們美顏,那我們還要它干嘛?
用戶啟動軟件之后,用戶自己根據(jù)主觀判斷,在軟件里做出了操作,我們的軟件獲取到用戶的意圖,執(zhí)行對應(yīng)的指令,對圖片像素點的某個數(shù)值進行修改,從而實現(xiàn)用戶目的.
下面是一個帶褶皺的老圖片的修復(fù).就是通過用戶指定了特定區(qū)域(x,y,width,height),然后讓計算機執(zhí)行對應(yīng)指令,從而實現(xiàn)修復(fù).


image.png

########3.2 可執(zhí)行程序是(數(shù)據(jù)+CPU指令)
為了實現(xiàn)上述"幫人類解決問題"這個美好愿景.App作為可執(zhí)行程序,由數(shù)據(jù)和指令共同構(gòu)成.可執(zhí)行程序啟動時,將對應(yīng)指令和最初的數(shù)據(jù)(包括一些初始化和未初始化的數(shù)據(jù),以及一些調(diào)用地址等)加載到存儲設(shè)備(內(nèi)存和寄存器)中.這一系列加載工作準(zhǔn)備完成后,操作系統(tǒng)還要根據(jù)已加載入內(nèi)存的可執(zhí)行程序進行一些環(huán)境準(zhǔn)備(例如一些動態(tài)庫綁定,這種屬于none-lazy),最后再把控制權(quán)交給我們熟悉的main方法.接下來CPU 把這些已經(jīng)加載到內(nèi)存中的指令 load 到指令流中一條一條執(zhí)行,這些指令會獲取他們關(guān)聯(lián)到的數(shù)據(jù)。


image.png

接下來的主要內(nèi)容,會專注在控制權(quán)轉(zhuǎn)交給main方法之前(上圖中,標(biāo)紅的2部分),通過這部分,你會明白計算機操作系統(tǒng)是如何理解并加載可執(zhí)行文件,為它準(zhǔn)備好運行前的一切工作.

  • 上圖中1:主要涉及到源碼接變成機器碼,這個過程主要涉及編譯器和鏈接器.包括了語法分析,語義分析,優(yōu)化等等流程.這也是一個很大的分支.
  • 上圖中2:會是我們本文接下來的主要內(nèi)容,涉及到iOS系統(tǒng)如何讀懂加載可執(zhí)行程序.這個在其他系統(tǒng)也是大同小異.
  • 上圖中3:主要是這些加載到內(nèi)存中的指令,在CPU中是如何被執(zhí)行.
    不同的 CPU 體系結(jié)構(gòu)的指令集是不一樣的,指令的長度和組成都有區(qū)別。我們拿三個具體的芯片來看看.
    iPhone5搭載的A6芯片,采用的是ARMv7-A 32-bit ,支持的指令集有 ARM, Thumb-2
    iPhone5s搭載的A7芯片,采用的是ARMv8-A (32/64-bit),支持的指令集有A64,A32, T32
    iPhone8搭載的是A11芯片,采用的是ARMv8?A compatible,支持的指令集有 A64
    所以當(dāng)年為了兼容iPhone4,iPhone4s,5,5s,6等等機器,我們的App里是包含了多份二進制程序,不同平臺執(zhí)行不同二進制程序,這就是fat-binary,我們前面已經(jīng)講到.
    image.png

#######3.3 操作系統(tǒng)讀懂可執(zhí)行程序
操作系統(tǒng)要完成某一個任務(wù),其實就是執(zhí)行一系列的指令,并操作對應(yīng)的數(shù)據(jù),如上面所說他們組成了可執(zhí)行文件.操作系統(tǒng)為了讀懂這個文件(可執(zhí)行程序說白了就是個文件),制定了對應(yīng)的文件格式,這樣它才能讀懂這個可執(zhí)行程序中的指令和數(shù)據(jù).這也就說明,為啥在某個OS下可成功運行的程序,在另一個操作系統(tǒng)下卻無法執(zhí)行,即使這倆操作系統(tǒng)跑在同樣的硬件配置下.在啟動執(zhí)行前,操作系統(tǒng)要讀懂這個可執(zhí)行程序的內(nèi)容,加載進入內(nèi)存中.
所以,如下圖,同樣的MacBook Pro硬件,可以跑不同的操作系統(tǒng),但是OSX的軟件無法在Windows系統(tǒng)下運行,即使他們基于同樣的硬件.


image.png

當(dāng)然,上面說的這個只是其中的一個原因,還有其他原因?qū)е露M制文件,不能跨OS運行(比如可執(zhí)行文件調(diào)用了特定OS接口).
如果各家操作系統(tǒng)都統(tǒng)一格式那多好....但是商業(yè)競爭,專利等等又怎么可能讓他們形成統(tǒng)一的標(biāo)準(zhǔn)呢?


image.png

還好,雖然沒有統(tǒng)一的標(biāo)準(zhǔn),但是套路還是很像.下圖是幾種最流行的文件格式以及他們被哪些平臺使用.


image.png

我們看到蘋果的可執(zhí)行文件采用的主要倆種格式
fat-binaries和Mach-O,我覺得你可以把問題簡單化,理解fat-binaries就是多個Mach-O的數(shù)組文件.接下來我們重點關(guān)注Mach-O文件就夠了.
雖然上圖中寫著OS X系統(tǒng),其實iOS也是一樣的.
#######3.4 文件格式到底長成什么樣?
我們用MachOView這個工具在mac下面就可以看清楚二進制文件格式到底什么樣.
用一個最簡單的代碼,來生成一個可執(zhí)行程序,然后用這個工具窺探它.

#include <stdio.h>

int main(int argc, const char * argv[]) {
    // insert code here...
    printf("Hello, World!\n");
    printf("2Hello, World!\n");
    return 0;
}

我們執(zhí)行一下 gcc -g main.c 最后會生成出來 a.out 就是對應(yīng)的二進制可執(zhí)行文件.
接下來要干的事情就是MachOView打開這個二進制文件,呈現(xiàn)在你眼前的是


image.png

是不是有點熟悉的感覺,熟悉的Text,Data,SymbolTable,StringTable,FunctionStarts
每一個單詞看起來都很熟悉,組合到一塊就不知所措了....
不過沒關(guān)系,且聽我從淺入深,慢慢道來.
上面這個格式,我們可以分成大的三部分,1.header, 2.Load commands, 3. payload;是不是有點TCP/IP協(xié)議的感覺.說白了,我們是要讓OS懂我們這個文件,那就需要制定規(guī)則(標(biāo)準(zhǔn)),讓文件遵守,操作系統(tǒng)才能懂,你要是亂來,那就沒有規(guī)矩,怎么可能有方圓.


image.png

好了,我們先來說header部分,header包含了這個文件的一些概要信息,比如說CPU類型(X86,arm或者其他),大小端;
Load commands說的是接下來要如何加載每個段的內(nèi)容,每個段是不同的類型,所以采用的加載命令是不一樣的,你可以看到這是一個數(shù)組.
第三個部分payload,存放的數(shù)據(jù)也是由一個數(shù)組組成,每個里邊你大概可以簡化理解是一個個segment,比如說_Text_Segment, _Data_Segment等等, segment里邊又存放著一個個section.
直接說,payload(對應(yīng)下圖就是Data,我個人感覺用payload不會和其他名字沖突)其實就是個二維數(shù)組.我們上一張圖,你就可以很好理解了.


image.png

總之一句話:header和load command說明了系統(tǒng)fork進程后,如何加載后面的內(nèi)容到內(nèi)存中,讓這個進程得以執(zhí)行對應(yīng)的程序.
有了前面的bird-view,我們再拉近看一下,我們就慢慢展開這三個部分
########3.4.1 Mach-O文件格式的Header

image.png

具體的字段內(nèi)容,這里不做展開介紹,網(wǎng)上有很多,總之這個header說明了,這個文件是32位還是64位,支持什么CPU架構(gòu),本文件包含了多少個加載命令需要執(zhí)行等等.
########3.4.2 Mach-O文件格式的Load Commands
header中已經(jīng)注明總共有多少個load command需要被加載,這個地方就詳細(xì)地說明了各個command都是什么.比如說有一些是 加載Text,有一些是加載Data的,有一些是加載動態(tài)鏈接器的等等.

image.png

上圖只是展示了一些常見的load command類型,如果你需要更加仔細(xì)的類型文檔,可以到這個地方查看
https://opensource.apple.com/source/xnu/xnu-2050.18.24/EXTERNAL_HEADERS/mach-o/loader.h

下面這個參考了網(wǎng)上,羅列了大部分命令的用途

Command 用途
LC_SEGMENT/LC_SEGMENT_64 將對應(yīng)的段中的數(shù)據(jù)加載并映射到進程的內(nèi)存空間去
LC_SYMTAB 符號表信息
LC_DYSYMTAB 動態(tài)符號表信息
LC_LOAD_DYLINKER 啟動動態(tài)加載連接器/usr/lib/dyld程序
LC_UUID 唯一的 UUID,標(biāo)示該二進制文件,128bit
LC_THREAD 開啟一個MACH線程,但是不分配??臻g
LC_UNIXTHREAD 開啟一個UNIX線程
LC_VERSION_MIN_IPHONEOS/MACOSX LC_VERSION_MIN_IPHONEOS/MACOSX
LC_MAIN 設(shè)置程序主線程的入口地址和棧大小
LC_ENCRYPTION_INFO 加密信息
LC_LOAD_DYLIB 加載的動態(tài)庫,包括動態(tài)庫地址、名稱、版本號等
LC_FUNCTION_STARTS 函數(shù)地址起始表
LC_CODE_SIGNATURE 代碼簽名信息

########3.4.3 Mach-O文件格式的Data
到這兒的時候,你是不是發(fā)現(xiàn)有點問題,我們之前提到的這張圖有點問題,


image.png

LoadCommand并不僅僅是Segment Command啊,還包括其他的,而且下圖,綠色部分也不是Segment啊!!!!


image.png

他們怎么和text段,data段,長得那么不像?
哦,是的,剛剛為了不一次性帶入那么多信息給你,確實做了簡化.其實他們和segment是有共性的,他們也是等待著對應(yīng)loadcommand來加載他們.
所以更加完整的理解姿勢是如下


image.png

這張圖的關(guān)鍵信息是command是如何指向?qū)?yīng)的segment和section,也就是那些箭頭.
這個關(guān)系的映射全依賴load command里的信息,例如下面這張圖,是LG_SEGMENT_64這個command的具體內(nèi)容,當(dāng)中有一個 Number of Sections說明這個segment下面有多少個section


接下來看LoadCommand下具體某個section header的時候, offset&Size恰好指定了他要加載segment所處文件的位置


image.png

至此,我們的整個文件,看起來結(jié)構(gòu)應(yīng)該是如下,其中的大致關(guān)聯(lián)關(guān)系你應(yīng)該也懂了:
1.文件頭 mach64 Header
2.加載命令 Load Commands
3.Data區(qū)域(主要由一系列的segment&section組成)
3.1文本段 __TEXT
3.2數(shù)據(jù)段 __Data
3.3動態(tài)庫加載信息 Dynamic Loader Info
3.4入口函數(shù) Function Starts
3.5符號表 Symbol Table
3.6動態(tài)庫符號表 Dynamic Symbol Table
3.7字符串表 String Table

#######3.5 細(xì)看segment
我們再拉近看,把重點關(guān)注到"段"上,Text段和Data段
前面說到LC_SEGMENT(32-bit架構(gòu))/LC_SEGMENT_64(64-bit架構(gòu)) 將對應(yīng)的段中的數(shù)據(jù)加載并映射到進程的內(nèi)存空間去.
#######3.5.1 如何根據(jù)load commands把segment&section映射到內(nèi)存
我們通過MachOView一樣可以看到他具體是如何load的.但是為了簡化,我們通過執(zhí)行
dwarfdump -R a.out 就可以看到對應(yīng)的映射情況

image.png

當(dāng)我們執(zhí)行一個可執(zhí)行文件,虛擬內(nèi)存系統(tǒng)會將segment映射到進程的地址空間中。上圖看起來是把整個文件都load進去,但是實際上虛擬內(nèi)存系統(tǒng)做了優(yōu)化,用一些技巧來規(guī)避一次性load這種低效操作.在這里我們先簡單的假設(shè)VM會將整個文件加載進內(nèi)存,雖然在實際上這不會發(fā)生。
當(dāng)虛擬內(nèi)存系統(tǒng)進行映射時,數(shù)據(jù)段和可執(zhí)行段會以不同的參數(shù)和權(quán)限被映射。這些參數(shù)和權(quán)限在load command已經(jīng)被規(guī)定好.
__TEXT段包含了可執(zhí)行的代碼。它們被以只讀和可執(zhí)行的方式映射。進程被允許執(zhí)行這些代碼,但是不能修改。這些代碼也不能改變它們自己,并且這些頁從來不會被污染。
__DATA段以可讀寫和不可執(zhí)行的方式映射。它包含了可以被更改的數(shù)據(jù)。
當(dāng)你仔細(xì)看這個的時候,你會發(fā)現(xiàn)__TEXT這個segment其實是從fileoff 0的位置開始.從wwdc2016 406 session可以看到,確實也是如此.不過對于大部分讀者來說,你可以忽略這個細(xì)節(jié).只需要大概知道這些segment會按照規(guī)則load到內(nèi)存里.
#######3.5.2 聊聊這些segment
既然這些segment都會被加載到內(nèi)存里,那我們就來看看他們都是干嘛的
########3.5.2.1 __PAGEZERO
第一個段是__PAGEZERO。__PAGEZERO段不包含任何section,該段被稱為空指針陷阱段,映射到虛擬內(nèi)存空間的第一頁,用于捕捉對NULL指針的引用.在64-bit架構(gòu)中,這個有4GB大小。這4GB并不是文件的真實大小,但是說明了進程的前4GB地址空間將會被映射為,不能執(zhí)行,不能讀,不能寫。這就是為什么在去寫NULL指針或者一些低位的指針的時候,你會得到一個EXC_BAD_ACCESS錯誤。這是操作系統(tǒng)在嘗試防止你引起系統(tǒng)崩潰。這是一個全用0填充的段,用于抓取空指針引用。通常不會占用磁盤空間 (或內(nèi)存空間),因為它運行時映射為一群0.

########3.5.2.2 __TEXT
__TEXT:本段只有可執(zhí)行代碼和其他只讀數(shù)據(jù)。
我們要注意一下,大小寫問題,__TEXT表示的是段, 小寫__text則是段中的區(qū)(section)
__text:本section是編譯后得到的可執(zhí)行機器碼。
_stubs和_stub_helper是給動態(tài)鏈接器用的。這允許動態(tài)鏈接的代碼延遲鏈接。
__cstring:表示代碼里的字符串常量。鏈接器在生成最終產(chǎn)品時會清除重復(fù)語句。


image.png

而你在__text中用到這邊常量,其實是鏈接了指向這個內(nèi)容的一個指針.
__stubs:間接符號存根。該區(qū)存放的是二進制文件中未定義符號的占位符.正如他的名字"樁",他只是預(yù)先拿來占位的,內(nèi)容指向了別的地方.
__stub_helper:這算是一個工具,可以簡單理解成另一個程序,他能夠告訴你,"樁"最后是指向了哪兒.
__unwind_info:一個緊湊格式,為了存儲堆棧展開信息供處理異常。此節(jié)由鏈接器生成,通過“__eh_frame”里供OS X異常處理的信息。
__eh_frame: 一個標(biāo)準(zhǔn)的節(jié),用于異常處理,它提供堆棧展開信息,以DWARF格式。

########3.5.2.3 __DATA
_DATA段包含了可讀寫數(shù)據(jù),可讀可寫的特性讓動態(tài)鏈接成為可能.鏈接有幾種方式,其中最主要有倆種:lazy binding & none-lazy binding
lazy binding顧名思義,就是在代碼在runtime的時候,才去尋找對應(yīng)的調(diào)用地址,解析出來,然后再執(zhí)行.
none-lazy binding則是在加載程序的時候,就把這些調(diào)用地址綁定后,后續(xù)run time直接調(diào)用.
在這個例子中,我們看到的就是:
__nl_symbol_ptr:非延遲導(dǎo)入符號指針表。在編譯的時候,這里的指針們指向解析助手。
__la_symbol_ptr:延遲導(dǎo)入符號指針表。
到這里,結(jié)合我們之前的代碼,我們來說一下代碼是如何動態(tài)鏈接到printf這個函數(shù).
我們先從 _text代碼開始,可以看到有倆條跳轉(zhuǎn)指令


image.png

這倆條跳轉(zhuǎn)指令都跳轉(zhuǎn)到 0x100000f76,那我們就到這個地址看看


image.png

image.png

我們通過打斷點,調(diào)試可以看到,這是讓跳轉(zhuǎn)到"Lazy Symbol Pointers"指向的地方(請仔細(xì)品位這句話),于是乎我們跑去看Lazy Symbol Pointers到底存放了什么東西,我靠, 里邊居然指向了_stub_helper,因為Lazy Symbol Pointers首次運行到這個動態(tài)鏈接的方法,他自己也不知道要去哪兒執(zhí)行,就告訴你說,你去找助手吧,讓助手告訴你. 于是助手做了一下參數(shù)壓棧,通過binder去查找這個動態(tài)庫的地址.


image.png

在完成這個流程后,把Lazy Symbol Pointers里對應(yīng)的實際方法調(diào)用地址給修改了,后續(xù)執(zhí)行到這個方法再也不用去找助手問地址了.
看到這兒我估計你還有點迷糊,我們通過下面這張圖會清晰一些


image.png

具體的細(xì)節(jié),可以參考這篇文章<Dynamic Linking of Imported Functions in Mach-O> ,這篇文章寫得真心好.
https://www.codeproject.com/Articles/187181/Dynamic-Linking-of-Imported-Functions-in-Mach-O
到了這兒,和linux下面的動態(tài)鏈接延遲綁定基本上差不多.具體可參考
《程序員的自我修養(yǎng)》第200頁
#######3.5.3 這些部件是如何組裝在一起工作

  1. 系統(tǒng)內(nèi)核為這個二進制文件先fork一個進程
  2. 調(diào)用execve讓系統(tǒng)內(nèi)核加載并執(zhí)行這個二進制文件
    2.1 這個時候內(nèi)核根據(jù)Mach-O的mach_header進行合法性校驗.
    2.2 根據(jù)load command加載此二進制文件
    2.3 根據(jù)load command中的LC_LOAD_DYLINKER命令加載動態(tài)鏈接器(dyld)
    2.4 系統(tǒng)內(nèi)核 控制權(quán)轉(zhuǎn)移到 動態(tài)鏈接器(dyld)
  3. dyld接手控制權(quán)
    3.1 dyld加載system framework以及一些dylib到內(nèi)存中.其實這些動態(tài)庫也是Mach-O文件.
    3.2 把動態(tài)庫鏈接到一起(這個也是個大話題,這里展開的話,這篇文章基本說不完) fix-ups 地址修正,讓各個部件關(guān)聯(lián)起來.確定地址被置于“_nl_symbol_ptr”和“__got”中.
    3.3 初始化方法 (這里多數(shù)事情都是遞歸的,從底向上的方法調(diào)用,因為初始化的時候要確保,他所依賴的已經(jīng)初始化)
  4. 完成以上這個工作之后,就把控制權(quán)交給main方法.

#######4. End
如果你看到這,可能覺得好像這些底層的東西我們現(xiàn)在都不需要關(guān)注了.其實并不是這樣.當(dāng)你碰到問題,體會到書到用時方恨少時,你就知道有時候問題解決并不是簡單耍機靈就可以.比如怎么樣讓應(yīng)用程序啟動加載更快;居然有壞人調(diào)用了APP端的加密算法來探測后臺接口等等.
寫到這,已經(jīng)有點體力不支了.所以就來個簡單結(jié)束吧.謝謝你看到這,希望有所收獲.不足請指正.


image.png

最后這里附上refer的內(nèi)容鏈接:
http://49be7714.wiz03.com/share/s/19LDsk0qDkfy2pziXv12a6L72jQmC90z5QVJ2GlBAh1uMBcF

?著作權(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)容