1 程序的地址空間布局
一個程序在內存中運行,它靠四個東西:代碼、棧、堆、數據段。代碼段主要存放的就是可執行文件中的代碼;數據段存放的就是程序中全局變量和靜態變量;堆中是程序的動態內存區域,當程序使用malloc或new得到的內存是來自堆的;棧中維護的是函數調用的上下文,離開了棧就不可能實現函數的調用。在linux中它們的地址空間分布如下:
其中最讓我迷惑的還是棧,它是怎么保存程序執行的上下文的?我對它的理解還是保留在數據結構學的棧,什么先進先出,只對棧頂進行操作,對於它的具體應用還真是不太了解。以前寫代碼就很好奇,當調用一個程序時,棧中到底保留了些什么東西?今天終於有了點理解。
2 堆棧幀
堆棧幀也叫活動記錄,保存的是一個函數調用所需要維護的所有信息。它主要包含三個內容:
- 函數的返回地址和參數
- 臨時變量:包括函數的非靜態局部變量以及編譯器自動生成的其它臨時變量
- 保存的上下文:包括在函數調用前后需要保存不變的寄存器值
在i386中,一個函數的活動記錄用ebp和esp這兩個寄存器來划定范圍。esp始終指向棧的頂部,同時也指向當前活動記錄的頂部。相對的,ebp指向函數活動記錄的一個固定位置,ebp又被稱為幀指針。一個很常見的活動記錄如下:
參數及參數之后的數據是當前的活動記錄,ebp固定在上圖的位置,不隨函數的執行而改變,相反地,esp始終指向棧頂,因此隨着函數的執行,它總是變化的。ebp+4是這個函數的返回地址,ebp+8、ebp+12等是這個函數的參數。ebp指向的是調用該函數前ebp的值,這樣在函數返回的時候,ebp就可以通過這個恢復到調用前的值。ebp下面的值是要保存的寄存器的值和函數中的局部變量,當然也可以不保存ebp的值,不過這樣會減慢幀上尋址速度和無法准確定位函數的調用軌跡。之所以會形成這樣的活動記錄,是因為一個i386中函數總是這樣調用的:
- 把所有或者部分參數壓入棧中
- 把當前call調用指令的下一條指令地址壓入棧中
- 跳轉到call調用的函數去執行
后面兩步由call指令自動完成,i386函數體的開頭一般是這樣的:
- push ebp;保存調用前的ebp值
- push ebp,esp;ebp指向棧中保存調用前ebp值的位置
- sub esp,xxx;在棧上分配xxx字節的空間,這個是可選的
- push xxx;保存xxx寄存器的值,這個也是可選的
在函數返回時,一般是這樣的:
- pop xxx;恢復寄存器的值;如果開頭有push xxx
- mov esp,ebp;恢復esp,同時回收局部變量的空間
- pop ebp;恢復調用前ebp的值
- mov eax,xxx;如果函數有返回值,那么返回值一般放在eax中
- ret;從棧中取回返回地址,並跳轉到該位置,棧中參數空間的回收和調用慣例有個,看以參考http://www.cnblogs.com/chengxuyuancc/archive/2013/05/28/3103956.html
第2和3條指令可以用leave指令代替
為了加深印象,下面反匯編下面一個函數看看:
int foo(int a) { int b; b = a * 100; return b; } int main ( int argc, char *argv[] ) { foo(2); return 0; } /* ---------- end of function main ---------- */
反匯編代碼:
可以明顯的看到foo和main函數中的代碼和前面我們給出的函數開始和結束的一般代碼是一樣的。在main函數中調用foo前先是用movl指令把參數放入棧頂。