軒轅問(wèn)了一個(gè)問(wèn)題,居然都翻車(chē)了~
大家好,我是軒轅,最近干貨有點(diǎn)少呀,我要檢討一下了。
因?yàn)槲以诿杉拢患敲χU忻嬖嚕硪患铮荣u(mài)個(gè)關(guān)子,過(guò)段時(shí)間告訴你們。

說(shuō)到面試,操作系統(tǒng)知識(shí)是必考內(nèi)容,基本上對(duì)所有人,我都問(wèn)了一個(gè)同一個(gè)問(wèn)題:進(jìn)程地址空間里有什么?
這個(gè)問(wèn)題展開(kāi)可以聊的東西非常多,從編程語(yǔ)言到可執(zhí)行文件,從堆棧空間到虛擬內(nèi)存,可以讓我快速了解候選人這部分的知識(shí)儲(chǔ)備。
而實(shí)際上,我發(fā)現(xiàn)對(duì)于這個(gè)問(wèn)題,基本上沒(méi)有人能夠說(shuō)得特別清楚,各種是似而非的回答:
“代碼段”
“靜態(tài)存儲(chǔ)區(qū)”
“動(dòng)態(tài)數(shù)據(jù)區(qū)”
“堆棧區(qū)”
一系列書(shū)本氣十足的說(shuō)法,不一而足。
確實(shí),很多同學(xué)手里那本譚浩強(qiáng)的《C程序設(shè)計(jì)》告訴我們,內(nèi)存中不就是這樣的嗎?難道書(shū)上寫(xiě)錯(cuò)了?

書(shū)上寫(xiě)的也不算錯(cuò),但它只是提出了一個(gè)非常非常簡(jiǎn)單的內(nèi)存模型,實(shí)際的操作系統(tǒng)上的進(jìn)程空間中,遠(yuǎn)比這復(fù)雜100倍。
虛擬內(nèi)存
眾所周知,現(xiàn)代操作系統(tǒng)采用虛擬內(nèi)存的方式管理內(nèi)存,雖然計(jì)算機(jī)上的內(nèi)存條只有幾個(gè)G,但卻為每個(gè)進(jìn)程營(yíng)造出了一個(gè)完整的地址空間,加起來(lái)遠(yuǎn)超內(nèi)存條容量的大小。

這個(gè)地址空間,在32位操作系統(tǒng)上是4GB大小,這是32位CPU在正常模式下能尋址的最大范圍,Linux和Windows均是如此。
至于64位系統(tǒng)的情況則變得更加復(fù)雜,尋址范圍更大。可以參考這兩篇文章:
Linux 64位地址空間:https://www.cnblogs.com/yizhanwillsucceed/p/13578076.html Windows 64位地址空間:https://www.cnblogs.com/xuanyuan/p/5260871.html
4GB,也就是4*1024*1024*1024 = 4294967294個(gè)字節(jié)。如果一個(gè)字節(jié)用一個(gè)小格子表示,那進(jìn)程的地址空間就是這么多個(gè)小格子排列組成:

不過(guò),這樣子看起來(lái)有些麻煩,我們一般不這樣畫(huà)圖,而是以4個(gè)字節(jié)為一組,畫(huà)出地址空間來(lái):

這篇文章要討論的就是,在上面這張圖中,到底有哪些內(nèi)容。我們一步步填充進(jìn)去,最后在文章的末尾你將會(huì)看到一個(gè)進(jìn)程空間內(nèi)容的全貌。
內(nèi)核地址空間
首先,在進(jìn)程地址空間中占據(jù)最大篇幅的當(dāng)屬操作系統(tǒng)內(nèi)核空間部分。

內(nèi)核空間的部分,所有進(jìn)程共享,在不同的進(jìn)程中,這部分內(nèi)存空間映射的內(nèi)存頁(yè)面是一樣的。
注:其實(shí)上面這句話(huà)也不是完全正確,如果你研究過(guò)操作系統(tǒng)內(nèi)核就會(huì)知道不同進(jìn)程的內(nèi)核空間部分也不是完全一致的。一個(gè)最簡(jiǎn)單的例子就是在Windows操作系統(tǒng)上,不同用戶(hù)登錄同一臺(tái)計(jì)算機(jī)后會(huì)產(chǎn)生會(huì)話(huà)session隔離,不同用戶(hù)啟動(dòng)的進(jìn)程位于各自的session中,而不同session在內(nèi)核空間部分頁(yè)面的映射會(huì)有差異。
內(nèi)核空間部分一般位于進(jìn)程地址空間中高地址區(qū)域,至于大小,在Windows 32位系統(tǒng)上是2GB,在Linux上是1GB。
可執(zhí)行文件
拋開(kāi)了系統(tǒng)內(nèi)核空間部分,接下來(lái)來(lái)看一下用戶(hù)態(tài)地址空間中有哪些東西?
第一個(gè)非常重要的區(qū)域就是可執(zhí)行文件所在的區(qū)域。
我們編寫(xiě)的程序,最終是轉(zhuǎn)換成對(duì)應(yīng)操作系統(tǒng)上可執(zhí)行文件在運(yùn)行,在Linux上是ELF格式,在Windows上是PE格式,比如exe。
程序運(yùn)行的時(shí)候,加載器會(huì)將目標(biāo)可執(zhí)行文件加載到進(jìn)程的地址空間中。
映射后的可執(zhí)行程序所占大小可能會(huì)比文件的真實(shí)尺寸更大,這是由于內(nèi)存頁(yè)面對(duì)齊的原因,導(dǎo)致可執(zhí)行文件中的不同節(jié)會(huì)通過(guò)填充0來(lái)對(duì)齊,從而占據(jù)了更大的空間。

你可能會(huì)問(wèn):那我寫(xiě)的Java程序、Python腳本程序呢?它們的進(jìn)程空間中沒(méi)有可執(zhí)行文件吧?
Java程序是通過(guò)JVM虛擬機(jī)在翻譯執(zhí)行,主進(jìn)程就是JVM的可執(zhí)行文件,執(zhí)行Java程序的時(shí)候,會(huì)先啟動(dòng)EXE/ELF格式的虛擬機(jī),再由虛擬機(jī)加載java字節(jié)碼文件執(zhí)行。
Python是解釋執(zhí)行的腳本語(yǔ)言,執(zhí)行Python腳本的時(shí)候,也是先啟動(dòng)Python的解釋器程序,這也是一個(gè)EXE/ELF格式的可執(zhí)行文件,再由解釋器解釋執(zhí)行Python腳本。
其他腳本語(yǔ)言也差不多類(lèi)似。
總之,所有程序的執(zhí)行,都會(huì)有一個(gè)核心的可執(zhí)行文件。

不管是Windows的PE格式,還是Linux的ELF格式,一般都會(huì)包含這幾個(gè)部分:
代碼區(qū):主要是程序編譯后的CPU指令,所有的函數(shù)代碼編譯后的指令都在這里。
數(shù)據(jù)區(qū):主要是程序中定義的全局變量,static變量。
常量區(qū):咱們程序中會(huì)用到常量字符串編譯后就存在這里。
可執(zhí)行文件區(qū)域在進(jìn)程地址空間哪個(gè)位置呢?
在早期的操作系統(tǒng)中,一般是在一個(gè)固定地址,比如Windows上,是在0x400000地址。但因?yàn)榘踩缘脑颍笃诘牟僮飨到y(tǒng)都開(kāi)啟了隨機(jī)加載的功能,每一次程序啟動(dòng)加載到地址空間中的地址都可能不一樣。
動(dòng)態(tài)鏈接庫(kù)
程序需要運(yùn)行,光靠自己的可執(zhí)行文件是不夠的,還需要依賴(lài)一些動(dòng)態(tài)鏈接庫(kù)。在Windows上是DLL文件,在Linux上是so文件。

即便你編寫(xiě)的程序只是一個(gè)單獨(dú)的可執(zhí)行程序,沒(méi)有指定依賴(lài)任何動(dòng)態(tài)庫(kù),它仍然需要依賴(lài)操作系統(tǒng)的一系列動(dòng)態(tài)鏈接庫(kù)才能工作。
程序需要依靠這些系統(tǒng)動(dòng)態(tài)鏈接庫(kù)才能使用操作系統(tǒng)提供的系統(tǒng)調(diào)用,這是用戶(hù)態(tài)進(jìn)程和操作系統(tǒng)之間的一個(gè)中間層。
在Windows上,可以通過(guò)ProcessExplore,看到一個(gè)進(jìn)程中加載了非常多的動(dòng)態(tài)鏈接庫(kù)。

