函數調用和局部變量


轉載:http://www.cnblogs.com/ZJAJS/archive/2013/03/08/2949162.html

 

函數調用和局部變量

要研究函數的調用過程,先來看下面的一段代碼:

復制代碼
 1 int Add(int x, int y) 
 2 {
 3     int sum;
 4     sum = x + y;
 5     return sum;
 6 }
 7 
 8 void main() 
 9 {
10     int z;
11     z = Add(1, 2);
12     printf("z = %d\n", z);
13 }
復制代碼

 對於 z = Add(1, 2); 這一句,我們可以看到其匯編代碼和機器碼如下:

復制代碼
1 z = Add(1, 2);
2 
3 00413762     6a 02                push  2       
4 00413764     6a 01                push  1
5 00413766     e8 7a da ff ff       call  004111e5
6 0041376B     83 c4 08             add   esp, 8
7 0041376E     89 45 18             mov   dword ptr [ebp-8], eax
復制代碼

上述指令表明主函數將跳轉到內存地址004111e5來進行Add函數的調用執行。在機器碼中,如果已知e8代表的是call指令,那后面的四個字節代表什么呢?call指令采用的是相對偏移量尋址,也就是是通過 基址 + 偏移量 的方式來獲取最終的目的地址的。根據對mov指令進行分析后的經驗,知道小端機內存中的7a da ff ff 代表 0xffffda7a 。但是00413766加上0xffffda7a得到的結果與目的地址004111e5相去甚遠。因此還要考慮0xffffda7a有可能為負數。在計算機中有符號數的第一位為1則說明該數為負數,顯然按照這樣來看0xffffda7a是一個負數,與其對應的相反數正數為0x2586。如果用call指令的地址00413766減去2586,得到的結果為0X4111e0,與真正的目的地之間相差5個字節。通過觀察發現call指令的機器碼長度剛好為5個字節,因此即可得出最終的結論:

  x86系列CPU的call指令的尋址方式為:用與call指令相關的偏移量定位跳轉到的地址

  偏移量計算如下:偏移量 = 跳轉到的地址 - call指令后一條指令的起始地址

進行函數調用的時候,要使用到兩個寄存器ESP和EIP,它們保存着與函數跳轉和返回相關的信息。通過在函數調用過程中對兩個寄存器的值進行觀察,可知EIP中存儲的是call跳轉到的指令的地址004111E5。而ESP中存儲的則是0X00116E60這個內存地址,顯然,它並不是call指令的返回地址0x0041376b。再根據該內存地址去獲取保存在其中的值,得到的結果為從該地址開始往高地址依次數四個字節的值分別為 6b 37 41 00,即0x0041376b,正好是call指令返回的的地址。由此可知:

  call指令將返回地址保存在內存中,而且ESP寄存器指向了該內存。call指令相當於以下兩條指令的組合:

  push  返回地址

  jmp   函數入口地址

調用函數的時候有時還需要給函數傳遞參數,在上面的 z = Add(1, 2) 這一賦值語句對應的幾句匯編代碼中,與傳遞參數相關的是下面的兩句:

1 00413762    6a 02                 push  2    
2 00413764    6a 01                 push  1

這兩句匯編代碼的作用是將兩個參數按照從右至左的順序壓入到內存棧中。在此有必要說明一下,內存中棧的棧頂內存地址保存在ESP寄存器中。由於棧在內存中是從高地址向低地址擴展的,因此每次壓入一個參數,ESP寄存器中的值指向的內存地址都會按照一定的字節數減少相應的值。在壓入第一個參數2之前,ESP寄存器中的值為0x00116e6c。壓入參數2之后,ESP寄存器的值變為0x00116e68,此時ESP寄存器指向的是參數2存放在內存中的地址。再壓入參數3之后,ESP寄存器的值減少為為0x00116e64,指向參數1存放在內存中的地址。在執行call指令之后,函數的返回地址被壓棧,ESP寄存器的值又減少了4個字節,變成0x00116e60。

由於參數存儲在內存棧中,因此被調用的函數要獲得參數,就必須借助esp寄存器中的值。由於參數之上還壓入了函數的返回地址,且每個內存地址長度都為四個字節,因此第一個參數的內存地址即為esp+4,而第二個參數的內存地址為esp+8,以此類推。但是由於esp的值會隨着棧的變化而變化,且難保在函數執行過程中不會改變棧的當前狀態,因此還需要另外一個寄存器ebp(擴展基址指針寄存器)來暫時存放esp寄存器中的值。但是在函數層層嵌套時,內層函數執行完畢退出后如果不改變ebp寄存器的值而讓外層函數繼續使用的話就會出現不可預知的錯誤情況。因此在每次調用一個函數時,要先將當前ebp的值push入棧保存起來,然后才將當前esp的值存入ebp寄存器中。內層函數執行完畢之后,要將函數執行之前保存的ebp寄存器的值出棧並恢復到ebp寄存器中,外層函數就可以繼續使用ebp的值了。要注意的是,由於ebp壓棧后esp的值又減小了4,所以在將esp的值賦給ebp后,第一個參數的內存地址應該為ebp+8,同時第二個參數的內存地址應該為ebp+0ch(即12),以此類推。

為了驗證上面的結論,先來看看下面的這一段代碼:

復制代碼
 1 int Add(int x, int y) 
 2 {
 3 
 4     00411430         push    ebp
 5     00411431         mov     ebp, esp
 6     00411433         sub     esp, 0cch
 7     00411439         push    ebx
 8     0041143a         push    esi
 9     0041143b         push    edi
10     0041143c         lea     edi, [ebp+ffffff34h]
11     00411442         mov     ecx, 33h
12     00411447         mov     eax, 0cccccccch
13     0041144c         rep     stos dword ptr es:[edi]
14 
15     int sum;
16     sum = x + y;
17     0041144e         mov     eax, dword ptr [ebp+8]
18     00411451         add     eax, dword ptr [ebp+0ch]
19 }
復制代碼

觀察上面的代碼可知,跳轉到Add函數之后,在執行第一條語句之前程序預先做了一連串的准備工作。先將ebp的值壓棧,然后將esp的值賦給ebp。在后面的語句中,獲取參數x是通過內存地址ebp+8,而獲取參數y則是通過內存地址ebp+12,與之前的結論相同。

 在函數的執行過程中可能還會使用到用戶定義的局部變量,如下面的代碼中就使用到了局部變量sum:

int Add(int x, int y) 
{
    int sum;
    sum = x + y;
}

在VS2008中反匯編得到與該函數中的賦值語句相對應的三條匯編語句如下:

1  0041144e         mov     eax, dword ptr [ebp+8]
2  00411451         add     eax, dword ptr [ebp+0ch]
3  00411454         mov     dword ptr [ebp-8], eax

在最后一條匯編語句中,將兩個參數相加得到的值保存在了地址為ebp-8的內存空間中。這是因為局部變量的特點與參數一樣,都是當函數調用完畢就不再使用,所以仿效參數將其分配在棧上。但是棧上方已經被參數和返回地址等使用,因此只能使用棧更低地址的空間,每分配一個局部變量都要進行一次壓棧。但是要注意的是,壓棧一次的話地址應該為ebp-4而非上面見到的ebp-8。事實上,在VC 6.0編譯器中反匯編得到的代碼如下:

1  00401038         mov     eax, dword ptr [ebp+8]
2  0040103b         add     eax, dword ptr [ebp+0ch]
3  0040103e         mov     dword ptr [ebp-4], eax

此處的局部變量地址確為ebp-4。造成這種不同的原因主要是在VS2008中為了防止溢出攻擊而采用的StackGaurd溢出攻擊防護機制,在ebp和局部變量的地址之間空出了四個字節。

 函數執行完畢就要返回調用的地方,由之前的敘述可知call指令已經將其后指令的地址壓棧保存,因此可以使用該地址進行返回。函數返回要用到返回指令ret,ret指令的介紹如下:

ret指令:將棧頂保存的地址彈入指令寄存器EIP,相當於"pop eip",從而讓程序跳轉到該地址。執行ret指令后,寄存器EIP(存儲了被彈出的棧頂地址)和ESP(在32位x86中加4)的值有變化

   3、函數調用和局部變量

