函數(shù)在被調用的時候,操作系統(tǒng)會在內存棧區(qū)為這個函數(shù)開辟一塊空間,提供給這個函數(shù)使用,這個??臻g就是該函數(shù)的棧幀。函數(shù)的返回地址、函數(shù)中創(chuàng)建的局部變量、以及一些寄存器信息都會保存在這塊??臻g中。在函數(shù)運行完畢后,函數(shù)棧幀會銷毀,將內存空間釋放出來。
2.基本的寄存器2.1 ebp、esp
esp即extended stack pointer,擴展棧指針寄存器,是指針寄存器的一種,用于存放函數(shù)棧頂指針。
ebp即extended base pointer,擴展基址指針寄存器,也屬于指針寄存器,與esp對應,ebp用于存放函數(shù)棧底指針。
函數(shù)的棧幀空間就是由這兩個寄存器來維護的。
2.2 edi、esi
這兩個屬于變址寄存器,用于存放存儲單元在段內的偏移量。
edi:源索引寄存器,一般用于在串操作中存放數(shù)據(jù)源的地址
esi:目標索引寄存器,一般用于在串操作中存放目標地址
2.3 ecx、ebx、eax
這三個屬于通用寄存器,在程序執(zhí)行的過程中,大部分時間都是通過操作這些寄存器來實現(xiàn)指令功能的。
ecx:計數(shù)器,用于存放重復、循環(huán)等指令的執(zhí)行次數(shù)計數(shù)
ebx:基地址寄存器,在內存尋址時存放基地址
eax:累加器,在進行加法運算時使用,也用于存放函數(shù)的返回值
2.4 psw
psw是標志寄存器,是存放標志信息的寄存器。標志信息的作用是為CPU執(zhí)行相關指令提供行為依據(jù),或者控制CPU的相關工作方式。
3.0 操作數(shù)
大部分的匯編指令中,左邊的操作數(shù)都是目標操作數(shù),右邊的操作數(shù)都是源操作數(shù)
3.1 push、pop
push即壓棧,使一個寄存器中的數(shù)據(jù)入棧,然后使棧頂指針的值相應減小
pop即彈出、出棧,將棧頂?shù)臄?shù)據(jù)存到一個寄存器中,然后使棧頂指針的值相應增加,相當于從棧里面彈出了一個數(shù)據(jù)
push ebp //將ebp中的值入棧
pop edi //將棧頂元素出棧,并儲存到寄存器edi中
3.2 mov
將源地址中的一個值賦到一個目標地址中,源地址中的值不受影響
mov ebp,esp //將esp中存的值賦給ebp,即esp中的值不變,ebp中的值變?yōu)閑sp中的值
3.3 sub、add
sub即減法,將一個數(shù)據(jù)減小一定的值
add即加法,將一個數(shù)據(jù)增加一定的值
sub esp,0E4h //將esp中存的值減小0E4h
add esp,0E4h //將esp中存的值增加0E4h
3.4 lea
lea即load effective address,加載有效地址,將一個地址加載到一個寄存器中
lea edi,[ebp-04Eh] //將[ebp-04Eh]這個地址加載到寄存器edi中
3.5 rep
rep即repeat,重復,這是一個前綴指令,指令的作用是重復執(zhí)行后面的指令
rep指令每次執(zhí)行的時候都從寄存器ecx里面讀取值,當ecx中的值大于0,就執(zhí)行后面語句,執(zhí)行完以后,會讓ecx - 1,然后再執(zhí)行rep后面的指令,直到減小到0,因此每次rep執(zhí)行之前,一般都會先將重復次數(shù)存到ecx中,執(zhí)行完后ecx中的值都會變?yōu)?
rep add esp,1 //將后面的add語句重復執(zhí)行,重復次數(shù)由寄存器ecx中的值決定
3.6 stos
stos即store string,串儲存,將寄存器eax中存的值賦值到目標地址(這個地址一般都是es:[edi])
stos dword ptr es:[edi] //將寄存器eax中一個雙字長度的數(shù)據(jù)賦值到es:[edi]這個地址
賦值之后還會執(zhí)行一次使edi中存的值加4或減4,具體是執(zhí)行加還是減由標志寄存器中的方向標志位DF來決定,DF為0時執(zhí)行加,DF為1時執(zhí)行減,可以使用cld指令和std指令來對DF進行設置
cld指令:將標志寄存器的DF位設置為0
std指令:將標志寄存器的DF位設置為1
3.7 call、jmp
call是子程序調用指令,使程序跳轉到目標地址來執(zhí)行子程序,跳轉之前會先將call指令的下一條指令的地址進行壓棧,執(zhí)行完子程序之后會跳轉回到call指令的下一條指令的位置,然后再繼續(xù)按順序執(zhí)行指令
jmp是無條件轉移指令,使程序直接跳轉到目標地址執(zhí)行下一條指令,之后程序按順序執(zhí)行
call @ILT+215(_SUB) (0DF10DCh) //首先將下一條語句的地址入棧,然后程序發(fā)生跳轉,跳轉的目標位置是0x0df10dc
jmp SUB (0DF1380h) //使程序跳轉到地址0xdf1380處(SUB函數(shù)的第一條指令)
3.8 ret
ret即return,返回,彈出棧頂?shù)脑兀⑹钩绦蛱D到棧頂元素儲存的地址對應的指令
3.9 xor
xor即exclusive or,異或,將源操作數(shù)與目標操作數(shù)進行按位異或,得到的值保存到目標操作數(shù)中
xor eax,ebx //將eax中存的值與ebx中存的值異或,所得結果存到eax中
使用一段簡單的C語言代碼,通過VS2010中的反匯編功能,從匯編代碼的角度觀察這段代碼中的函數(shù)在內存中是怎么調用、怎么實現(xiàn)功能的。
//非常簡單的一段C語言代碼,用作觀察對象
#includeint SUB(int x, int y)
{int z = 0;
z = x - y;
return z;
}
int main()
{int a = 1;
int b = 3;
int c = 0;
c = SUB(a, b);
return 0;
}
在VS2010中對這段代碼進行調試,按F10進入調試模式后,可以看見反匯編、調用堆棧、監(jiān)視、內存這幾個窗口。
接下來通過這幾個窗口來觀察這段代碼在內存中是怎么執(zhí)行的。
首先,在調用堆棧窗口可以看到函數(shù)的調用情況。容易發(fā)現(xiàn)main函數(shù)是被__tmainCRTstartup函數(shù)調用的,而__tmainCRTstartup函數(shù)又是被mainCRTstartup函數(shù)調用的。其中mainCRTstartup函數(shù)是啟動函數(shù),與C語言程序的啟動有關,功能大致是在程序啟動之前做一些準備工作。
這說明在調用main函數(shù)之前,內存中已經(jīng)為之前的__tmainCRTstartup函數(shù)開辟了棧幀空間,因此在程序剛開始運行的時候,ebp和esp寄存器正在維護的是__tmainCRTstartup函數(shù)的棧幀,此時內存棧區(qū)中的情況是這樣的:
此時程序還沒開始執(zhí)行第一條語句,處于準備進入main函數(shù)的狀態(tài)。
在執(zhí)行第一條語句之前,先在內存中為main函數(shù)開辟一塊空間,即創(chuàng)建棧幀。這部分對應的匯編代碼如下:
int main()
{00DF13D0 push ebp //將ebp的值入棧,此時棧頂指針esp的值減小,因為棧中壓入了新元素
00DF13D1 mov ebp,esp //將esp的值賦給ebp,即令ebp指向esp指向的地址
00DF13D3 sub esp,0E4h //將esp的值減小0E4h
00DF13D9 push ebx //ebx入棧
00DF13DA push esi //esi入棧
00DF13DB push edi //edi入棧
00DF13DC lea edi,[ebp-0E4h] //將ebp-0E4h這個地址賦給edi(作為開始地址)
00DF13E2 mov ecx,39h //將39h賦給ecx(作為重復次數(shù))
00DF13E7 mov eax,0CCCCCCCCh //將0CCCCCCCCh賦給eax(用作賦值內容)
00DF13EC rep stos dword ptr es:[edi] //重復賦值,dword表示雙字(作為每次賦值的長度),es:[edi]為賦值的目標地址
//上面四個語句合起來的效果是:
//從es:[edi]這個地址開始,向高地址方向重復賦值,每次賦值的長度為雙字(四個字節(jié))
//每次賦值后edi中的地址的值會增加4,從而實現(xiàn)向高地址方向重復多次賦值
//賦值的內容為eax中的值,重復次數(shù)為ecx中的值(39h次)
int a = 1;
......
這段過程中,內存中的情況是這樣的:
接下來在主函數(shù)中創(chuàng)建并初始化變量。這部分對應的匯編代碼如下:
......
int a = 1;
00DF13EE mov dword ptr [ebp-8],1 //將1這個值存到ebp-8這個地址中
int b = 3;
00DF13F5 mov dword ptr [ebp-14h],3 //將3這個值存到ebp-14h這個地址中
int c = 0;
00DF13FC mov dword ptr [ebp-20h],0 //將0這個值存到ebp-20h這個地址中
c = SUB(a, b);
......
由此可見這幾個int變量的存放位置恰好是從ebp-8開始,每隔8個字節(jié)存放一個數(shù)據(jù)。變量的數(shù)據(jù)在棧幀存放的位置是由編譯器決定的,不同的編譯器下存放的位置可能不同。
這段過程中,內存中的情況是這樣的:
接下來調用SUB函數(shù),首先進行的是函數(shù)傳參以及程序的跳轉。這部分對應的匯編代碼如下:
......
c = SUB(a, b);
//下面四條指令完成的是函數(shù)傳參
00DF1403 mov eax,dword ptr [ebp-14h] //將雙字指針ebp-14h中的值(即b的值)存入寄存器eax中
00DF1406 push eax //eax入棧
00DF1407 mov ecx,dword ptr [ebp-8] //將ebp-8中的值(即a的值)存入寄存器ecx中
00DF140A push ecx //ecx入棧
//call指令調用SUB函數(shù)
00DF140B call @ILT+215(_SUB) (0DF10DCh) //子程序調用指令
//首先將下一條語句的地址(0x00df1410)入棧,然后程序發(fā)生跳轉,跳轉的目標位置是0x0df10dc,對應一條使程序跳轉到SUB函數(shù)的jmp語句
//執(zhí)行完SUB函數(shù)之后,程序會再跳轉回到此處,執(zhí)行下一條語句
00DF1410 add esp,8
00DF1413 mov dword ptr [c],eax
return 0;
......
......
@ILT+215(_SUB):
00DF10DC jmp SUB (0DF1380h) //無條件轉移指令
//前面的call指令會使程序跳轉到這條指令,而這條指令會使程序跳轉到SUB函數(shù)
//地址0x0df1380h對應的就是SUB函數(shù)中第一條指令的地址
......
由此可見:
(1)SUB函數(shù)的形參在函數(shù)棧幀創(chuàng)建之前就已經(jīng)創(chuàng)建好了,而且形參是實參的一份臨時拷貝,對形參的修改不影響實參。
(2)call函數(shù)在調用子程序之前會先將其下一條指令的地址入棧,用于在結束調用之后使程序返回到原來的位置繼續(xù)執(zhí)行指令。
這段過程中,內存中的情況是這樣的:
jmp指令完成跳轉之后,就開始創(chuàng)建SUB函數(shù)的棧幀。這部分對應的匯編代碼如下:
......
int SUB(int x, int y)
{00DF1380 push ebp //ebp入棧
00DF1381 mov ebp,esp //將esp中存的值賦給ebp
00DF1383 sub esp,0CCh //將esp的值減小0CCh
00DF1389 push ebx //ebx入棧
00DF138A push esi //esi入棧
00DF138B push edi //edi入棧
00DF138C lea edi,[ebp-0CCh] //將ebp-0CCh這個地址賦給edi(作為開始地址)
00DF1392 mov ecx,33h //將33h賦給ecx(作為重復次數(shù))
00DF1397 mov eax,0CCCCCCCCh //將0CCCCCCCCh賦給eax(用作賦值內容)
00DF139C rep stos dword ptr es:[edi] //重復賦值,dword表示雙字(作為每次賦值的長度),es:[edi]為賦值的目標地址
//上面四個語句合起來的效果是:
//從es:[edi]這個地址開始,向高地址方向重復多次賦值,每次賦值的長度為雙字(四個字節(jié))
//每次賦值后edi中的地址的值會增加4,從而實現(xiàn)向高地址方向重復多次賦值
//賦值的內容為eax中的值,重復次數(shù)為ecx中的值(33h次)
int z = 0;
......
這段過程中,內存中的情況是這樣的:
可以觀察到,SUB函數(shù)棧幀的創(chuàng)建過程與前面main函數(shù)棧幀的創(chuàng)建幾乎是完全一樣的。
5.SUB函數(shù)中變量的創(chuàng)建以及運算接下來在SUB函數(shù)中創(chuàng)建變量,并與傳過來的形參進行運算,然后把返回值返回到主函數(shù)。這部分對應的匯編代碼如下:
......
int z = 0;
00DF139E mov dword ptr [ebp-8],0 //將0這個值存到ebp-8這個地址中
z = x - y;
00DF13A5 mov eax,dword ptr [ebp+8] //將ebp+8這個地址中存的值存到寄存器eax中
//ebp+8這個地址是0x00d3fd38,存的是形參x的值
00DF13A8 sub eax,dword ptr [ebp+0Ch] //將eax中存的數(shù)據(jù)減小一定值,減小的值為ebp+0Ch中存的值
//ebp+0Ch這個地址是0x00d3fd3c,存的是形參y的值
00DF13AB mov dword ptr [ebp-8],eax //將eax中存的值存到ebp-8這個地址中(即存到變量z中)
return z;
00DF13AE mov eax,dword ptr [ebp-8] //將ebp-8中存的值存到寄存器eax中(相當于將變量z中的值返回)
//局部變量z會隨著SUB函數(shù)運行結束而銷毀,將z的值存到寄存器eax中就可以保存下來,并返回到主函數(shù)中
}
......
這段過程中,內存中的情況是這樣的:
可以觀察到,SUB函數(shù)的返回值儲存在了寄存器eax中,如果主函數(shù)中需要接收返回值,就可以從eax中取出。
6.SUB函數(shù)棧幀的銷毀以及返回值的接收接下來進行SUB函數(shù)棧幀的銷毀以及返回值的接收。這部分對應的匯編代碼如下:
......
return z;
}
00DF13B1 pop edi //將棧頂元素出棧,并存到寄存器edi中,同時棧頂指針的值相應增加
00DF13B2 pop esi //將棧頂元素出棧,并存到寄存器esi中
00DF13B3 pop ebx //將棧頂元素出棧,并存到寄存器ebx中
00DF13B4 mov esp,ebp //將ebp的值賦給esp,即令esp指向ebp指向的地址,
00DF13B6 pop ebp //將棧頂元素出棧,并存到寄存器ebp中,即令ebp指向main函數(shù)的棧底
00DF13B7 ret //返回,彈出棧頂?shù)脑?,并使程序跳轉到棧頂元素儲存的地址對應的指令
//此時的棧頂元素是0x00df1410,正好是call指令的下一條指令的地址,因此程序返回到call指令的下一條指令
......
00DF140B call 00DF10DC //子程序調用指令
//執(zhí)行ret后,程序返回到此處,往下接著執(zhí)行指令
00DF1410 add esp,8 //將esp中存的值增加8(相當于銷毀了形參x和y)
00DF1413 mov dword ptr [ebp-20h],eax //將eax中存的值存放到ebp-20h這個地址中(相當于變量c接收了返回值)
return 0;
這段過程中,內存中的情況是這樣的:
由此可見,形參x和y的銷毀是在SUB函數(shù)棧幀銷毀之后進行的,變量c是通過寄存器eax來接收SUB函數(shù)的返回值的。
7.main函數(shù)棧幀的銷毀接下來進行main函數(shù)棧幀的銷毀。這部分對應的匯編代碼如下:
......
return 0;
00DF1416 xor eax,eax //將eax中存的值與eax中存的值異或,所得結果存到eax中(相當于把eax存的值置為0)
}
00DF1418 pop edi //棧頂元素出棧到edi中
00DF1419 pop esi //棧頂元素出棧到esi中
00DF141A pop ebx //棧頂元素出棧到ebx中
00DF141B add esp,0E4h //將esp中存的值增加0E4h(相當于銷毀了main函數(shù)的棧幀)
......
這段過程中,內存中的情況是這樣的:
可以觀察到,main函數(shù)棧幀的銷毀過程與SUB函數(shù)略有不同,但基本是一致的,都是通過將棧頂指針向高地址移動來實現(xiàn)的。
至此,雖然整個程序還沒有徹底運行結束,但是main函數(shù)和SUB函數(shù)的棧幀的創(chuàng)建和銷毀都已經(jīng)完成。
這篇博客的主要目標是觀察函數(shù)棧幀的創(chuàng)建銷毀過程并記錄,通過觀察匯編代碼對內存的操作,可以加深對內存管理的理解,還可以清楚地感受到程序員前輩們設計邏輯的嚴密。
如果文章中有任何問題,歡迎來糾正我。這是我第一次寫博客,以后一定會更加細心。
你是否還在尋找穩(wěn)定的海外服務器提供商?創(chuàng)新互聯(lián)www.cdcxhl.cn海外機房具備T級流量清洗系統(tǒng)配攻擊溯源,準確流量調度確保服務器高可用性,企業(yè)級服務器適合批量采購,新人活動首月15元起,快前往官網(wǎng)查看詳情吧