在Linux上,可以通過(guò)pmap命令查看一個(gè)進(jìn)程中的動(dòng)態(tài)鏈接庫(kù)。

和可執(zhí)行文件的加載類(lèi)似,現(xiàn)代操作系統(tǒng)加載動(dòng)態(tài)鏈接庫(kù)的地址一般都不固定,而是隨機(jī)的。
線(xiàn)程棧
棧是程序執(zhí)行過(guò)程中非常重要的一個(gè)東西,程序執(zhí)行時(shí)的局部變量,函數(shù)調(diào)用時(shí)傳參、返回地址這些東西都是存儲(chǔ)在棧中的。

很多同學(xué)都知道程序執(zhí)行會(huì)用到棧,但又經(jīng)常弄錯(cuò),比如有兩個(gè)很多人容易弄錯(cuò)的點(diǎn)。
1、堆棧
很多人經(jīng)常把“堆棧”兩字掛在嘴邊,但“堆”和“棧”其實(shí)是兩個(gè)東西。
2、棧不止一個(gè)
棧不是程序所有,而是線(xiàn)程所有,如果一個(gè)程序運(yùn)行后開(kāi)啟了多個(gè)線(xiàn)程,則每一個(gè)線(xiàn)程都會(huì)有一個(gè)自己的棧。
所有線(xiàn)程的棧都在進(jìn)程的地址空間中,具體位置是由操作系統(tǒng)內(nèi)核在創(chuàng)建線(xiàn)程的時(shí)候確定的,用戶(hù)程序無(wú)法控制。
進(jìn)程堆
說(shuō)到棧,那就必然離不開(kāi)它的好基友——堆。
堆大家應(yīng)該不會(huì)陌生,C語(yǔ)言中malloc、C++中的new都是在堆區(qū)域中分配內(nèi)存。
堆是一大塊內(nèi)存,由C和C++語(yǔ)言的運(yùn)行時(shí)庫(kù)Runtime初始化時(shí)向系統(tǒng)申請(qǐng)的,后續(xù)調(diào)用malloc和new的時(shí)候再去堆中分配。
不同于前面介紹的部分,堆這個(gè)東西是語(yǔ)言層面的東西,理論上完全可能存在一個(gè)沒(méi)有動(dòng)態(tài)內(nèi)存分配的語(yǔ)言寫(xiě)出的程序,進(jìn)程地址空間中就沒(méi)有堆。
不過(guò)這樣貌似也不行,因?yàn)閃indows和Linux的動(dòng)態(tài)庫(kù)都是用C語(yǔ)言寫(xiě)成的,它們也會(huì)用到堆。
除了棧可能有多個(gè),堆其實(shí)也是可以有多個(gè)。
文件映射
除了棧和堆,我們?cè)诰幊讨校€經(jīng)常用到共享內(nèi)存、內(nèi)存文件映射、或者直接使用VirtualAlloc/mmap分配內(nèi)存等操作,這些操作,是直接在進(jìn)程地址空間中的空余部分,劃出的一塊單獨(dú)區(qū)域。他們也是地址空間中經(jīng)常出現(xiàn)的部分。
最后推薦一個(gè)神器:VMMap,用來(lái)查看進(jìn)程地址空間中所有內(nèi)容的占用情況。

在軟件開(kāi)發(fā)過(guò)程中,常用來(lái)定位程序問(wèn)題,觀(guān)察進(jìn)程內(nèi)存空間使用情況,比如觀(guān)察線(xiàn)程棧暴漲,則程序有可能陷入了死循環(huán),無(wú)限遞歸,如果堆內(nèi)存暴漲,則很有可能有內(nèi)存泄露。
在軟件漏洞安全研究中,也常用來(lái)分析程序被攻擊的原理。
如果你需要這個(gè)神器,可以添加我的微信,我發(fā)給你。
