天天看點

《程式員的自我修養——連結,裝載與庫》讀後總結

第一章 溫故而知新

1.1 從Hello World說起

一開始從列印一個“hello world”這個程式說起,不僅僅停留在表面,給我提出了一個疑問?列印出 “hello world”是如何實作的,提出了一系列的問題,這部分問題的大部分答案,我也不清楚,這裡留下了一個懸念,問題放到了下面。

《程式員的自我修養——連結,裝載與庫》讀後總結

1.2 萬變不離其宗

這一部分将抽象的計算機概念,進行了具體化,一個計算機的組成最重要的三個部件,中央處理器CPU,記憶體和I/O控制晶片。為了協調CPU,記憶體和高速的圖形裝置,人們想到了一系列的方法,設計南橋和北橋,然後技術越來越先進,在2004年,cpu的頻率從那時再也沒有發生質的提高,原因是人類在cpu的工藝達到實體極限,然後人類通過增加cpu的數量來提高效率,然後就有了多核處理器。

《程式員的自我修養——連結,裝載與庫》讀後總結

1.3 站的高,望的遠

本段介紹了系統軟體的體系結構,如下圖:

《程式員的自我修養——連結,裝載與庫》讀後總結

每個層次之間都需要通信,既然需要通信就必須有一個通信的協定,我們将其稱位 “接口”,接口的下面那層是接口的提供者,每個中間層都是對其下層的包裝和擴充。從整個層次結構上看,開發工具與應用工具屬于同一個層次,他們都使用作業系統應用程式程式設計接口。應用程式接口的提供者是運作庫,運作庫是對系統調用接口的包裝和擴充。系統調用是在作業系統核心實作的,系統調用接口的實作往往以軟中斷的方式提供。作業系統核心層,是對硬體接口的使用者,而硬體是接口的定義者。

這裡提供兩個圖來更好的了解:

《程式員的自我修養——連結,裝載與庫》讀後總結
《程式員的自我修養——連結,裝載與庫》讀後總結

1.4 作業系統做什麼

提供抽象的接口,管理硬體資源

1.4.1 不要讓cpu打盹

開啟多道設計程式,分時系統,cpu配置設定方式,搶占式,讓cpu盡最大可能的去忙碌。

1.4.2 裝置驅動

通過作業系統中的硬體驅動程式,去完成硬體的驅動,例如檔案的讀取,通過硬體驅動程式,讀取磁盤上的資料,将資料讀取到,事先設定好的記憶體位址中。

1.5 記憶體不夠怎麼辦

這一段其實主要講的是虛拟記憶體,由于主存的存儲空間不足,通過輔存,從邏輯上擴充記憶體,這個是一個虛拟的概念,對我們程式員來說,我們看到的記憶體是虛拟的,當我們通路到的虛拟位址不存在于主存中時,cpu會陷入缺頁中斷,通過交換,分頁和排程,會将磁盤中的一頁調入記憶體,或者當主存被占滿時,通過排程機制,将不常用的一頁調出主存,寫入磁盤,将我們需要的一頁調入主存,然後給我們一個假象,我們擁有很大的記憶體,并通過虛拟記憶體可以實作程序的虛拟位址空間的隔離。

1.6 衆人拾柴火焰高

這一段主要講線程,然後闡述了線程與程序的關系:

《程式員的自我修養——連結,裝載與庫》讀後總結

然後線程的優點,通路權限,線程之間共享程序中的全局變量,堆上資料,函數裡的靜态變量,程式代碼,任何線程都有權利執行讀取程序中的任何代碼,打開檔案,A線程打開檔案,B線程對檔案進行讀寫。線程私有,局部變量,函數參數,線程局部資料。線程的執行狀态:等待,就緒,運作

還有三态轉換:

《程式員的自我修養——連結,裝載與庫》讀後總結

線程排程,每個線程都被配置設定了一定的時間片和優先級,線程優先級改變的方法:

使用者指定優先級,根據進入等待狀态的頻繁程度來提高或降低優先級。長時間得不到執行而被提升優先級。

1.6.2 線程安全

主要講述了線程安全的一些措施,利用鎖,讀寫鎖,信号量,條件變量,原子操作。但是我覺比較新奇的是,過度優化那裡,編譯器過度優化會改變指令順序,cpu會改變指令的順序,在多線程下會造成資料不一緻的問題,但是許多體系結構的cpu會提供barrier指令,防止線程不安全。還有就是核心線程和使用者線程的對應關系,現在基本采用多對多的關系,好處是,使用者現程數量可以多個,使用者線程中一個阻塞一般不會影響其他使用者線程。

第二部分編譯和連結

2.1 被隐藏了的過程

一個簡單的hello.c 程式如下:

#include<stdio.h>
int main(){
printf("hello world!");
return 0;
}      

在Linux下當我們使用 gcc hello.c -o hello 時,會一氣呵成的生成一個hello 可執行檔案

./hello 就可以輸出 “hello world!”,事實上上述過程可以分為四個步驟,預處理,編譯,彙編,連結,如下圖所示:

《程式員的自我修養——連結,裝載與庫》讀後總結

2.1.1 預編譯

預編譯的過程就是對一些#進行一些處理,處理步規則如下:

《程式員的自我修養——連結,裝載與庫》讀後總結

2.1.2 編譯

編譯部分其實就是将預處理後的代碼變成彙編代碼,這裡涉及語義分析,詞法分析,文法分析及優化後的彙編代碼,這個過程很複雜。

2.1.3 彙編

這部分就是将彙編代碼,變成機器可以執行的機器碼。

2.1.4 連結

連結通常是一個比較讓人費解的過程。文章裡提供了一系列問題。

《程式員的自我修養——連結,裝載與庫》讀後總結

連結就是把一大堆檔案連結成一個可執行檔案。

2.2 編譯器做了什麼

編譯器就是将進階語言翻譯成機器語言的一個工具。

編譯的過程一般分為六部:掃描,文法分析,語義分析,源代碼優化,代碼生成和目标代碼優化。

《程式員的自我修養——連結,裝載與庫》讀後總結

2.2.1 詞法分析

源代碼程式被輸入到掃描器,掃描器的任務很簡單,他隻是簡單的進行詞法分析,運用類似于有限狀态機的算法,将字元串序列分割成一系列記号。例如這個程式:

《程式員的自我修養——連結,裝載與庫》讀後總結

他被分割後,如下圖所示,字元串被分割成一些列的記号,記号有自己的類型:

《程式員的自我修養——連結,裝載與庫》讀後總結

詞法分析産生的記号一般可以分為如下幾類:關鍵詞,辨別符,字面量和特殊字元。

《程式員的自我修養——連結,裝載與庫》讀後總結

掃描器做了以上的工作。有一個叫lex的程式可以實作詞法掃描,他會按照使用者之前描述好的詞法規則對字元串分割成一個個記号。因為這樣一個程式的存在,程式開發者就無須為每一個編譯器開發一個獨立的詞法掃描器,而是根據需要改變詞法規則就可以了。

2.2.2 文法分析

接下來文法分析器将對掃描器産生的記号進行文法分析,進而産生文法分析樹,整個分析過程采用上下文無關文法的分析手段。

《程式員的自我修養——連結,裝載與庫》讀後總結

由于我根本不懂上下文無關文法,是以我決定去看看。

《程式員的自我修養——連結,裝載與庫》讀後總結

看完定義,說實話沒看懂,等會去可靠他說的例子,我們先看看它上面的哪些東西是如何定義的。

《程式員的自我修養——連結,裝載與庫》讀後總結

這些東西這裡還是不做深入了解了,畢竟我也不懂,也不是一時半會能學會的,都怪我編譯原理沒好好學,想要深入去學編譯原理吧,這裡淺嘗辄止即可。簡單的講由文法分析器産生的文法分析樹就是以表達式為節點的樹。類似于表達式求值那道算法題,不知道你們寫過沒有,解法有用棧來寫,還有用文法分析樹來寫。下圖是上圖表達式的文法分析樹:

《程式員的自我修養——連結,裝載與庫》讀後總結
《程式員的自我修養——連結,裝載與庫》讀後總結

正如前面詞法分析有lex一樣,文法分析也有一個現成的工具叫做yacc。他像lex一樣可以根據使用者給定的文法規則,對輸入的記号序進行解析,進而建構出一顆文法樹。對于不同的編譯語言,編譯器開發者隻需改變文法規則即可,無需為每一個編譯器寫一個文法分析器。

2.2.3 語義分析

《程式員的自我修養——連結,裝載與庫》讀後總結

文法分析僅是完成表達式方面的分析,但是她并不了解這個語句是否有真正的意義。檢查合法性

《程式員的自我修養——連結,裝載與庫》讀後總結

比如這個,我讓一個野指針去和b相乘,a和b不是同一個類型,這樣在文法上面是沒有任何問題的,但是語義上面卻是不被允許的。

《程式員的自我修養——連結,裝載與庫》讀後總結
《程式員的自我修養——連結,裝載與庫》讀後總結

通過上圖的話,應該可以了解,語義分析的重要性。

《程式員的自我修養——連結,裝載與庫》讀後總結

被辨別了類型的文法樹,經過語義分析的文法樹成為上圖的樣子。

中間層語言生産

現代編譯器有着很多成優化,有些代碼寫的實在累贅,是以編譯器需要把他優化。

《程式員的自我修養——連結,裝載與庫》讀後總結

優化後,就會變成另一個模樣的文法樹:

《程式員的自我修養——連結,裝載與庫》讀後總結

2.2.5 目标代碼生産與優化

《程式員的自我修養——連結,裝載與庫》讀後總結

這裡講了下如何生成目标代碼要有代碼生成器和目标代碼優化器,讓我們得到的最終代碼是最優的形式。

《程式員的自我修養——連結,裝載與庫》讀後總結

這句話很有趣,其實他的意思就是多檔案編寫,比如我昨天做的那個手動連結那個小實驗,兩個代碼不在同一個檔案,當我們在一個檔案裡面使用了另一個檔案的函數,然後需要重定位,各種各種的東西,連結器可以幫我們做了,可以看我的那篇部落格,寫的很清晰:​​使用裸ld手動連結c程式​​

2.3 連結器年齡比編譯器長

這部分前面講了連結器的曆史,然後講了一下程式并不是一寫好就是一直不變的。

《程式員的自我修養——連結,裝載與庫》讀後總結

解釋了什麼是重定位。

《程式員的自我修養——連結,裝載與庫》讀後總結

2.4 子產品拼裝——靜态連結

寫一個數百萬行的代碼在一個檔案裡,是十分難以維護的,是以出現量子產品化,将一個大項目,分割成很小的子產品,每個子產品獨立編譯,然後按照要求将他們組裝起來,就成了一個大的子產品,這個過程稱位連結。

《程式員的自我修養——連結,裝載與庫》讀後總結

這句話清楚的解釋了連結的含義。

《程式員的自我修養——連結,裝載與庫》讀後總結

闡述靜态連結和動态連結在細節上的命名規範。

《程式員的自我修養——連結,裝載與庫》讀後總結

上圖這個過程,在預編譯階段将頭檔案個複制一份插入源代碼,經曆編譯彙編生成可重定位的目标代碼,再連接配接階段,将運作庫裡的目标代碼連結進來,最終成為一個可執行檔案。

《程式員的自我修養——連結,裝載與庫》讀後總結

運作時庫,就是對系統調用的進一步分裝。

《程式員的自我修養——連結,裝載與庫》讀後總結

這斷話舉了一個很好的例子,看完他,會有一個自己的了解。

2.5 本章小結

《程式員的自我修養——連結,裝載與庫》讀後總結

第三章 目标檔案裡有什麼

《程式員的自我修養——連結,裝載與庫》讀後總結

3.1 目标檔案的格式

Windows下的可執行檔案格式為PE,和Linux的ELF,他們都是COFF格式的變種。目标檔案就是源代碼編譯後 但未進行連結的那些中間檔案(Window下的.obj和Linux下的.o)他跟可執行檔案的内容與結構很相似,是以一般跟可執行檔案的格式一起采用一種格式存儲。動态連結庫和靜态連結庫都按照可執行檔案格式進行存儲。

《程式員的自我修養——連結,裝載與庫》讀後總結
《程式員的自我修養——連結,裝載與庫》讀後總結

3.2目标檔案是什麼樣的

目标檔案的内容至少有編譯後的機器指令代碼,資料。沒錯除了這些資訊,還包括連結時所必要的資訊,字元串符号表等。

《程式員的自我修養——連結,裝載與庫》讀後總結
《程式員的自我修養——連結,裝載與庫》讀後總結

上圖的File Header檔案描述了檔案的很多資訊,包括段表,檔案是否可執行是靜态連結還是動态連結等資訊。

《程式員的自我修養——連結,裝載與庫》讀後總結

接下來講了c語言編譯後的執行語句都儲存在.text段;初始化的全局變量和局部靜态變量都儲存在.data段,而未初始化的全局變量和靜态局部變量儲存至.bss段,運作時這一段确實占用記憶體空間,但是.bss段在檔案中并不占據空間,隻是為未初始化的全局變量和靜态變量預留位置而已。

《程式員的自我修養——連結,裝載與庫》讀後總結

為什麼要把程式的指令和資料分開存放,這樣有什麼優勢?

下面是解釋:

《程式員的自我修養——連結,裝載與庫》讀後總結
《程式員的自我修養——連結,裝載與庫》讀後總結

3.3 挖掘SimpleSection.o

以下面這個代碼為例來講解:

《程式員的自我修養——連結,裝載與庫》讀後總結
《程式員的自我修養——連結,裝載與庫》讀後總結

我們使用gcc去編譯這個代碼:

gcc -c SimpleSection.c

然後我們檢視這個檔案的内部結構

《程式員的自我修養——連結,裝載與庫》讀後總結

結構圖如下:

《程式員的自我修養——連結,裝載與庫》讀後總結

然後接下來說SimpleSection.o段的數量比我們想象的要多,接下來将段的種類:

《程式員的自我修養——連結,裝載與庫》讀後總結
《程式員的自我修養——連結,裝載與庫》讀後總結

有一個專門的指令叫做size他可以用來檢視ELF檔案的代碼段,資料段和BSS段的長度。

《程式員的自我修養——連結,裝載與庫》讀後總結

3.3.1 代碼段

将.o檔案以16進制的形式輸出,.text段最左邊是偏移量。

《程式員的自我修養——連結,裝載與庫》讀後總結
《程式員的自我修養——連結,裝載與庫》讀後總結

3.3.2 資料段和隻讀資料段

隻讀資料段的好處:

《程式員的自我修養——連結,裝載與庫》讀後總結

3.3.3 BSS段

講bss段的内容

《程式員的自我修養——連結,裝載與庫》讀後總結

Quiz 變量存放的位置

現在來做個小實驗

static int x1=0;

static int x2=1;

x1,x2會被存放在什麼段中呢?

答案:x1會被存放在.bss段,x2會被存放在.data段,原因:bss段的資料預設為0,而x1=0,是以被優化掉了,放到bss段,節省磁盤空間。

3.3.4 其他段

除了.text,.data,.bss這三個最常用的段之外,ELF檔案也有可能包含其他段,用來儲存與程式相關的其他資訊。表3-2中列舉了ELF的一些常見段。

《程式員的自我修養——連結,裝載與庫》讀後總結

插入的段名不能和不能使用”.“作為字首,否則容易根系統保留的段名起沖突。

《程式員的自我修養——連結,裝載與庫》讀後總結

在代碼中使用圖檔,音頻,等其他東西作為目标檔案,将其制成一個段,該怎麼做?

《程式員的自我修養——連結,裝載與庫》讀後總結

從上面,我覺得通過代碼,操作檔案可能就是把檔案制程一個二進制的.obj,連結進來進行操作。自定義段

通過__attribute__((section())) 變量 可以将 變量放到我們指定的段中。

《程式員的自我修養——連結,裝載與庫》讀後總結

3.4 ELF檔案結構描述

《程式員的自我修養——連結,裝載與庫》讀後總結

3.4.1 檔案頭

《程式員的自我修養——連結,裝載與庫》讀後總結
《程式員的自我修養——連結,裝載與庫》讀後總結

魔術的作用:

《程式員的自我修養——連結,裝載與庫》讀後總結

可以去搜一下elf檔案中魔術的由來

3.4.2 段表

《程式員的自我修養——連結,裝載與庫》讀後總結

段表是一個結構體Elf32_Shdr結構體數組,數組元素等于段的個數。

《程式員的自我修養——連結,裝載與庫》讀後總結

結構體的結構,感興趣的可以自行去查。

3.4.3 重定位表

《程式員的自我修養——連結,裝載與庫》讀後總結

3.4.4 字元串表

在ELF檔案頭就可以得到字元串表和段表的位置,進而解析整個ELF檔案。

3.5 連結的接口——符号

連結過程的本質就是要把多個不同的目标檔案之間互相黏在一起。

《程式員的自我修養——連結,裝載與庫》讀後總結
《程式員的自我修養——連結,裝載與庫》讀後總結

3.5.2 特殊符号

《程式員的自我修養——連結,裝載與庫》讀後總結

3.5.3 符号修飾與簽名

防止函數重名!

《程式員的自我修養——連結,裝載與庫》讀後總結

C++修飾符

這裡主要講c++對函數的修飾,函數簽名的一些規則

3.5.4 extern “C”

講C++相容C

《程式員的自我修養——連結,裝載與庫》讀後總結
《程式員的自我修養——連結,裝載與庫》讀後總結

為什麼C++相容C?

3.5.3 強符号與若符号

繼續閱讀