落影l(fā)oyinglin
研讀《程序員的自我修養(yǎng)—鏈接、裝載與庫(kù)》
編譯與鏈接過(guò)程的思考
靜態(tài)庫(kù)與動(dòng)態(tài)庫(kù)的思考
** 說(shuō)真的挎峦,有些人系枪,真的是優(yōu)秀到讓你茧泪,不自覺(jué)就感到自卑了右犹。**
一. 從源碼到程序
程序最初的存在形式是源代碼,也就是若干個(gè)** .c **文件尾抑,它想要變成一個(gè)可執(zhí)行程序,需要以下幾個(gè)步驟:
** 1. 預(yù)編譯(P39): **
負(fù)責(zé)這一步工作的叫“預(yù)編譯器”蒂培。它主要負(fù)責(zé)處理所有的 #define
宏定義 再愈;所有的預(yù)編譯指令, 比如 #if
护戳、#endif
等翎冲。接下來(lái)會(huì)遞歸處理 #include
指令,用被包含的文件替換這個(gè)預(yù)編譯指令媳荒。 .c
文件 經(jīng)過(guò)預(yù)編譯抗悍,變成 .i
文件。
主要處理規(guī)則:
- 將所有的
#define
刪除钳枕, 并且展開(kāi)所有的宏定義缴渊; - 處理所有條件預(yù)編譯指令,比如
#if
鱼炒、#ifdef
衔沼、#elif
、#else
昔瞧、#endif
指蚁。 - 處理
#include
預(yù)編譯指令,將包含的文件插入到該預(yù)編譯指令的位置自晰。注意凝化,這個(gè)過(guò)程是遞歸進(jìn)行的,也就是說(shuō)被包含的文件可能還包含其他文件酬荞。 - 刪除所有的注釋
//
和/* */
搓劫。 - 添加行號(hào)和文件名標(biāo)識(shí),比如#2
hello.c
2,以便于編譯時(shí)編譯器產(chǎn)生調(diào)試用行號(hào)信息及用于編譯時(shí)產(chǎn)生編譯錯(cuò)誤或警告能夠顯示行號(hào)混巧。 - 保留所有的
#pragma
編譯器指令糟把,因?yàn)榫幾g器需要使用它們。
** 2. 編譯(p42): **
這一步由編譯器負(fù)責(zé)牲剃,主要又有詞法分析遣疯、語(yǔ)法分析、語(yǔ)義分析凿傅,優(yōu)化和生成匯編代碼五個(gè)部分缠犀。
-
** 詞法分析:**
源代碼程序被輸入到 ** 掃描器 ,掃描器運(yùn)用一種類(lèi)似于有限狀態(tài)機(jī)**的算法將源代碼的字符序列分割成一系列記號(hào)聪舒。
簡(jiǎn)單說(shuō)就是辨液,識(shí)別源代碼中的各種括號(hào),數(shù)字箱残,標(biāo)點(diǎn)等滔迈。比如有左括號(hào) ** "(" ** 但沒(méi)有 右括號(hào) ** ")" **, 這一步就能發(fā)現(xiàn)錯(cuò)誤止吁。
比如:array[index] = (index + 4 ) * (2 + 6);
分析器進(jìn)行標(biāo)記:
- **語(yǔ)法分析: **
** 語(yǔ)法分析器 ** 將由 ** 掃描器 ** 產(chǎn)生的記號(hào)進(jìn)行語(yǔ)法分析, 從而產(chǎn)生 語(yǔ)法樹(shù)燎悍, 整個(gè)分析過(guò)程采用 **上下文語(yǔ)法無(wú)關(guān) **的分析手段敬惦。比如 2+6
就是一顆根節(jié)點(diǎn)為 +
,左右葉子節(jié)點(diǎn)分別為 2
和 6
的語(yǔ)法樹(shù)谈山,如果你只寫(xiě)2+
俄删,在這一步就會(huì)報(bào)錯(cuò)。
- **語(yǔ)義分析: **
由 語(yǔ)義分析器來(lái)完成奏路,這一步主要考慮類(lèi)型聲明畴椰、匹配和轉(zhuǎn)換,比如當(dāng)一個(gè)浮點(diǎn)型的表達(dá)式賦值給一個(gè)整型的表達(dá)式時(shí)鸽粉,其中隱含了一個(gè)浮點(diǎn)型到整型轉(zhuǎn)換的過(guò)程斜脂,這些都屬于靜態(tài)語(yǔ)義分析。動(dòng)態(tài)語(yǔ)義一般指在運(yùn)行期出現(xiàn)的語(yǔ)義相關(guān)的問(wèn)題触机,比如將0作為除數(shù)是一個(gè)運(yùn)行期語(yǔ)義錯(cuò)誤秽褒。
- ** 中間語(yǔ)言生成 :**
** 源代碼優(yōu)化器 ** 將整個(gè)語(yǔ)法樹(shù)轉(zhuǎn)換成中間代碼,比較常見(jiàn)的中間代碼有: 三地址碼威兜,比如2 + 3
會(huì)寫(xiě)成t1 = 2 + 3
,同時(shí)也會(huì)把編譯器就可以確定的表達(dá)式進(jìn)行優(yōu)化销斟。
- ** 目標(biāo)代碼生成與優(yōu)化: **
代碼生成器
根據(jù)三地址碼生成依賴(lài)于目標(biāo)機(jī)器的代碼,也就是匯編語(yǔ)言椒舵。
目標(biāo)代碼優(yōu)化器對(duì)目標(biāo)代碼進(jìn)行優(yōu)化:
**.i
經(jīng)過(guò)編譯蚂踊,得到匯編文件,后綴是.s
**
** 3.匯編(P40): **
這一步由匯編器負(fù)責(zé)笔宿,將匯編語(yǔ)言轉(zhuǎn)換成機(jī)器
可以執(zhí)行的語(yǔ)言(完全由0
和1
組成)犁钟。匯編文件經(jīng)過(guò)匯編,變成目標(biāo)文件后綴 .o
泼橘。
** 4.鏈接(P41): **
這一步是重點(diǎn)涝动。之前的步驟,都是以 .c
文件為基本單位炬灭,一個(gè).c
源文件最終被匯編醋粟,生成目標(biāo)文件。這一步就是將多個(gè)目標(biāo)文件鏈接起來(lái)重归,生成可執(zhí)行文件米愿。
考慮一個(gè) .c
文件中,用到了另一個(gè) .c
文件中的變量或函數(shù)鼻吮。 在編譯這個(gè)文件時(shí)育苟,我們無(wú)法再編譯期確定這個(gè)變量或函數(shù)的地址。只能把所有目標(biāo)文件鏈接起來(lái)以后椎木,才能確定违柏。因此鏈接主要負(fù)責(zé)地址重分配
博烂,符號(hào)名稱(chēng)綁定和重定位。
二. 軟件調(diào)用層次
**1. 應(yīng)用層: **
不管是瀏覽器漱竖、游戲禽篱,還是我們使用的各種開(kāi)發(fā)工具,如Xcode
闲孤,VS
谆级,匯編器自身等烤礁,都屬于這一范疇讼积。
** 2.操作系統(tǒng)運(yùn)行庫(kù): **
我們?cè)诔绦蚶镎{(diào)用系統(tǒng)API,比如文件讀寫(xiě)脚仔,就是調(diào)用了第二層提供的相應(yīng)服務(wù)勤众。這種調(diào)用通過(guò)操作系統(tǒng)的API完成,它溝通了應(yīng)用層和操作系統(tǒng)的運(yùn)行庫(kù)鲤脏。這也就是為什么不管是在Mac
還是Windows
上編程们颜,我們都可以調(diào)用 printf()
或fread()
等函數(shù)。因?yàn)椴煌牟僮飨到y(tǒng)的運(yùn)行庫(kù)提供了不同底層的實(shí)現(xiàn)猎醇,但對(duì)應(yīng)用層提供的API總是一樣的窥突。
** 3. 操作系統(tǒng)內(nèi)核: **
操作系統(tǒng)的運(yùn)行庫(kù)通過(guò)系統(tǒng)調(diào)用(System Call)
調(diào)用系統(tǒng)內(nèi)核提供的函數(shù)。比如fread
屬于API硫嘶,它在Linux
下會(huì)調(diào)用read()
這個(gè)系統(tǒng)調(diào)用阻问,而在Windows
下會(huì)調(diào)用ReadFile()
這個(gè)系統(tǒng)調(diào)用。應(yīng)用程序可以直接調(diào)用系統(tǒng)調(diào)用沦疾,但是這樣一來(lái)称近,我們需要考慮各個(gè)操作系統(tǒng)下系統(tǒng)調(diào)用的不同,而且系統(tǒng)調(diào)用由于更加底層哮塞,實(shí)現(xiàn)起來(lái)也就更加困難刨秆。最關(guān)鍵的是,系統(tǒng)調(diào)用是通過(guò)中斷來(lái)完成的忆畅,涉及到堆棧的保存與恢復(fù)衡未,頻繁的系統(tǒng)調(diào)用會(huì)影響性能。
** 4.硬件層: **
程序無(wú)法直接訪問(wèn)這一層家凯,只有操作系統(tǒng)的內(nèi)核眠屎,通過(guò)硬件廠商提供的接口才能訪問(wèn)。
三. 虛擬地址空間
在程序運(yùn)行的過(guò)程中肆饶,最重要的概念就是虛擬地址空間
改衩。所謂的虛擬地址空間
,是指應(yīng)用程序自己認(rèn)為驯镊,自己所處的地址空間葫督。它區(qū)別于物理地址空間竭鞍。后者是真實(shí)存在的,比如電腦有一根8G的內(nèi)存條橄镜,物理地址空間就是0~8Gb偎快。CPU
的MMU
負(fù)責(zé)把虛擬地址轉(zhuǎn)換成物理地址。
引入虛擬地址的第一個(gè)好處是洽胶,程序員不再關(guān)心真實(shí)的物理內(nèi)存空間是什么樣的晒夹,理論上來(lái)說(shuō),程序員有幾乎無(wú)限大的虛擬內(nèi)存空間可用姊氓,最后只要建立虛擬地址和物理地址的對(duì)應(yīng)關(guān)系即可丐怯。另一方面,操作系統(tǒng)屏蔽了物理內(nèi)存空間的細(xì)節(jié)翔横,進(jìn)程無(wú)法訪問(wèn)到操作系統(tǒng)禁止訪問(wèn)的物理地址读跷,也不能訪問(wèn)到別的進(jìn)程的地址空間,這大大增強(qiáng)了程序安全性禾唁。
由虛擬地址空間引申出來(lái)的分頁(yè)(Paging)技術(shù)
效览,大大提高了內(nèi)存的使用效率。要想運(yùn)行一個(gè)程序荡短,不再需要把整個(gè)程序都放入內(nèi)存中執(zhí)行丐枉,我們只要保證將要執(zhí)行的頁(yè)在內(nèi)存中即可,如果不存在則導(dǎo)致頁(yè)錯(cuò)誤掘托。
關(guān)于地址空間的理解非常重要瘦锹,書(shū)中有很多關(guān)于內(nèi)存、和地址的描述烫映,需要我們自己分析這是虛擬地址還是物理地址沼本。如果分析錯(cuò)了,理解問(wèn)題會(huì)比較麻煩锭沟。
四. 鏈接與重定位
我們把foo函數(shù)定義在另一個(gè)文件中抽兆,然后在main.c中調(diào)用這個(gè)函數(shù),單獨(dú)編譯main.c后代碼如下:
……
0000000000000024 callq 0x29
0000000000000029 xorl %ecx, %ecx
……
可以看到族淮,本該調(diào)用foo函數(shù)的地方辫红,我們直接調(diào)用了下一條命令,但是當(dāng)main.o和foo.o鏈接起來(lái)后祝辣,就變成了:
0000000100000f30 pushq %rbp
0000000100000f31 movq %rsp, %rbp
0000000100000f34 movl $0x7b, %eax
0000000100000f39 movl %edi, -0x4(%rbp)
0000000100000f3c movl %esi, -0x8(%rbp)
0000000100000f3f popq %rbp
//以上為foo函數(shù)實(shí)現(xiàn)
……
0000000100000f74 callq 0x100000f30
0000000100000f79 xorl %ecx, %ecx
……
這時(shí)候foo函數(shù)的位置就正確設(shè)置了贴妻。原因在于在main.c這個(gè)編譯模塊單獨(dú)編譯時(shí),編譯器無(wú)法確定foo的位置蝙斜,只好臨時(shí)用下一條指令的位置代替一下名惩。
鏈接器在鏈接過(guò)程中,就是要對(duì)這樣的符號(hào)進(jìn)行重定位孕荠。在重定位時(shí)娩鹉,main.o
中有foo函數(shù)經(jīng)過(guò)修飾的符號(hào)名攻谁,同樣的符號(hào)名在foo.o
中也有,于是兩者一拍即合弯予,就這樣被鏈接器連在了一起戚宦。0x29
這個(gè)臨時(shí)的調(diào)用地址被更新成了0x100000f30
。這個(gè)過(guò)程類(lèi)似于拼圖游戲锈嫩,程序在鏈接時(shí)就是處理各種各樣類(lèi)似的問(wèn)題受楼,當(dāng)所有編譯模塊都按照符號(hào)名完整的鏈接起來(lái)時(shí),程序也就可以開(kāi)始運(yùn)行了呼寸。
五. 知識(shí)概要
目標(biāo)文件結(jié)構(gòu) (P58) :
** 文件頭:** 描述了整個(gè)文件的文件屬性艳汽,包括文件是否可執(zhí)行,是靜態(tài)連接還是動(dòng)態(tài)鏈接及入口地址(如果是可執(zhí)行文件)等舔、目標(biāo)硬件骚灸、目標(biāo)操作系統(tǒng)等信息糟趾。同時(shí)文件頭還包含
段表
慌植。** 段表:** 一個(gè)描述文件中各個(gè)段的數(shù)組。段表描述了文件中各個(gè)段在文件中的偏移位置及段的屬性等义郑,從段表中可以得到每個(gè)段的所有信息蝶柿。
**.text段: **一般C語(yǔ)言編譯后的執(zhí)行語(yǔ)句都編譯成機(jī)器代碼 ,保存在
.text
段**.data段: ** 已經(jīng)初始化的全局變量和局部變量都保存在
.data
端非驮。.bss段: 未初始化的全局變量和局部靜態(tài)變量一般都放在
.bss
端里面交汤,.bss
端只是為未初始化的全局變量和局部變量預(yù)留位置而已,它并沒(méi)有內(nèi)容劫笙,所以它在文件中也不占據(jù)空間芙扎。
文件頭 (P70) :
ELF的文件頭定義了 ELF魔數(shù)
、文件機(jī)器字節(jié)長(zhǎng)度
填大、數(shù)據(jù)存儲(chǔ)方式
戒洼、版本
、運(yùn)行平臺(tái) **允华、
ABI版本圈浇、
ELF重定位類(lèi)型 、
硬件平臺(tái) 靴寂、
硬件平臺(tái)版本 磷蜀、
入口地址 、
程序頭入口和長(zhǎng)度 百炬、
段表的位置和長(zhǎng)度 褐隆、
段的數(shù)量``等。
**ELF文件頭結(jié)構(gòu): **
ELF文件頭結(jié)構(gòu)成員含義:
重定位表 (P79) :
鏈接器在處理目標(biāo)文件時(shí)剖踊,須要對(duì)目標(biāo)文件中某些部位進(jìn)行重定位庶弃,即代碼段和數(shù)據(jù)段中那些對(duì)絕對(duì)地址的引用位置轨蛤,這些重定位信息記錄在ELF文件的重定位表里面。
每個(gè)須要重定位的代碼段或數(shù)據(jù)段都會(huì)有一個(gè)相應(yīng)的重定位表虫埂。比如.rel.text
就是針對(duì).text
段的重定位表祥山。
一個(gè)重定位表同時(shí)也是ELF的一個(gè)段,這個(gè)段的類(lèi)型就是"SHT_REL
類(lèi)型掉伏, 它的 sh_link
表示符號(hào)表的下標(biāo)缝呕, 它的sh_info
表示它作用于哪個(gè)段。比如.rel.text
作用于 .text"段
斧散,而.text"段
的下標(biāo)為1
那么rel.text
的.sh_info
為1.
鏈接的接口 -- 符號(hào)(P81) :
在鏈接中供常,我們將函數(shù)和變量統(tǒng)稱(chēng)為符號(hào),函數(shù)名或變量名統(tǒng)稱(chēng)為符號(hào)名鸡捐。
整個(gè)鏈接過(guò)程正是基于符號(hào)才能夠正確完成栈暇。鏈接過(guò)程中很關(guān)鍵的一部分就是符號(hào)的管理源祈,每一個(gè)目標(biāo)文件都會(huì)有一個(gè)相應(yīng)的符號(hào)表色迂,這個(gè)表里面記錄了目標(biāo)文件中所用到的所有符號(hào)歇僧。每個(gè)訂閱的符號(hào)都有對(duì)個(gè)對(duì)應(yīng)的值,叫做符號(hào)值
祸轮,對(duì)于變量和函數(shù)來(lái)說(shuō)适袜,符號(hào)值就是它們的地址。
**符號(hào)的分類(lèi) : **
鏈接過(guò)程中之關(guān)系全局符號(hào)的相互 “粘合”慕趴,局部符號(hào)躏啰、段名、行號(hào)等都是次要的耙册。
符號(hào)修飾和函數(shù)簽名(P86) :
** C++符號(hào)修飾 :**
函數(shù)簽名包含一個(gè)函數(shù)的信息给僵,包括函數(shù)名、它的參數(shù)類(lèi)型、它所在的類(lèi)和名稱(chēng)空間及其他信息帝际。
如例子所示:
這段代碼中有6個(gè)同名函數(shù)func
,只是返回類(lèi)型和參數(shù)及所在的名稱(chēng)空間不同蔓同。在編譯器和鏈接器處理函數(shù)符號(hào)時(shí),它們使用某種名稱(chēng)修飾的方法蹲诀,使得每個(gè)函數(shù)簽名對(duì)應(yīng)一個(gè)修飾后的名稱(chēng)斑粱。也就是說(shuō)C++編譯器
編譯后的目標(biāo)文件中所使用的符號(hào)名是相應(yīng)函數(shù)和變量修飾后的名稱(chēng)。
以上6個(gè)函數(shù)簽名在GCC編譯器
下脯爪,相對(duì)應(yīng)的修飾后名稱(chēng)如表所示:
簽名生成規(guī)則:
由于不同的編譯器采用不同的名字修飾方法则北,必然會(huì)導(dǎo)致不同編譯器編譯產(chǎn)生的目標(biāo)文件無(wú)法正常相互連接,這是導(dǎo)致不同編譯器之間不能互相操作的主要原因之一痕慢。
強(qiáng)弱符號(hào)與強(qiáng)弱引用(P92) :
對(duì)于C/C++語(yǔ)言來(lái)說(shuō)尚揣,編譯器默認(rèn)函數(shù)和初始化了的全局變量為強(qiáng)符號(hào),未初始化的全局變量為弱符號(hào)掖举。
我們可以通過(guò)GCC的_attribute_((weak))
來(lái)定義任何一個(gè)強(qiáng)符號(hào)為弱符號(hào)快骗。
注意:強(qiáng)符號(hào)和弱符號(hào)都是針對(duì)定義來(lái)說(shuō)的,不是針對(duì)符號(hào)的引用
比如如下程序:
extern int ext;
int weak;
int strong = 1;
_attribute_((weak)) weak2 = 2;
int main() {
return 0;
}
這里塔次,weak
和 weak2
是弱符號(hào)方篮, strong
和 main
是強(qiáng)符號(hào),而ext
既非強(qiáng)符號(hào)也非弱符號(hào)俺叭,因?yàn)樗皇且粋€(gè)外部變量的引用恭取。
針對(duì)強(qiáng)弱符號(hào)的概念泰偿,鏈接器會(huì)按如下規(guī)則處理和選擇被多次定義的全局不好:
** 規(guī)則1:**不允許強(qiáng)符號(hào)被多次定義(即不同的目標(biāo)文件不能有同名的強(qiáng)符號(hào))熄守;如果有多個(gè)強(qiáng)符號(hào)定義,則鏈接器包符號(hào)重復(fù)定義錯(cuò)誤耗跛。
** 規(guī)則2:** 如果一個(gè)符號(hào)在某個(gè)目標(biāo)文件中是強(qiáng)符號(hào)裕照,在其他文件中都是弱符號(hào),那么選擇強(qiáng)符號(hào)调塌。
** 規(guī)則3:** 如果一個(gè)符號(hào)在所有目標(biāo)文件中都是弱符號(hào)晋南,那么選擇其中占用空間最大的一個(gè)。
同樣對(duì)于符號(hào)名的引用也分為強(qiáng)引用和弱引用羔砾,強(qiáng)引用表示如果找不到符號(hào)定義會(huì)報(bào)錯(cuò)负间,弱引用不報(bào)錯(cuò),默認(rèn)為0或某個(gè)特殊值姜凄。
空間與地址分配(P99) :
現(xiàn)在鏈接器空間分配的策略基本上都是采用兩步鏈接的方法政溃。
** 第一步: 空間與地址分配 ** 掃描所有的輸入目標(biāo)文件,并且獲得它們各個(gè)段的長(zhǎng)度态秧、屬性和位置董虱,并且將輸入目標(biāo)文件中的符號(hào)定義和符號(hào)引用收集起來(lái),統(tǒng)一放到一個(gè)全局符號(hào)表。這一步愤诱,鏈接器將能夠獲得所有輸入目標(biāo)文件的段長(zhǎng)度云头,并且將它們合并,計(jì)算并輸出文件中各個(gè)端合并后的長(zhǎng)度和位置淫半,并建立映射關(guān)系溃槐。
第二步 符號(hào)解析與重定位 使用上一步收集到的所有信息,讀取輸入文件中段的數(shù)據(jù)科吭、重定位信息竿痰、并且進(jìn)行符號(hào)解析與重定位、調(diào)整代碼中的地址等砌溺。鏈接完成后影涉,我們就得到靜態(tài)庫(kù)。
靜態(tài)庫(kù)鏈接(P118) :
靜態(tài)庫(kù)可以看做一組目標(biāo)文件的集合规伐,同一個(gè)靜態(tài)庫(kù)中的不同目標(biāo)文件可能相互依賴(lài)蟹倾,不同的靜態(tài)庫(kù)也可以相互依賴(lài)。
鏈接控制腳本(P127) :
鏈接控制腳本控制鏈接器的運(yùn)行猖闪,將目標(biāo)文件和庫(kù)文件轉(zhuǎn)換為可執(zhí)行文件鲜棠。鏈接控制腳本由鏈接腳本語(yǔ)言寫(xiě)成,可以人為的控制程序入口培慌、某幾個(gè)段合并豁陆、某幾個(gè)段舍棄等。
動(dòng)態(tài)鏈接
這一部分主要討論經(jīng)過(guò)鏈接后吵护,可執(zhí)行文件如何裝載到內(nèi)存趟妥。
裝載的方式(P153) :
兩種典型的動(dòng)態(tài)裝載方法:覆蓋裝入和頁(yè)映射神僵。覆蓋裝入允許互不依賴(lài)的兩個(gè)模塊共同享有同一塊內(nèi)存,在使用中互相替換。速度較慢灌砖,用時(shí)間換空間浦旱。我們常用的方案是頁(yè)映射痢掠,把程序虛擬的內(nèi)存空間分成多個(gè)頁(yè)产还,由專(zhuān)門(mén)的頁(yè)裝載管理器負(fù)責(zé)管理虛擬頁(yè)和物理內(nèi)存中頁(yè)的對(duì)應(yīng)關(guān)系。
進(jìn)程的建立(P157) :
創(chuàng)建一個(gè)進(jìn)程屯蹦,然后裝載相應(yīng)的可執(zhí)行文件并且執(zhí)行维哈,在有虛擬存儲(chǔ)情況下,上述過(guò)程最開(kāi)始只需要三件事:
創(chuàng)建一個(gè)獨(dú)立的虛擬地址空間
讀取可執(zhí)行文件頭登澜,并且建立虛擬空間與可執(zhí)行文件的映射關(guān)系
將
CPU
的指令寄存器設(shè)置成可執(zhí)行文件的入口地址阔挠,啟動(dòng)運(yùn)行。
Linux
下帖渠,目標(biāo)文件的每個(gè)段都有自己在虛擬內(nèi)存中的位置谒亦,這叫做虛擬內(nèi)存區(qū)域(VMA)
,表示它裝載在虛擬內(nèi)存中的位置。
頁(yè)錯(cuò)誤(P159) :
進(jìn)程創(chuàng)建后,只有物理頁(yè)與虛擬頁(yè)的對(duì)應(yīng)關(guān)系份招,但是真正的指令和數(shù)據(jù)還沒(méi)有放入物理頁(yè)中切揭,物理頁(yè)的內(nèi)存處于未分配狀態(tài)。一旦訪問(wèn)到這個(gè)物理頁(yè)锁摔,就會(huì)發(fā)生頁(yè)錯(cuò)誤廓旬。
發(fā)生頁(yè)錯(cuò)誤時(shí),操作系統(tǒng)立刻根據(jù)物理內(nèi)存的頁(yè)與虛擬內(nèi)存的頁(yè)的對(duì)應(yīng)關(guān)系谐腰,找到這個(gè)頁(yè)對(duì)應(yīng)的虛擬內(nèi)存孕豹,然后再查詢(xún)每個(gè)段的VMA
,就可以找這個(gè)頁(yè)面在可執(zhí)行文件中的偏移量十气。這時(shí)候操作系統(tǒng)先為物理頁(yè)分配內(nèi)存空間励背,然后把可執(zhí)行文件中的數(shù)據(jù)和指令寫(xiě)入物理頁(yè),最后建立物理頁(yè)和虛擬頁(yè)聯(lián)系即可砸西。然后進(jìn)程從發(fā)生頁(yè)錯(cuò)誤的地方重新執(zhí)行叶眉。
進(jìn)程虛存空間分布(P160) :
ELF文件被映射時(shí),是以系統(tǒng)的頁(yè)長(zhǎng)度作為單位芹枷,每個(gè)段在映射時(shí)不可能都是系統(tǒng)頁(yè)長(zhǎng)度的整數(shù)倍衅疙,所以多余部分也將占用一個(gè)頁(yè)。因此造成大量浪費(fèi)鸳慈。
由于操作系統(tǒng)不關(guān)心可執(zhí)行文件每個(gè)section
的具體作用饱溢,但是關(guān)心它們的讀寫(xiě)權(quán)限(是否可讀、可寫(xiě)走芋、可執(zhí)行)绩郎,所以往往把具有權(quán)限的Section
合并成一個(gè)Segment
.
比如兩個(gè)段分別叫.text
和 .init
,它們分別包含程序的可執(zhí)行代碼和初始化代碼绿聘,并且它們的權(quán)限相同都是可讀可執(zhí)行嗽上,假設(shè).text
為4097字節(jié),.init
為512字節(jié)熄攘,這兩個(gè)段分別映射的話要占用3個(gè)頁(yè)面,但是合并就只須占用兩個(gè)頁(yè)面彼念。
進(jìn)程棧初始化(P172) :
進(jìn)程運(yùn)行后挪圾,操作系統(tǒng)會(huì)初始化進(jìn)程的堆棧,其中存放了環(huán)境變量和命令行參數(shù)逐沙。這些參數(shù)被傳給main函數(shù)(argc
和argv
兩個(gè)參數(shù)對(duì)應(yīng)參數(shù)數(shù)量和參數(shù)數(shù)組)
動(dòng)態(tài)鏈接
動(dòng)態(tài)鏈接(P181) :
靜態(tài)鏈接存在空間浪費(fèi)和更新困難等問(wèn)題哲思,而動(dòng)態(tài)鏈接的基本思想是把程序按模塊拆分成各個(gè)相對(duì)獨(dú)立的部分,在程序運(yùn)行時(shí)才將它們鏈接在一起形成一個(gè)完整的程序,而不是像靜態(tài)連接一樣把所有的程序模塊都鏈接成一個(gè)單獨(dú)的可執(zhí)行文件吩案。
在Linux系統(tǒng)中棚赔,ELF的動(dòng)態(tài)鏈接文件成為動(dòng)態(tài)共享對(duì)象(DSO)
,后綴一般為為.so
;而在Windows系統(tǒng)中靠益,動(dòng)態(tài)鏈接文件被稱(chēng)為動(dòng)態(tài)鏈接庫(kù)丧肴,后綴一般為.dll
。動(dòng)態(tài)鏈接的過(guò)程由動(dòng)態(tài)鏈接器完成胧后。動(dòng)態(tài)鏈接可以節(jié)約內(nèi)存(多個(gè)進(jìn)程共享內(nèi)存中的某一個(gè)模塊)芋浮、方便升級(jí)(靜態(tài)鏈接的每一個(gè)模塊都會(huì)影響整個(gè)可執(zhí)行文件)。
裝載時(shí)重定位(P188) :
由于動(dòng)態(tài)共享對(duì)象會(huì)被多個(gè)程序使用壳快,導(dǎo)致它在虛擬地址空間中的位置難以確定纸巷。不同模塊的目標(biāo)裝載地址如果有相同的,那么同時(shí)導(dǎo)入這兩個(gè)模塊就會(huì)出問(wèn)題眶痰。如果都不一樣也不行瘤旨,因?yàn)榭赡艽嬖诘哪K太多了。沒(méi)有那么多內(nèi)存竖伯。所以動(dòng)態(tài)共享對(duì)象需要在裝載時(shí)重定位裆站。
裝載時(shí)重定位就是:在鏈接時(shí),對(duì)所有絕對(duì)地址的引用不做重定位黔夭,而把這一步推遲到裝載時(shí)再完成宏胯。一旦模塊裝在地址確定,即目標(biāo)地址確定本姥,那么系統(tǒng)就對(duì)程序中所有的絕對(duì)地址進(jìn)行重定位肩袍。
地址無(wú)關(guān)代碼(P191) :
由于裝載時(shí)重定位使得指令部分無(wú)法在多個(gè)進(jìn)程之間共享,目前采用的方案是地址無(wú)關(guān)代碼技術(shù)婚惫。
基本相符就是把指令中那些需要被修改的部分分離出來(lái)氛赐,跟數(shù)據(jù)部分放在一起,這樣指令部分就可以保持不變先舷,而數(shù)據(jù)部分可以再每個(gè)進(jìn)程都擁有一個(gè)副本艰管。
動(dòng)態(tài)對(duì)象中的地址引用分為模塊內(nèi)部引用和外部引用,指令引用和數(shù)據(jù)引用蒋川,兩兩組合成四種牲芋。對(duì)于模塊內(nèi)部的指令或數(shù)據(jù)引用,采用相對(duì)偏移調(diào)用的方法捺球。
全局偏移表(P195) :
把地址相關(guān)需要重定位的部分放到數(shù)據(jù)段中缸浦,而對(duì)于其他模塊的全局變量地址、模塊間的調(diào)用和跳轉(zhuǎn)氮兵,則通過(guò)在數(shù)據(jù)段里面建立一個(gè)指向這些變量的指針數(shù)組裂逐,即全局偏移表(GOT)
,來(lái)間接指向。
用.got
和.got.plt
表來(lái)分別處理數(shù)據(jù)和函數(shù)引用泣栈。
延遲綁定(P200) :
當(dāng)函數(shù)第一次被用到時(shí)才進(jìn)行綁定(符號(hào)查找卜高、重定位等)弥姻,如果沒(méi)用到則不進(jìn)行綁定。所以程序開(kāi)始執(zhí)行時(shí)掺涛,模塊間的函數(shù)調(diào)用都沒(méi)有進(jìn)行綁定庭敦,而是需要用到時(shí)才由動(dòng)態(tài)鏈接器來(lái)負(fù)責(zé)綁定,這種做法可以加快程序的啟動(dòng)速度鸽照。這種方法叫做延遲綁定螺捐。
Linux維護(hù)一個(gè)PLT表來(lái)保存符號(hào)和真實(shí)地址之間的對(duì)應(yīng)關(guān)系。
動(dòng)態(tài)鏈接重定位表(P208) :
動(dòng)態(tài)鏈接中有兩個(gè)重定位表.rel.dyn
和.rel.plt
分別對(duì)應(yīng).rel.text
和.rel.data
矮燎。前者對(duì)數(shù)據(jù)引用(.got)進(jìn)行修正定血,它所修正的位置位于.got
以及數(shù)據(jù)段,后者對(duì)函數(shù)引用(.got.plt)進(jìn)行修正诞外,修正位置位于.got.plt
澜沟。
動(dòng)態(tài)鏈接器的實(shí)現(xiàn)和步驟(P214) :
動(dòng)態(tài)鏈接器本身不可以依賴(lài)于其他任何共享對(duì)象;其次是動(dòng)態(tài)鏈接器本身所需要的全局和靜態(tài)變量的重定位工作由它本身完成峡谊。對(duì)于第二個(gè)條件茫虽,動(dòng)態(tài)鏈接器必須在啟動(dòng)時(shí)有一段非常精巧的代碼可以完成這項(xiàng)艱巨的工作而同時(shí)又不可以使用到全局變量和靜態(tài)變量,這種具有一定限制條件的啟動(dòng)代碼往往被稱(chēng)為自舉既们。
內(nèi)存與庫(kù)
動(dòng)態(tài)鏈接器的實(shí)現(xiàn)和步驟(P214) :
棧(P286):
棧是遵循先入棧的數(shù)據(jù)后出棧的一個(gè)特殊容器濒析。
在i386
處理器下,棧頂有esp
寄存器定位啥纸,由于棧向下生長(zhǎng)号杏,壓棧使得棧頂?shù)刂窚p小,出棧是的棧頂?shù)刂吩龃蟆?/p>
活動(dòng)記錄(P287):
棧保存了函數(shù)調(diào)用所需要的維護(hù)信息,被稱(chēng)為堆棧幀(Stack Frame)
或活動(dòng)記錄斯棒,主要包含:
- 函數(shù)的返回地址和函數(shù)盾致;
- 臨時(shí)變量:包括函數(shù)的非靜態(tài)局部變量以及編譯器自動(dòng)生成的其他臨時(shí)變量
- 保存的上下文: 包括在函數(shù)調(diào)用前后需要保持不變的寄存器。
在i386中荣暮,一個(gè)函數(shù)的活動(dòng)記錄用edp
和esp
這兩個(gè)寄存器劃定范圍庭惜。esp寄存器始終指向棧的頂部,同時(shí)也就指向了當(dāng)前函數(shù)的活動(dòng)記錄的頂部穗酥。而相對(duì)的护赊,edp
寄存器指向了函數(shù)活動(dòng)記錄的一個(gè)固定位置,edp
寄存器又被稱(chēng)為幀指針迷扇。
P294:
函數(shù)的調(diào)用方和被調(diào)用方要遵守同一個(gè)“調(diào)用慣例”百揭。默認(rèn)的cdecl
慣例要求函數(shù)參數(shù)以從右到左的順序入棧,由函數(shù)調(diào)用方負(fù)責(zé)參數(shù)的出棧蜓席。
P301:
函數(shù)返回值的獲取:如果是四個(gè)字節(jié)课锌,放在eax
中厨内。4-8字節(jié)的返回值通過(guò)eax(低位
)和edx(高位)
聯(lián)合存儲(chǔ)祈秕。超過(guò)8字節(jié)的返回值,把返回值在棧中存放的地址放到eax
中雏胃。
堆(P306):
棧上的數(shù)據(jù)在函數(shù)返回時(shí)就會(huì)被釋放请毛,全局地、動(dòng)態(tài)的申請(qǐng)內(nèi)存的方式是利用堆瞭亮。如果由操作系統(tǒng)管理堆方仿,由于總是進(jìn)行系統(tǒng)調(diào)用,性能開(kāi)銷(xiāo)比較大统翩,所以一般由應(yīng)用程序“批發(fā)”一大塊內(nèi)存空間仙蚜,然后自己進(jìn)行內(nèi)存管理,具體來(lái)講厂汗,管理著堆空間分配的往往是程序的運(yùn)行庫(kù)委粉。
堆管理 (P307):
堆并不總是向上生長(zhǎng)(如Windows
的HeapCreate
系列),調(diào)用malloc
有可能產(chǎn)生系統(tǒng)調(diào)用(取決于進(jìn)程預(yù)申請(qǐng)的空間是否足夠)娶桦,堆內(nèi)存在進(jìn)程結(jié)束后被操作系統(tǒng)回收贾节,堆內(nèi)存在虛擬地址空間中連續(xù),在物理空間中可能不連續(xù)衷畦。
堆分配算法(P312):
堆分配三種算法:
- 空閑鏈表:把堆中各個(gè)空閑的塊按照鏈表的方式連接起來(lái)栗涂,當(dāng)用戶(hù)請(qǐng)求一塊空間時(shí),可以遍歷整個(gè)列表祈争,直到找到合適大小的塊并且將它拆分斤程,當(dāng)用戶(hù)釋放空間時(shí)將它合并到空閑鏈表中。
特點(diǎn):實(shí)現(xiàn)簡(jiǎn)單铛嘱、但記錄長(zhǎng)度的字節(jié)容易被數(shù)組越界破壞
- 位圖: 將整個(gè)堆劃分為大量的塊暖释,每個(gè)快的大小相同。當(dāng)用戶(hù)請(qǐng)求內(nèi)存的時(shí)候墨吓,總是分配整數(shù)個(gè)塊的空間給用戶(hù)球匕,第一個(gè)快我們稱(chēng)為已分配區(qū)域的頭,其余的稱(chēng)為已分配區(qū)域的主體帖烘。我們可以使用一個(gè)整數(shù)數(shù)組來(lái)記錄塊的使用情況亮曹,由于每個(gè)塊只有頭、主體秘症、空閑三種狀態(tài)照卦,因此只需兩位即可標(biāo)識(shí)一個(gè)塊。
特點(diǎn): 速度快乡摹、 穩(wěn)定性好役耕、塊不需要額外信息,易于管理聪廉、分配內(nèi)存的時(shí)候容易產(chǎn)生碎片瞬痘、位圖可能過(guò)大
- 對(duì)象池: 如果每一次分配的空間大小都一樣故慈,那么就可以按照這個(gè)每次請(qǐng)求分配大小作為一個(gè)單位,把整個(gè)堆空間劃分為大量的小塊框全,每次請(qǐng)求的時(shí)候察绷,只需要找到一個(gè)小塊就可以了。
特點(diǎn): 針對(duì)固定大小的分配空間
入口函數(shù)和程序初始化(P319):
程序運(yùn)行步驟:
操作系統(tǒng)在創(chuàng)建進(jìn)程后津辩,把控制權(quán)交到程序的入口拆撼,這個(gè)入口往往是運(yùn)行庫(kù)中的某個(gè)入口函數(shù)
入口函數(shù)對(duì)運(yùn)行庫(kù)和程序運(yùn)行環(huán)境進(jìn)行初始化,包括堆喘沿、 I/O闸度、線程、全局變量構(gòu)造等等
入口函數(shù)在完成初始化之后摹恨,調(diào)用main函數(shù)筋岛,正式開(kāi)始執(zhí)行程序主題部分。
main 函數(shù)執(zhí)行完畢后晒哄,返回到入口函數(shù)睁宰,入口函數(shù)進(jìn)行清理工作,包括全局變量析構(gòu)寝凌、堆銷(xiāo)毀柒傻、關(guān)閉I/O等,然后進(jìn)行系統(tǒng)調(diào)用結(jié)束進(jìn)程较木。
三. 最后
送上一張喜歡的圖片: