程序員的自我修養總結_第1頁
程序員的自我修養總結_第2頁
程序員的自我修養總結_第3頁
程序員的自我修養總結_第4頁
程序員的自我修養總結_第5頁
已閱讀5頁,還剩71頁未讀, 繼續免費閱讀

下載本文檔

版權說明:本文檔由用戶提供并上傳,收益歸屬內容提供方,若內容存在侵權,請進行舉報或認領

文檔簡介

1、目錄第一章 溫故而知新6第二節 萬變不離其宗6第3節 站得高看得遠7第4節 操作系統的功能71.4.1 不要讓CPU打盹71.4.2 設備驅動81.5 內存不夠怎么辦?81.5.1 關于隔離91.5.2 分段91.5.3 分頁91.6 眾人拾柴火焰高101.6.1 線程基礎101.6.2 線程安全111.6.3 多線程內部情況14第二章 編譯和鏈接152.1 被隱藏了的過程152.1.1 預編譯152.1.2 編譯152.1.3 匯編152.1.4 鏈接162.2 編譯器做了什么162.2.1 詞法分析162.2.2 語法分析162.2.3 語義分析162.2.4 中間語言生成172.2.5

2、目標代碼的生成與優化172.3 鏈接器年齡比編譯器長182.4 模塊拼接靜態鏈接18第三章 目標文件里有什么183.1 目標文件的格式193.2 目標文件是什么樣的193.3 挖掘SimpleSection.o203.3.3 BSS段203.3.4 其他段203.4 ELF文件結構描述203.4.1 文件頭213.4.2 段表213.4.3 重定位表223.4.4 字符串表223.5 鏈接的接口符號223.5.1 ELF符號表結構233.5.2 特殊符號233.5.3 符號修飾與函數簽名243.5.5 弱符號和強符號243.6 調試信息25第4章 靜態鏈接254.1 空間與地址分配254.1.

3、2 相似段合并254.1.3 符號地址的確定264.2 符號解析與重定位264.2.2 重定位表264.2.3 符號解析274.2.4 指令修正方式274.3 COMMON塊274.4.1 重復代碼消除284.4.2 全局構造與析構294.4.3 C+與ABI294.5 靜態庫鏈接304.6 鏈接過程控制304.6.1 鏈接過程腳本304.6.2 最“小”的程序314.6.3 使用ld鏈接腳本314.6.4 ld鏈接腳本語法簡介314.7 BFD庫31第5章 WINDOWS PE/COFF315.1 Windows的二進制文件格式PE/COFF315.2 PE的前身COFF325.3 鏈接指示

4、信息325.4 調試信息325.5 大家都有符號表325.6 WINDOWS下的ELFPE32第6章 可執行文件的裝載與進程336.1 進程的虛擬地址空間336.2 裝載的方式336.2.1 覆蓋裝入336.2.2 頁映射346.3 從操作系統的角度看可執行文件的裝載346.3.1 進程的建立346.4 進程虛存空間的分布356.4.1 ELF文件鏈接視圖和執行視圖356.4.2 堆和棧366.4.3 堆的最大申請數量366.4.4 段地址對齊366.4.5 進程棧初始化376.5 Linux內核裝載ELF過程簡介376.6 Windows PE的裝載38第7章 動態鏈接387.1 為什么要動

5、態鏈接387.2 簡單的動態鏈接例子397.3 地址無關代碼407.3.1 固定裝載地址的困擾407.3.2 裝載時重定位407.3.3 地址無關代碼407.3.4 共享模塊的全局變量問題427.3.5 代碼段地址無關性437.4 延遲綁定(PLT)437.5 動態鏈接相關結構447.5.1 “.interp”段457.5.2 “dynamic”段457.5.3 動態符號表457.5.4 動態鏈接重定位表457.5.5 動態鏈接時進程堆棧初始化信息467.6 動態鏈接的步驟和實現467.6.1 動態鏈接器自舉467.6.2 裝載共享對象477.6.3 重定位和初始化477.6.4 Linux動

6、態鏈接器的實現477.7 顯示運行時鏈接487.7.1 打開動態庫487.7.2 dlsym()487.7.3 dlerror()487.7.4 dlclose()49第8章 Linux共享庫的組織498.1 共享庫版本498.1.1 共享庫兼容性498.1.2 共享庫版本命名498.1.3 SO-NAME程序需要記錄什么508.2 符號版本508.2.1 基于符號的版本機制508.2.3 Linux中的符號版本518.3 共享庫系統路徑518.4 共享庫的查找過程518.5 環境變量528.6 共享庫的創建與安裝528.6.1 共享庫的創建528.6.3 共享庫的安裝538.6.4 共享庫構

7、造和析構函數538.6.5 共享庫腳本53第9章 Windows下的動態鏈接549.1 dll介紹549.1.2 基地址和RVA549.1.3 dll共享數據段549.1.4 dll的簡單例子549.1.7 使用模塊定義文件559.1.8 DLL顯示運行時鏈接559.2 符號導出導入表559.2.1 導出表559.2.2 EXP文件569.2.4 導入表569.2.5 導入函數的調用569.3 DLL優化579.3.1 重定基地址579.3.2 序號589.3.3 導入函數綁定589.4 C+與動態鏈接589.5 DLL HELL59第4部分 庫與運行庫60第10章 內存6010.1 程序的內

