關於linux棧的乙個深層次的問題

2021-10-09 05:52:39 字數 4414 閱讀 5290

原文:

記憶體不用白不用,何必在一開始就限制棧的大小,linux的機制是盡量多盡量緊湊的使用虛擬記憶體,原則就是你現在不用我就用,沒有預留的概念,當然你可以通過系統呼叫實現預留,就像glibc的堆管理那樣,這裡所說的完全是針對於作業系統核心的,使用者空間程式完全可以向作業系統通過brk或者mmap實現使用者空間的記憶體預留。windows的實現就不是這樣,windows要求程式在執行之前就限制好棧使用的記憶體的大小,一旦超過這個大小,哪怕向下伸展的棧下方的記憶體沒有實體使用,那麼也會觸發異常,windows將棧記憶體的使用完全暴露給了使用者空間,而linux卻沒有,linux透明的實現了棧記憶體的動態管理,一開始分配給棧的記憶體很小,隨著函式呼叫深度的增加和區域性變數空間的增加,棧會動態得到擴充套件,當前這個動態擴充套件的前提是向下擴充套件的棧的下方的記憶體沒有被對映,也就是這些棧需要的記憶體還不屬於任何的vm_area_struct,一旦其它非棧的記憶體對映對映到了離3g界限非常近的地方,那麼linux使用者棧將會被限制的非常小,但是那一塊記憶體對映到**完全取決於應用程式自己,核心根本不管,核心只是接受brk或者mmap的引數,然後實現記憶體對映(本文前提,棧是向下擴充套件的)。

正如以上所說,linux可以實現像windows那樣的棧記憶體的限制,也可以不限制棧記憶體,然後將一切交給核心來完成,下面看一下我作的實驗:

#include

#define page_size 4096

int g;

void stubfunc()

g++;

printf("g:%d/n",g);

char as[page_size]; //作為區域性變數,效果就是每呼叫一次該stubfunc函式,棧就會增長最少乙個頁面

int i = 0;

for( i=0;i < page_size ;i++)

memset(as+i,1,1);

//getchar();這個呼叫使得試驗者有機會去檢視/proc/***/maped檔案,實時看到棧在擴充套件

stubfunc();

int main( int argc, char* ar** )

stubfunc(); //開啟遞迴函式呼叫,目的是測試linux的棧可以動態擴充套件到多少

我執行了好幾次該程式,在g達到3500到4200的時候出現段錯誤,這說明linux的棧的大小可以擴充套件到3500到4200個頁面的大小,當然這完全取決於核心的實現和使用者空間c庫的實現,如果用彙編實現那麼將完全取決於核心的對映策略,但是不管怎樣棧是在動態擴充套件。如果將此程式放到windows上執行,那麼g能達到多少將完全取決於核心的實現和在鏈結程式的時候指定的stack_size的大小,比如我將stack_size指定為32個頁面,那麼g的值將在達到32左右的時候出現段錯誤,因為windows的棧管理是用一套很嚴格的機制完成的,分為提交頁面和保留頁面,具體請參考我的文章《windows和linux的記憶體管理》,一旦超越了了事先設定好的界限,那麼就會出現異常,即使棧的下面都是空閒記憶體也不行,因為windows的執行完全是靜態指定的,這裡可以看到windows僅僅適合桌面應用,靈活性非常差,它只要保證桌面小應用能高效穩定執行而不能充分動態利用大型機上的充足的資源,另外,作業系統本來不應該提供過多的策略,除非作業系統的設計者認為程式設計師都是白痴,棧越界檢查其實應該是使用者空間自己的事情,核心何必插手,搞windows正是被這種策略慣壞了才不求甚解的,這種方式也不能說一無是處,最起碼可以讓更多的人低門檻的步入windows程式設計領域,然後高效快速的進行生產,而在linux下程式設計的幾乎都是有兩下子的,因為核心幾乎不會為你做什麼。下面看一下linux下如何進行對棧的限制,這裡僅僅簡單的談談原理。

如果想限制棧的大小,那麼最起碼可以檢測到棧的越界,一種很顯然的做法就是在棧的限制頁面下面對映乙個不可訪問的頁面,一旦棧伸展到該頁面就會出現異常,其實windows也是這麼做的,最起碼大致原理是這樣的,下面看看具體實現,注意,如果想實現乙個完整的棧限制還要密切注意其它記憶體對映而不僅僅是棧下面的那個對映,你要保證你為了限制棧而設定的不可訪問的記憶體段是棧緊接著下面的,並且保證這個對映一定成功,也就是說不能讓別的記憶體段對映到此位置,但是這是乙個複雜的過程,涉及到libc庫對可重定位共享庫對映的實現,本文不談:

#include

#define page_size 4096

int g;

void stubfunc()

int main( int argc, char* ar** )

int * ap = &arg; //棧的大概位置,因為argc在棧上,我的結果是0xbfbcddf8

unsigned long address = ( unsigned long )ap; address = (address-x*4096)&0xfffff000; //x為乙個整數,並且最後將address圓整到4096的倍數,這是map_fixed的要求

