VC6 雖然是幾乎被淘汰的IDE,可是它在調試時容許咱們直觀地查看寄存器和內存空間中的值和地址,轉換爲彙編語言後每條指令在內存空間的地址的特性,可讓咱們更直觀地看到一些操做。算法
本文但願經過 VC6 更直觀一些地看到C語言在爲局部變量和函數調用分配內存空間的具體細節,從而驗證從書上學到的知識。相關知識主要參考《深刻理解計算機系統》(中文第二版)第三章,其中3.1節和3.7節尤其重要。函數
代碼:spa
#include<stdio.h> void functionA(int, double); int main(){ int i = 1; double d = 8.0; functionA(i,d); short int s = 10; return 0; } void functionA(int i, double d){ int fa_i = i; double fa_d=d; }
VC6調試視角:3d
7: int i = 1; 00401038 mov dword ptr [ebp-4],1 8: double d = 8.0; 0040103F mov dword ptr [ebp-0Ch],0 00401046 mov dword ptr [ebp-8],40200000h
解讀:
首先要明確一下這些彙編指令中的取值規則。ebp
:取出ebp中存儲的值(一個地址)做爲取來用的值。[ebp]
:取出ebp中存儲的值(一個地址)後,取出改值(地址)指向的內存空間中存儲的值做爲用的值。
所以mov dword ptr [ebp-4],1
,要求將1存入ebp-4
這個地址對應的內存空間。
留意一下此時的寄存器和內存窗口:
執行完00401038 mov dword ptr [ebp-4],1
後
那麼對於指針
8: double d = 8.0; 0040103F mov dword ptr [ebp-0Ch],0 00401046 mov dword ptr [ebp-8],40200000h
會發生什麼,也就沒必要多說了。調試
爲了更加直觀地查看內存空間的變化,用excel製做了下圖,記該圖的狀態爲狀態一。excel
10: functionA(i,d); 0040104D mov eax,dword ptr [ebp-8] 00401050 push eax 00401051 mov ecx,dword ptr [ebp-0Ch] 00401054 push ecx 00401055 mov edx,dword ptr [ebp-4] 00401058 push edx 00401059 call @ILT+0(functionA) (00401005) 0040105E add esp,0Ch
解讀了前兩行,就能解讀前六行。
那麼前兩行作了什麼呢?把ebp-8
對應的內存空間的值取出,並存入寄存器eax中,而後將eax中的值壓入棧。
觀察執行後的寄存器以及內存空間值:
結合變化能夠發現,push操做將數據壓入(向低地址方向)棧中,棧指針指向新的棧頂。
此時咱們已經明白了內存中的行爲,那麼這個被操做的數據有什麼意義呢?也就是說ebp-8
對應的內存空間的值是什麼?其實看完整個六行就知道了,ebp-8
和ebp-0Ch
組合起來就正好是main函數幀棧中的局部變量d(double d
),而ebp-4
則是局部變量i(int i
)。這二者都是要被調用函數的傳入的參數。也就是說這一步將被傳入的參數的值複製了一份增加在main的棧幀空間的棧中(也就是圖3-21的參數u構造區域)。
當斷點繼續執行,知道停在00401059 call @ILT+0(functionA) (00401005)
時,獲得狀態二:code
以後執行call @ILT+0(functionA) (00401005)
,關於call指令,書本上其實有較爲詳細的描述,我我的總結以下——寄存器eip用來存儲當前指令執行完成後應該執行的指令所對應的地址(由於指令歸根結底也是數據,顯然也須要空間存放)。調用call指令時,將寄存器eip中的值壓入棧中(push操做)後算做保存了函數調用結束了以後要執行的指令的地址,而後將eip寄存器的值改成functionA函數相關指令流的首指令的地址。
結合執行後寄存器和內存空間中的值變化,可能更爲直觀。blog
繼續執行下去,斷點前進,將會進入函數的內部。函數內部在執行int fa_i = i
前尚有本身的一些初始化操做:ip
首先來解析int fa_i=i
前的彙編指令
00401090 push ebp //將ebp寄存器中的值壓入棧中 00401091 mov ebp,esp //將ebp=esp 00401093 sub esp,4Ch //讓esp=esp-0x4C 00401096 push ebx //將ebx的值壓入棧 00401097 push esi //將esi的值壓入棧 00401098 push edi //將edi的值壓入棧 00401099 lea edi,[ebp-4Ch] //將&(ebp-4Ch)存入寄存器edi。mov a b是讓a=b,而lea a b則是讓a=&b. 0040109C mov ecx,13h //將常數0x13存入寄存器ecx 004010A1 mov eax,0CCCCCCCCh //將0xCCCCCCCC存入寄存器eax(帶e的寄存器中的e就是extend的意思,這些寄存器均可以存儲32bits的數據) 004010A6 rep stos dword ptr [edi] //將eax寄存器中的值存入edi寄存器中的值(一個地址)指向的內存空間,而後edi的值加一個eax的長度(即4,4個字節),eax中的值-1。
執行完這麼一套東西后,咱們獲得狀態三:
到此,其實對於以後的代碼,即functionA中局部變量的賦值,內存空間會如何分配,咱們不該該能夠推理出來了麼?0x0019FEBC會用來存放fa_i,0x0019FEB8和0x0019FEB4會被用來聯合存放fa_d,即本地局部變量等以幀指針(ebp)爲基準低地址生長,而棧指針被調用者(如本例中的main函數)爲被調用過程準備參數,備份返回地址,備份調用結束後回到本身的幀棧空間的幀指針。
狀態四:
20: } 004010BA pop edi //從棧中彈出棧頂的值,存到edi寄存器中 004010BB pop esi //彈出 004010BC pop ebx //彈出 004010BD mov esp,ebp //將esp=ebp 004010BF pop ebp //彈出棧頂的值並存到ebp
這些指令都沒必要多說。棧彈出數據後棧指針指向上一個舊數據的位置,即仍保持棧指針指向棧頂。
將esp=ebp意味着esp和ebp之間的空間沒有了,即functionA的幀棧空間已經被銷燬。
esp回到ebp的位置,請看狀態四的圖,此時彈出的值將會是舊ebp的值,即main函數的幀棧空間的幀指針的值。將其存到ebp後,幀棧指針指針已經回到main函數的幀棧空間內。
004010C0 ret
ret指令和call指令正好相反。當該執行ret指令時,正常狀況下棧指針必然指向「返回地址」的存儲空間。執行ret指令時,彈出棧的值並存入eip中。即將返回地址存入eip,從而使下一條指令將會回到被調用函數結束後該執行的位置繼續執行。
這一段結束後獲得狀態五:
這樣函數的調用就完全結束了,回到main函數的指令流了。此時後面待執行的有:
00401059 call @ILT+0(functionA) (00401005) 0040105E add esp,0Ch 11: 12: short int s = 10; 00401061 mov word ptr [ebp-10h],offset main+45h (00401065) 13: 14: return 0; 00401067 xor eax,eax 15: }
值得注意的是0040105E add esp,0Ch
使得esp=esp+0xC=esp+12 而12正好是一個int加一個double的空間所用字節數,所以esp又從新回到最開始的棧指針位置。
到此,咱們就認爲這個函數調用的過程所有結束。幀棧指針所有復位到未調用時的狀態。而爲functionA所使用的空間中的值雖然沒有被重置,但能夠預見這塊空間再度被使用時,舊值要麼被0xCCCCCCCC覆蓋,要麼被有意義的新值覆蓋,等同於已經被銷燬了。
最終狀態狀態六:
調用者的棧被依次壓入被調用者所需的參數(第一個參數最早被壓入)。
返回地址和舊ebp的壓入工做不禁調用者負責。
調用者在壓入參數後(若沒有參數則直接)使用call彙編指令喚醒被調用者開始它的工做。
將被調用者調用結束後應當執行的指令所在的空間的地址壓入。
將ebp中的值——即調用者棧幀空間的幀空間起始位置的地址——壓入棧中保存。
將ebp中的值改成本身的棧幀空間的的幀空間的起始地址——使ebp=esp。這裏請注意:esp由於壓入舊ebp值,esp保存的地址值又減少了4(通常來講ebp的長度是四個字節)。
若是須要使用參數,被調用者經過ebp+4+4*i
來獲取「調用者調用前的工做」所準備的參數。其中i表明參數在參數表中的位置(第一個參數則i=1)。其中之因此會有一個+4
是由於幀指針+4所指向的空間存儲的是返回地址的變量。
使esp=esp-x(esp表明棧空間的基地址)從而擴充了幀空間。其中x是編譯器根據必定算法獲得的被調用者應有的幀空間的大小。算法具體細節此處不作討論。以後將分配出來的幀空間用0xCC填滿(初始化。)
將ebx,esi,edi等由被調用者保存其值的寄存器的值壓入棧中。
將edi,esi,ebx等數據從棧中彈出到它們對應的寄存器中。而後將棧指針收回到幀指針處,意味着被調用者的棧幀空間已被銷燬——儘管實際上那些數據並無被覆蓋(銷燬)。
繼續彈出,此時彈出的是舊的ebp值,即調用者的棧幀空間的幀指針的值。彈出的值被賦給ebp寄存器,意味着調用者幀指針復位。
調用ret彙編指令。
繼續要求棧彈出,此時彈出的是以前保存的被調用者調用結束後要執行的指令所存儲的空間的地址。彈出給edi寄存器。彈出後棧指針指在被調用者最後一個參數所在空間。
經過esp=esp+x復位esp至調用者棧幀空間的棧指針。其中x由被調用者的參數的個數和類型所決定。由於此時esp距離復位的位置中間只隔着它以前爲參數準備的空間了。