從內存的角度詳細的分析C語言中的函數調用過程:
首先寫一個測試用的代碼:
#include <stdio.h> int add(int x, int y) { int z = 0; z = x + y; return z; } int main() { int a = 1, b = 2; int c = 0; c = add(a, b); return 0; }
這是一個簡單的的求和函數。
其次,讓我們確定一下,程序是從哪裏開始運行的:
調試程序,按一下F10(博主用的VS2013),
進入main函數:
然後進調試--->窗口--->調用堆棧(用來顯示函數的調用關係)。
發現正在調用main這個函數,但現在我想知道是誰在調用main函數,F10一路走到return 0,接着換F11(逐語句調試),然後會發現,main函數返回後,我們來到了這裏:
再看看此時的調用堆棧:
直接來看,現在運行的函數是__tmainCRTStartup(),這個函數又被mainCRTStratup()調用,而我們剛剛是從main()函數返回來的,所以,main()函數是由__tmainCRTStartup()這個函數調用的。
瞭解了main()函數是被誰調用後,我們可以進一步分析這其中的細節了!
現在重新F10進入調試,到這一步:
進入main()函數後還沒有執行任何一條語句,我們 右擊-->轉到反彙編:
看到了彙編語言的代碼,圖中的ebp和esp是什麼東西呢?我們知道,調用函數的時候操作系統要給這個函數分配一段內存空間,之前又說了main()函數是由—__tCRTStartup()函數調用的,所以請看:
mainCRTStratup()函數調用__tmainCRTStra()函數的時候就會從棧上爲__tmainCRTStra()分配類似圖中這麼一塊空間,把這塊空間叫做棧幀。我們知道棧是由高地址向低地址擴展的。其中ebp叫做棧底指針,esp叫做棧頂指針(當然也有其它叫法)。ebp,esp本身是一個寄存器,其中存放了地址時,我們就稱之爲指針!
現在再來看彙編程序:
按一下F10執行第一條語句,箭頭指向下一條語句,變成這樣:
(和我們在外邊的調試是一樣的)這句 push ebp 就是將ebp中的值進行壓棧,而此時ebp存放的是系統分給__tmainCRTStartup()函數的空間的起始地址。因爲我們現在要調用main()函數了,所以當然要先把__tmainCRTStartup()函數的運行狀態保存下來,這樣main()函數才能返回的時候才能找得到!push是在棧頂進行的,所以,push之後,esp要向上移動:
剛剛說了,棧是由高地址向低地址擴展的,所以這個push操作應該是對esp進行一個減操作,具體見了多少,可以在內存裏查一查:
先看一下push之前esp的的值:
esp的當前值爲0x00ABFA30,代表它指向0x00ABFA30這個地址代表的內存。
再看一下push之後esp的值發生了什麼變化:
變成了0x00ABFA2C,差了4個字節,就是放進去的地址的大小。
然後繼續執行下一條語句: mov ebp,esp
即把esp的值賦給ebp,這樣,ebp也就指向了現在esp的位置,如下圖:
接着又執行語句:sub esp,0E4h
即將esp的值減去E4h,所以esp向上移動了E4h個位置(相當於申請了這麼大的一塊空間),新申請的這塊空間就給main()用了。如下:
接下來緊接着三條push語句將後面要用到的寄存器中原來的值存儲起來,等我們借用完寄存器後再給人家pop回去,不管它,這裏esp再向上移動三次。
(ps:圖片太大,所以只截了當前要用到的)
緊接着的四條語句共同完成一個任務,就是將圖中最大長方形區域初始化爲0CCCCCCCh(你經常看到的:燙燙燙燙......)
第一句:lea edi,[ebp-0E4h]
就是將ebp減去E4h的值賦給edi,這個E4h是不是很眼熟呢?它就是我們上一步分配給main()的空間的大小,即edi指向了3次push之前的esp的位置;
第二句:mov ecx,39h
把39h放在ecx中(充當了計數器)
第三句:mov eax,0CCCCCCCCh
把要初始化的數據寫入eax
最後一句:rep stos dword ptr es:[edi]
循環的從低地址(ebp-0E4h)向高地址(ebp)寫0CCCCCCCCh,循環了39h次!
我們在執行之前轉到內存中看一下:
先查找ebp:
(我往下拖了一點,左下角的光標處的地址就是ebp當前值0z00ABFA2C)
四條語句執行後:
相應的位置已經被初始化爲0CCCCCCCh,其它部分是亂碼(此時ebp值爲0x00ABFA2C,它之上的一段空間是分配給main()的)
程序繼續往下執行:
mov dword ptr [ebp-8],1 在ebp-8h的位置放一個1,
mov dword ptr [ebp-14h], 2 在ebp-14h的位置放一個2
即分別創建了a,b兩個變量,如圖:
接着創建c:
此時我們的內存分配變成了這樣:
然後到了這裏,調用add()準備工作:
mov eax,dword ptr [ebp-14h] 是把ebp-14h位置的值放入eax(此時ebp-14h的值是我們的變量b的值),然後:push eax , 即eax壓棧;
同理,mov ecx,dword ptr [ebp-8] 把ebp-8位置的值放入ecx,然後ecx壓棧。如下(傳遞形參給x和y):
程序到這:
在彙編裏我們用call調用一個函數(_add是一個標號,它代表了一個地址,是add()函數的首地址),而call在執行的同時,會把它下一條指令的地址(就是圖中的00D1450)push到main()的棧楨中去,以便add()執行完後返回的時候還可以找到程序當初執行到了哪裏,然後接着執行。
爲了證明這一點,我們先查看一下esp所指向內存的值:
然後F11跟進去到這裏:
再查看esp所指內存:
可以看到esp的位置發生了改變,此時內存中的值 50 14 0d 00 是不是很像剛剛的call語句下一條指令地址呢?對它就是00 0d 14 50 的小端字節序,這裏不再解釋小端字節序,只需理解它是內存中字節存儲的一種方式,有興趣的可以查看:http://blog.csdn.net/qq_33724710/article/details/51056542
棧楨分配圖變成了這樣:
接着F11執行剛剛的jmp語句:
歷盡千辛萬苦終於進入add()!現在貼出來的這幾句代碼就和我們剛剛進入main()函數的語句大同小異了。
push ebp //ebp壓棧
mov ebp,esp //ebp指向esp所指
sub esp,0CCh //esp - 0CCh, 開闢了新的棧楨
push ebx //3個push,照舊不管它
push esi
push edi
lea edi,[ebp-0CCh] //初始化燙燙燙燙......
mov ecx,33h
mov eax,0CCCCCCCCh
rep stos dword ptr es:[edi]
然後到這裏:
給ebp-8處放了個0,就是創建z啦!
再接着到這裏:
eax,dword ptr [ebp+8] //注意是加了8,取出的是我們之前傳遞進來的形參值1,放到eax
add eax,dword ptr [ebp+0Ch] //取epb+0Ch,取出的是我們之前傳遞進來的形參值2,加到eax
dword ptr [ebp-8],eax //再把求和後的值eax賦給epb - 8的位置,就是z嘍!
程序執行到這,準備返回main()了:
因爲z是個臨時變量,出了add()就會銷燬,要返回z的值,就要把它的值放進寄存器:
mov eax,dword ptr [ebp-8] //epb-8找到的就是z,賦給eax
pop edi //連續三個pop,之前連續三個push我們沒管它,現在仍然不管它
pop esi
pop ebx
3次pop後,esp高地址處移動了3個單位:
雖然esp上邊的空間還在,但是已經不屬於當前的棧楨了,相當於釋放掉了!
然後:
mov esp,ebp //esp指向當前ebp
pop ebp //main()起始地址賦給給ebp,esp往高地址處移動一次
所以變成這樣:
最後執行ret,程序回到這裏:
看見了沒,ret指令自動取出了call的下一條語句地址(ret自動執行了pop,esp又往高地址處移動了一次)賦給了PC(PC總是指向下一條要執行的語句)。
接着的add esp,8 使esp繼續往高地址方向移動,並跳過1,2兩個參數,如下:
mov dword ptr [ebp-20h],eax //還記得eax嗎,當初我們把求和的結果,即 z 的值賦給了它,ebp-20h依然是當初的c
現在,我們要的結果已經賦給 c 了!
xor eax,eax //eax沒用了,異或eax,清零
pop edi //又是連續3個pop
pop esi
pop ebx
add esp,0E4h //oE4h,當出開闢的main()棧楨的大小,現在釋放掉
cmp ebp,esp //不管它
call 000D113B //不管它
mov esp,ebp //釋放main()棧楨
pop ebp //ebp指向__tmainCRTStartup()起始地址,esp下移
ret //返回到__tmainCRTStartup()
__tmainCRTStartup()和mainCRTStart()裏邊的過程就不在分析了!