[讀書筆記]C語言函數調用過程

*** 本文是《老碼識途》第一章的讀書筆記 ***html

函數調用

例子代碼以下所示:函數

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

void main() {
    int z;
    z = Add(1, 2);
    printf("z=%d\n", z);
}

下面分析一下 Add函數的調用過程。佈局

首先斷點在z = Add(1, 2);處, 反彙編以下所示:3d

int z;
    z = Add(1, 2);
002C141E 6A 02                push        2  
002C1420 6A 01                push        1  
002C1422 E8 60 FC FF FF       call        002C1087  
002C1427 83 C4 08             add         esp,8  
002C142A 89 45 F8             mov         dword ptr [ebp-8],eax

首先壓入參數1和2:指針

002C141E 6A 02                push        2  
002C1420 6A 01                push        1

經過觀察ESP能夠看到參數從右到左依次入棧,ESP往低內存方向移動8字節:code

ESP=0025FCCC
...
0x0025FCAA  00 00 78 4c 33 00 bc fc 25 00 a9 fe aa 0f 78 4c 33 00 c8 fc 25 00 3d 5a b2 0f *** 01 00 00 00 02 00 00 00 ***
0x0025FCCC  00 00 00 00

而後執行:htm

002C1422 E8 60 FC FF FF       call        002C1087

call指令執行時,首先壓入call指令的返回地址,即add esp,8這一句的地址002C1427:blog

0x0025FCAA  00 00 78 4c 33 00 bc fc 25 00 a9 fe aa 0f 78 4c 33 00 c8 fc 25 00 *** 27 14 2c 00 *** 01 00 00 00 02 00 00 00

而後跳轉到02C1087。02C1087處爲jmp語句,跳轉到Add函數入口地址002C13C0:ip

int Add(int x, int y) {
002C13C0 55                   push        ebp  
002C13C1 8B EC                mov         ebp,esp  
002C13C3 81 EC CC 00 00 00    sub         esp,0CCh  
002C13C9 53                   push        ebx  
002C13CA 56                   push        esi  
002C13CB 57                   push        edi  
002C13CC 8D BD 34 FF FF FF    lea         edi,[ebp+FFFFFF34h]  
002C13D2 B9 33 00 00 00       mov         ecx,33h  
002C13D7 B8 CC CC CC CC       mov         eax,0CCCCCCCCh  
002C13DC F3 AB                rep stos    dword ptr es:[edi]  
    int sum;
    sum = x + y;
002C13DE 8B 45 08             mov         eax,dword ptr [ebp+8]  
002C13E1 03 45 0C             add         eax,dword ptr [ebp+0Ch]  
002C13E4 89 45 F8             mov         dword ptr [ebp-8],eax  
    return sum;
002C13E7 8B 45 F8             mov         eax,dword ptr [ebp-8]  
}
002C13EA 5F                   pop         edi  
002C13EB 5E                   pop         esi  
002C13EC 5B                   pop         ebx  
002C13ED 8B E5                mov         esp,ebp  
002C13EF 5D                   pop         ebp  
002C13F0 C3                   ret

獲取參數

目前爲止,棧上的狀況以下圖所示,從上往下內存地址從高到低:內存

+----------------+
          |       2        |
          +----------------+
          |       1        |
          +----------------+
ESP       | return address |
 +------> +----------------+

此時參數可由ESP + 4,ESP + 8得到。可是因爲程序執行時ESP會變化,爲了方便定位棧上的數據,引入EBP(Extended Base Pointer,擴展基址指針寄存器),保存進入函數時ESP的值。
因爲函數能夠嵌套調用,因此在進入函數時必須將EBP的舊值保存起來,以防覆蓋EBP致使函數返回後沒法恢復EBP。這裏經過將EBP壓入棧來保存舊值。如:

002C13C0 55                   push        ebp  
002C13C1 8B EC                mov         ebp,esp  
...
002C13EF 5D                   pop         ebp

因此在函數開頭有以下代碼:

int Add(int x, int y) {
002C13C0 55                   push        ebp  
002C13C1 8B EC                mov         ebp,esp

此時棧上的內存佈局以下圖所示:

+----------------+
          |       2        |
          +----------------+
          |       1        |
          +----------------+
          | return address |
          +----------------+
ESP       |     ebp        |
 +------> +----------------+

取出1,2參數的代碼以下所示:

int sum;
    sum = x + y;
002C13DE 8B 45 08             mov         eax,dword ptr [ebp+8]  
002C13E1 03 45 0C             add         eax,dword ptr [ebp+0Ch]

其中ebp+8取出參數1,ebp+0Ch取出參數2(0C爲十進制的12),而後計算結果放在EAX中。

初始化堆棧和分配局部變量

接下來有以下代碼段,將esp下移0CC,而後push ebx,esi,edi這三個寄存器:

002C13C3 81 EC CC 00 00 00    sub         esp,0CCh  // 1
002C13C9 53                   push        ebx  
002C13CA 56                   push        esi  
002C13CB 57                   push        edi

其中語句1是爲了給局部變量分配足夠大的棧空間,而後再保存三個寄存器的值。局部變量利用ebp定位,存於ebp和OCCh之間。

ebx,esi,edi的做用以下所示來源

寄存器%ebx、%esi和%edi爲被調函數保存寄存器(callee-saved registers),即被調函數在覆蓋這些寄存器的值時,必須先將寄存器原值壓入棧中保存起來,並在函數返回前從棧中恢復其原值,由於主調函數可能也在使用這些寄存器。

而後運行:

002C13CC 8D BD 34 FF FF FF    lea         edi,[ebp+FFFFFF34h]  
002C13D2 B9 33 00 00 00       mov         ecx,33h  
002C13D7 B8 CC CC CC CC       mov         eax,0CCCCCCCCh  
002C13DC F3 AB                rep stos    dword ptr es:[edi]

首先,經過lea指令,將ebp+FFFFFF34h地址(棧幀的底部)寫入edi。而後設置ecx和eax,最後運行rep stos語句。

rep stos dword ptr es:[edi]語句的意思是:將棧上從ebp+FFFFFF34h開始的位置向高地址方向的內存賦值eax(0xCCCCCCCC),次數重複ecx(0x33, 51)次。每運行一次edi的值會增長。注意0xCCCCCCCC表明着未被初始化(int3中斷)。這樣作的緣由是防止分配好的局部變量空間中的代碼被意外執行。

局部變量

示例代碼中,x+y的結果保存在局部變量sum中,由以下代碼可知,sum分配在棧上。

int sum;
    sum = x + y;
002C13DE 8B 45 08             mov         eax,dword ptr [ebp+8]  
002C13E1 03 45 0C             add         eax,dword ptr [ebp+0Ch]
002C13E4 89 45 F8             mov         dword ptr [ebp-8],eax   <<==== 結果保存在局部變量sum中

目前棧的分配狀況以下所示:

+----------------+                
          |       2        |                
          +----------------+                
          |       1        |                
          +----------------+                
          | return address |                
          +----------------+                
          |      ebp       |                
          +----------------+                
          |       ?        |                
          +----------------+               
ESP       |      sum       |                
 +------> +----------------+

「?」處的4字節是編譯器爲了防止溢出攻擊而設置的。

返回值

函數返回處的代碼以下:

002C13EA 5F                   pop         edi  
002C13EB 5E                   pop         esi  
002C13EC 5B                   pop         ebx  
002C13ED 8B E5                mov         esp,ebp  
002C13EF 5D                   pop         ebp  
002C13F0 C3                   ret

函數返回時須要考慮兩件事情:恢復棧和保存返回值。

恢復棧

首先,經過pop棧恢復edi,esi和ebx的值,而後將esp恢復到ebp處,而後pop ebp,將ebp恢復舊值。此時esp指向return address:

+----------------+                
          |       2        |                
          +----------------+                
          |       1        |                
          +----------------+                
 ESP      | return address |                
  +------>+----------------+                
          |      ebp       |                
          +----------------+                
          |       ?        |                
          +----------------+               
          |      sum       |                
          +----------------+

接下來運行ret指令。ret指令會將棧頂保存的地址壓入指令寄存器EIP,至關於pop eip。運行後EIP和ESP都會有變化。

而後程序跳轉到return address處,以下所示:

int z;
    z = Add(1, 2);
002C141E 6A 02                push        2  
002C1420 6A 01                push        1  
002C1422 E8 60 FC FF FF       call        002C1087  
002C1427 83 C4 08             add         esp,8   // ret跳轉到此處
002C142A 89 45 F8             mov         dword ptr [ebp-8],eax

其中add esp,8語句的目的是將1,2參數出棧,將棧恢復到函數調用以前的狀態。接下來即可以從eax中取出返回值:

002C142A 89 45 F8             mov         dword ptr [ebp-8],eax

因爲ebp已被恢復,故其中ebp-8即爲臨時變量z的地址

相關文章
相關標籤/搜索