您好,登錄后才能下訂單哦!
從內存的角度詳細的分析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()里邊的過程就不在分析了!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。