8、存布局6010.2 棧與調用慣例6110.2.1 什么是棧6110.2.2 調用慣例6110.2.3 函數返回值傳遞6210.3 堆與內存管理6310.3.1 什么是堆6310.3.2 Linux進程堆管理6310.3.3 Windows進程堆管理6410.3.4 堆分配算法64第11章 運行庫6411.1 入口函數和程序初始化6411.1.1 程序從main開始執行嗎6411.1.2 入口函數是如何實現的6511.1.3 運行庫與I/O6611.1.4 MSVC CRT的入口函數初始化6611.2 C/C+運行庫6711.2.1 C語言運行庫6711.2.2 C語言標準庫6711.2.3 g

9、libc和MSVC CRT6711.3 運行庫與多線程6811.3.1 CRT的多線程困擾6811.3.2 CRT改進6811.3.3 線程局部存儲實現6811.4 C+全局構造和析構6911.4.1 glibc全局構造和析構6911.4.2 MSVC的全局構造和析構7011.5 fread的實現7111.5.1 緩沖7111.5.2 fread_s7111.5.3 _fread_nolock_s7111.5.4 _read7111.5.5 文本換行7111.5.6 fread回顧72第12章 系統調用與API7212.1 系統調用介紹7212.1.1 什么是系統調用7212.1.3 系統調用

10、的弊端7212.2 系統調用原理7312.2.2 基于INT的Linux的經典系統調用實現7312.2.3 Linux的新型系統調用機制7312.3 Windows API7312.3.1 Windows API概覽7412.3.2 為什么要使用Windows API?74第13章 運行庫的實現7413.1 C語言運行庫74A.1 字節序74第一章 溫故而知新第二節 萬變不離其宗凡是單純講史的章節我全部略去。本節講的主要是由CPU、內存和I/O之間速度不匹配而設計的硬件架構及其發展。這個就不用細說了CPU最快,內存次之,I/O更慢。由于CPU和內存速度還算接近,所以把CPU和內存算作一類,I/

11、O單獨算作一類。當然這里說的I/O是指I/O設備,并不是操作。隨著發展CPU頻率越來越高,處理速度越來越快,內存跟不上節奏了,它們之間的I/O也出現了速度不匹配的問題。因為I/O設備可分為高速設備和低速設備兩種,所以為高速搭配北橋,低速搭配南橋。它們之間的關系可用下圖表示:CPU的頻率只能達到4GHz無法提升,這是由CPU制造工藝決定的,是個瓶頸,目前還無法突破。一個CPU能力有限,那就讓多個CPU共同工作提升效率。但是這樣的CPU陣列各部件利用率不高,于是,發展出了多核心,其他部件共享的多核CPU設計。說白了,原來的CPU里面每個CPU一個核心,除此之外還有圍繞這個核的其他部件。但是現在多核

12、CPU除了核心彼此獨立外,其他的部件是共享的。這一節就這么點內容。第3節 站得高看得遠從下圖可以看出計算機的結構大概是這樣的:最底層是硬件,它提供硬件規格描述。再往上是操作系統內核,它提供系統調用。再往上是運行庫,它提供各種系統API。再往上就是各種系統軟件了。這種設計具有上層屏蔽下層,上層提供接口的特點。這一節對接口的解釋非常好。作者說接口是一種協議,協議二字比較貼切。當然這個協議不是計算機網絡中的protocol。第4節 操作系統的功能有二。1、提供抽象接口。2、管理硬件。1.4.1 不要讓CPU打盹操作系統經歷了從多道程序設計、分時操作系統、到多任務操作系統等階段。多道程序設計是指CPU

13、空閑的時候出讓CPU以提高CPU利用率的設計;分時是指給每個程序固定的時間片執行,時間片一到就停止的設計,不過這個時間片是輪轉著用的,不是一個程序用完了就沒了;多任務就是現在操作系統設計了,程序以進程的方式存在。搶占:OS對程序執行具有絕對的控制權,OS依據一定標準判斷該剝奪哪個程序的執行就剝奪,想讓哪個程序執行就讓哪個程序執行。1.4.2 設備驅動GDI和directX等都是硬件的抽象,是一個中間層,它們屏蔽了硬件的具體細節,提供了通用的操作接口。LBA(Logical Block Address):因為硬盤結構復雜,概念繁多,尋找一個扇區要經過很多步驟,這個比較麻煩。與其如此,不如干脆為每

14、個扇區配置一個邏輯編號,這樣找扇區就好像是哈希算法一樣快。1.5 內存不夠怎么辦?程序在內存中的地址空間是需要相互隔離的。這是為了防止一個程序在無意間修改其他程序造成意料之外的結果,另外,這也是為了信息安全。內存利用率要高,要不然程序在內存和硬盤之間進行I/O操作所花費的時間可就多了。程序運行的地址應該是確定的。因為多數程序指令跳轉的目標地址是固定的,如果運行地址不確定就不能保證每次都在目標地址上運行,這就需要重定向進行調整,浪費時間。解決上述問題的辦法是使用中間層,即把程序的運行地址與目標地址建立一種映射關系。1.5.1 關于隔離我們平時說的什么32位,64位CPU啥的都是指CPU的處理能力

