本節我們重點討論棧指標esp和幀指標ebp,圍繞這兩個重要的暫存器,推導出函式棧幀結構。閱讀本文之前補充乙個概念:棧幀 每個函式的棧稱為一幀,也就是該函式的棧幀。函式棧的基位址(ebp)稱為棧幀指標,訪問函式中的引數或區域性變數,都是通過ebp加上偏移量來獲得。
一:壓棧和出棧的操作本質
上一節我們了解到push和pop是彙編中壓棧和出棧的指令。棧這個東東,當某個程式執行時,會劃分乙個塊固定大小的區域(儲存器對映),而棧就屬於這個區域的一部分。要了解出入棧首先要了解棧的結構:
位址 棧中內容
最大位址
資料(棧底)
…………
0x108
資料30x104
資料20x100
%esp資料1(棧頂)
%fc新%esp資料0
(新棧頂)
從上圖看出,棧的增長方向是向下的。棧有個最大位址,這個位址成為棧底,也是儲存棧裡面儲存第乙個元素的位置,隨著入棧個數增加,棧頂的位址不斷減小。
esp暫存器就是專門用來儲存棧頂位址的。在彙編中,%esp讀出棧頂位址,(%esp)就能讀出棧頂裡的數值。如上圖所示,如果再進行一次入棧push操作時,那麼棧頂%esp就跳到位址0xfc(0x100-4)處,新壓的資料也會存在這個位址上。如果上圖不執行push,而是直接執行pop出棧時,esp將儲存位址0x104。
push和pop這兩個彙編操作指令,是可以用基本的彙編操作代替的,事實上,push和pop在彙編中對應的操作是:
push %ebp:subl$4, %esp
movl %ebp, (%esp)
pop %eax:movl (%esp), %eax
addl $4, %esp
在分析上面彙編**之前再複習一下,%eax直接獲取裡面的值,(%eax)類似c指標『*』間接定址操作,是取出%eax裡的值作為位址來看,再根據這個位址找到相應位置,並取出其中的值。
還以上圖為例。先來看push壓棧,壓棧是增加棧的元素,由於有新的資料(ebp裡的值為資料0,具體什麼值先不關心)要入棧,而棧又是向下生長的,因此需要將存有棧頂位址資訊的esp進行調整,具體操作是將esp減4,得到增長後的下乙個棧頂位址,subl$4, %esp操作使得esp的值從0x100跳變到0xfc,實現了棧頂的生長;接著是賦值,我們需要把ebp裡的值傳送到新的棧頂指向的空間中去(位址0xfc代表的空間),完成入棧。語句movl %ebp, (%esp)比較好理解,就是把ebp裡的值,通過「()」對棧指標進行間接引用,傳送到位址0xfc的空間裡面去,esp是棧指標(叫棧頂指標更好理解)。
為啥%esp要加括號?如果不加括號,棧指標所存的位址資料將被破壞,本來跳變好了新棧頂位址0xfc,會因為你的乙個不加括號的語句而使棧指標%esp被覆蓋成%ebp的值(資料0)。而加了括號,則會做間接定址操作,通過%esp,找到位址為0xfc的空間(也就是新的棧頂空間),並把資料0成功傳送進去。
一旦你理解了上面冗長的廢話,再理解pop就很簡單了,出棧無非就是把操作反過來。比如剛才push完了,我們再執行pop %eax,就是要把棧頂元素的值彈出來,傳送到%eax中去,然後棧頂更新狀態。那麼movl (%esp), %eax語句就是將當前棧頂裡的值(資料0),傳送到eax中去;而addl $4, %esp就是更新棧指標,把位址值加回去(從0xfc變回0x100)。
這裡有個細節問題,關於出棧,有沒有發現,只有資料出和棧頂更新,並沒有資料刪除操作。也就是說,剛才連續執行了push %ebp和pop %eax後,棧指標指向的是0x100位址,棧頂的值是資料1。那麼位址0xfc裡存的什麼呢?答案當然是資料0,因為沒有任何語句刪除它,所以才會出現有時候你除錯c語言程式,指標越界訪問後,會讀出一些已經失效函式裡面的臨時變數值,就是這個原因。
用彙編語句理解出棧入棧,對於接下來的函式棧空間的理解是至關重要的。
二:函式呼叫的棧幀結構
在我看來,從某種意義上說,c語言就是個函式巢狀語言,從乙個主函式開始,內部生出幾個子函式,每個子函式下面還有更細的子函式,有時父子函式之間還會出現遞迴巢狀呼叫,在加上迴圈和條件判斷,如此複雜的操作,編譯器是怎麼翻譯成彙編來實現的?這依賴於簡單實用的棧幀結構,這裡我們引用網上的乙個火圖:
說句老實話,本來這個圖並不是那麼難理解的,無論函式巢狀有多複雜,總有個先後吧?這個幀那個幀不就是根據呼叫的先後排列順序的,先呼叫的函式,其棧幀結構就整體先入棧,後呼叫的函式就**棧,那麼棧頂所代表的函式幀(當前幀),就是當前正在呼叫的函式,所需要的資料對映,解釋完畢。
言歸正傳。要理解%ebp,首先還是要複習一下上面講的間接引用,搞清楚暫存器所存值的概念。暫存器裡存的值本質上就是數值,關鍵是我們如何看待它的意義,就比如棧指標%esp,叫它棧指標是因為它一般來說存的都是某個空間的位址,這是編譯器的習慣分配。如果你是做編譯器的,完全可以用%esp當成%eax或者其他什麼暫存器來臨時存放一下其他數值,再把位址賦回值給它,如果不嫌麻煩的話。因此類似棧幀結構的這些知識,其實是編譯器事先定義好的對暫存器的使用規則,記住,暫存器裡的值我們要怎麼理解,那是由編譯器說了算的。
為了簡單好理解,我們討論最簡單的函式巢狀,假如函式grand呼叫函式father,而father呼叫函式son,father的棧幀就是上圖所說的「呼叫者的幀」,而son就是「當前幀」,grand自然就包含在「較早的幀」之中。father有1~n,n個變數要作為引數傳給son。從上圖能明顯看出,n個引數是倒著排列的,這是由棧結構決定。在引數傳遞中,son(1,2,3,…,n)**順序,在棧幀結構上是位址由小增大排列的。引數下面是返回位址,這個返回位址,其實就是father函式自己的位址,同時也是father函式棧幀的末尾(注意和棧頂或者棧底概念完全無關)。
好,回過頭來看,那麼引數n以上的省略號是什麼呢?其實,son函式棧幀的「引數構造區域」,和father的引數1~n是一回事,也許裡面放著的是引數1~m,用於son來呼叫孫子函式時用,因此引數n上面的省略號,就是father函式被儲存的暫存器、本地變數和臨時變數,再往上就是father函式自己的「被儲存的%ebp」。
再往上呢?就進入grand函式的棧幀結構(較早的棧幀),往上第乙個一定也是「返回位址」,其實就是grand函式執行完father後應該繼續執行的**的位址。
說到這裡可能你覺得還好,按照呼叫順序,函式的棧幀結構維護得很清楚。可以想象,當某個函式要呼叫其他函式呼叫時,先通過一系列壓棧操作,在棧裡面備份函式自身的本地臨時變數,還有傳遞給子函式的引數變數資訊,最後壓上函式自個兒的位址,完事,下面的空間就留給子函式玩了。
這裡問題就來了,cpu如何區分不同的棧幀?如何搞清楚棧裡面哪部分是子函式哪部分是父函式?棧指標%esp只知道自己現在在哪玩,對於具體玩的是哪個函式的內容,那是一頭霧水啊。於是我們有必要解開%ebp面紗了。
三:神秘的%ebp
%ebp叫幀指標,相信熟悉c指標的朋友看到名字時,對%ebp的工作原理就基本明白個七八分了。沒錯,既然叫幀指標,那就是用來存放各幀首位址的指標。
設想,當father函式要呼叫son函式時,需要對棧幀資訊進行修改和維護,如何在son函式執行完後讓cpu順利的找到father的棧幀位址並成功返回呢?這就要在呼叫son之前做好充分的準備工作。比如,father棧幀有自己的幀首,在father函式執行時,%ebp就儲存了這個幀首的位址值,或者說%ebp正指向幀首。當呼叫子函式son時,%ebp就會儲存son的幀首位址,為了讓son在返回時能夠順利更新%ebp,使得幀指標順利指回到father的幀來,有必要在%ebp指向son幀首的同時,更改幀首空間內所儲存的值為father幀首位址,也就是son的所謂「儲存的%ebp」,或者說舊的%ebp值,父函式呼叫時%ebp的值。
這裡感覺很繞的同學,一定是指標基礎還不夠紮實。來區分下乙個概念,乙個儲存單元空間,有兩個屬性:1、cpu訪問這個儲存單元需要依賴的位址值;2、這個儲存單元所儲存的數值,空間位址值和空間內儲存的數值,區分這兩個值是理解指標概念的基礎。現在討論函式的幀,每個幀都幀首,幀首作為儲存單元空間,當然有標識自己的空間位址,同時空間裡存了乙個數值。棧幀結構恰恰巧妙的利用了這種概念,讓%ebp始終儲存當前呼叫函式的幀首位址,而當前幀首內又儲存著父函式的幀首位址,以此類推,每乙個當前呼叫函式的幀首內都保留著父函式的幀首位址,函式執行完成時都能順利更新棧指標%ebp的值,一直可以推到main函式的幀首,通過棧指標%ebp的修改和被儲存,就能確保棧幀結構的訪問順利進行,是不是很奇妙?
函式的執行過程
1.通過函式名字找到函式入口 2.給形參分配空間 3.傳值 4.執行函式體語句 5.返回,釋放空間。include void func char ptr int main int argc,char argv 讀者認為輸出的結果是什麼?輸出結果為 hello world 函式的本來意圖是讓字串從e往...
js 函式執行過程
函式執行過程中 1.每使用乙個變數,函式都會由近到遠的遍歷自己的好友列表中的作用域物件。2.如果在離自己近的格仔中找到了區域性變數,就優先使用區域性變數,不再去全域性找。3.如果在離自己近的格仔中沒找到要用的區域性變數,才被迫去全域性找,如果在全域性找到了想用的變數,則本次修改結果,會影響全部變數的...
C 函式整個呼叫過程即函式棧幀!
在c語言中,函式從記憶體的角度來分析函式的整個呼叫過程呢!1 首先了解兩個暫存器變數,esp和ebp,他們分別是存放的是棧頂的位址和棧底的位址。2 要觀察函式的呼叫過程需要從彙編 開始分析。include int add int x,int y int main 下圖是函式呼叫的簡單草圖 首先進入的...