前言
C\C++代碼在編譯鏈接后生成機器碼文件。我們打開此機器碼文件(即打開應用程序)后,系統自動為這個程序分配一個2^32(操作系統位數)大小的虛擬內存地址空間。這個地址空間會被系統安排成幾個分區,比如用戶模式分區、內核模式分區等等[1]。
其中,用戶模式分區又被分為常量區、靜態數據區、堆區、棧區和代碼區(而機器碼內容就被分配到用戶模式分區下,這些機器碼指令隨后會按照順序被送往CPU里運行)。今天我說的內容就涉及棧區和代碼區兩個部分。
環境
1.OS - windows
2. IDE - CodeBlocks 16.01
3. 編譯器 - mingw32-g++.exe
代碼
1 int Add(int x,int &y) 2 { 3 int z = 0; 4 z = x + y; 5 return z; 6 } 7
8 int main(int argc, char *argv[]) 9 { 10 int attest = 10; 11 int bttest = 20; 12 int cttest = Add(attest,bttest); 13 printf("Programme End!\n"); 14
15 return 0; 16 }
esp和ebp
CPU里有兩類寄存器,通用寄存器和專用寄存器。它們就屬於專用寄存器。
在一個函數棧里,esp里始終保存着當前棧的棧頂地址;而ebp始終保存着當前棧的棧底地址。比如根據圖三中的esp和ebp的值,就可以用下圖來表示當前函數棧的位置情況:
圖一
函數棧棧底一般在高地址處,而棧頂在低地址處。在棧底和棧頂之間,會存放函數體里定義的一些變量值。
當我們在函數體里定義一個變量,編譯器會以一個固定值(esp或ebp里的值)加偏移量的形式來表示一個變量的地址,比如圖二中的0x1c(%esp)。[2]
調試
圖二
如上圖所示:右半部分是指令的匯編代碼,左半部分是指令的機器碼所在的虛擬內存地址。
我們首先來看看0x40136d處這條匯編指令——把位於常量區的'10'放入esp寄存器所指內存地址處,再往高地址方向偏移0x1C個字節的地方,放入值的長度為32位(因為'movl'指令)。
圖三
由上圖可知,現在esp里的值是0x28ff00,而我們要查看attest和bttest變量在內存地址中的值是否改變了?&attest的值是(0x28ff00 + 0x1c);&bttest的值是(0x28ff00 + 0x14)。
圖四
由上圖可知,相應地址里的值確實被修改了。現在0x1c(%esp)的內容是變量attest的值——10,0x14(%esp)的內容是變量bttest的值——20。
下面我們進入正題:
圖五
由上圖,即將進入Add()函數。在執行0x40138c的調用函數指令前,系統先把兩個參數入棧,C\C++語言中函數參數入棧順序從右往左。
因為形參y是引用類型,所以0x40137d指令是把變量bbtest所在內存地址直接拷貝到通用寄存器eax里;0x401381指令是把eax的內容拷貝到棧頂指針所指內存地址往右0x4個字節的地址處,拷貝值長度為32位(因為eax寄存器的大小是32位)。下圖為執行完這兩條指令后,0x4(%esp)里的值:
圖六
由上圖,0x4(%esp)里的內容就是0x14(%esp)的地址。此時%esp + 0x4 = 0x28ff04。①
而形參x是非引用類型,后面兩條指令則是把變量attest的值拷貝到棧頂指針所指內存地址處。下圖為執行完這兩條指令后,(%esp)里的值:
圖七
由上圖,(%esp)里的內容就是把0x1c(%esp)的內容拷貝了一份過來。此時%esp = 0x28ff00。②這就是C\C++里函數傳參時所說的臨時變量,以及'值傳遞'造成的拷貝。而'引用傳遞'則不會產生臨時變量和拷貝,因為它直接把變量的地址傳到棧里。
圖八
下面我們在進入函數內部前,需要把調用函數下一條指令地址入棧,接着把調用函數的棧底指針入棧,即圖五里的0x401391這個地址入棧。然后我們就可以把調用函數的棧頂指針值賦值為當前棧的棧底指針,另外給當前棧的棧頂指針重新附一個值(就像0x401341和0x401343兩條指令一樣)。進入函數內部后就走到了0x401346這條指令——給z賦值。重點在於后面幾條指令,我們先看一下當前ebp的值為:
它正是通過圖三里的esp里保存的棧頂指針值0x28ff00、0x28ff00 - 4 - 4后得到的值,第一個4是放入棧里的調用函數下一條指令地址的的大小(因為32位編譯器里的指針大小為4字節),后一個4是ebp里存放的調用函數棧底指針的大小。減法運算是因為棧的生長方式是由高地址向低地址生長。這樣就可以得到當前函數棧的棧底指針。現在ebp里保存的棧底指針值為0x28fef8。
了解這些后,我們計算0x40134d指令中的0xc(%ebp)值,為0x28ff04,即上述①所述地址,里面存放着實參bttest的地址;0x401352指令中的0x8(%ebp)值,為0x28ff00,即上述②所述地址,里面存放這臨時變量的值。這樣就獲取到實參的內容了。
我們看看目前函數棧的情況:
- 0x28fef4,存放局部變量z的值,即執行過了圖八里的0x401346指令;
- 0x28fef8,存放調用函數的棧底指針地址,即圖三里的ebp里的內容;
- 0x28fefc,存放調用函數下一條指令的內存地址,即圖四里的0x401391這個地址;
- 0x28ff00,存放參數x的臨時變量值;
- 0x28ff04,存放參數y的地址。
當圖八所示的函數執行完畢后,需要把ebp的內容恢復成調用函數的棧底指針,所以應該有'pop epb'這句匯編。然后需要告訴CPU從哪一條指令繼續執行,所以還應該把函數地址pop到相應寄存器里。但是這款編譯器沒有給出相應pop指令,我們必須知道其實是有pop這個步驟的。
標簽
[1]. 詳見<Windows核心編程>--(美)Jeffrey Richter,其中的內存結構章節。
[2]. 有關匯編語言中操作數的意義請戳這里:http://blog.chinaunix.net/uid-28458801-id-3558498.html