15、,從硬件的角度講,即,計算機的地址總線的條數。從CPU的設計上講就是CPU一次能夠處理的二進制位數,而這個位數還有一個學名叫字長。內存的物理地址空間就是真實的內存空間,虛擬地址空間則是應用于進程的邏輯地址空間。1.5.2 分段我在想如何從16進制的差值一下推斷出地址空間的大???以下是我的想法。1位16進制數字代表4位2進制數字,換句話說16進制數字轉換為2進制數字是以24為單位進行換算的。那么根據某個16進制數字所在位置乘以當前權值就可以得到該位置上的16進制數字所代表的2進制數字。而16進制某位的權值等于低一位的權值乘以24,并且16進制最低位的權值是20,因此可以根據這個規律換算出相應的2

16、進制數字。來看個例子。書上說從0X00000000到0X00A00000的地址空間大小就等于|0x00A00000-0x00000000|=|A00000|因為A是10所以其等價于|1000000|,現在按照上述規律進行換算。10220+0216+0212+028+024+020=10M(byte)。分段的方法可以使各進程彼此隔離,并且可以使程序運行的地址確定。分段的缺點就是它以程序為單位進行處理,但是根據程序運行的局部性原理,程序通常情況下只有一少部分需要常駐內存,因此以程序為單位換進換出嚴重影響了內存的利用率和處理速度。1.5.3 分頁頁面有3種:1、虛擬頁;2、內存頁;3、磁盤頁。MMU

17、(Memory Management Unit)負責把虛擬地址轉換成物理地址。1.6 眾人拾柴火焰高1.6.1 線程基礎使用線程的好處?1、多線程可以有效利用等待時間。因為某線程陷入等待狀態后別的線程可以繼續執行;2、多線程不會使與用戶的交互中斷。因為可以一個線程負責與用戶交互,另一個線程負責計算;3、能夠實現程序內部并發執行操作;4、多核CPU等硬件的潛力只有多線程才能使其充分發揮;5、在數據共享方面更高效。線程的私有存儲空間?1、 棧;2、線程局部存儲(Thread Local Storage,TLS);3、寄存器。線程真正的并發執行和非真正并發執行?在同一時間只有處理器核心數量大于等于執

18、行線程數量的時候才是真并發執行,除此之外都是模擬出來的。線程調度:在同一時間處理器的核心數量小于執行線程的數量時就需要在同一核心不斷切換來執行線程。改變線程優先級的3種方式1、用戶指定優先級;2、根據等待狀態的頻繁程度調整優先級;3、長時間得不到執行而被提升優先級??蓳屨紙绦芯€程和不可搶占執行線程:線程的各種狀態完全由操作系統來控制這就叫可搶占,就像某線程的時間片用完進入就緒態一樣,這就是由操作系統來控制的。除此之外的就是不可搶占線程。不可搶占線程主動放棄執行的時機:1、線程等待某事件發生時。2、線程主動放棄時間片。因為就這倆條件所以不可搶占線程調度的時機是確定的。Linux下的多線程:不像W

19、indows那樣把線程和進程分得那樣清楚,Linux是以任務為單位的,如果某幾個任務的執行是做同一件事的各個部分,那么這幾個任務就可以看成是線程,而這件事就可以看成是進程。所以Linux下的線程和進程是動態的概念。Linux下的fork函數:fork是叉子的意思,我不知道為啥Linux用它來給函數命名。它的作用就是復制任務,新任務和原任務共享同一塊內存空間,并且是寫時復制。所謂寫時復制就是寫的時候才從內存空間里面復制出一塊給你寫,原內存空間內容不變。讀的時候新舊任務讀同一塊內存空間。Linux下的exec函數:fork產生的是本任務的鏡像,也就是復制品。兩個同樣的任務完成同樣的功能是浪費啊,所

20、以fork是個半成品函數,必須搭配別的函數才有用,這個函數就是exec函數。Exec函數用來執行別的可執行文件,換句話說就是干別的事。所以可以把fork理解成在一塊內存空間上創造出個接口給exec執行新任務。Linux下的clone函數:我對它的理解就是fork和exec二合一,clone的作用就是產生新線程。1.6.2 線程安全要知道線程安全就得知道啥叫線程不安全。所謂線程不安全就是指多個線程同時訪問共享數據造成結果的不確定性。原子操作:絕對不會被打斷的操作。因為原子是化學反應中的最小微粒不可再分所以拿這個來比擬原子操作。它適用于簡單應用環境。解決線程不安全的通用方法是鎖。線程同步:一開始我

21、還以為是多個線程一起訪問某個資源呢,其實不然,線程同步是解決線程訪問同一數據資源的解決方式,保證了同一時間只有同一線程訪問數據資源,從而保證了線程安全。鎖二元信號量:最簡單的鎖機制。只允許一個線程獨占,一旦有線程占用,鎖就呈現占用狀態,其他線程無法訪問資源。否則,非占用狀態,可以接受線程。鎖多元信號量:就是它允許多個線程同步訪問資源,比二元信號量高能一些。我感覺信號量就像管道。一個線程想訪問資源它就必須首先獲取一個管道,這樣原來的管道數就少1,于是信號量首先減1。但是如果信號量減1以后成為負值,說明原來的管道數為0,即原來就已經沒有管道了,那么此時信號量機制就只能讓該線程等待了,這就是P原語。

22、而如果一個線程用完了資源想要釋放,那么它必須歸還它所使用的管道,那么管道總數應該加1,即信號量加1。正因為信號量已經加1,如果此時的信號量值為小于1,那說明在加1之前管道總量就已經透支了,而且先前那些因為沒有獲得管道的線程還在那等著呢。正好有個線程歸還了管道,V原語趕緊從那些等待的線程中找一個出來把管道給它,這就是在信號量值小于1的情況下喚醒線程的意思。鎖互斥量(Mutex):信號量與互斥量的區別是一個信號量可以被一個線程獲取并釋放給另一個線程使用,正如V原語的操作。而互斥量始終都是一個線程,上鎖是這個線程,這個線程不執行完就不解鎖。鎖臨界區:獲取臨界區的鎖為進入臨界區,釋放鎖為離開臨界區。它

