請使勁回答一個關於UNIX/Linux自動擴充stack的問題,unixstack
有本事就出來,沒本事就當鱉!
如果讓我回答關於進程棧,線程棧的問題,只要問題不籠統,只要問題明確,我會一五一十地回答,正確率上九成,然而,可悲的是,問題往往他媽的都不是很明確,因此,遊戲到此結束!!艸。但是如果給我一個問的機會,我會問下面一個問題,記住,使出你拉屎的勁來回答(該問題足夠糙!不必太當回事,重要的東西在下面-):
UNIX/Linux的stack在大多數平台是向下擴充的(注意,我已經告訴事實了,我並沒有問...是如何擴充的,這是可以背誦下來並朗讀出來的),在一個執行流調用了一個函數A,而該函數A在stack上分配了一個大數組導致了stack擴充(注意,這又是一段陳述,我還沒有給出問題),然後A返回了,UNIX/Linux理應回收調A裡面大數組分配stack空間-因為它再也沒有用了,但是它並沒有這麼做(這裡可能是一個陷阱,真的是UNIX/Linux理應這麼做卻沒有做,還是說我只是在逗你玩...不確定,但陳述就是如此)。(注意,我的問題來了),請問,UNIX/Linux為什麼這麼做???!!!
凡是回答作業系統這麼規定的之類的答案,一律零分!況且,你能證明我說的一定對嗎?萬一我是逗你玩呢?作業系統能規定一個錯的東西嗎?或者我可以繼續問為什麼這麼規定,直到像我初中的曆史老師被我問到朝我眼角猛打一拳那樣,如果我能找回點賤人所謂的快感,那麼來打吧!問題就是這樣,不管這個問題是一個偽命題還是你有自己的想法,能說5分鐘的,我覺得也夠可以了。該題目的答題要求如下:
時間限制:5分鐘。
作答方式:全口述,不能畫圖,不能打手勢...語言含糊不清的,表達能力不好的,算錯誤。
答題建議:如果你對OS虛擬記憶體管理以及Linux的VMA實現細節沒有相當深入的理解,請不要猜測答案。請直接回答“不知道”,然後看完此文。
.................................
5分鐘過去。我要公布一點我的想法了。
首先,這個問題在本身看來,有問題。因為雖然Linux理應這麼做,但它:
第一,它不一定能做到;
第二,它根本沒有必要做。
那麼論據是什嗎?憑什麼這樣說?
積極論點沒必要這樣做。執行流還會調用別的函數或者再次調用A,頻繁回收棧損耗效能;
消極論點很難或者不能做到。stack操作是處理器控制的,和OS核心地址空間管理機制之間沒有同步機制,一個函數調用結束後,CPU自動處理stack寄存器的收縮,彈出棧幀,然而它無法通知OS記憶體管理系統去更新進程地址空間的映射關係。
如何處理stack所在地址空間地區的爭議stack會一直擴充到碰到異常的地址B,B可能是一個readonly的地址或者是一個保護空洞,在向下擴充stack情況下,如果地址B偏上,會導致stack空間變小,如果偏下,一旦函數局部變數幾乎佔滿了stack底到B的空間,mmap雖然也能unmap掉這段地區然後remap,然而這會使資料混亂,造成嚴重問題。
拍腦袋的結論mmap或者brk期間,比較stack頂部與esp寄存器,若小於則回收(等於是正常的,大於是不可能的)。
Linux真實的做法Linux沒有判斷什麼esp寄存器,Linux的原則很簡單,只要一個地址處在一個vma範圍內或者處在stack可擴充的範圍內,且擁有許可權的,它就是可以訪問,核心是不管這個VMA是屬於stack還是heap或者別的什麼,具體由應用程式自己控制,也就是說,你完全可以寫一段代碼,把地址空間中所有可以寫的地區全部清零,這完全有可能,緩衝區溢位可能是一種蓄意的破壞,然而程式員偶然的錯誤也會造成破壞,雖然他們大多數都不知道錯誤是如何發生的。我不想用文字長篇大論Linux是如何管理VMA的,你知道這個應該是一個前提,你必須知道這個。我用一段代碼以及兩個圖示來展示Linux系統核心是如何管理stack附近的地址空間映射的,並且在第二張圖中給出,如果你非要蓄意破壞,會造成什麼問題。也就是說,一旦發生莫名奇妙的錯誤,你必須能從細節上理解這個錯誤是如何發生的。
示範代碼
#include <stdio.h>#include <stdlib.h>#include <sys/types.h>#include <unistd.h>#include <sys/mman.h>#define LARGE 70000000#define PAGESIZE 4096// 該函數什麼都不做,僅僅為了把stack向下擴充// 請注意用ulimit將stack大小限制去掉,這樣會更容易說明問題void call(){ int i; char a[LARGE]; // 請相信,一定是在中間的賦值中觸發segfault,因為兩邊的元素要麼處在stack/fixmap vma, // 要麼處在游離的,及其孤獨的,被fixmap給截斷了的vma中。因此下面的賦值不會引發段錯誤: // a[0] = 1; // a[LARGE-1] = 1; for (i = 0; i < LARGE; i++) { a[i] = 1; } }int main(int argc, char **argv){ int i; char *p_map, *p_base, *p_base2; printf("%d\ninit state\n", getpid()); // 擷取stack的大致地址,並且PAGESIZE對齊。 p_base = (char *)&i; p_base2 = (char *)(((unsigned long)p_base) & ~4095); // 擷取pagesize對齊的用來fixmap的地址,該地址起點在當前stack的下面。 p_base2 = (char *)((unsigned long)p_base2 - (unsigned long)36*PAGESIZE); getchar(); // 調用fixmap,顯然,如果你仔細在getchar期間分析了/proc/xx/maps檔案並且 // 得到了上述的那些magic number,下面的mmap無論如何是會成功的! p_map = (char *)mmap((void*)p_base2, PAGESIZE*3, PROT_READ | PROT_WRITE, MAP_FIXED |MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (p_map == MAP_FAILED) { printf("failed 1\n"); } else { printf("before unmap fixmap around stack\n"); getchar(); // 成功了就釋放掉它,此時的地址空間恢複成mmap之前的狀況 munmap(p_map, PAGESIZE*3); } printf("after unmap fixmap around stack\n"); getchar(); call(); printf("after extend stack[first]\n"); getchar(); // 依然調用之前的那個一模一樣的mmap進行fixmap,由於調用了call,stack // 空間已經擴充到了這個fixmap的fixaddress,很遺憾,成功了,然而它將stack vma // 一刀切成了兩段。不管怎樣,訪問還是可以進行的。 p_map = (char *)mmap((void*)p_base2, PAGESIZE*3, PROT_READ | PROT_WRITE, MAP_FIXED |MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); if (p_map == MAP_FAILED) { printf("failed 2\n"); } printf("after second fixmap around stack at the same address\n"); getchar(); // 這裡更狠!部分unmap了上面那個fixmap vma,留下一個空洞。 munmap(p_map, PAGESIZE); printf("after unmap fixmap around stack incompletely\n"); getchar(); // 當空洞被touch的時候,不會引發stack extend!而是直接segfault!爆炸! call(); // 永遠不會到達這裡! printf("after extend stack[second]\n"); getchar(); return 0;}
針對上述代碼的圖解下面一幅圖展示一直到出事之前,該進程的stack附近的地址空間映射地區是怎麼演化的:
下面一幅圖展示出事的過程以及這個事故的原因:
測試方式如果你覺得圖是我自己畫出來的,那麼肯定有一個疑問,我是基於什麼畫出來的,事實上,我並不是通過看代碼畫出來的,我是通過不斷查看procfs的maps檔案即時瞭解該進程地址空間的細節,將其轉換成了上面的圖示,為了讓我有機會到另一個終端去查maps檔案,我在代碼中增加了getchar調用,每次查看完maps檔案,我會拍一下鍵盤的斷行符號鍵。我的測試如下:
代碼編譯後的進程輸出
root@abcd:~# ./a.out
7846
init state
before unmap fixmap around stack
after unmap fixmap around stack
after extend stack[first]
after second fixmap around stack at the same address
after unmap fixmap around stack incompletely
段錯誤 (core dumped)
以下是查看每一步進程maps檔案的輸出:
root@abcd:~# cat /proc/`ps -e|grep a.out|awk '{print $1}'`/maps |tail -n 6|head -n 4
2b2c01483000-2b2c01487000 r--p 00157000 fe:00 387296 /lib/libc-2.11.2.so
2b2c01487000-2b2c01488000 rw-p 0015b000 fe:00 387296 /lib/libc-2.11.2.so
2b2c01488000-2b2c0148f000 rw-p 00000000 00:00 0
7fff6236a000-7fff6237f000 rw-p 00000000 00:00 0 [stack]
root@abcd:~# cat /proc/`ps -e|grep a.out|awk '{print $1}'`/maps |tail -n 6|head -n 4
2b2c01487000-2b2c01488000 rw-p 0015b000 fe:00 387296 /lib/libc-2.11.2.so
2b2c01488000-2b2c0148f000 rw-p 00000000 00:00 0
7fff62359000-7fff6235c000 rw-p 00000000 00:00 0
7fff6236a000-7fff6237f000 rw-p 00000000 00:00 0 [stack]
root@abcd:~# cat /proc/`ps -e|grep a.out|awk '{print $1}'`/maps |tail -n 6|head -n 4
2b2c01483000-2b2c01487000 r--p 00157000 fe:00 387296 /lib/libc-2.11.2.so
2b2c01487000-2b2c01488000 rw-p 0015b000 fe:00 387296 /lib/libc-2.11.2.so
2b2c01488000-2b2c0148f000 rw-p 00000000 00:00 0
7fff6236a000-7fff6237f000 rw-p 00000000 00:00 0 [stack]
root@abcd:~# cat /proc/`ps -e|grep a.out|awk '{print $1}'`/maps |tail -n 6|head -n 4
2b2c01483000-2b2c01487000 r--p 00157000 fe:00 387296 /lib/libc-2.11.2.so
2b2c01487000-2b2c01488000 rw-p 0015b000 fe:00 387296 /lib/libc-2.11.2.so
2b2c01488000-2b2c0148f000 rw-p 00000000 00:00 0
7fff5e0bb000-7fff6237f000 rw-p 00000000 00:00 0 [stack]
root@abcd:~# cat /proc/`ps -e|grep a.out|awk '{print $1}'`/maps |tail -n 6|head -n 4
2b2c01488000-2b2c0148f000 rw-p 00000000 00:00 0
7fff5e0bb000-7fff62359000 rw-p 00000000 00:00 0
7fff62359000-7fff6235c000 rw-p 00000000 00:00 0
7fff6235d000-7fff6237f000 rw-p 00000000 00:00 0 [stack]
root@abcd:~# cat /proc/`ps -e|grep a.out|awk '{print $1}'`/maps |tail -n 6|head -n 4
2b2c01488000-2b2c0148f000 rw-p 00000000 00:00 0
7fff5e0bb000-7fff62359000 rw-p 00000000 00:00 0
7fff6235a000-7fff6235c000 rw-p 00000000 00:00 0
7fff6235d000-7fff6237f000 rw-p 00000000 00:00 0 [stack]
我怕上面的文字資訊太亂,格式在不同瀏覽器會有問題,我還特意截了一張圖:
有什麼用你可以用這種方式徹底限制一個進程的stack的大小,越界了不是報錯,而是segfault。然後你可以signal捕獲這個segfault,在裡面把那個未完全unmap的fixmap vma以及那個可憐且孤獨的殘缺的stack vma給徹底unmap掉。不過這確實沒什麼好玩的。有什麼用呢?它的作用就是讓你更加深入理解Linux對虛擬位址空間的管理方式。
小Tips本文不涉及線程棧,但是倒也不難,線程棧一般在heap區或者中間的大塊mmap區動態分配,mmap的時候給它一個MAP_GROWSDOWN標誌就可以了。關於它的管理方式,沒啥差別。核心問題在於,缺頁例外處理常式是怎麼識別到
一個缺頁是一個vma內部的缺頁(結果就是調頁),還是vma外部的缺頁。在後一種情況下,缺頁處理邏輯還要進一步識別是stack的缺頁(結果就是extend stack然後調頁),還是非stack缺頁(結果就是segfault...)。
Linux的stack除非遇到本文所述的這種方式的擠兌收縮,它是永遠擴充的。如果你想閱讀Linux的核心代碼,那麼也需要理解下面的事實:
1.find_vma函數能找到vma只有一個限制,即輸入地址只要小於尋找vma的end即可,並非很多人想象的那樣輸入地址必須處在尋找vma的start和end之間;
2.find_vma函數之所以實現得如此incompletely,是因為為了簡化缺頁中斷的處理,同時也是為了提供一種更加統一的方式同時處理upgrows和downgrows的vma。
後話雖然這個問題問得有點亂,但是如果能找上述回答連續扯5分鐘的,應該是真行!不過我不知道怎樣的語言表達能力可以不用圖解和代碼把上面的每一個細節說清楚...總之,我覺得我的這個題目是一個好題目。可以建議給看到此文的人,把它做面試題吧。凡是發現不了題目問題的以及說不出所以然的,一律不要!這真是一道好測試題啊,它是如此之好,以至於我還想再出幾道比它更好的。