別被譚浩強的《C程序設(shè)計》帶偏了!
共 3843字,需瀏覽 8分鐘
·
2024-04-14 16:13
周末好各位,我是軒轅,最近干貨有點少呀,我要檢討一下了。
今天就來跟大家分享一個干貨,這是來自于一位粉絲的問題。
這位粉絲最近面試騰訊,被問到了一個問題:進程地址空間里有什么?
這個問題展開可以聊的東西非常多,從編程語言到可執(zhí)行文件,從堆棧空間到虛擬內(nèi)存,可以幫助面試官快速了解候選人這部分的知識儲備。
而實際上,我發(fā)現(xiàn)對于這個問題,基本上沒有人能夠說得特別清楚,各種是似而非的回答:
“代碼段”
“靜態(tài)存儲區(qū)”
“動態(tài)數(shù)據(jù)區(qū)”
“堆棧區(qū)”
一系列書本氣十足的說法,不一而足。
確實,很多同學手里那本譚浩強的《C程序設(shè)計》告訴我們,內(nèi)存中不就是這樣的嗎?難道書上寫錯了?
書上寫的也不算錯,但它只是提出了一個非常非常簡單的內(nèi)存模型,實際的操作系統(tǒng)上的進程空間中,遠比這復(fù)雜100倍。
虛擬內(nèi)存
眾所周知,現(xiàn)代操作系統(tǒng)采用虛擬內(nèi)存的方式管理內(nèi)存,雖然計算機上的內(nèi)存條只有幾個G,但卻為每個進程營造出了一個完整的地址空間,加起來遠超內(nèi)存條容量的大小。
這個地址空間,在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個字節(jié)。如果一個字節(jié)用一個小格子表示,那進程的地址空間就是這么多個小格子排列組成:
不過,這樣子看起來有些麻煩,我們一般不這樣畫圖,而是以4個字節(jié)為一組,畫出地址空間來:
這篇文章要討論的就是,在上面這張圖中,到底有哪些內(nèi)容。我們一步步填充進去,最后在文章的末尾你將會看到一個進程空間內(nèi)容的全貌。
內(nèi)核地址空間
首先,在進程地址空間中占據(jù)最大篇幅的當屬操作系統(tǒng)內(nèi)核空間部分。
內(nèi)核空間的部分,所有進程共享,在不同的進程中,這部分內(nèi)存空間映射的內(nèi)存頁面是一樣的。
注:其實上面這句話也不是完全正確,如果你研究過操作系統(tǒng)內(nèi)核就會知道不同進程的內(nèi)核空間部分也不是完全一致的。一個最簡單的例子就是在Windows操作系統(tǒng)上,不同用戶登錄同一臺計算機后會產(chǎn)生會話session隔離,不同用戶啟動的進程位于各自的session中,而不同session在內(nèi)核空間部分頁面的映射會有差異。
內(nèi)核空間部分一般位于進程地址空間中高地址區(qū)域,至于大小,在Windows 32位系統(tǒng)上是2GB,在Linux上是1GB。
可執(zhí)行文件
拋開了系統(tǒng)內(nèi)核空間部分,接下來來看一下用戶態(tài)地址空間中有哪些東西?
第一個非常重要的區(qū)域就是可執(zhí)行文件所在的區(qū)域。
我們編寫的程序,最終是轉(zhuǎn)換成對應(yīng)操作系統(tǒng)上可執(zhí)行文件在運行,在Linux上是ELF格式,在Windows上是PE格式,比如exe。
程序運行的時候,加載器會將目標可執(zhí)行文件加載到進程的地址空間中。
映射后的可執(zhí)行程序所占大小可能會比文件的真實尺寸更大,這是由于內(nèi)存頁面對齊的原因,導致可執(zhí)行文件中的不同節(jié)會通過填充0來對齊,從而占據(jù)了更大的空間。
你可能會問:那我寫的Java程序、Python腳本程序呢?它們的進程空間中沒有可執(zhí)行文件吧?
Java程序是通過JVM虛擬機在翻譯執(zhí)行,主進程就是JVM的可執(zhí)行文件,執(zhí)行Java程序的時候,會先啟動EXE/ELF格式的虛擬機,再由虛擬機加載java字節(jié)碼文件執(zhí)行。
Python是解釋執(zhí)行的腳本語言,執(zhí)行Python腳本的時候,也是先啟動Python的解釋器程序,這也是一個EXE/ELF格式的可執(zhí)行文件,再由解釋器解釋執(zhí)行Python腳本。
其他腳本語言也差不多類似。
總之,所有程序的執(zhí)行,都會有一個核心的可執(zhí)行文件。
不管是Windows的PE格式,還是Linux的ELF格式,一般都會包含這幾個部分:
代碼區(qū):主要是程序編譯后的CPU指令,所有的函數(shù)代碼編譯后的指令都在這里。
數(shù)據(jù)區(qū):主要是程序中定義的全局變量,static變量。
常量區(qū):咱們程序中會用到常量字符串編譯后就存在這里。
可執(zhí)行文件區(qū)域在進程地址空間哪個位置呢?
在早期的操作系統(tǒng)中,一般是在一個固定地址,比如Windows上,是在0x400000地址。但因為安全性的原因,后期的操作系統(tǒng)都開啟了隨機加載的功能,每一次程序啟動加載到地址空間中的地址都可能不一樣。
動態(tài)鏈接庫
程序需要運行,光靠自己的可執(zhí)行文件是不夠的,還需要依賴一些動態(tài)鏈接庫。在Windows上是DLL文件,在Linux上是so文件。
即便你編寫的程序只是一個單獨的可執(zhí)行程序,沒有指定依賴任何動態(tài)庫,它仍然需要依賴操作系統(tǒng)的一系列動態(tài)鏈接庫才能工作。
程序需要依靠這些系統(tǒng)動態(tài)鏈接庫才能使用操作系統(tǒng)提供的系統(tǒng)調(diào)用,這是用戶態(tài)進程和操作系統(tǒng)之間的一個中間層。
在Windows上,可以通過ProcessExplore,看到一個進程中加載了非常多的動態(tài)鏈接庫。
在Linux上,可以通過pmap命令查看一個進程中的動態(tài)鏈接庫。
和可執(zhí)行文件的加載類似,現(xiàn)代操作系統(tǒng)加載動態(tài)鏈接庫的地址一般都不固定,而是隨機的。
線程棧
棧是程序執(zhí)行過程中非常重要的一個東西,程序執(zhí)行時的局部變量,函數(shù)調(diào)用時傳參、返回地址這些東西都是存儲在棧中的。
很多同學都知道程序執(zhí)行會用到棧,但又經(jīng)常弄錯,比如有兩個很多人容易弄錯的點。
1、堆棧
很多人經(jīng)常把“堆?!眱勺謷煸谧爝?,但“堆”和“?!逼鋵嵤莾蓚€東西。
2、棧不止一個
棧不是程序所有,而是線程所有,如果一個程序運行后開啟了多個線程,則每一個線程都會有一個自己的棧。
所有線程的棧都在進程的地址空間中,具體位置是由操作系統(tǒng)內(nèi)核在創(chuàng)建線程的時候確定的,用戶程序無法控制。
進程堆
說到棧,那就必然離不開它的好基友——堆。
堆大家應(yīng)該不會陌生,C語言中malloc、C++中的new都是在堆區(qū)域中分配內(nèi)存。
堆是一大塊內(nèi)存,由C和C++語言的運行時庫Runtime初始化時向系統(tǒng)申請的,后續(xù)調(diào)用malloc和new的時候再去堆中分配。
不同于前面介紹的部分,堆這個東西是語言層面的東西,理論上完全可能存在一個沒有動態(tài)內(nèi)存分配的語言寫出的程序,進程地址空間中就沒有堆。
不過這樣貌似也不行,因為Windows和Linux的動態(tài)庫都是用C語言寫成的,它們也會用到堆。
除了棧可能有多個,堆其實也是可以有多個。
文件映射
除了棧和堆,我們在編程中,還經(jīng)常用到共享內(nèi)存、內(nèi)存文件映射、或者直接使用VirtualAlloc/mmap分配內(nèi)存等操作,這些操作,是直接在進程地址空間中的空余部分,劃出的一塊單獨區(qū)域。他們也是地址空間中經(jīng)常出現(xiàn)的部分。
最后推薦一個神器:VMMap,用來查看進程地址空間中所有內(nèi)容的占用情況。
在軟件開發(fā)過程中,常用來定位程序問題,觀察進程內(nèi)存空間使用情況,比如觀察線程棧暴漲,則程序有可能陷入了死循環(huán),無限遞歸,如果堆內(nèi)存暴漲,則很有可能有內(nèi)存泄露。
在軟件漏洞安全研究中,也常用來分析程序被攻擊的原理。
今天的分享就是這樣,現(xiàn)在,你知道進程的內(nèi)存空間里都有什么了嗎?
