函式呼叫的時候棧發生了什麼?

2021-07-22 22:26:30 字數 3418 閱讀 4675

問題分析

本文分析的問題是函式的棧呼叫機理。

先說結論

所謂的暫存器入棧 實際上是指的一組暫存器入棧。

因為在新呼叫的函式中,這些暫存器仍然會被用到,

為了退出呼叫函式後能恢復狀態,凡是有可能被修改的暫存器都要入棧。

出棧順序和入棧順序相反。這個過程由編譯器維護。

在現在普遍應用的單指令流,單資料流計算機上,編譯後的程式都是基於棧來排程的。

程式裝載入記憶體後,

**指令對映到記憶體空間的指令區

操作的資料則在對應的棧空間和堆空間上。

堆空間用於動態記憶體的分配、應用。本文分析暫不考慮分析堆的問題。

我們以如下例子為例

char* get_memory()

int main()

這段**在 get_memory 中,犯了乙個經典的錯誤。

返回了已經釋放過的區域性變數。

我們來仔細分析一下

記憶體空間可以看成線性的儲存空間。

而棧處於程式的資料區,對應的是每乙個函式的區域性變數的儲存空間。

程式執行到了main函式中第一條指令時,即建立了main函式的棧,

標誌是cpu的 esp 暫存器和 ebp 暫存器,前者指向main 函式棧的棧頂,後者指向棧底。

具體如圖1所示,圖1是當執行到了char* str=null

這段**後的棧狀態。計算機已經為指標 str分配了4個位元組的空間,當然現在的內容是null,不指向任何內容。

需要注意的一點是,在x86平台上,棧的增長空間是由高位向低位增長的,而堆記憶體是由低位向高位增長的。

當前的棧還只是main函式的棧,區域性變數只有乙個char指標string,佔了4個位元組,esp指向棧頂。

當上面的程式呼叫 get_memory 函式時,就進入了新函式的棧空間,

期間為了能在新函式執行結束後正確返回main函式,需要保護好呼叫現場。

我們的程式中需要保護的就是就是乙個ebp位址和乙個main函式中斷執行的執行點,亦即返回位址,按照c函式呼叫管理,先入棧的是返回位址,其後才是ebp指標指向的位址。ebp入棧後的main函式棧如圖2所示:

此時,只是返回位址和ebp指標入棧,函式尚未進入get_memory()函式。但esp棧頂指標已經指向了新的位址,main函式的棧 空間也隨之增大。 真正進入get_memory函式,並且執行了char p=」hello world」 語句之後的棧空間如圖3所示:

此時,已經進入了get_memory函式的棧,所以ebp暫存器指向新函式棧的棧底,這個棧底是在進入新函式之前最後時刻的 esp所指向的位址。所以我們在檢視彙編**時,能看到所有函式的頭兩個指令都類似於:

pushl   %ebp

movl %esp, %ebp

新的ebp前乙個位置儲存的是old ebp,從新的ebp開始就是get_memory的棧空間了。可以看到新函式中的區域性變數,陣列p 分配到了12個位元組儲存」hello wolrd」。計算機通過調整esp的位置來分配了空間,這就是在棧上分配記憶體的原理。就是簡單 的調整esp而已。

注意圖中的eax暫存器指向了p陣列的首位址,這是因為,eax在x86平台上充當著傳遞返回值的作用。get_momeory函式返回的 是p陣列的首位址,自然eax儲存的就是p陣列首位址了。

函式退出的過程和進入的過程正好相反,也只有這樣才能正確恢復中斷狀態。在本文中的例子就是,首先釋放p陣列空間,釋 放的過程和分配過程一樣,只需調整esp暫存器的值就可以了。釋放掉區域性變數後,esp就指向了old ebp了,然後執行pop ebp指令,就恢復了ebp在main函式中的值,即main函式的棧底位址。之後就可以獲取到返回位址,jmp到這個位址,就從 get_memory函式中回到了main函式中。此時的棧狀態如圖4所示:

此時esp已經指向了string變數的下乙個位址,esp之後的所有的空間此時都是被釋放掉了的,但此時因為還沒有被重新 分配,所以他們的值還是原來的值,並沒有變化。而string變數是get_memory函式的返回值,它還是指在原來p陣列的首 位址位置:0xcffffff0。

