欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

匯編 函數(shù)調(diào)用的實(shí)現(xiàn)

 更新時(shí)間:2020年02月07日 11:27:45   作者:豆腐der  
這篇文章主要介紹了匯編 函數(shù)調(diào)用的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧

1. 從代碼的順序執(zhí)行說起

每一個(gè)程序員腦子里應(yīng)該都有這么一種印象:“程序是順序執(zhí)行的”。這個(gè)觀點(diǎn)其實(shí)和我們開篇所講的cpu的流水線執(zhí)行過程直接相關(guān)。
讓我們?cè)倩貞浺幌履X海中關(guān)于函數(shù)調(diào)用的概念,也許會(huì)是這個(gè)樣子:

這里的“控制流轉(zhuǎn)移”又是如何發(fā)生的呢?在解釋這個(gè)之前,也許我們需要科普一點(diǎn)有關(guān)于匯編的知識(shí)。

2. 函數(shù)調(diào)用中的一些細(xì)節(jié)說明

2.1 函數(shù)調(diào)用中的關(guān)鍵寄存器

2.1.1 程序計(jì)數(shù)器PC

程序計(jì)數(shù)器是一個(gè)計(jì)算機(jī)組成原理中講過的概念,下面給出一個(gè)百度百科中的簡(jiǎn)單解釋

程序計(jì)數(shù)器是用于存放下一條指令所在單元的地址的地方。
當(dāng)執(zhí)行一條指令時(shí),首先需要根據(jù)PC中存放的指令地址,將指令由內(nèi)存取到指令寄存器中,此過程稱為“取指令”。與此同時(shí),PC中的地址或自動(dòng)加1或由轉(zhuǎn)移指針給出下一條指令的地址。此后經(jīng)過分析指令,執(zhí)行指令。完成第一條指令的執(zhí)行,而后根據(jù)PC取出第二條指令的地址,如此循環(huán),執(zhí)行每一條指令。

可以看到,程序計(jì)數(shù)器是一個(gè)cpu執(zhí)行指令代碼過程中的關(guān)鍵寄存器:它指向了當(dāng)前計(jì)算機(jī)要執(zhí)行的指令地址,CPU總是從程序計(jì)數(shù)器取出當(dāng)前指令來執(zhí)行。當(dāng)指令執(zhí)行后,程序計(jì)數(shù)器的值自動(dòng)增加,指向下一條將要執(zhí)行的指令。
在x86匯編中,執(zhí)行程序計(jì)數(shù)器功能的寄存器被叫做EIP,也叫作指令指針寄存器。

2.1.2 基址指針,棧指針和程序棧

棧是程序設(shè)計(jì)中的一種經(jīng)典數(shù)據(jù)結(jié)構(gòu),每個(gè)程序都擁有自己的程序棧。很重要的一點(diǎn)是,棧是向下生長(zhǎng)的。所謂向下生長(zhǎng)是指從內(nèi)存高地址->低地址的路徑延伸,那么就很明顯了,棧有棧底和棧頂,那么棧頂?shù)牡刂芬葪5椎汀?duì)x86體系的CPU而言,其中
—> 寄存器ebp(base pointer )可稱為“幀指針”或“基址指針”,其實(shí)語(yǔ)意是相同的。
—> 寄存器esp(stack pointer)可稱為“ 棧指針”。
在C和C++語(yǔ)言中,臨時(shí)變量分配在棧中,臨時(shí)變量擁有函數(shù)級(jí)的生命周期,即“在當(dāng)前函數(shù)中有效,在函數(shù)外無效”。這種現(xiàn)象就是函數(shù)調(diào)用過程中的參數(shù)壓棧,堆棧平衡所帶來的。對(duì)于這種實(shí)現(xiàn)的細(xì)節(jié),我們會(huì)在接下來的環(huán)節(jié)中詳細(xì)討論。

2.2. 堆棧平衡

堆棧平衡這個(gè)概念指的是函數(shù)調(diào)完成后,要返還所有使用過的棧空間。這種說法可能有點(diǎn)抽象,我們可以舉一個(gè)簡(jiǎn)單的例子來類比:
我們都知道函數(shù)的臨時(shí)變量存放在棧中。那我們來看下面的代碼,它是一個(gè)很簡(jiǎn)單的函數(shù),用來交換傳入的2個(gè)參數(shù)的值:

void __stdcall swap(int& a,int& b)
{
 int c = a;
 a = b;
 b = c;
}

我們可以看到,在這個(gè)函數(shù)中使用了一個(gè)臨時(shí)變量int c;這個(gè)變量分配在棧中,我們可以簡(jiǎn)單的理解為,在聲明臨時(shí)變量c后,我們就向當(dāng)前的程序棧中壓入了一個(gè)int值:

int c = a; <==> push(a);  //簡(jiǎn)單粗暴,臨時(shí)變量的聲明理解為簡(jiǎn)單地向棧中push一個(gè)值。

那現(xiàn)在這個(gè)函數(shù)swap調(diào)用結(jié)束了,我們是否需要退棧,把之前臨時(shí)變量c使用的??臻g返還回去?需要嗎?不需要嗎?
我們假設(shè)不需要,當(dāng)我們頻繁調(diào)用swap的時(shí)候,會(huì)發(fā)生什么?每次調(diào)用,程序棧都在生長(zhǎng)。直到棧滿,我們就會(huì)收到stack overflow錯(cuò)誤,程序掛掉了。
所以為了避免這種烏龍的事情發(fā)生,我們需要在函數(shù)調(diào)用結(jié)束后,退棧,把堆棧還原到函數(shù)調(diào)用前的狀態(tài),這些被pop掉的臨時(shí)變量,自然也就失效了,這也解釋了我們一直以來關(guān)于臨時(shí)變量?jī)H在當(dāng)前函數(shù)內(nèi)有效的認(rèn)知。其實(shí)堆棧平衡這個(gè)概念本身比這種粗淺的理解要復(fù)雜的多,還應(yīng)包括壓棧參數(shù)的平衡,暫時(shí)我們可以簡(jiǎn)單地這樣理解,后面再做詳細(xì)說明。

2.3. 函數(shù)的參數(shù)傳遞和調(diào)用約定

函數(shù)的參數(shù)傳遞是一個(gè)參數(shù)壓棧的過程。函數(shù)的所有參數(shù),都會(huì)依次被push到棧中。那調(diào)用約定有是什么呢?
C和C++程序員應(yīng)該對(duì)所謂的調(diào)用約定有一定的印象,就像下面這種代碼:

void __stdcall add(int a,int b);

函數(shù)聲明中的__stdcall就是關(guān)于調(diào)用約定的聲明。其中標(biāo)準(zhǔn)C函數(shù)的默認(rèn)調(diào)用約定是__stdcall,C++全局函數(shù)和靜態(tài)成員函數(shù)的默認(rèn)調(diào)用約定是__cdecl,類的成員函數(shù)的調(diào)用約定是__thiscall。剩下的還有__fastcall,__naked等。
為什么要用所謂的調(diào)用約定?調(diào)用約定其實(shí)是一種約定方式,它指明了函數(shù)調(diào)用中的參數(shù)傳遞方式和堆棧平衡方式。

2.3.1 參數(shù)傳遞方式

還是之前那個(gè)例子,swap函數(shù)有2個(gè)參數(shù),int a,int b。這兩個(gè)參數(shù),入棧的順序誰先誰后?
其實(shí)是從左到右入棧還是從右到左入棧都可以,只要函數(shù)調(diào)用者和函數(shù)內(nèi)部使用相同的順序存取參數(shù)即可。在上述的所有調(diào)用約定中,參數(shù)總是從右到左壓棧,也就是最后一個(gè)參數(shù)先入棧。我們可以使用一份偽代碼描述這個(gè)過程

push b;   //先壓入?yún)?shù)b
push a;   //再壓入?yún)?shù)a
call swap; //調(diào)用swap函數(shù)

