前前后后花了3個(gè)月的閑余時(shí)間,認(rèn)真研讀了這本書(shū),知道這本書(shū)名,源于進(jìn)入58時(shí)組長(zhǎng)交代的一個(gè)分享任務(wù)——《iOS的內(nèi)存管理機(jī)制》。在研讀WWDC相關(guān)章節(jié)時(shí),對(duì)里面的虛擬內(nèi)存和物理內(nèi)存的相互轉(zhuǎn)換、物理內(nèi)存的占用和回收、堆棧的管理等內(nèi)容,有很多疑問(wèn),在網(wǎng)上搜索相關(guān)解答時(shí),發(fā)現(xiàn)線索最終都指向了《程序員的自我修養(yǎng)》。自己首先找了一份電子版,閱讀了第一部分的內(nèi)容后,有種《桃花源記》中 “初極狹,才通人。復(fù)行數(shù)十步,豁然開(kāi)朗” 的感覺(jué),立即毫不猶豫的買了紙質(zhì)版。
關(guān)于內(nèi)容,書(shū)的開(kāi)頭寫(xiě)的很明白:“描述一個(gè)應(yīng)用程序在編譯、鏈接和運(yùn)行時(shí)刻發(fā)生的各種事項(xiàng):代碼指令是如何保存的、庫(kù)文件如何與應(yīng)用程序代碼靜態(tài)鏈接,應(yīng)用程序如何裝載到內(nèi)存中并開(kāi)始運(yùn)行,動(dòng)態(tài)鏈接如何實(shí)現(xiàn),C/C++運(yùn)行庫(kù)如何工作,以及操作系統(tǒng)提供的系統(tǒng)服務(wù)是如何被調(diào)用的?!?內(nèi)容以Linux和Windows兩個(gè)系統(tǒng)平臺(tái)的實(shí)現(xiàn)為例,詳盡講解了它們實(shí)現(xiàn)的是什么、為什么的問(wèn)題。
計(jì)算機(jī)結(jié)構(gòu)
計(jì)算機(jī)整體分為硬件部分和軟件部分。比如大家最關(guān)注的內(nèi)存、CPU、硬盤、顯示器等屬于硬件部分。而具體使用它的各種程序:辦公三件套、IDE、游戲等,則屬于軟件部分。從最初的軟件開(kāi)始運(yùn)行,到最終具體的硬件執(zhí)行,中間使用層層的結(jié)構(gòu)進(jìn)行傳遞,這些層次的劃分和組織過(guò)程,構(gòu)成了完整的計(jì)算機(jī)結(jié)構(gòu)。
歷史演變
1 結(jié)構(gòu)
計(jì)算機(jī)硬件部分的核心包括中央處理器(CPU)、內(nèi)存和I/O控制芯片,作為程序開(kāi)發(fā)者,最多關(guān)注的是內(nèi)存。開(kāi)始時(shí),CPU頻率和內(nèi)存頻率相似,二者連接在同一總線。后來(lái)CPU頻率大幅提高,人們開(kāi)始設(shè)計(jì)北橋系統(tǒng),使用PCI總線連接CPU和內(nèi)存、高速圖像設(shè)備,南橋使用ISA總線連接鍵盤、鼠標(biāo)等低速設(shè)備,然后通過(guò)PCIBridge和北橋相連。2 內(nèi)存
剛開(kāi)始的程序直接運(yùn)行在物理內(nèi)存中,但是存在程序可能越界訪問(wèn)的錯(cuò)誤,以及多個(gè)程序運(yùn)行時(shí)存在的頻繁換入、換出問(wèn)題,后來(lái)通過(guò)虛擬內(nèi)存的方式對(duì)物理內(nèi)存進(jìn)行抽象,解決了該問(wèn)題。3 操作系統(tǒng)
為了匹配運(yùn)行速率不斷提高的CPU,依次出現(xiàn)多道作業(yè)系統(tǒng)、分時(shí)任務(wù)系統(tǒng)和多任務(wù)操作系統(tǒng)。操作系統(tǒng)做了兩件事情:1 為程序運(yùn)行提供抽象的接口;2 管理硬件資源。硬件資源多入牛毛,操作系統(tǒng)為了適配不同的硬件,為硬件提供一系列接口和框架,由硬件生產(chǎn)廠家負(fù)責(zé)驅(qū)動(dòng)程序的實(shí)現(xiàn)。4 線程
在CPU運(yùn)行頻率提高到4GHz以后,開(kāi)始進(jìn)入了瓶頸期,為了繼續(xù)提升其計(jì)算速度,人們采用了集成多個(gè)CPU的方式。通過(guò)多線程,可能將一些耗時(shí)的任務(wù)進(jìn)行拆分,分別在不同的CPU上進(jìn)行計(jì)算,提高效率。一個(gè)標(biāo)準(zhǔn)的線程由線程ID、當(dāng)前指令指針(PC)、寄存器集合和堆棧組成。多線程執(zhí)行過(guò)程中,涉及到線程安全,通過(guò)加鎖的方式實(shí)現(xiàn)不同線程的同步執(zhí)行。具體實(shí)現(xiàn)方式包括3種:1 信號(hào)量;2 互斥量;3 臨界區(qū)。
靜態(tài)鏈接
程序從編譯到運(yùn)行:預(yù)編譯、編譯、匯編、鏈接、運(yùn)行。預(yù)編譯階段進(jìn)行宏展開(kāi)、文件替換、注釋刪除等;編譯階段進(jìn)行詞法分析、語(yǔ)法分析(成為表達(dá)式語(yǔ)法樹(shù))、語(yǔ)義分析(添加各表達(dá)式語(yǔ)法樹(shù)節(jié)點(diǎn)的類型),然后編譯成中間代碼(IR),最后對(duì)中間代碼進(jìn)行優(yōu)化,比如減少變量、合并指令等;匯編階段將優(yōu)化后的中間代碼,轉(zhuǎn)變?yōu)闄C(jī)器碼,成為目標(biāo)文件;鏈接階段將不同的目標(biāo)文件進(jìn)行合并,對(duì)模塊間的變量進(jìn)行重定位等,最終成為可執(zhí)行文件,以供運(yùn)行。
可執(zhí)行文件本身是一個(gè)文件,可執(zhí)行是它的格式。文件內(nèi)容是16進(jìn)制的機(jī)器碼,其通過(guò)段(Section)的方式來(lái)組織??梢苑譃轭^部、代碼段、數(shù)據(jù)段、BSS 、段表、字符串表和符號(hào)表等。通過(guò)頭部信息,可以確定可執(zhí)行文件的類型、運(yùn)行環(huán)境、文件機(jī)器字節(jié)長(zhǎng)度、入口地址等信息。代碼段存儲(chǔ)代碼指令,大部分是函數(shù)的具體實(shí)現(xiàn)。數(shù)據(jù)段中存儲(chǔ)初始化的全局變量、局部變量等。BSS段中存放未初始化的全局變量,其在可執(zhí)行文件中的大小為0(可以理解為只標(biāo)識(shí)了其起始地址,大小記錄在段表中,比較費(fèi)解,這點(diǎn)也困擾了我好久)。段表記錄段的個(gè)數(shù)和每個(gè)段的詳情,比如段的名稱、大小等信息。字符串表記錄文件中所有的字符串信息。符號(hào)表存放符號(hào)的名稱(字符串下標(biāo))、所在的段(段表中的下標(biāo))、類型、大小等信息。個(gè)人認(rèn)為符號(hào)表是理解可執(zhí)行文件的核心,程序中所定義的函數(shù)名、變量名,在可執(zhí)行文件中是一個(gè)個(gè)全局唯一的符號(hào),其名稱存放在字符串表,其值對(duì)應(yīng)了具體的可執(zhí)行文件的地址,鏈接過(guò)程就是對(duì)它們的有效替換,最終生成了具備可執(zhí)行能力的目標(biāo)文件。
靜態(tài)鏈接是鏈接器將不同的目標(biāo)文件組織起來(lái)的過(guò)程。比如將目標(biāo)文件a.o和b.o中的二進(jìn)制內(nèi)容,按照.text(a.o)+.text(b.o)、.bss(a.o)+.bss(b.o)等鏈接為一個(gè)統(tǒng)一的整體。在合并的文件中,不同函數(shù)和變量的虛擬地址已經(jīng)確定,使用確定的新值來(lái)更新符號(hào)表段的內(nèi)容。接下來(lái)依據(jù)各個(gè)段的重定位表,將引用的符號(hào)內(nèi)容,替換為符號(hào)表段中確定的新地址值。自此,一個(gè)完成的目標(biāo)文件已經(jīng)完成。
最終目標(biāo)文件的內(nèi)容形式,由編譯器、鏈接器來(lái)決定,編譯器和鏈接器在不同硬件和平臺(tái)上的實(shí)現(xiàn)又不一致。所以不同硬件和平臺(tái)上生成的二進(jìn)制目標(biāo)文件無(wú)法相互兼容。為了解決這個(gè)問(wèn)題,人們?cè)噲D建立目標(biāo)文件的統(tǒng)一抽象模式,比如BFU庫(kù),首先將源代碼文件編譯為BFU格式的文件,然后由BFU轉(zhuǎn)換為適配不同硬件和平臺(tái)的目標(biāo)文件。這樣,如果新增一種平臺(tái)和硬件,只要在BFD庫(kù)中添加支持即可。
裝載與動(dòng)態(tài)鏈接
1 裝載
裝載是可執(zhí)行文件映射到虛擬內(nèi)存空間的過(guò)程。操作系統(tǒng)來(lái)實(shí)現(xiàn)裝載過(guò)程,然后由內(nèi)核態(tài)切換到用戶態(tài),執(zhí)行程序。原始的裝載方式直接將程序裝載到物理內(nèi)存中,需要開(kāi)發(fā)者管理物理內(nèi)存的具體分配,比較繁瑣。現(xiàn)在的操作系統(tǒng)首先把虛擬內(nèi)存和物理內(nèi)存劃分為大小相同的頁(yè),然后通過(guò)頁(yè)映射的方式實(shí)現(xiàn)虛擬內(nèi)存到物理內(nèi)存的裝載,二者通過(guò)硬件MMA實(shí)現(xiàn)快速的地址轉(zhuǎn)換。為了適配分頁(yè)要求,映射的虛擬空間需要對(duì)可執(zhí)行文件中的內(nèi)容進(jìn)行合理組織,比如相似段的合并、按照頁(yè)大小進(jìn)行放置、BSS空間的分配等,這個(gè)過(guò)程操作系統(tǒng)通過(guò)將不同的Section組織成Segment來(lái)實(shí)現(xiàn)。2 動(dòng)態(tài)鏈接
靜態(tài)鏈接出的可執(zhí)行文件,存在大量相同的依賴庫(kù),如果在運(yùn)行每個(gè)進(jìn)程時(shí),都為這些依賴庫(kù)分配單獨(dú)的物理空間,會(huì)照成巨大浪費(fèi)。除此之外,如果依賴中的某個(gè)子庫(kù)進(jìn)行了更新,那么靜態(tài)鏈接的可執(zhí)行文件也要隨之更新,否者就無(wú)法使用新功能。為此,動(dòng)態(tài)鏈接方式應(yīng)運(yùn)而生。
動(dòng)態(tài)鏈接實(shí)現(xiàn)相同子庫(kù)的共享,需要將子庫(kù)編譯為地址無(wú)關(guān)的形式:對(duì)內(nèi)部變量數(shù)據(jù)、函數(shù)引用改為相對(duì)地址尋址方式,對(duì)外部變量、函數(shù)引用采用全局偏移表(Gobal Offet Table)的間接引用方式。其數(shù)據(jù)段、BSS段、GOT等部分,在各進(jìn)程的虛擬空間中創(chuàng)建共享庫(kù)的副本,適應(yīng)變化;將地址無(wú)關(guān)的指令和數(shù)據(jù)部分進(jìn)行共享,節(jié)省空間。
相比靜態(tài)鏈接,動(dòng)態(tài)鏈接庫(kù)將可執(zhí)行文件與依賴子庫(kù)的的鏈接過(guò)程放到加載階段,執(zhí)行時(shí)通過(guò)GOT的間接方式尋找指令,速度會(huì)降低,是操作系統(tǒng)使用時(shí)間換取空間的方式。為了節(jié)省動(dòng)態(tài)加載占用的時(shí)間,操作系統(tǒng)動(dòng)態(tài)加載時(shí)采用延遲綁定,在函數(shù)第一次被使用時(shí)才對(duì)引用變量和地址等進(jìn)行重定位。
庫(kù)與運(yùn)行庫(kù)
通過(guò)對(duì)C語(yǔ)言運(yùn)行庫(kù) (Runtime) 的實(shí)現(xiàn),讓我對(duì)運(yùn)行時(shí)的理解更加深刻:它是構(gòu)建在C語(yǔ)言和操作系統(tǒng)API之間的橋梁,讓開(kāi)發(fā)程序的人專注于使用C語(yǔ)言進(jìn)行邏輯實(shí)現(xiàn),不用關(guān)心操作系統(tǒng)相關(guān)的問(wèn)題(進(jìn)程創(chuàng)建銷毀、堆棧管理、圖形操作、網(wǎng)絡(luò)使用、文件管理等)和不同操作系統(tǒng)實(shí)現(xiàn)的差異(Linux和Windows平臺(tái)等)。比如我們常用的函數(shù)printf,實(shí)際是通過(guò)C語(yǔ)言的 Runtime (CRT), 最終調(diào)用了操作系統(tǒng)的命令行輸出功能,在Windows平臺(tái)上, CRT 通過(guò)調(diào)用 Winows API,在Linux平臺(tái)上通過(guò)write函數(shù)來(lái)實(shí)現(xiàn)。推而廣之,CRT是高級(jí)語(yǔ)言C對(duì)系統(tǒng)調(diào)用的封裝方式,OCRT(OC Runtime)是Object-C對(duì)iOS系統(tǒng)的調(diào)用方式的封裝,ART(Android Runtime)是Java對(duì)Android操作系統(tǒng)的封裝。一切高級(jí)語(yǔ)言都有運(yùn)行時(shí),通過(guò)運(yùn)行時(shí)實(shí)現(xiàn)了對(duì)底層操作系統(tǒng)的各種封裝,讓程序開(kāi)發(fā)者愉快地(無(wú)腦地)專注于應(yīng)用實(shí)現(xiàn)。運(yùn)行時(shí)支持跨平臺(tái)(操作系統(tǒng)時(shí)),使用該高級(jí)語(yǔ)言創(chuàng)建的程序,就能在不同的操作系統(tǒng)上運(yùn)行。
最后
作為iOS開(kāi)發(fā)者,在內(nèi)存管理方面,最基本的要求是避免內(nèi)存泄漏,對(duì)其認(rèn)識(shí)從理論上可以解釋很清楚,但是具體到系統(tǒng)的內(nèi)存布局,比如虛擬內(nèi)存的大小、物理內(nèi)存的占用計(jì)算等,認(rèn)知往往很模糊。對(duì)最終編譯的可執(zhí)行文件MachO,通過(guò)MachOView可以看到具體的文件頭和段信息,但是對(duì)于具體信息的含義,以及如此組織的原因更是一頭霧水。具體開(kāi)發(fā)時(shí),涉及到動(dòng)態(tài)庫(kù)和靜態(tài)庫(kù)的具體區(qū)別,除了能夠說(shuō)出靜態(tài)庫(kù)相比動(dòng)態(tài)庫(kù)是以空間換時(shí)間外,其他的區(qū)別就不清楚了。對(duì)應(yīng)用啟動(dòng)時(shí)的介紹,不止一遍聽(tīng)過(guò)遞歸加載依賴的動(dòng)態(tài)庫(kù),然后進(jìn)行Rebasing和Rebing,但是對(duì)具體的實(shí)現(xiàn)過(guò)程就云里霧里了...等等很多疑問(wèn),在這本書(shū)中都得到了解答。
這本書(shū)我先通讀了一遍,利用假期又精度了一遍,通過(guò)不同章節(jié)間的相互印證、對(duì)實(shí)例的反復(fù)思考,對(duì)整個(gè)計(jì)算機(jī)體系和程序的編譯、裝載和運(yùn)行有了更近一步的認(rèn)識(shí),就像江湖傳言中的易筋經(jīng),頗有被打通了任督二脈的感覺(jué)。雖說(shuō)具體編程技術(shù)日新月異,但是這種沉淀的心法,卻穩(wěn)如基石,以后還需要不斷學(xué)習(xí),不斷領(lǐng)悟。