從上面的分析過程,可以看出函式在呼叫過程中,所有的區域性變數都是在棧上分配的,一旦退出了函式,就被釋放。這就 是c函式中區域性變數作用域僅在函式內有效的原因。需要明了的是呼叫過程中的壓棧,出棧次序:先進入的是返回位址,然 後是old ebp。

首先需要補充一點,在上文中小結中提到,呼叫乙個函式時先入棧的是返回位址,實際上,比返回位址更先入棧的是呼叫 函式的引數。上面的get_memory函式沒有引數,所以直接先入棧了返回位址。在有引數的函式呼叫時,實際是需要先入棧 引數的。而且,對c/c++函式而言,入棧次序是從右向左的,最右的引數最先入棧。

因為我們的程式下乙個呼叫的是printf函式,這個函式是有引數的,而且在我們的程式中中是兩個引數。我們先討論第一種 呼叫方法,即printf(「%s」,string)這個呼叫。進入該函式後的棧空間如圖5所示:

進入函式時,最右邊的引數arg2先入棧,按照c函式的值傳遞特性,此時傳入的是string的副本,即arg2也是乙個位址,指 向0xcffffff0。然後arg1入棧,接著是返回位址入棧。因為arg2是4個位元組,arg1也是乙個字串常量的位址,也是4個字 節。可以看到,此時的0xcffffff0位址已經被返回位址覆蓋掉了,而這個位址正是上次呼叫時的陣列p的起始位置,並且 main中的區域性變數string和printf的第二個引數arg2都指向這個位址,但此時該位址中的的值已經不是』h』了,同樣的,因 為printf要為其區域性變數分配記憶體,hello world的12個位元組全部被覆寫。

綜上所述,printf在一進入的瞬間,哪怕不執行任何**,原hello world的空間就被覆蓋了,自然也不會得到正確的輸 出。得到的全是隨機的亂碼。實際上也不能簡單說是隨機的,因為返回位址,printf的區域性變數都是確定的,只是把這些 位址,區域性變數都當成char輸出時,肯定是亂碼了,但肯定是確定的亂碼。

再看第二個問題。同前者不一樣的是,這次呼叫的第二個引數不是乙個位址了,而是乙個char。按照 值傳遞的特性,此時的arg2是string的乙個拷貝,即arg2=string。而且,這個賦值過程發生在進入函式printf之前。 如圖6所示:

圖6顯示了剛剛把printf的第2個引數arg2入棧後的情況,因為string是0xcffffff0,且該位置此時還沒有被覆寫,所以 *string=』h』,而值傳遞後,argv2=』h』。這就是arg2壓棧後的棧狀態。

隨後,繼續把第乙個引數壓棧,再把函式返回位址壓棧,此時壓入了4+1+4=9個位元組,esp到達了0xcfffffef位置,如 圖7所示:

圖7顯示的是,進入printf函式棧後的棧狀態,此時雖然原來的0xcffffff0位置開始的」hello world」被覆蓋了,但argv2 值中所存的依然是』h』這個拷貝,所以,第二個程式最終輸出的是』h』也就很正常了。

函式呼叫時發生了什麼

我們下面就來 一下高階語言中函式的呼叫和遞迴等性質是怎樣通過系統棧巧妙實現的。請看如下 int func b int arg b1,int arg b2 int func a int arg a1,int arg a2 int main int argc,char argv,char envp 這段...

函式呼叫時發生了什麼

第一步 函式呼叫 1 對實參表從右向左,一次計算出實參的值,並且將值壓棧。2 將函式呼叫語句 儲存到在棧中,以便函式呼叫完成後返回。壓棧 3 跳轉到函式體處。第二步 函式體執行 4 如果函式體中定義了變數,將變數壓棧 5 將每乙個形參以棧中對應的 實參值取代,執行函式體的功能體。6 將函式體中的變數...

函式呼叫時發生了什麼?

我們以如下 示例,描述函式呼叫過程中,棧的操作過程 voidf intg int x 每次函式呼叫,作業系統都會在棧中建立乙個棧幀 stack frame 正在執行的函式引數 區域性變數 申請的記憶體位址等都在當前棧幀中,也就是堆疊的頂部棧幀中。如下圖所示 當 f 函式執行的時候,f 函式就在棧頂,...