要研究函數的調用過程,先來看下面的一段代碼:

復制代碼
 1 int Add(int x, int y) 
 2 {
 3     int sum;
 4     sum = x + y;
 5     return sum;
 6 }
 7 
 8 void main() 
 9 {
10     int z;
11     z = Add(1, 2);
12     printf("z = %d\n", z);
13 }
復制代碼

 對於 z = Add(1, 2); 這一句,我們可以看到其匯編代碼和機器碼如下:

復制代碼
1 z = Add(1, 2);
2 
3 00413762     6a 02                push  2       
4 00413764     6a 01                push  1
5 00413766     e8 7a da ff ff       call  004111e5
6 0041376B     83 c4 08             add   esp, 8
7 0041376E     89 45 18             mov   dword ptr [ebp-8], eax
復制代碼

上述指令表明主函數將跳轉到內存地址004111e5來進行Add函數的調用執行。在機器碼中,如果已知e8代表的是call指令,那后面的四個字節代表什么呢?call指令采用的是相對偏移量尋址,也就是是通過 基址 + 偏移量 的方式來獲取最終的目的地址的。根據對mov指令進行分析后的經驗,知道小端機內存中的7a da ff ff 代表 0xffffda7a 。但是00413766加上0xffffda7a得到的結果與目的地址004111e5相去甚遠。因此還要考慮0xffffda7a有可能為負數。在計算機中有符號數的第一位為1則說明該數為負數,顯然按照這樣來看0xffffda7a是一個負數,與其對應的相反數正數為0x2586。如果用call指令的地址00413766減去2586,得到的結果為0X4111e0,與真正的目的地之間相差5個字節。通過觀察發現call指令的機器碼長度剛好為5個字節,因此即可得出最終的結論:

  x86系列CPU的call指令的尋址方式為:用與call指令相關的偏移量定位跳轉到的地址

  偏移量計算如下:偏移量 = 跳轉到的地址 - call指令后一條指令的起始地址

進行函數調用的時候,要使用到兩個寄存器ESP和EIP,它們保存着與函數跳轉和返回相關的信息。通過在函數調用過程中對兩個寄存器的值進行觀察,可知EIP中存儲的是call跳轉到的指令的地址004111E5。而ESP中存儲的則是0X00116E60這個內存地址,顯然,它並不是call指令的返回地址0x0041376b。再根據該內存地址去獲取保存在其中的值,得到的結果為從該地址開始往高地址依次數四個字節的值分別為 6b 37 41 00,即0x0041376b,正好是call指令返回的的地址。由此可知:

  call指令將返回地址保存在內存中,而且ESP寄存器指向了該內存。call指令相當於以下兩條指令的組合:

  push  返回地址

  jmp   函數入口地址

調用函數的時候有時還需要給函數傳遞參數,在上面的 z = Add(1, 2) 這一賦值語句對應的幾句匯編代碼中,與傳遞參數相關的是下面的兩句:

1 00413762    6a 02                 push  2    
2 00413764    6a 01                 push  1

這兩句匯編代碼的作用是將兩個參數按照從右至左的順序壓入到內存棧中。在此有必要說明一下,內存中棧的棧頂內存地址保存在ESP寄存器中。由於棧在內存中是從高地址向低地址擴展的,因此每次壓入一個參數,ESP寄存器中的值指向的內存地址都會按照一定的字節數減少相應的值。在壓入第一個參數2之前,ESP寄存器中的值為0x00116e6c。壓入參數2之后,ESP寄存器的值變為0x00116e68,此時ESP寄存器指向的是參數2存放在內存中的地址。再壓入參數3之后,ESP寄存器的值減少為為0x00116e64,指向參數1存放在內存中的地址。在執行call指令之后,函數的返回地址被壓棧,ESP寄存器的值又減少了4個字節,變成0x00116e60。

由於參數存儲在內存棧中,因此被調用的函數要獲得參數,就必須借助esp寄存器中的值。由於參數之上還壓入了函數的返回地址,且每個內存地址長度都為四個字節,因此第一個參數的內存地址即為esp+4,而第二個參數的內存地址為esp+8,以此類推。但是由於esp的值會隨着棧的變化而變化,且難保在函數執行過程中不會改變棧的當前狀態,因此還需要另外一個寄存器ebp(擴展基址指針寄存器)來暫時存放esp寄存器中的值。但是在函數層層嵌套時,內層函數執行完畢退出后如果不改變ebp寄存器的值而讓外層函數繼續使用的話就會出現不可預知的錯誤情況。因此在每次調用一個函數時,要先將當前ebp的值push入棧保存起來,然后才將當前esp的值存入ebp寄存器中。內層函數執行完畢之后,要將函數執行之前保存的ebp寄存器的值出棧並恢復到ebp寄存器中,外層函數就可以繼續使用ebp的值了。要注意的是,由於ebp壓棧后esp的值又減小了4,所以在將esp的值賦給ebp后,第一個參數的內存地址應該為ebp+8,同時第二個參數的內存地址應該為ebp+0ch(即12),以此類推。

為了驗證上面的結論,先來看看下面的這一段代碼:

復制代碼
 1 int Add(int x, int y) 
 2 {
 3 
 4     00411430         push    ebp
 5     00411431         mov     ebp, esp
 6     00411433         sub     esp, 0cch
 7     00411439         push    ebx
 8     0041143a         push    esi
 9     0041143b         push    edi
10     0041143c         lea     edi, [ebp+ffffff34h]
11     00411442         mov     ecx, 33h
12     00411447         mov     eax, 0cccccccch
13     0041144c         rep     stos dword ptr es:[edi]
14 
15     int sum;
16     sum = x + y;
17     0041144e         mov     eax, dword ptr [ebp+8]
18     00411451         add     eax, dword ptr [ebp+0ch]
19 }
復制代碼

觀察上面的代碼可知,跳轉到Add函數之后,在執行第一條語句之前程序預先做了一連串的准備工作。先將ebp的值壓棧,然后將esp的值賦給ebp。在后面的語句中,獲取參數x是通過內存地址ebp+8,而獲取參數y則是通過內存地址ebp+12,與之前的結論相同。

 在函數的執行過程中可能還會使用到用戶定義的局部變量,如下面的代碼中就使用到了局部變量sum:

int Add(int x, int y) 
{
    int sum;
    sum = x + y;
}

在VS2008中反匯編得到與該函數中的賦值語句相對應的三條匯編語句如下:

1  0041144e         mov     eax, dword ptr [ebp+8]
2  00411451         add     eax, dword ptr [ebp+0ch]
3  00411454         mov     dword ptr [ebp-8], eax

在最后一條匯編語句中,將兩個參數相加得到的值保存在了地址為ebp-8的內存空間中。這是因為局部變量的特點與參數一樣,都是當函數調用完畢就不再使用,所以仿效參數將其分配在棧上。但是棧上方已經被參數和返回地址等使用,因此只能使用棧更低地址的空間,每分配一個局部變量都要進行一次壓棧。但是要注意的是,壓棧一次的話地址應該為ebp-4而非上面見到的ebp-8。事實上,在VC 6.0編譯器中反匯編得到的代碼如下:

1  00401038         mov     eax, dword ptr [ebp+8]
2  0040103b         add     eax, dword ptr [ebp+0ch]
3  0040103e         mov     dword ptr [ebp-4], eax

此處的局部變量地址確為ebp-4。造成這種不同的原因主要是在VS2008中為了防止溢出攻擊而采用的StackGaurd溢出攻擊防護機制,在ebp和局部變量的地址之間空出了四個字節。

 函數執行完畢就要返回調用的地方,由之前的敘述可知call指令已經將其后指令的地址壓棧保存,因此可以使用該地址進行返回。函數返回要用到返回指令ret,ret指令的介紹如下:

ret指令:將棧頂保存的地址彈入指令寄存器EIP,相當於"pop eip",從而讓程序跳轉到該地址。執行ret指令后,寄存器EIP(存儲了被彈出的棧頂地址)和ESP(在32位x86中加4)的值有變化


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM