函數調用時程序內存地址空間里棧的變化


前言

  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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM