《程序員的自我修養(yǎng)》讀書(shū)筆記

bestswifter:

我推薦的iOS開(kāi)發(fā)書(shū)單
《程序員的自我修養(yǎng)》讀書(shū)總結(jié)**

落影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)記:

詞法分析.png
  • **語(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)分別為 26的語(yǔ)法樹(shù)谈山,如果你只寫(xiě)2+俄删,在這一步就會(huì)報(bào)錯(cuò)。

語(yǔ)法分析..png
  • **語(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ǔ)義分析.png
  • ** 中間語(yǔ)言生成 :**
    ** 源代碼優(yōu)化器 ** 將整個(gè)語(yǔ)法樹(shù)轉(zhuǎn)換成中間代碼,比較常見(jiàn)的中間代碼有: 三地址碼威兜,比如 2 + 3 會(huì)寫(xiě)成 t1 = 2 + 3,同時(shí)也會(huì)把編譯器就可以確定的表達(dá)式進(jìn)行優(yōu)化销斟。
中間語(yǔ)言生成.png
  • ** 目標(biāo)代碼生成與優(yōu)化: **
    代碼生成器根據(jù)三地址碼生成依賴(lài)于目標(biāo)機(jī)器的代碼,也就是匯編語(yǔ)言椒舵。
目標(biāo)代碼.png

目標(biāo)代碼優(yōu)化器對(duì)目標(biāo)代碼進(jìn)行優(yōu)化:

優(yōu)化后代碼.png

**.i 經(jīng)過(guò)編譯蚂踊,得到匯編文件,后綴是.s **

** 3.匯編(P40): **
這一步由匯編器負(fù)責(zé)笔宿,將匯編語(yǔ)言轉(zhuǎn)換成機(jī)器
可以執(zhí)行的語(yǔ)言(完全由01組成)犁钟。匯編文件經(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)用層次

軟件系統(tǒng)調(diào)用層次.png

**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偎快。CPUMMU負(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) :

程序與目標(biāo)文件.png
  • ** 文件頭:** 描述了整個(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文件頭輸出.png

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)體.png

ELF文件頭結(jié)構(gòu)成員含義:

ELF文件頭結(jié)構(gòu)(1).png
ELF文件頭結(jié)構(gòu)(2).png

重定位表 (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) : **

符號(hào)分類(lèi).png

鏈接過(guò)程中之關(guān)系全局符號(hào)的相互 “粘合”慕趴,局部符號(hào)躏啰、段名、行號(hào)等都是次要的耙册。

符號(hào)輸出.png

符號(hào)修飾和函數(shù)簽名(P86) :

** C++符號(hào)修飾 :**
函數(shù)簽名包含一個(gè)函數(shù)的信息给僵,包括函數(shù)名、它的參數(shù)類(lèi)型、它所在的類(lèi)和名稱(chēng)空間及其他信息帝际。
如例子所示:

C++函數(shù).png

這段代碼中有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)如表所示:

函數(shù)簽名.png

簽名生成規(guī)則:

簽名生成規(guī)則.png

由于不同的編譯器采用不同的名字修飾方法则北,必然會(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;
}

這里塔次,weakweak2 是弱符號(hào)方篮, strongmain 是強(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)系。

簡(jiǎn)單覆蓋裝入.png
頁(yè)映射和頁(yè)裝載.png

進(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í)行叶眉。

頁(yè)錯(cuò)誤.png

進(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è)面彼念。

段映射.png

進(jìn)程棧初始化(P172) :

進(jìn)程運(yùn)行后挪圾,操作系統(tǒng)會(huì)初始化進(jìn)程的堆棧,其中存放了環(huán)境變量和命令行參數(shù)逐沙。這些參數(shù)被傳給main函數(shù)(argcargv兩個(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>

程序棧實(shí)例.png

活動(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)記錄用edpesp這兩個(gè)寄存器劃定范圍庭惜。esp寄存器始終指向棧的頂部,同時(shí)也就指向了當(dāng)前函數(shù)的活動(dòng)記錄的頂部穗酥。而相對(duì)的护赊,edp寄存器指向了函數(shù)活動(dòng)記錄的一個(gè)固定位置,edp寄存器又被稱(chēng)為幀指針迷扇。

活動(dòng)記錄.png

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)(如WindowsHeapCreate系列),調(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)程较木。

三. 最后

送上一張喜歡的圖片:

碟.jpg
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末红符,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子伐债,更是在濱河造成了極大的恐慌预侯,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件峰锁,死亡現(xiàn)場(chǎng)離奇詭異萎馅,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)虹蒋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)糜芳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人魄衅,你說(shuō)我怎么就攤上這事峭竣。” “怎么了晃虫?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵皆撩,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我哲银,道長(zhǎng)毅访,這世上最難降的妖魔是什么沮榜? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任盘榨,我火速辦了婚禮喻粹,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘草巡。我一直安慰自己守呜,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布山憨。 她就那樣靜靜地躺著查乒,像睡著了一般。 火紅的嫁衣襯著肌膚如雪郁竟。 梳的紋絲不亂的頭發(fā)上玛迄,一...
    開(kāi)封第一講書(shū)人閱讀 49,007評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音棚亩,去河邊找鬼蓖议。 笑死,一個(gè)胖子當(dāng)著我的面吹牛讥蟆,可吹牛的內(nèi)容都是我干的勒虾。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼瘸彤,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼修然!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起质况,我...
    開(kāi)封第一講書(shū)人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤愕宋,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后结榄,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體中贝,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年潭陪,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了雄妥。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡依溯,死狀恐怖老厌,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情黎炉,我是刑警寧澤枝秤,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站慷嗜,受9級(jí)特大地震影響淀弹,放射性物質(zhì)發(fā)生泄漏丹壕。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一薇溃、第九天 我趴在偏房一處隱蔽的房頂上張望菌赖。 院中可真熱鬧,春花似錦沐序、人聲如沸琉用。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)邑时。三九已至,卻和暖如春特姐,著一層夾襖步出監(jiān)牢的瞬間晶丘,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工唐含, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留浅浮,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓觉壶,卻偏偏與公主長(zhǎng)得像脑题,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子铜靶,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容