之前說了call其實包含了兩步,分別是
函數調用棧
我們常用函數,知道使用函數時會跳到函數定義的代碼段去執行,然後執行完後再返回到調用函數去,但以下的一些問題卻仍不清楚。
之前說了call其實包含了兩步,分別是
這個調用過程的原理是什麼
調用函數前要做什麼事情
函數的參數是如何傳遞的
如何跳轉到被調用函數
執行完被調函數後如何返回調用函數並且保證能接着運行
要知道這些,需要結合代碼的反彙編來看。
寫了一段簡單的函數調用的代碼
以下爲main函數的反彙編
其中ebp爲棧底指針,esp爲棧頂指針。
可以看到我們所說的指令 比如int a=15;這個指令,它靠的是ebp棧底指針的偏移量來確定某處有個4個字節的地方存儲15這個值的。
具體彙編就是
00EA2C2E mov dword ptr [a],0Fh
00EA2C2E是 mov dword ptr [a],0Fh 這個指令在代碼段的地址,這個地址是虛擬地址空間地址。並非物理地址
0Fh就是15的十六進制
word代表2個字節,dword代表4個字節
ptr[a]就是a的地址處,其實這個的真正模樣應該是ptr[ebp-4]
這句彙編的意思就是執行將0Fh這個值移動到ebp-4這個地址,佔用4個字節,簡單來講就是給棧底上的4個字節後賦值15,也佔4個字節。
後面
int b=10;
00EA2C35 mov dword ptr [b],0Ah
int result=0;
00EA2C3C mov dword ptr [result],0
就是分別壓入了2個整型的0入棧
重點來了
到了函數調用這塊了,我們看看反彙編
result=sum(a,b);
00EA2C43 mov eax,dword ptr [b]
00EA2C46 push eax
00EA2C47 mov ecx,dword ptr [a]
00EA2C4A push ecx
00EA2C4B call sum (0EA143Dh)
00EA2C50 add esp,8
00EA2C53 mov dword ptr [result],eax
其中call就表示跳到被調用函數指令地址。 然而其實call裏面分爲兩步,我們稍後再說
重點是call的前4行彙編是做什麼的?
eax ecx都指的是寄存器。
那麼前四行意思就是給eax寄存器賦值爲b的值,然後eax壓棧,再給ecx寄存器賦值爲a的值,然後ecx壓棧。
這樣看來就是先後把b和a的值壓入棧頂。而我們可以發現b和a就是sum函數所需要的實參。
目前來看是這樣的
之前說了call其實包含了兩步,分別是
1.把調用方 使用調用函數這條指令的下一條指令的地址push壓棧
2.跳到call指令裏那個的指令地址,即被調函數的指令地址。
00EA2C4B call sum (0EA143Dh)
00EA2C50 add esp,8
00EA2C53 mov dword ptr [result],eax
代到這塊的彙編就是將00EA2C50壓棧,然後再跳轉到0EA143Dh
我們看看0EA143Dh 是什麼。
00EA143D jmp sum (0EA4450h)
括號裏 0EA4450h就是我們剛看的sum定義的地方
int sum(int a,int b)
{
00EA4450 push ebp
00EA4451 mov ebp,esp
00EA4453 sub esp,0CCh
00EA4459 push ebx
00EA445A push esi
00EA445B push edi
00EA445C lea edi,[ebp-0CCh]
00EA4462 mov ecx,33h
00EA4467 mov eax,0CCCCCCCCh
00EA446C rep stos dword ptr es:[edi]
int r=0;
00EA446E mov
。。。。
。。。
。。
。
這樣是不是就很明晰了
然而函數的一開始又有一大串彙編指令,main函數也有,剛纔忽略沒講 ,現在來看,這些指令到底是做什麼的?
.......
00EA4450 push ebp
00EA4451 mov ebp,esp
00EA4453 sub esp,0CCh
00EA4459 push ebx
00EA445A push esi
00EA445B push edi
00EA445C lea edi,[ebp-0CCh]
00EA4462 mov ecx,33h
00EA4467 mov eax,0CCCCCCCCh
00EA446C rep stos dword ptr es:[edi]
int r=0;
.......
首先ebp壓入棧,這個ebp是main的棧底指針的值,然後esp的值給ebp,也就是讓棧底指針指向棧頂esp指向的地方,簡言這兩步就是爲了保存原先的main棧底地址,然後讓棧底指針ebp移到最上方,這就變成了開闢了新的棧了,新棧就是被調用函數的棧。
00EA4453 sub esp,0CCh
讓esp棧頂指針sub減等0cch,也就是讓新棧開闢了0xcc字節的空間,即204個字節。
之後push了三個寄存器 ebx esi edi
然後lea edi,[ebp-0CCh] 這句指令意思爲讓edi指向ebp-0cch處的地址,也就是讓edi寄存器存儲了新棧頂指針的值。
之後又給ecx 存儲了33h,eax存儲了0cccccccch。
33h的十進制爲51。是不是剛好51 *4=204,204是我們新棧開闢的大小。
所以說
00EA4462 mov ecx,33h
00EA4467 mov eax,0CCCCCCCCh
00EA446C rep stos dword ptr es:[edi]
這三行的意思,就是循環ecx次,edi從棧頂向棧底依次賦值爲eax。
也就是循環51次,從棧頂向棧底賦值4個字節的數據0cccccccch,直到edi走向棧底了,把棧內的數據全部賦值了。這就是每個函數開始後,創建了棧,把棧內數據全部清理爲0cccccccc,我們有時會遇到打印越界的數組出現 燙燙燙燙 其實一對 cc 對應的字符就是 燙。
棧開闢完了之後,進入函數運算
.....
int r=0;
00EA446E mov dword ptr [r],0
r=a+b;
00EA4475 mov eax,dword ptr [a]
00EA4478 add eax,dword ptr [b]
00EA447B mov dword ptr [r],eax
return r;
00EA447E mov eax,dword ptr [r]
}
00EA4481 pop edi
00EA4482 pop esi
00EA4483 pop ebx
00EA4484 mov esp,ebp
00EA4486 pop ebp
}
00EA4487 ret
.......
我們先看這部分
r=a+b;
00EA4475 mov eax,dword ptr [a]
00EA4478 add eax,dword ptr [b]
00EA447B mov dword ptr [r],eax
可以看到 這個r=a+b的過程是這樣的。把實參a的值先賦值給eax寄存器,然後再讓eax寄存器加等實參b的值。
也就是a+b的結果先一步計算好了存儲在eax中,然後再把eax裏的值賦值給棧中的r。
return r;
00EA447E mov eax,dword ptr [r]
返回r,可以看到是把r中的值給了寄存器,通過寄存器帶回調用方函數的。
重點又來了
看看棧的銷燬是怎麼做的
00EA4481 pop edi
00EA4482 pop esi
00EA4483 pop ebx
00EA4484 mov esp,ebp
00EA4486 pop ebp
}
00EA4487 ret
首先3個寄存器出棧。
然後讓esp的值變爲ebp,也就是讓棧頂指針指向棧底。然後ebp出棧,意思就把存儲的main的原先的ebp的值出棧,並賦值給ebp。這樣,ebp就重新指向main的棧底了。
然後ret指令就是讓棧頂的值出棧,現在的棧頂就是存儲那個下一條指令地址的值,出棧就可以跳回到調用方剛執行完函數的地方。就實現了回退並連接上次運行地方的功能。
然後轉到主函數彙編
.......
00EA2C4B call sum (0EA143Dh)
00EA2C50 add esp,8
00EA2C53 mov dword ptr [result],eax
........
讓esp加等8,意思就是把兩個4個字節累積8個字節的棧幀捨棄。然後將eax裏保存的return的結果賦值給result。
流程圖如下,紅色代表順序
還有一個遺留問題是剛纔的sum函數的參數只有兩個四字節數據,因此用的是寄存器帶的數據,可是寄存器非常有限的,如果我的實參是個結構體類型,大小遠遠大於四個字節呢,這是參數該如何帶呢?
小於4個字節時用1個寄存器,大於4小於8時 用2個寄存器
如果大於8個字節那就不能用寄存器了,而是直接讓棧頂指針esp減等參數的大小,然後類似開闢棧時,循環拷貝0ccccccc那樣,用2個寄存器。一個記錄調用方函數的那個實參的其實地址。一個記錄拷貝循環次數。這樣循環拷貝進行傳參。
返回值也是通樣,如果返回的值大於8個字節時,將在調用方函數的棧內開闢一塊返回值臨時量區域,然後把return的值循環拷貝回調用方。所以在新棧開闢的時候會多壓入一個臨時量的地址。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.