使用到的工具
- VC6.0(觀察寄存器變化)
- Excel(畫堆棧圖)
函數定義
函數定義的格式如下:
返回類型 函數名(參數列表)
{
功能
return;
}
例子:
int plus(int,x int,y)
{
return x+y;
}
int代表的是字節寬度,除int外,還有兩個常使用的變量類型
變量類型 | 數據寬度 |
---|---|
int | 4個字節 |
short | 2個字節 |
char | 1個字節 |
畫堆棧圖
int plus(x,y)
{
return x+y;
}
void main() //程序入口
{
plus(1,2); //函數調用
return; //執行結束
}
就從上面這個程序來說,它究竟是怎麼執行的呢?我們下個斷點,追一下程序內存的變化,就像這樣:
這裏一定要記住兩個寄存器的變化,一個是ESP棧頂寄存器,一個是EBP棧底寄存器,我們現在還沒有運行,先記錄一下棧頂:
接着看一下彙編指令,在這裏可以看到,參數的傳遞有兩個特徵:第一是從右向左傳遞,第二是使用push指令在彙編中實現。
push 2
接着畫圖,這行指令執行完堆棧會發生什麼變化呢?push 2,向堆棧中壓入參數2,棧頂指針ESP的值-4,如圖:
接着執行,push 1向堆棧壓入參數1,esp的值-4。
push 1
接着往下走,這裏push(1,2);對應這call,也就是說call指令就代表着函數調用,哪這個call指令執行以後要不要改堆棧?要,它會修改esp的的值-4,並將它下一行的地址壓入棧頂。
接着F11跟進去看一下堆棧的變化,是不是與我們畫的圖返回的結果相同?
接着往下走,這裏的這個jmp是VC6自己生成的,是它的特點,並不是直接跳到函數的指令那裏,而是通過一個jmp來跳轉,我們知道jmp是無條件跳轉,不影響堆棧,所以跳就行了。
jmp plus(00101020)
接着往下看,這裏它push了一個ebp,那麼運行後堆棧的變化就是,向堆棧中壓入ebp的值,棧頂指針esp的值-4,我們接着畫一下。
push ebp
運行看一下結果是不是跟我們畫的相同。
接着往下走,這裏使用mov ebp,esp來提升堆棧,進行ebp尋址。
mov ebp,esp
在堆棧中應該是這樣反應的。
我們運行一下看看結果:
接着往下走,這裏esp的值要-40來提升堆棧,爲這個函數的運行騰出空間,這裏提升了40個堆棧,我們的堆棧一格是4個字節,這裏需要進制轉換,40轉換爲10進制是64,64÷4是16,也就是說我們要提升16個格,在堆棧中應該是這樣顯示:
sub esp,40h
單步一下看看結果:
接着往下看,這裏push了三個寄存器,這其實可以理解爲“備份”,因爲程序後續運行可能會覆蓋掉寄存器的值,但又會用到它原先的值,這樣被覆蓋了程序就出錯了,我們畫一下它的運行後的堆棧圖。
push ebx
push esi
push edi
單步一下看看結果是不是跟我們畫的一樣。
緩衝區
在本程序中,從0012FF20到0012FEE4這一塊內存,就是程序的緩衝區。
當前的函數在執行過程中,它需要用內存,那麼它就會提升堆棧,自己給自己分配一塊內存,這塊內存,就是所謂的緩衝區。
接着往下走,可以看到這4行指令
lea edi,[ebp-40h]
mov ecx,10h
mov eax,0CCCCCCCCh
rep stos dword ptr [edi]
我們一行一行的分析,首先第一行,lea指令的意思就是,將源操作數給出的有效地址傳送到指定的的寄存器中。
也就是說,這一行的意思就是,把ebp地址減去40的結果賦給edi,在堆棧中就是這樣顯示:
第二、三行的意思不必多說,把10賦給ecx,把CCCCCCCC賦給eax,第四行,stos就是把eax的值放到edi的位置,結合rep指令重複執行,這裏重複執行多少次看的是ecx的值,這裏重複10次換算過來剛好是16次。總結一下就是,把緩衝區的值,全部換成4個CC。
這裏的CC可以理解爲下的斷點,程序一遇到CC它就會停止運行,這樣就避免程序自己運行時的溢緩衝區出了。
看一下它在堆棧中的結果:
我們運行一下程序看看結果是否相同:
進行計算
接着往下看,這裏的兩行指令就是進行1+2的運算了
mov eax,dword ptr [ebp+8]
add eax,dword ptr [ebp+0Ch]
第一行指令:把ebp+8的值放到eax裏
第二行指令:令eax與ebp+c的值相加,結果返回給eax
那麼此時eax裏邊的值應該是3
看看堆棧圖:
運行一下程序看看結果:
恢復堆棧
接着是三個出棧操作:
pop edi //把棧頂的值取出賦給edi。esp+4
pop esi //把棧頂的值取出賦給esi,esp+4
pop ebx //把棧頂的值取出賦給ebx,esp+4
這三個操作剛好對應了之前的壓棧操作,這時候esp的值應該+c,我們看一下它在堆棧中的結果:
運行程序看看結果:
繼續往下走,這裏它把ebp的值賦給了esp
mov esp,ebp
這行代碼執行完就意味着,esp=ebp,我們畫一下堆棧圖:
運行一下程序看看結果:
繼續往下走,又是一個出棧操作,把當前棧頂的值賦給ebp,棧頂指針esp的值+4
pop ebp
我們畫一下程序運行後的堆棧圖:
運行一下程序看看結果是否相同:
程序結束
最開始運行前程序的操作是提升堆棧,現在程序即將執行完畢它就開始慢慢的恢復堆棧,這一波操作就是彙編中的堆棧平衡。
接着程序執行完畢,ret這一行指令的意思是:把ESP當前堆棧中的值賦給EIP,所以ret也等於pop eip
ret
畫一下它在堆棧中的變化:
運行一下程序,所有的變化都跟我們分析的一摸一樣:
最後的這一行指令是爲了保持堆棧平衡,也就是函數執行前堆棧是什麼樣子,執行後堆棧就恢復到什麼樣子,堆棧平衡有內平棧與外平棧兩種方法,這裏就是採用的外平棧。
add esp,8 //esp的值+8,結果返回到esp中
畫一下堆棧圖:
運行一下程序,完全一致:
到這裏,整個程序就執行完畢了,這裏有兩點變化:
- EAX的值發生了變化,儲存了程序計算的結果。
- 堆棧中多了很多“垃圾”,這些如果是有價值的信息,是非常值得黑客去挖掘的。
好了,到這裏我們就分析完畢了,剩下的指令就是程序剛開始main()函數生成的代碼,這裏我們就沒必要再去跟了,因爲我們已經分析了整個函數從傳參、調用、執行、結束的全部過程了。
總結
- C語言中的參數傳遞:堆棧傳參,從右到左。
- C語言中,返回值存儲在寄存器EAX中。
- C語言中,參數傳遞用PUSH,函數調用用CALL。