23、的作用對象是某一位以進程,一旦某進程進入臨界區,其他進程就無法進入。除此之外,臨界區與互斥量相同。鎖讀寫鎖:互斥量、臨界區和信號量適用于讀寫都非常頻繁的場合,而讀寫鎖適用于讀頻繁而寫不頻繁的場合。它的工作規律可用下表表示:鎖寫鎖狀態以共享方式獲取以獨占方式獲取自由成功成功共享成功等待獨占等待等待鎖條件變量:相當于一個開關,它可以讓等待它的線程繼續等待也可以讓它們繼續執行。而這個開關需要一些其他的線程打開或關閉它。可重入函數:一個函數沒有執行完全,但是由于內部因素或者外部調用,又一次開始執行該函數。它不產生任何不良后果。產生可重入的條件:1、多線程共同執行該函數。2、函數自己直接或者間接調用自身

24、。可重入函數的特點:1、不使用任何(局部)靜態或全局的非const變量。因為如果使用的話它就涉嫌操縱共享數據,這樣會導致線程不安全。2、不返回任何(局部)靜態或全局的非const變量的指針。因為這同樣涉及到共享數據。3、僅依賴于調用方提供的參數。因為這樣可以把函數的執行過程局限在局部。4、不依賴于任何單個資源的鎖。單個資源的鎖不允許被中斷,這不符合可重入函數的定義。5、不調用任何不可重入函數。這個沒啥好說的,如果調用了,可重入函數就成了不可重入函數。可重入性質是并發安全的強力保證可在多線程環境下大膽使用。過度優化:P53這個例子就是說本來2個x+結果是2,但是經過上鎖以后卻是1,這證明即使通過

25、鎖機制也不能完全保障計算正確,這是計算機內部工作機制造成的線程不安全。CPU對程序的優化可能導致線程不安全,因為它會調整程序語句執行順序以達到CPU所謂的優化,這有時候很麻煩。Volatile關鍵字可以阻止這種優化。1、它阻止編譯器為提高程序執行速度將一個變量緩存到寄存器內而不寫回。2、它阻止編譯器調整語句執行順序。這兩件事就是volatile所做的具體工作。但是,volatile能管住編譯器管不了CPU,CPU還是能對指令進行動態調整。P54舉了一個double-check的例子,雖然現在我對這個沒有多深的理解,但是從這個例子中我看到作者是怎么分析的。它是將各個語句內部實際所進行的操作都列出

26、來進行分析的,這個值得我學習。雖然volatile管不了CPU,但是CPU有CPU相當于volatile的指令,一般這個指令叫做barrier。 多線程內部情況線程分為內核級線程和用戶級線程,內核級線程是用戶直接接觸不到的,用戶只能接觸到用戶級線程。3種內核級線程與用戶級線程的模型。1、一對一模型:就是每個用戶級線程都對應一個內核級線程,但反過來不是,因為內核級線程可能沒有用戶級線程與之對應。一般直接使用API或者系統調用創建的線程均為一對一模型。它的優點:真正實現線程的并發執行,線程之間彼此互不影響。它的缺點:1、許多操作系統限制了內核級線程的數量導致用戶級線程數量受限。2、許多操作系統用在

27、內核級線程調度上的開銷較大,主要為上下文切換開銷,致使用戶級線程執行效率低下。2、多對一模型:多個用戶級線程對應同一個內核級線程,線程的切換由用戶級代碼決定。作者說多處理器對提升處理速度沒有明顯幫助,這是當然的了,CPU處理的是內核級線程,而這個模型就在那擺著,CPU也只能按照這個模式來處理。再說了,一個線程只能在一個核上跑,你再多給幾個核也沒用啊。它的優點:它比一對一模型快,還有高效的上下文切換和近似無限制的線程數量。它的缺點:只要有一個線程阻塞,對應于同一個內核級線程的其他線程也無法執行,該內核級線程也阻塞,這很好理解,因為只有一條通路。3、多對多模型:是上面二者的合體。很顯然它能克服上述

28、二者的缺點,同理多處理器也無法顯著提升它的執行效率。第二章 編譯和鏈接2.1 被隱藏了的過程以前學的程序的執行過程是編輯、編譯、鏈接、執行。今天這本書把這個過程更加細化了,它以C語言中的helloworld程序為例進行說明,講的大概是從編譯到鏈接的過程。也是包括4步:1、預處理;2、編譯;3、匯編;4、鏈接。從這個順序可以看出在C語言中預處理是在編譯之前。2.1.1 預編譯預編譯是個獨立的過程,不同于源文件的.cpp格式和頭文件的.h格式,預編譯得到的文件后綴是.i或者.ii。預編譯的主要動作就是處理代碼中以#開頭的指令,具體可見P64這些步驟。因為宏已經展開所以.i文件不包含任何宏定義。可以

29、根據.i文件查看宏定義和文件包含是否正確。預編譯需要預編譯器。2.1.2 編譯編譯的過程是把預處理得到的文件進行詞法分析、語法分析、語義分析和優化后生成相應的匯編代碼文件。2.1.3 匯編匯編階段是通過匯編器完成的,其作用就是把匯編指令轉換成機器指令。匯編結束以后生成目標文件.obj。2.1.4 鏈接鏈接簡而言之就是把目標文件鏈接在一起生成可執行文件的過程,但是實際上這是一個非常復雜的過程,并不像看上去那么簡單。2.2 編譯器做了什么編譯的過程可以分為掃描、語法分析、語義分析、源代碼優化、代碼生成、目標代碼優化等6步。2.2.1 詞法分析這一過程是交給掃描器執行的,目的是把程序語句劃分成若干記

30、號。這些記號一般包括:1、關鍵字;2、標識符;3、字面量(數字,字符串等);4、特殊符號(加號,等號等)。此外,掃描器還將標識符放到符號表,將字面量放到文字表中以備后用。詞法分析需要此法掃描器。2.2.2 語法分析它是對詞法分析產生的各種記號進行語法分析,并產生一顆語法樹。語句內容含義的區分,語法的檢查等都是在此階段完成的。語法分析需要語法分析器。2.2.3 語義分析語義分析需要語義分析器。語義分析就是分析該語句的意思,就是它能做什么,有啥用。編譯器所能做的包括靜態語義分析和動態語義分析。靜態語義:編譯期能夠確定的語義,它主要包括類型和聲明的匹配,類型的轉換等。我想C+中的靜態綁定應該也屬于靜

31、態語義吧。動態語義:運行期能夠確定的語義以及相關問題,比如說異常處理。我同時在想C+中的動態綁定應該屬于動態語義。語義分析對語法樹各節點進行了類型標記和類型轉換,還更新了符號表里的符號類型。2.2.4 中間語言生成編譯器有很多層次的優化,源碼級別的優化是其中一個層次。源碼級的優化需要源碼級優化器。這個優化是把語法樹轉換成中間代碼,并在中間代碼上進行的。常見的中間代碼有三地址碼和P代碼。中間代碼將編譯器分成了前端和后端,前端負責產生與機器無關的中間代碼,后端負責把中間代碼轉換成目標代碼??缙脚_的編譯器并不是放在任意一個平臺上都絕對能用,只不過它能支持的平臺很多而已。這是因為編譯器使用同一個前端,

32、而針對不同的平臺使用不同的后端。2.2.5 目標代碼的生成與優化編譯器的后端包括代碼生成器和目標代碼優化器。代碼生成器將中間代碼轉換成目標代碼,該過程依賴于目標機器。目標代碼優化器對目標代碼進行優化,比如選擇合適的尋址方式,以移位代替數乘等?,F在的編譯器非常復雜,上述提到的這些方面也變得非常復雜。變量和函數的地址都是在最終鏈接的時候才確定的,然后變成可執行文件。2.3 鏈接器年齡比編譯器長作者把鏈接比喻為拼圖的拼接。2.4 模塊拼接靜態鏈接將源代碼模塊組裝起來的過程就是鏈接。鏈接的過程包括:1、地址和空間分配;2、符號決議;3、重定位等。.obj文件即目標文件和庫一起鏈接成可執行文件。庫是由一

33、些常用的代碼編譯成的目標文件的包,是一個集合。最常見的庫是運行時庫,是支持程序運行的基本函數的集合。每個目標文件都是單獨編譯的。模塊A想要調用模塊B的C函數,A必須要知道C的地址,但是現在A不知道C的地址,但是A給C留了位置,等到鏈接器鏈接時再在這個位置上填上C的地址。如果C的地址被改動了,A中所有調用C的地方都需要進行相應的更改,這些都可藉由鏈接器完成。這是靜態鏈接的基本功能和作用。在鏈接的過程中需要對目標文件中定義在其他目標文件中的函數和變量的調用指令進行重新調整,注意這里說的是指令!書中舉的例子意在說明,當目標文件A調用目標文件B中的變量C時,因為暫時無法知道C的位置,所以指令先把表示C

34、的位置置為某一值,等到鏈接的時候再把這值修正為C的地址,這一過程叫做重定位,像C這樣的位置被稱為重定位入口。第三章 目標文件里有什么.obj是目標文件,所以可以知道目標文件是指編譯后生成的文件,目標文件幾乎和可執行文件相同只是稍微有點不同而已。其不同之處在于有些符號和地址沒有被調整。3.1 目標文件的格式正是因為目標文件與可執行文件幾乎相同,所以它們的存儲格式是一樣的,可以把它們近似看成同一種文件。Linux下的動態鏈接庫格式為.so,Windows和Linux下的靜態鏈接庫格式分別為.lib和.a。靜態鏈接庫是一個文件,該文件包含了很多目標文件,它是一個整體。Linux下的可執行文件是按照E

35、LF格式存儲的,ELF標準包含4種文件,請看P81。我所熟悉的Windows下的DLL就屬于共享目標文件。3.2 目標文件是什么樣的目標文件一般包含了哪些內容?編譯后的機器指令代碼、數據、連接所需的信息、符號表、調試信息、字符串等。目標文件把信息按照屬性的不同分段存儲。寫到這里我感覺這書上說的與老師課上講的程序在內存中的分段方法有些相似。在目標文件中,編譯后的機器指令代碼放在代碼段(Code Section)中,段名一般為.code和.text。全局變量和靜態變量放在數據段(Data Section)中,段名一般為.data。BSS段(Block Started By Symbol)用來存儲未

36、初始化的靜態變量和全局變量。話雖如此bss中并沒有這些變量的內容,它只是為這些變量按照所占空間大小預留空間而已。由于這些變量默認就是0,所以壓根沒必要再為它們分配一個數據0,也沒有必要讓它們待在data段中。因此bss的作用是為這些變量預留空間。另外目標代碼還有一個文件頭用來保存該目標文件的信息,它里面還有一個段表。源代碼被編譯以后生成兩種段數據段和指令段,.code.text屬于指令段.data.bss屬于數據段。這樣分主要有3點好處:1、防止程序被有意無意篡改。這是因為指令段只讀,數據段可讀寫。2、提高了緩存命中率。3、節省內存空間。因為指令段可被多個副本共享,但是副本可以擁有自己的數據段

37、。3.3 挖掘SimpleSection.o原來目標文件中的段還有只讀數據段(.rodata)、注釋信息段(.comment)、堆棧提示段(.note.GNU-stack)。從書中所給的例子來看一個ELF文件只有4個段是由內容的,即.data、.text、.rodata、.comment。從圖3-3可以看出在內存中,從低地址到高地址是按照ELF header、text、data、rodata、comment、other data的順序存放的。3.3.3 BSS段由本小節可知,全局變量可能因為語言和編譯器的不同不一定存放在bss段,但是靜態變量一定存放在bss段。雖說bss存放的是未初始化的靜態

38、和全局變量,但是有些變量如果被初始化為0,它也會被放在bss中,這是編譯器的優化,有時候這種優化會帶來麻煩。3.3.4 其他段表3-2列出了其他段及意義。此外,這個段還可以自定義。3.4 ELF文件結構描述圖3-4展示了ELF的層次結構。最重要的兩個部分就是ELF文件頭和段表。ELF文件頭描述整個文件的基本屬性,段表描述各段的信息。3.4.1 文件頭清單3-2清楚地描述了ELF文件頭的信息,P95黑體部分列舉了ELF文件頭包含的信息。ELF文件兼容各平臺,它的文件結構和相關參數定義在”/usr/include/elf.h”里,它有32位和64位兩種。表3-3展示了elf.h的自定義變量體系。表

39、3-4展示了ELF文件頭結構成員含義。ELF魔數:ELF文件頭的第一個字段是Magic,包含16bytes,對應于Elf32_Ehdr中的e_ident成員。Magic用來表示平臺的各種屬性。14個字節是所有ELF文件都相同的標識碼,分別對應del、E、L、F,這四個字節就是ELF魔數。操作系統通過確認魔術是否正確以決定是否加載可執行文件。第5個字節用來表示ELF文件是32位的還是64位的。第6個字節用來表示ELF字節序。第7個字節用來表示ELF文件版本號。后面的9個字節用來預留,有些平臺可能用來作為擴展標志。Elf32_Ehdr中的e_type成員表示ELF文件類型,ELF總共有三種文件類型

40、如表3-5所示。操作系統是通過判斷文件類型而不是擴展名來確定ELF文件類型的。Elf32_Ehdr中的e_machine成員表示ELF文件的平臺屬性。雖然ELF遵循統一標準但不代表同一ELF文件可以在不同平臺上使用。3.4.2 段表它用來表示各個段的信息,ELF文件中的段是由段表決定的。一個ELF文件不僅僅包含像data、text、bss這樣的段,還包括其他的輔助性段。段表是一個Elf32_Shdr類型的結構體數組,元素的個數代表段的個數,每個元素對應一個段。這個Elf32_Shdr被稱為段描述符。表3-7描述了Elf32_Shdr中各字段的意義。段的名稱對于編譯和鏈接有意義,對操作系統無意義

41、。決定段的類型的是段的類型字段,并不是段的后綴名和名稱。段的類型和段的標志位字段決定了段的屬性。表3-8展示了段的各種類型。段的標志位表示該段在進程虛擬地址空間中的屬性,如是否可讀。表3-9列出了段的各種屬性。表3-10列出了系統保留段的各種屬性。段的連接信息包括sh_link和sh_info,它們與鏈接相關,如表3-11所示。3.4.3 重定位表目標文件中有一個SHT_REL的.rel.text字段,它是重定位表。重定位發生在連接的過程中,這個在前面已經講過,重定位表記錄了重定位相關信息。3.4.4 字符串表顧名思義,就是用來表示各種名稱的字符串的表。它是一個裝有各種字符串的表格,每個字符在

42、表中都有一個固定的位置。這種表在ELF文件中保存為2種形式.strtab和.shstrtab,它們分別是字符串表和段字符串表,它們在ELF文件中都以獨立的段而存在。為了輕松地找到這個段,在ELF文件頭中包含了這兩個段的下標,名為e_shstrndx。3.5 鏈接的接口符號鏈接是組合目標文件的過程,目標文件是根據彼此之間的地址相互引用,從而組合成可執行文件的。而,這個地址可以簡單地理解為目標文件中的函數和變量。在這里,函數和變量統稱為符號,函數名和變量名統稱為符號名。鏈接器的著眼點主要在定義在本目標文件和定義在其他目標文件的全局性符號,因為只有這些涉及到目標文件之間的組合。3.5.1 ELF符號

