一.編譯和鏈接
1.預處理
命令:gcc -E hello.c -o hello.i
主要處理.c文件中以“#”開頭的預編譯指令
2.編譯
命令:gcc -S hello.i -o hello.s
[1]詞法分析
[2]語法分析
[3]語義分析
編譯器只能分析靜態語義(編譯期確定的語義)
靜態語義有聲明,類型轉換,類型匹配
[4]優化後生成相應的彙編代碼文件
中間語言生成,目標代碼生成與優化。
3.彙編
命令:gcc -c hello.s -o hello.o
彙編器是將彙編代碼轉化成機器可以執行的指令。
4.鏈接
重定位:絕對地址引用的位置“打補丁”,使其指向正確的地址
符號:函數或變量的起始地址
[1]地址和空間分配
[2]符號決議
[3]重定位
二.目標文件
基礎知識:
1】可執行文件格式有windows下的PE和linux下的ELF,都是COFF格式的變種。
2】靜態鏈接庫(windows下的.lib,linux下的.a)動態鏈接庫(windows下的.dll,linux下的.so)都按可執行文件格式存儲。
爲何將可執行文件的代碼段和數據段分開存放?
1】代碼段只讀,數據段可讀可寫,有利於分別保護
2】現代cpu的緩存被設計爲指令緩存和數據緩存分離,分開存放可提高cpu的緩存命中率。
3】運行多個進程時,有各自的數據段,共享代碼段,節省內存
程序示例:
查看目標文件的結構和內容:
目標文件 段的基本分佈
1.代碼段(.text)
存放機器指令
2.數據段(.data)
存放已經初始化的靜態變量,全局變量
3.只讀數據段(.rodata)
存放只讀數據,一般爲只讀變量(const修飾的變量)和常量字符串
4.數據段(.bss)
存放未初始化或初始化爲0的靜態變量,全局變量
因爲數據全爲0,.data段存儲數據0是沒有必要的,因此在目標文件中.bss是預留的,沒有內容,不佔內存空間,運行時的確佔內存空間
注:未初始化的全局變量在.comment段,故.bss的大小爲0x14=20字節,並非24字節。
ELF文件結構描述
1.文件頭
2.段表:描述每個段的基本信息
編譯器,鏈接器,裝載器都是通過段表來訪問和定位段的屬性的
ELF32_Shdr段描述符結構:每一個ELF32_Shdr結構體對應一個段
mian.o的段表及所有段的位置和長度
注:以2^2=4字節對齊,故有一小部分空餘。
3.重定位表
.rel.text是針對.text的重定位表。在.text段有絕對地址的引用,那就是printf函數。.data段包含幾個常量,沒有絕對地址的引用。
4.字符串表
字符串表(.strtab):保存普通的字符串,比如符號名。
段表字符串表(.shstrtab):保存段表中的字符串,比如段名。
鏈接的接口----符號
1】在鏈接中,目標文件的相互拼合實際上是目標文件之間對地址的引用,即對函數和變量的地址的引用。
比如目標文件B用到了目標文件A中的foo函數,則稱目標文件A定義了foo函數,目標文件B引用了目標文件A中的foo函數。(同樣適用於變量)
2】在鏈接中,將函數和變量統稱爲符號,函數名和變量命爲符號名。
3】每一個目標文件有一個相對應的符號表。
符號表記錄了目標文件的所有符號,每一個符號對應一個符號值。對函數和變量來說,符號值就是它們的地址。
符號的類型
1】全局符號 @@@@@@鏈接過程只關心全局符號的相互粘合。
1)定義在本目標文件的,可以被其他目標文件引用。eg:main,gdata1,gdata2,gdata3
2)外部符號:沒有定義在本目標文件,在本目標文件中引用。eg:printf
2】局部符號
只在編譯單元內部可見,對於鏈接過程沒有作用。eg:d.e.f,gdata4,gdata5,gdata6
5.符號表(.symtab)
符號修飾與函數簽名
1】爲了避免庫文件中的函數和全局變量名與目標文件中的名字起衝突,函數經編譯後要在符號名前加"_"。eg:foo----->_foo (C語言)
2】名稱空間:解決多模塊的符號衝突問題 (c++)
3】c++符號修飾
函數簽名:用於識別不同的函數。包含了函數的所有信息,包括函數名,參數列表,它所在的名稱空間和類。
c++編譯器在編譯時會將函數(函數簽名)和變量的名字進行修飾,形成符號名。
extern"c" 符號的引用
c++編譯器會將 extern"c" 大括號內部的代碼當作C語言代碼處理。
C語言不支持 extern "c" 語法,爲兼容C語言和c++定義兩套頭文件,c++的宏"_cplusplus",c++編譯器在c++編譯程序時默認調用該宏。
弱符號與強符號-----》針對符號的定義,並非符號的引用。 (只適用於C語言)
1】 強符號:函數和初始化了的全局變量。弱符號:未初始化的全局變量。(在.COMMON塊)
2】鏈接器按如下規則處理不同目標文件中重複定義的符號:
1)同名強符號,編譯錯誤。
2)同名強,弱符號,選擇強符號。
3)同名弱符號,選擇佔用內存大的。
3】強引用和弱引用:
強引用:若沒有找到符號的定義,鏈接器會報符號未定義的錯誤。
弱引用:若符號有定義,鏈接器將該符號的引用決議。若沒有定義,鏈接器不會報錯。主要用於庫的鏈接過程。
爲何將未初始化的全局變量放在.comment段,不放在.bss段?????
答:未初始化的全局變量放在.comment段只針對編譯後的目標文件。在鏈接時,兩個目標文件鏈接爲一個可執行文件,若兩個目標文件出現了同名的弱符號,則選擇內存佔用大的,實際上未初始化的全局變量在鏈接後是放在.bss段的(此時已經選擇出了佔用內存大的弱符號)。而在編譯時,並不確定在別的源文件中是否有同名的弱符號,不可確定其最終的大小,因此將未初始化的全局變量暫時存放在.comment段。
三.靜態鏈接
空間與地址分配
1】相似段合併:相同性質的段進行合併,obj文件以2^2=4字節對齊,合併後以頁面(4k)對齊。
.bss段不佔目標文件和可執行文件的空間,裝載時爲其分配空間,其只有虛擬地址空間。
2】調整段偏移和段長度,合併符號表。
程序示例:
a.c b.c編譯爲目標文件a.o b.o
a.o b.o 鏈接爲ab可執行文件
查看鏈接前後地址分配情況:
符號解析與重定位
1】重定位
2】重定位表
3】符號解析
鏈接時符號未定義的原因:1)鏈接時缺少了某個庫。2)輸入目標文件路徑不正確。3)符號的聲明與定義不一樣。
鏈接器掃描完所有輸入目標文件後,目標文件中未定義的符號應該能夠在全局符號表中找到,否則鏈接器報符號未定義錯誤。
(所有obj符號表中對符號引用的地方要找到符號定義的地方)
四.可執行文件的裝載與進程
可執行文件只有裝載到內存才能被CPU執行。
1.進程虛擬地址空間
1】進程和程序的區別:
程序:靜態的概念,預先編譯好的數據和指令的集合的文件。
進程:動態的概念,運行中的程序。
2】虛擬地址空間的大小與CPU的位數有關。
32位CPU大小爲2^32=4G
硬件決定了地址空間的最大理論上限,即硬件的尋址空間大小。
2.裝載的方式
1】靜態裝入:將程序運行時需要的指令和數據全部加載到內存中執行。
2】動態裝入:程序所需要的內存大於物理內存。
思想:程序需要哪個模塊,就把哪個模塊裝入內存,如果不需要,就將其存放在磁盤。
1)覆蓋裝入
2)頁映射 頁面置換算法:FIFO LRU
3.從操作系統的角度看可執行文件的加載
1】進程的建立
1)創建獨立的虛擬地址空間。
頁映射函數:創建虛擬地址空間到物理空間的映射關係。
2)讀取可執行文件頭,建立可執行文件與虛擬地址空間的映射。
程序執行發生頁錯誤時,操作系統在物理內存中分配一個物理頁,將缺頁從磁盤讀取到物理內存,建立虛擬頁到物理頁的映射關係。同時操作系統要知道缺頁位於可執行文件的哪個位置,於是建立可執行文件與虛擬地址空間的映射關係。
可執行文件又叫映像文件。
Linux中將虛擬地址空間的一個段叫做虛擬內存區域(VMA)。
3)將CPU的指令寄存器設置爲可執行文件的入口地址,啓動運行。
2】頁錯誤
4.進程虛存空間分佈
1】ELF文件鏈接視圖和執行視圖
段數量增多時,爲減少空間浪費,可執行文件到虛擬地址空間的映射時,對於相同權限的段,合併到一起當作一個段進行映射。映射到同一個VMA。
合併後的一個段叫做"segment",其中包含一個或多個屬性類似的"section"。
從鏈接的角度,可執行文件按“section”存儲,可執行文件爲鏈接視圖。從裝載的角度,可執行文件按“segment”劃分,可執行文件爲執行視圖。
目標文件鏈接成可執行文件時,鏈接器儘量將相同權限屬性的段分配在同一空間。可執行文件映射時,是以“segment”來映射的。
正如描述“section”屬性的結構叫做段表,而描述“segment”屬性的結構叫做程序頭(program header)。描述了ELF文件該如何被操作系統映射到進程的虛擬空間。
ELF可執行文件與進程虛擬空間的映射關係
2】堆和棧
通過查看/proc來查看進程虛擬空間分佈:
進程虛擬地址空間的概念:操作系統通過將進程劃分成一個個VMA來管理進程的虛擬空間,基本原則是將相同屬性的,有相同映像文件的映射成一個VMA。
3】堆的最大申請數量
4】段地址對齊
各個段接壤的部分共享一個物理頁面,然後將該物理頁面分別映射兩次。
4】進程棧初始化
5】Linux內核裝載ELF過程簡介
詳情請參見《程序員的自我修養-鏈接,裝載與庫》