從底層分析C語言中的參數傳遞與返回值

使用到的工具

  • 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中

畫一下堆棧圖:
在這裏插入圖片描述
運行一下程序,完全一致:
在這裏插入圖片描述
到這裏,整個程序就執行完畢了,這裏有兩點變化:

  1. EAX的值發生了變化,儲存了程序計算的結果。
  2. 堆棧中多了很多“垃圾”,這些如果是有價值的信息,是非常值得黑客去挖掘的。

好了,到這裏我們就分析完畢了,剩下的指令就是程序剛開始main()函數生成的代碼,這裏我們就沒必要再去跟了,因爲我們已經分析了整個函數從傳參、調用、執行、結束的全部過程了。

總結

  • C語言中的參數傳遞:堆棧傳參,從右到左。
  • C語言中,返回值存儲在寄存器EAX中。
  • C語言中,參數傳遞用PUSH,函數調用用CALL。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章