43、表結構ELF文件的符號表是一個段,段名為“.symtab”,它是一個Elf32_sym類型的數組,每個數組元素代表一個符號。在Elf32_sym結構體中有一個32bit成員叫st_info,低4bit表示符號的類型,高28bit符號的綁定信息。綁定信息具體可見表3-15,符號類型可參見表3-16。Elf32_sym.st_shndx:如果符號定義在本目標文件中,它表示該符號所在的段在段表中的下標,否則它具有其他意義。st_shndx具體信息可見表3-17。Elf32_sym.st_value:每個符號都有一個對應值,它一般為變量和函數的地址。st_value的意義有如下幾種:1、如果符號定義在

44、目標文件中,并且它不是COMMON塊類型,則st_value代表符號在段中的偏移。2、如果符號定義在目標文件中并且是COMMON塊類型,則st_value表示符號的對齊屬性。3、在可執行文件中st_value表示符號的虛擬地址。3.5.2 特殊符號鏈接器本身自帶的,不是你定義的,定義在鏈接腳本中的,但是你可以用的,這樣的符號是特殊符號。它們存在的時機是鏈接器鏈接生成可執行文件時,此時鏈接器會將它們解析成正確的值,書中P110舉了幾個具有代表性的特殊符號。3.5.3 符號修飾與函數簽名本小節明確了函數簽名的概念。函數簽名:主要是指函數名和參數類型,其次是所在類和命名空間等。它用于區分不同函數。編

45、譯器和連接器會使用名稱修飾的辦法加工函數簽名使之成為修飾后名稱,在C+中為符號名。不同的編譯器對函數簽名的修飾方法不同,這導致不同種類的目標文件無法互連。原來C+編譯器已經默認定義了宏_cplusplus來兼容C語言和C+。3.5.5 弱符號和強符號在不同目標文件中含有相同全局性符號定義,這種情況被稱為強符號,它會引起符號重定義。C/C+編譯器認為未初始化的全局變量是弱符號。這個強弱符號是可以被定義的,所以強弱之別是根據定義來劃分的,并不針對符號的引用,P117代碼說明了這一點。鏈接器根據符號的強弱來處理和選擇定義的全局變量:1、不允許多次定義強符號,否則報錯。2、同一個符號在各目標文件中出現

46、了多次,但只有一個是強符號,那么編譯器選擇強符號的那個。3、如果一個符號在所有目標文件中都是弱符號,那么編譯器選擇占用空間最大的一個。由此可見編譯器對于弱符號的選擇并不明顯,所以由弱符號造成的錯誤也相對難以發現。強引用:目標文件對于非本目標文件的符號引用,在鏈接成可執行文件的過程中,如果找不到該符號的定義,就報未定義錯誤。弱引用:與強引用差不多,只不過在找不到符號時不報錯。強弱引用主要用于庫的鏈接。對于未定義的弱引用,編譯器為便于識別把它看作是某一值,一般為0。弱符號與COMMON塊聯系較密切。弱引用是可以手動聲明的,如P118第一段代碼所示。弱符號的作用在于提供一個默認的庫符號,但是當用戶想

47、要自定義該符號的時候,該自定義符號就獲得了更高的優先級。而弱引用的作用在于增強了程序的可擴展性,因為有了弱引用程序功能更強,沒有弱引用程序也能正常運行。3.6 調試信息目標文件和可執行文件中都可能保存調試信息,ELF文件采用DWARF格式保存調試信息。由于調試信息與可執行文件最終結果無關,而且占用大量空間,所以在發布軟件時應該去掉這些調試信息。第4章 靜態鏈接靜態鏈接是指將目標文件鏈接在一起形成可執行文件的過程。4.1 空間與地址分配4.1.2 相似段合并靜態鏈接過程是把各目標文件中的各段合并到可執行文件中的相應段中。鏈接器為目標文件分配地址和空間。這個空間有兩層含義,既包括在可執行文件中占有

48、的空間也包括在虛擬地址中分配的空間。其中虛擬地址空間的分配關系重大。靜態鏈接的過程一般分兩步1、空間與地址分配。2、符號解析與重定位。第一步就是獲取段信息,合并段將它們映射到可執行文件的段表信息中。整理符號和引用并放入全局符號表中。第二步,實際上就是鏈接,把目標文件中的地址呀、符號呀、數據等進行重定位然后鏈接。VMA:Virtual Memory AddressLMA:Load Memory Address鏈接前的VMA都是0,鏈接后就有實實在在的地址了。4.1.3 符號地址的確定符號地址在原來的目標文件中的每個段中都有一個偏移量,這個偏移量是固定的,所以在鏈接的過程中只要在虛擬地址的基礎上再

49、加上這個偏移量就是某符號在虛擬地址空間中的地址。4.2 符號解析與重定位在空間和地址分配完成以后,鏈接器即將進行符號解析與重定位。本小節舉了個例子,用了很多匯編代碼,有些晦澀難懂。目標文件中使用的都是虛擬地址不是物理地址,這一點很重要。目標文件的起始地址都是0。4.2.2 重定位表它存儲著與重定位相關的信息。每個要被重定位的ELF段都對應一個重定位表,重定位表本身也是一個段,所以你也可以叫重定位表為重定位段。每一個要被重定位的地方叫做重定位入口。重定位入口的偏移表示入口在要被重定位的段中的位置。重定位表的實質是一個Elf32_Rel的結構體數組,每個數組元素對應一個重定位入口。4.2.3 符號

50、解析重定位的過程伴隨著符號解析的過程。每個重定位的入口對應一個符號引用,鏈接器會查找有所有目標文件的符號表所組成的全局符號表,然后根據這個全局符號表進行重定位。4.2.4 指令修正方式32位x86平臺下的ELF文件的重定位入口所修正的指令尋址方式只有2種:絕對近址32位尋址和相對近址32位尋址。修正的位置長度為4bytes。經過絕對地址修正方式修正得到的地址是該符號的實際地址,而相對地址尋址方式得到的是符號與被修正位置的距離。4.3 COMMON塊相同的符號定義在多個不同的目標文件中,但是類型各不相同,這說明它們不是同一個變量或者函數,因此不能對它們進行相同的操作。但是鏈接器只認符號不認類型,

51、它認為它們都一樣。這種情況主要分為3種:1、至少2個強符號類型不一致。2、一個強符號和多個弱符號類型不一致。3、至少2個弱符號類型不一致。強符號是指定義在目標文件中全局性符號,包括函數和變量,顯然它們如果有相同的多個,那就是重定義,這本身就會報錯?,F在的編譯器和鏈接器都支持COMMON塊機制。它主要針對的對象是弱符號。如果在眾多符號之中有一個符號是強符號,那么符號所占空間與強符號相同。如果弱符號大小超過強符號,編譯器會發出警告。編譯器為什么不把未初始化的全局變量當做未初始化的局部靜態變量處理?為什么不在bss中給它們分配空間,而非要把它們標記為COMMON類型呢?因為編譯時編譯器不知道弱符號需

52、要多大空間,所以這時無法為其在BSS中分配空間,只能當做局部靜態變量處理。但是在鏈接的時候可以確定,所以鏈接以后才在BSS中分配空間。編譯器把所有未初始化的全局變量都當成COMMON類型處理,這樣做是為了與強類型分開,凡是非COMMON類型的都是強類型。多個強類型的符號會發生重復定義的錯誤。4.4.1 重復代碼消除C+在很多時候會產生重復代碼,模版是其中最具代表性的一個。模版可以在不同的編譯單元被實例化成相同的類型,兩個完全一樣的類是完全沒有必要的,一個足矣。不解決代碼重復問題會導致:1、空間浪費。這個根本就不用解釋。2、地址容易出錯。因為是多個相同的實例嘛,就會有多個指針分別指向這些實例,但

53、是這些實例之間沒差別,它們在邏輯上是同一函數,這就容易造成指針的誤指。3、指令運行效率較低。緩存機制會緩存多份重復的代碼,但是程序只會用特定的一份,在這么多份相同的代碼中找特定的一份不好找,成功率較低,即,緩存命中率低。解決方案:把每個編譯單元中的每個模版的不同實例分別放進不同的段中,并且對不同的單元都這樣做,這樣在最后鏈接的時候不同編譯單元中的相同實例段就合并從而消除多份相同的實例。缺點:不同的編譯單元可能使用了不同的編譯器版本或者優化選項,這會導致實際產生的代碼不同,鏈接器必須選擇其中一個副本。函數級別鏈接:默認情況下鏈接器會把所有的目標文件鏈接在一起,不管有用的代碼還是沒用的代碼,這會導

54、致可執行文件很大。所謂函數級別鏈接就是每個編譯單元也把函數單獨放進一個段中,在鏈接的時候只鏈接那些有用的函數段。這種做法會減慢編譯和鏈接的過程,因為段的數量增加了。4.4.2 全局構造與析構在C+中全局對象的構造在main之前完成,析構在main之后完成。在ELF文件中有.init和.fini兩個段。init段包含了進程的初始化代碼,在main之前執行。fini段包含了進程的終止代碼,在main之后執行。C+的全局構造和析構由此實現。4.4.3 C+與ABI把不同編譯器產生的目標文件鏈接在一起需要特定的條件相同的ABI(Application Binary Interface)。ABI:符號修

55、飾標準、變量內存布局、函數調用方式等與二進制兼容性相關的內容。C語言間的目標文件能否互相兼容具體決定于如下幾個方面:1、內置類型大小和存儲方式。2、組合類型大小和存儲方式。3、外部符號與用戶定義的符號之間的命名方式和解析方式。4、函數調用方式。5、堆棧分布方式。6、寄存器使用方式。C+在這方面的決定因素P141+P142介紹。C+代碼不僅對于由不同編譯器編譯得到的目標文件不兼容,而且就算是同一編譯器的不同版本編譯得到的目標文件也不兼容。這都是ABI鬧的。4.5 靜態庫鏈接開發環境往往附帶語言庫,這些庫是對系統API的封裝。大部分的C語言庫函數都調用了系統API,少數除外。靜態庫實際上可以看成是一組目標文件的集合。C語言中看似簡單的庫函數和系統中眾多的API存在著依賴關系。靜態鏈接的過程分為三步:1、調用C語言編譯器。把C語言程序變成匯編語言程序。2、調用匯編器。把匯編程序變成目標文件。3、調用鏈接器鏈接成可執行文件。4.6 鏈接過程控制WINDOWS內核其實就是一個文件WINDOWSsystem32ntoskrnl.exe。雖然大多數情況下,鏈接器是對目標文件進行鏈接,但是對

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯系上傳者。文件的所有權益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網頁內容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
  • 4. 未經權益所有人同意不得將文件中的內容挪作商業或盈利用途。
  • 5. 人人文庫網僅提供信息存儲空間,僅對用戶上傳內容的表現方式做保護處理,對用戶上傳分享的文檔內容本身不做任何修改或編輯,并不能對任何下載內容負責。
  • 6. 下載文件中如有侵權或不適當內容,請與我們聯系,我們立即糾正。
  • 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論