程序员的自我修养 - 读书笔记文字版
第1部分 簡(jiǎn)介
第1章 溫故而知新
程序在運(yùn)行的時(shí)候先通過分段(segmentation)的方式將虛擬地址空間與真實(shí)的物理內(nèi)存地址空間進(jìn)行一一的映射,但是這種方式每次換入換出的是整個(gè)程序,導(dǎo)致IO變大,更具局部性原理,可以采用分頁(Paging)來解決. 分頁就是將地址空間人為的等分成固定大小的頁,每頁的大小(4KB或者4MB)由操作系統(tǒng)確定。 幾乎所有操作系統(tǒng)都采用4KB的分頁,那么對(duì)于一個(gè)32位的程序來說,最多只能有4GB/4KB = 1048576頁, 物理空間也是同樣的分法。但是當(dāng)真實(shí)物理空間不夠虛擬空間的頁數(shù)的時(shí)候,真正有效的空間以真實(shí)內(nèi)存空間為準(zhǔn)。頁的映射是由MMU部件來完成的。MMU一般集成在CPU內(nèi)部。
外設(shè)使用總線地址,CPU使用物理地址。x86平臺(tái)上,物理地址和總線地址相同。
線程可以分為IO密集型線程和CPU密集型線程,當(dāng)CPU密集型線程的優(yōu)先級(jí)較高的時(shí)候,可能會(huì)導(dǎo)致低優(yōu)先級(jí)的線程被餓死,而IO密集型線程獲得較高優(yōu)先級(jí)的時(shí)候,由于大部分時(shí)間是處于等待狀態(tài),所以不叫不容易造成其他線程的餓死。 如果一個(gè)線程長(zhǎng)時(shí)間得不到執(zhí)行,調(diào)度系統(tǒng)會(huì)逐步提升它的優(yōu)先級(jí)讓它執(zhí)行。
線程還可以分為搶占線程和不搶占線程,搶占線程是那些時(shí)間片用完之后會(huì)被剝奪執(zhí)行權(quán)的線程,不可搶占線程是那些除非執(zhí)行完畢否則不能剝奪執(zhí)行權(quán)的線程。時(shí)至今日,非搶占線程已經(jīng)十分罕見。
windows對(duì)于進(jìn)程和線程的實(shí)現(xiàn)如同教科書一般的標(biāo)準(zhǔn),但是在linux中其實(shí)并沒有嚴(yán)格的線程和進(jìn)程的概念,Linux中所有的執(zhí)行實(shí)體都被成為任務(wù)Task,每一個(gè)任務(wù)概念上類似于一個(gè)單線程的進(jìn)程,但是它的不同任務(wù)之間可以選擇共享內(nèi)存空間,因此在實(shí)際意義上共享了同一內(nèi)存空間的多個(gè)任務(wù)構(gòu)成了一個(gè)進(jìn)程。
為了保證多線程環(huán)境下操作的原子性,有以下幾種辦法:
6.1 鎖
6.2 二元信號(hào)量和多元信號(hào)量
6.3 互斥量,和上面兩個(gè)的區(qū)別是哪個(gè)線程建立的互斥量就必須由哪個(gè)線程去釋放
6.4 臨界區(qū),和上面幾種的區(qū)別是,哪個(gè)進(jìn)程創(chuàng)建臨界區(qū)的鎖,就由哪個(gè)進(jìn)程獲取,對(duì)其他進(jìn)程不可見。
6.5 讀寫鎖,有兩種獲取方式獨(dú)占式和共享式,區(qū)別如下
可重入線程是并發(fā)安全的強(qiáng)力保障,一個(gè)可重入的函數(shù)可以在多線程環(huán)境下放心使用。
三種線程模型
8.1 一對(duì)一模型
8.2 多對(duì)一模型
8.3 多對(duì)多模型
第2部分 - 靜態(tài)鏈接
第2章 編譯和鏈接
程序編譯運(yùn)行的流程是 預(yù)編譯-編譯-匯編-鏈接, 命令分別是:
gcc -E xx.c -o xx.i
gcc -S xx.i -o xx.s 或者(cc1 xx.i)
gcc -c xx.s -o xx.o 或者(as xx.s -s xx.o)
以及 ld xxx.o xxxx.o xxxx.o等
目標(biāo)文件即.obj文件或者.o文件,本質(zhì)上是函數(shù)的集合,用于重定位, 他們的內(nèi)部函數(shù)和變量的存儲(chǔ)方式和真正的可執(zhí)行文件一樣只是在結(jié)構(gòu)上稍有不同。
第3章 目標(biāo)文件里有什么
PC流行的PE文件格式和ELF文件格式都是COEF格式的變種。
bss段只是為全局變量和局部靜態(tài)變量預(yù)留位置,在elf文件中不占空間。
x86的cpu中字節(jié)序采用小端模式存儲(chǔ)(所以elf文件中變量的存儲(chǔ)采用小端), arm架構(gòu)的cpu中采用大端,網(wǎng)絡(luò)字節(jié)序(TCP/IP)采用大端傳輸。
elf文件主要包含,代碼段(.text)和數(shù)據(jù)段(.data, .rodata, .bss), 未初始化的全局變量和和未初始化的局部靜態(tài)變量被存放在.bss段(有時(shí)候未初始化的全局變量也會(huì)存放在符號(hào)表中)。 字符串一般在.rodata段,也有的在.data段。值得一提的是賦值為0的局部靜態(tài)變量也會(huì)被認(rèn)為是未賦值從而放置在bss 段而不是data段。 attribute 命令可以在代碼中指定變量或者函數(shù)存放在elf文件的那一段
reaelf -h xx.o可以輸出elf文件的文件頭信息。 ELF文件最開始的16個(gè)字節(jié)代表了ELF文件的平臺(tái)屬性,其中前四個(gè)字節(jié)是ELF文件的魔數(shù),不同平臺(tái)的魔術(shù)不同(ELF的魔數(shù)是0x7f, 0x45, 0x4c, 0x46 即 DEL控制符,‘E’,‘L’,‘F’ PE/COEF的魔數(shù)是0x01, 0x07, 即’M’,‘Z’), 操作系統(tǒng)在加載可執(zhí)行文件的時(shí)候會(huì)確認(rèn)魔數(shù)是否正確,如果不正確會(huì)拒絕加載。
第5個(gè)字節(jié)是用來標(biāo)識(shí)文件類型,0x01是32位的,0x02是64位的。第6個(gè)字節(jié)表示字節(jié)序是大段或者小端,第七個(gè)字節(jié)表示ELF的主版本,一般是1。后面9個(gè)字節(jié)沒有指定,表示可擴(kuò)展。
ELF文件中段表的位置由e_shoff成員決定,即(Start of section headers的值決定段表的起始位置)。 readelf -S xx.o 可以顯示真正的段表結(jié)構(gòu)。
elf文件中段表是其中的一個(gè)段,段表里面存儲(chǔ)了其他各個(gè)段的起始地址和大小還有其他一些信息。
符號(hào)表里面存儲(chǔ)了全局變量,全局函數(shù)和行號(hào)和用于調(diào)式和核心轉(zhuǎn)儲(chǔ)的局部符號(hào),行號(hào)等信息,鏈接器在鏈接的時(shí)候只關(guān)注全局函數(shù)和變量。 可重定位文件中包含的局部信息對(duì)其他重定位文件來說都是不可見的,只有全局函數(shù)和變可見。可以用nm來查看elf文件的符號(hào)結(jié)果。 符號(hào)表也是elf文件中的一個(gè)段,段名一般叫做.symtab
readelf -s xx.o可以打印輸出elf包含的符號(hào)表的信息。 分別有符號(hào)的類型,值(函數(shù)或者變量的地址),大小,綁定信息(局部,全局,弱引用), Ndx表示符號(hào)所在的段的下標(biāo),該下標(biāo)可以通過readelf -a xx.o 看到。 值得注意的是符號(hào)表中第一個(gè)符號(hào),即下標(biāo)為0的符號(hào)永遠(yuǎn)是一個(gè)未定義的符號(hào)。
對(duì)于STT_SECTION類型的符號(hào),它們的符號(hào)名沒有顯示,其Ndx所對(duì)應(yīng)的段名也就是這里的符號(hào)名, 因?yàn)樗麄兪嵌蚊?hào)。可以通過 objdump -t 看到這種段名符號(hào)
特殊符號(hào)是由ld鏈接器定義的,程序中只需要申明就可以使用,程序在最終鏈接的時(shí)候會(huì)自動(dòng)轉(zhuǎn)化為正確的值,例如__executeable_start, __etext或_etext或etext, _edata或者edata, _end或者end等等。
為了防止函數(shù)和全局變量在各文件之間的命名沖突。但是隨著操作系統(tǒng)和編譯器的分化,GCC已經(jīng)不用在符號(hào)前面加_但是windows平臺(tái)下的編譯器還保持著前面加_這樣的傳統(tǒng)。此外GCC在windows平臺(tái)下下編譯器例如cywin和mingw還保持著這樣的傳統(tǒng)。GCC本身可以通過編譯器選項(xiàng)-fleading-underscore或者-fno-leading-underscore來打開或關(guān)閉是否在C語言符號(hào)前加下劃線。
函數(shù)簽名是C++引入的區(qū)別不同類,命名空間等不同作用域名中相同名稱的成員的機(jī)制 binutils提供的 c++filt命令可以解析一個(gè)函數(shù)修飾后名稱對(duì)應(yīng)的真正的函數(shù)簽名。一般規(guī)則是對(duì)于在命名空間或者類的中的變量和函數(shù)其前面一般是_ZN開頭,以E(+)i/f/d結(jié)尾。最后值得說明的是不同平臺(tái)的編譯器的對(duì)同一函數(shù)的函數(shù)簽名可能是不同的。 VC++的函數(shù)簽名方法沒用向外公開,但是其UndecorateSymbolName()的api可以將修飾后的名稱轉(zhuǎn)換成函數(shù)簽名。
由于不同編譯器采用不同的名字修飾方法,所以導(dǎo)致了不同編譯器產(chǎn)生的目標(biāo)文件無法正常的相互鏈接,這也是導(dǎo)致不同編譯器之間不能互操作的主要原因之一。
目標(biāo)文件即OBJ文件是跨平臺(tái)的
extern "C"{}語句會(huì)導(dǎo)致受作用的變量和函數(shù)名在修飾之后采用的是C語言的格式而不是C++. 對(duì)于同一個(gè)變量或者函數(shù),C++和C語言的修飾不一樣,為了讓C++能正確引用并使用C語言的符號(hào),通常在聲明這個(gè)函數(shù)的時(shí)候會(huì)先判斷當(dāng)前的編譯單元是C還是C++即使用下面的語句
這種技巧幾乎出現(xiàn)在任何系統(tǒng)頭文件中(源文件已經(jīng)判斷了C或者cpp所以不需要這樣寫)
弱符號(hào)和鏈接器的COMMON塊概念的聯(lián)系很緊密
15 強(qiáng)引用和弱引用: 如果引用的一個(gè)庫中的符號(hào)沒用被定義,則鏈接時(shí)候會(huì)報(bào)為定義錯(cuò)誤,這是強(qiáng)引用, 這種情況不報(bào)錯(cuò)的就屬于弱引用。 弱引用和弱符號(hào)對(duì)庫十分有用,因?yàn)檫@樣用戶可以自定義庫中函數(shù),也可以在去掉了某些模塊之后程序依然可以正常鏈接,著使得程序更容易裁剪和組合。
16 使用gcc/g++ -g參數(shù)可以在目標(biāo)文件中保存調(diào)試信息,ELF文件的標(biāo)準(zhǔn)調(diào)試信息格式是DWARF, 目前是 DWARF 3, 微軟的調(diào)試信息標(biāo)準(zhǔn)格式叫CodeView. 調(diào)試信息通常數(shù)倍于ELF文件本身的內(nèi)容, 發(fā)布時(shí)候必須去掉。 在Linux下,可以使用strip去掉ELF文件中的調(diào)試信息
第4章 靜態(tài)鏈接
空間地址分配有按序疊加和相似段合并兩種方法,一般都使用相似段合并的方法。最后的可執(zhí)行文件當(dāng)中包含了可重定位的.o文件里面的所有指令。
Linux下,ELF可執(zhí)行文件默認(rèn)地址從0x08048000開始分配。生成的可執(zhí)行elf文件中的.text段是各個(gè).o文件的.text段大小之和
elf文件中需要重定位的段都有一個(gè)相對(duì)于重定位段(表), 利用objdump -r xx.o 可以查看目標(biāo)文件的重定位表。
對(duì)于弱符號(hào),即未初始化的全局變量,由于在編譯成目標(biāo)文件之后,編譯器不能確定其大小,所以將其放在COMMON塊中,但是當(dāng)鏈接器分析完各個(gè)目標(biāo)文件之后就可以確定其大小,從而將其放在BSS段,所以總體來看,未初始化的全局變量還是放在BSS段的。可以在GCC編譯的時(shí)候使用fno-common指定未賦值的全局變量不在COMMON塊中, 也可可以在代碼中寫__attribbute__ ((nocommon))將其當(dāng)成強(qiáng)符號(hào)處理。
重復(fù)代碼消除,例如C++的模板技術(shù)使得模板可以在多個(gè)源文件中別實(shí)例化但是編譯器并不能知道它在多處被同一種數(shù)據(jù)類型實(shí)例化,所以現(xiàn)在主流編譯器例如GNU 的做法是在每一個(gè)目標(biāo)文件中對(duì)于一個(gè)模板的同一種實(shí)例化使用一種相同的名稱,這樣在鏈接階段,鏈接器會(huì)檢查這些重復(fù)的段并只保留一份。GCC把這種段叫"Link Once"命名為".gnu.linkonce.name" . VC++叫做COMDAT,
這種做法的一個(gè)潛在的問題是,當(dāng)編譯器對(duì)不同的編譯單元使用不同的編譯優(yōu)化選項(xiàng)的時(shí)候,可能會(huì)使得相同名稱的段有不同的內(nèi)容,編譯器的做法是隨意選擇一個(gè)作為鏈接的輸入且提供警告信息。
函數(shù)級(jí)別鏈接: 通常的鏈接過程都是文件或者編譯單元級(jí)別的鏈接,但是當(dāng)只需要使用某個(gè)目標(biāo)為見中的一個(gè)函數(shù)或變量的時(shí)候,就需要全部包含該文件,導(dǎo)致體積很大,編譯器為此專門提供了函數(shù)級(jí)別的鏈接,與重復(fù)代碼消除和相似,編譯器將所有函數(shù)都想模板函數(shù)一樣單獨(dú)保存到一個(gè)段中,需要的時(shí)候再將其包含到輸出文件,其他的則直接拋棄,這雖然較小的最終文件的體積但是由于段的數(shù)目增減,減慢了編譯和鏈接的過程。GCC使用-fdata-sections和-ffunction-sections可以將變量或者函數(shù)分別保存到獨(dú)立的段中。
全局構(gòu)造與析構(gòu): 全局對(duì)象的構(gòu)造在main函數(shù)之前執(zhí)行,全局對(duì)象的析構(gòu)在main函數(shù)之后哦執(zhí)行,Linux下的入口函數(shù)是_start,用于在main執(zhí)行前進(jìn)行初始化。為此,ELF文件提供了兩個(gè)特殊的段.init和.fini。其中.init中保存的指令是main執(zhí)行之前Glibc的初始化部分, .fini中是main函數(shù)正常退出之后Glibc會(huì)安排執(zhí)行的代碼。
為了使得不同平臺(tái)的目標(biāo)文件兼容,即可以相互鏈接,這些文件必須有一直的ABI(Application Binary Interface),即二進(jìn)制兼容,ABI內(nèi)容包括符號(hào)修飾標(biāo)準(zhǔn),變量?jī)?nèi)存布局,函數(shù)調(diào)用方式等等。廠商不希望用戶看見自己的源代碼所以會(huì)提供二進(jìn)制版本,所以二進(jìn)制兼容在大型項(xiàng)目中變得很重要。目前編譯器的兩大陣營(yíng) VISUAL C++和GNU 的GCC各執(zhí)己見互不兼容。ABI兼容問題還有待解決
一個(gè)靜態(tài)庫文件(.a)是由許多.o文件合并而來的,linux下使用ar -t xx.a可以查看.a文件中包含的.o文件。在windows平臺(tái)下可以使用lib /LIST xx.lib查看
可以使用objdump -t xx.a | grep xxx查找特定的目標(biāo)文件。使用gcc -static --verbose -fno-builtin hello.c可以將編譯鏈接過程的中間步驟打印出來, 即使我們寫的代碼非常簡(jiǎn)單,這也是一個(gè)非常長(zhǎng)的依賴關(guān)系。這個(gè)過程會(huì)鏈接部分會(huì)顯示collect2這是ld的一個(gè)包裝,會(huì)調(diào)用ld
BFD(Binary File Descriptor library)是基于所有硬件平臺(tái)(不同的處理器和目標(biāo)文件格式)的一個(gè)抽象層,基于BFD可以不用關(guān)心具體的硬件格式,而進(jìn)行統(tǒng)一操作,因?yàn)锽FD中已經(jīng)包含了這些CPU和可執(zhí)行文件的格式信息,ubuntu下BFD軟件包的名字叫binutils-dev.
第5章
windows上的目標(biāo)文件為COEF格式,而可執(zhí)行文件是PE格式,PE又是COEF格式衍生出來的, 所以將這類文件統(tǒng)稱為PE/COEF格式
64位的Windows中對(duì)PE文件格式做了一點(diǎn)小小的修改,叫做PE32+格式,只是將32位的字段換成了64位而已。
VC++有一些對(duì)C/C++的專用拓展,使用cl編譯的時(shí)候,可以使用 /Za來禁用這些拓展,也可以在程序中使用宏__STDC__來查看VC++是否禁用了這些語法拓展
和GNU對(duì)象,Windows上的cl就是gcc, link就是ld, dumpbin就是objdump,
PE中有兩個(gè)ELF文件中不存在的段,分別是.drectve和 .debug$S. .drectve段是編譯器傳遞給鏈接器的指令,其中的flags表示了他的特點(diǎn)。 .debug$S表示的是符號(hào)相關(guān)的調(diào)試信息。其中有原始文件信息和編譯器信息
可以在cl命令中通過/ZI來關(guān)掉默認(rèn)C庫的鏈接指令
PE文件為了兼容DOS的MZ文件結(jié)構(gòu)在PE文件中加入了DOS的相關(guān)設(shè)置,所以將windows下的可執(zhí)行文件在DOS上運(yùn)行的時(shí)候會(huì)輸出"This program cannot be run in DOS"
第3部分 裝在與動(dòng)態(tài)鏈接
第 6章
程序和進(jìn)程的區(qū)別,程序就是菜譜,是一個(gè)靜態(tài)概念, 進(jìn)程就是菜,是一個(gè)動(dòng)態(tài)概念。
程序的尋址空間由CPU的位數(shù)決定,所以32位下的程序?qū)ぶ房臻g是 2 32 b i t 2^{32} bit 232bit即4GB, 64位下是17179869184 GB, 32位下C語言指針的長(zhǎng)度是4字節(jié),64位系統(tǒng)下長(zhǎng)度是8字節(jié).
對(duì)于一個(gè)32位的程序,尋址空間雖然是4GB,但是程序并不能全部使用,例如在Linux下,1GB是留給操作系統(tǒng)的,剩下的3GB給進(jìn)程,且這3GB內(nèi)存程序也不能完全使用,還有一部分給其他用途; 在Windows上,默認(rèn)情況下2GB留給系統(tǒng),2GB留給進(jìn)程, 但是可以通過修改winows根目錄下Boot.ini文件調(diào)整內(nèi)存分配和linux下一樣。
1995年P(guān)entium Pro CPU使用PAE(Physical Address Extension),將地址線擴(kuò)充到了36位,所以理論上計(jì)算機(jī)可以尋址的空間變成了64G, 但是進(jìn)程的尋址空間仍然是4G(32位系統(tǒng)下指針是4個(gè)字節(jié))
為了使應(yīng)用程序能使用超過32位的內(nèi)存,Windows上可以使用AWE(Address Window Extension)的方式, 在Linux上可以使用mmap(),但是這只是一種補(bǔ)救32地址線的方法, 在原來16位的DOS上也曾有過這樣的做法。
由于每次將同一頁程序裝入內(nèi)存中時(shí)的地址可能并不一樣,所以加入按實(shí)際物理地址進(jìn)行操作,那每次都需要重新讀取地址,所以MMU就用來在實(shí)際物理地址和虛擬地址之間轉(zhuǎn)換,
4.1 創(chuàng)建虛擬地址空間,也就是分配一個(gè)頁目錄。
4.2 讀取可執(zhí)行文件頭,建立虛擬地址空間和可執(zhí)行文件的對(duì)應(yīng)關(guān)系(可執(zhí)行文件被裝載時(shí)其實(shí)就是被映射的虛擬空間,所以也被稱為映像文件)
4.3 將CPU指令寄存器設(shè)置成可執(zhí)行文件的入口(也就是可執(zhí)行文件代碼段的其實(shí)地址)
readelf -S xx可以輸出可執(zhí)行文件中的section,而readelf -l xx可以輸出可執(zhí)行文件中的Segment(即程序頭表),即怎樣被裝入進(jìn)程空間, Segment中只有類型是LOAD的部分會(huì)被映射,這部分在裝載之后又會(huì)被映射到兩段VMA,分別是可讀可寫的部分和可讀可執(zhí)行的部分,
第7章 動(dòng)態(tài)鏈接
第七章7.6節(jié)以后略去不寫
第八章
總結(jié)
以上是生活随笔為你收集整理的程序员的自我修养 - 读书笔记文字版的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 使用家庭或宿舍宽带将个人电脑变为服务器
- 下一篇: 深入浅出Docker 读书笔记(九)