轉載: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)的值有變化