其實(shí)從這里我們就可以理解為什么在函數(shù)內(nèi)部,不能改變函數(shù)外部參數(shù)的值:因?yàn)楹瘮?shù)內(nèi)部訪問到的參數(shù)其實(shí)是壓入棧的變量值,對(duì)它的修改只是修改了棧中的"副本"。指針和引用參數(shù)才能真正地改變外部變量的值。

2.3.2 堆棧平衡方式

因?yàn)楹瘮?shù)調(diào)用過程中,參數(shù)需要壓棧,所以在函數(shù)調(diào)用結(jié)束后,用于函數(shù)調(diào)用的壓棧參數(shù)也需要退棧。那這個(gè)工作是交給調(diào)用者完成,還是在函數(shù)內(nèi)部自己完成?其實(shí)兩種都可以。調(diào)用者負(fù)責(zé)平衡堆棧的主要好處是可以實(shí)現(xiàn)可變參數(shù)(關(guān)于可變參數(shù)的話題,在此不做過多討論。如果可能的話,我們可以以一篇單獨(dú)的文章來講這個(gè)問題),因?yàn)樵趨?shù)可變的情況下,只有調(diào)用者才知道具體的壓棧參數(shù)有幾個(gè)。
下面列出了常見調(diào)用約定的堆棧平衡方式:

調(diào)用約定 堆棧平衡方式
__stdcall 函數(shù)自己平衡
__cdecl 調(diào)用者負(fù)責(zé)平衡
__thiscall 調(diào)用者負(fù)責(zé)平衡
__fastcall 調(diào)用者負(fù)責(zé)平衡
__naked 編譯器不負(fù)責(zé)平衡,由編寫者自己負(fù)責(zé)

2.4. 棧幀的概念:從esp和ebp說起

為什么我們需要ebp和esp2個(gè)寄存器來訪問棧?這種觀念其實(shí)來自于函數(shù)的層級(jí)調(diào)用:函數(shù)A調(diào)用函數(shù)B,函數(shù)B調(diào)用函數(shù)C,函數(shù)C調(diào)用函數(shù)D…
這種調(diào)用可能會(huì)涉及非常多的層次。編譯器需要保證在這種復(fù)雜的嵌套調(diào)用中,能夠正確地處理每個(gè)函數(shù)調(diào)用的堆棧平衡。所以我們引入了2個(gè)寄存器:

ebp指向了本次函數(shù)調(diào)用開始時(shí)的棧頂指針,它也是本次函數(shù)調(diào)用時(shí)的“棧底”(這里的意思是,在一次函數(shù)調(diào)用中,ebp向下是函數(shù)的臨時(shí)變量使用的空間)。在函數(shù)調(diào)用開始時(shí),我們會(huì)使用把當(dāng)前的esp保存在ebp中。

mov ebp,esp 

esp,它指向當(dāng)前的棧頂,它是動(dòng)態(tài)變化的,隨著我們申請(qǐng)更多的臨時(shí)變量,esp值不斷減小(正如前文所說,棧是向下生長(zhǎng)的)。函數(shù)調(diào)用結(jié)束,我們使用來還原之前保存的esp

mov esp,ebp

在函數(shù)調(diào)用過程中,ebp和esp之間的空間被稱為本次函數(shù)調(diào)用的“棧幀”。函數(shù)調(diào)用結(jié)束后,處于棧幀之前的所有內(nèi)容都是本次函數(shù)調(diào)用過程中分配的臨時(shí)變量,都需要被“返還”。這樣在概念上,給了函數(shù)調(diào)用一個(gè)更明顯的分界。下圖是一個(gè)程序運(yùn)行的某一時(shí)刻的棧幀圖:

3. 匯編中關(guān)于“函數(shù)調(diào)用”的實(shí)現(xiàn)

上面鋪陳了很多的匯編層面的概念后,我們終于可以切回到我們本次的主題:函數(shù)調(diào)用。
函數(shù)調(diào)用其實(shí)可以看做4個(gè)過程,也就是本篇標(biāo)題:

  • 壓棧: 函數(shù)參數(shù)壓棧,返回地址壓棧
  • 跳轉(zhuǎn): 跳轉(zhuǎn)到函數(shù)所在代碼處執(zhí)行
  • 執(zhí)行: 執(zhí)行函數(shù)代碼
  • 返回: 平衡堆棧,找出之前的返回地址,跳轉(zhuǎn)回之前的調(diào)用點(diǎn)之后,完成函數(shù)調(diào)用

3.1 call指令 壓棧和跳轉(zhuǎn)

下面我們看一下函數(shù)調(diào)用指令

0x210000 call swap;
0x210005 mov ecx,eax; 

我們可以把它理解為2個(gè)指令:

push 0x210005;
jmp swap;

也就是,首先把call指令的下一條指令地址作為本次函數(shù)調(diào)用的返回地址壓棧,然后使用jmp指令修改指令指針寄存器EIP,使cpu執(zhí)行swap函數(shù)的指令代碼。

3.2 ret指令 返回

匯編中有ret相關(guān)的指令,它表示取出當(dāng)前棧頂值,作為返回地址,并將指令指針寄存器EIP修改為該值,實(shí)現(xiàn)函數(shù)返回。
下面給出一組示意圖來演示函數(shù)的返回過程:

當(dāng)前EIP的值為0x210004,指向指令ret 4,程序需要返回

執(zhí)行ret指令,將當(dāng)前esp指向的堆棧值當(dāng)做返回地址,設(shè)置eip跳轉(zhuǎn)到此處并彈出該值

經(jīng)過這兩步,函數(shù)就返回到了調(diào)用處。

4. 從實(shí)際匯編代碼看函數(shù)調(diào)用

4.1 程序源碼和運(yùn)行結(jié)果

源碼:

main.cpp

#include <stdio.h>
 
void __stdcall swap(int& a, int& b);
 
int main(int argc, char* argv)
{
  int a = 1, b = 2;
  printf("before swap: a = %d, b = %d\r\n", a, b);
  swap(a, b);
  printf("after swap: a = %d, b = %d\r\n", a, b);
}
 
 
void __stdcall swap(int& a, int& b)
{
  int c = a;
  a = b;
  b = c;
}

程序運(yùn)行結(jié)果:

4.2 反匯編

可以看到,在函數(shù)調(diào)用前,函數(shù)參數(shù)已被壓棧,此時(shí):
EBP = 00AFFCAC
ESP = 00AFFBBC
EIP = 00BF1853
我們按F11,進(jìn)入函數(shù)內(nèi)部,此時(shí):

其實(shí)就是call swap指令的下一條指令地址,它就是本次函數(shù)調(diào)用的返回地址。

下面是一個(gè)swap函數(shù)的詳細(xì)注釋:

當(dāng)程序運(yùn)行到ret 8時(shí)

執(zhí)行返回后:

在返回前,ESP = 00AFFBB8,返回后 ESP = 00AFFBC4
0x00AFFBC4 - 0x00AFFBB8 = 0xC
這里的數(shù)值是字節(jié)數(shù),而我們知道,int是4字節(jié)長(zhǎng)度。所以0xC/4 = 3
正好是2個(gè)壓棧參數(shù)+一個(gè)返回地址。

4.3 調(diào)用堆棧

調(diào)試程序的時(shí)候,我們經(jīng)常關(guān)注的一個(gè)點(diǎn)就是VisualStudio顯示給我們的“調(diào)用堆棧”功能,這次讓我們來仔細(xì)看一下它:
我們重新執(zhí)行一次程序,這次我們關(guān)注一下vs顯示的調(diào)用堆棧,如下圖

第一行是當(dāng)前指令地址
第二行是外層調(diào)用者,我們雙擊它,跳轉(zhuǎn)到如下地址:

也許這也是為什么這個(gè)功能被叫做“調(diào)用堆棧”的原因:它正是通過對(duì)程序棧的分析實(shí)現(xiàn)的。

以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。

相關(guān)文章

最新評(píng)論