char * p = (char*)mmap( address, //,在address處確定性對映,我的結果是0xbf000000

0x1000, //乙個頁面的長度

prot_none, //不可訪問,一旦棧擴充套件到這裡將會出錯

map_fixed|map_private|map_anonymous|map_locked, //載入物理記憶體,嚴格按照所給位址分配

-1, 0 );

stubfunc(); //開始遞迴呼叫檢測棧限制

通過執行可以看到結果,g幾乎可以到了x附近,為何說是附近呢?因為該程式取的ap是乙個大致位置,並不是esp的位置,用c實現完全定位esp是不容易的,用彙編比較簡單,直接取esp就可以了,這個實現簡單的闡述了棧限制的原理。

但是問題來了,既然linux沒有要求棧的確切位置,那麼是不是就是說只要訪問到當前棧的vm_area_struct頂端和棧以下的對映記憶體的末尾之間的記憶體,通過缺頁異常都會將棧擴充套件到該位置呢?從do_page_fault可以看出一些端倪,事實上並不是這樣,這裡可以看出linux實現的隨意性,本來的想法很好,就是說只要被訪問的記憶體位址比esp低,那麼就視為出錯,然而有乙個enter指令和pusha指令,這兩個指令都會在重新設定esp之前將棧擴充套件,其實就是壓入很多的資料,如此一來事情就不是那麼顯然了,惡意程式完全可以利用這個漏洞,看看do_page_fault的相關邏輯:

fastcall void __kprobes do_page_fault(struct pt_regs *regs, unsigned long error_code)

vma = find_vma(mm, address);

if (!vma)

goto bad_area;

if (vma->vm_start <= address)

goto good_area;

if (!(vma->vm_flags & vm_growsdown))

goto bad_area;

if (error_code & 4) { //從2.6.18開始,以下的「+ 65536 + 32 * sizeof(unsigned long)」代替了原來的「+32」

if (address + 65536 + 32 * sizeof(unsigned long) < regs->esp)

goto bad_area;

在上面最後乙個if之前有一段注釋被我刪去了,之所以在2.6.28之後做了更改完全是為了支援enter指令,因此才有了注釋中所說的那個很厚的墊子,只要訪問這個厚墊子內部的位址程式是不會出錯的,這個實在不應該,因為這個位址可能已經不是棧空間,並且當前執行的也不是enter或者pusha指令,請看下面的**:

int main( int argc, char* ar** )

char stub[***]; //***是乙個比較大的填充數,我取的是32768,只要能保證在下面執行psp-65664之後的結果pi的值是棧棧之外的就可以

int psp;

asm volatile("movl %%esp,%0":"=m" (psp):); //得到esp的值

int* pi = (int *)(psp-65664); //65564也就是65536+32*sizeof(unsigned long)

*pi = 10; //訪問之,將導致缺頁,棧將會被擴充套件,然而實際上這是乙個十足的棧越界

上面的程式十分簡單,如果將psp-65664換成psp-65664-n(n為正數),那麼程式將出錯,實際上早就應該出錯了,因為棧已經越界了,在2.6.18之前的核心中測試上述**,即使將65664換成60000也會出錯,因為那些早期的版本中只允許位址在esp下面32位元組的位置以內。下面看一下最後乙個**,這個**同步推進了esp指標8個單位,那麼結果就是可以允許psp-65664-8以內的位址訪問不會出錯:

int main( int argc, char* ar** )

char stub[***]; //同上

int psp;

asm volatile("movl %%esp,%0":"=m" (psp):);

int* pi = (int *)(psp-65664);

__asm__("subl $8, %%esp/n/t":);

*pi = 10;

linux不是十全十美的,但是已經不錯了,反正我是這麼認為的!

關於linux棧的乙個深層次的問題

記憶體不用白不用,何必在一開始就限制棧的大小,linux的機制是盡量多盡量緊湊的使用虛擬記憶體,原則就是你現在不用我就用,沒有預留的概念,當然你可以通過系統呼叫實現預留,就像glibc的堆管理那樣,這裡所說的完全是針對於作業系統核心的,使用者空間程式完全可以向作業系統通過brk或者mmap實現使用者...

物件的深層次獲取

故心故心故心故心小故衝啊 在寫 的時候遇到乙個問題,在訪問乙個物件巢狀物件在巢狀物件,例如 var obj 獲取c的值 obj.a.c 123那麼如果只能通過obj 的方式應該如何去獲取呢?這樣獲取嗎?obj a.c 錯誤那麼如何實現obj 這樣的方式獲取呢?可以從上面可以知道obj.a.c 是可以...

怎麼深層次的學習程式設計

人在經歷過一些事情後,總會習慣記得自己的感覺和心得。我也不例外,畢業兩年多,在職場摸爬滾打著,在 的海洋中來回遊蕩著,對此有一些感受一一說下!1 當初選擇it,進入程式設計的大軍中,一直以來,就夢想成為某一方面的技術大牛,但是兩年過去了,自己感覺沒有什麼提高,技術還是不過硬,雖然能滿足日常的工作需要...