程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C >> 關於C >> c函數調用過程原理及函數棧幀分析

c函數調用過程原理及函數棧幀分析

編輯:關於C

    今天突然想分析一下函數在相互調用過程中棧幀的變化,還是想盡量以比較清晰的思路把這一過程描述出來,關於c函數調用原理的理解是很重要的。

1.關於棧

        首先必須明確一點也是非常重要的一點,棧是向下生長的,所謂向下生長是指從內存高地址->地地址的路徑延伸,那麼就很明顯了,棧有棧底和棧頂,那麼棧頂的地址要比棧底低。對x86體系的CPU而言,其中

---> 寄存器ebp(base pointer )可稱為“幀指針”或“基址指針”,其實語意是相同的。

---> 寄存器esp(stack pointer)可稱為“ 棧指針”。

       要知道的是:

---> ebp 在未受改變之前始終指向棧幀的開始,也就是棧底,所以ebp的用途是在堆棧中尋址用的。

---> esp是會隨著數據的入棧和出棧移動的,也就是說,esp始終指向棧頂。

       見下圖,假設函數A調用函數B,我們稱A函數為"調用者",B函數為“被調用者”則函數調用過程可以這麼描述:

(1)先將調用者(A)的堆棧的基址(ebp)入棧,以保存之前任務的信息。

(2)然後將調用者(A)的棧頂指針(esp)的值賦給ebp,作為新的基址(即被調用者B的棧底)。

(3)然後在這個基址(被調用者B的棧底)上開辟(一般用sub指令)相應的空間用作被調用者B的棧空間。

(4)函數B返回後,從當前棧幀的ebp即恢復為調用者A的棧頂(esp),使棧頂恢復函數B被調用前的位置;然後調用者A再從恢復後的棧頂可彈出之前的ebp值(可以這麼做是因為這個值在函數調用前一步被壓入堆棧)。這樣,ebp和esp就都恢復了調用函數B前的位置,也就是棧恢復函數B調用前的狀態。


這個過程在AT&T匯編中通過兩條指令完成,即:

       leave

       ret

      這兩條指令更直白點就相當於:

      mov   %ebp , %esp

      pop    %ebp

 

\


2.舉個簡單的實例,從匯編的視角看函數調用

2.1建立一個簡單的程序,程序文件名為  main.c

    開發測試環境:

    Ubuntu 12.04

    gcc版本:4.6.3 (Ubuntu/Linaro 4.6.3-1ubuntu5)  (是Ubuntu自帶的)

<SPAN style="FONT-SIZE: 18px">/*main.c代碼:*/

void swap(int *a,int *b)
{
   int c;
   c = *a; 
   *a = *b;
   *b = c;
}

int main(void)
{
   int a ;
   int b ;
   int ret;
   a =16;
   b = 64;
   ret = 0;
   swap(&a,&b);
   ret = a - b;
   return ret;
}</SPAN>


2.2編譯

#gcc    -g   -o   main   main.c

#objdump   -d  main   >   main.dump

#gcc   -Wall   -S  -o   main.s   main.c

 


        這樣大家可以看main.s也可以看main.dump,這裡我們選擇使用main.dump。

        截取關鍵的部分,即_start,   swap  ,  main,為什麼會有_start呢,因為ELF格式的入口其實是_start而不是main()。下面的圖展示了main()函數調用swap()前後的棧空間的結構。右邊的數字代表相對幀指針的偏移字節數。後面我們使用GDB調試就會發現棧的變化跟下圖是一致的。

(!!!請注意,由於棧對齊的緣故,編譯器分配棧空間時可能會有沒用到的內存地址,而這些沒使用到的內存地址就沒在下圖表示出來,所以下圖只能當作示意圖來了解函數棧幀結構!!具體的棧內存內容以下文的GDB調試的信息為准!!!)


 

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved