今天下午寫篇博客吧,分析分析c語言中函數調用的本質,首先我們知道c語言中函數的本質就是一段代碼,但是給這段代碼起了一個名字,這個名字就是他的的這段代碼的開始地址
這也是函數名的本質,其實也就是匯編中的標號。下面我們會接觸到一些東西 比如 eip 就是我們常常說的程序計數器,還有ebp和esp (這里是倆個指針,記得我們以前學8086也就一個sp堆棧指針)分別為EBP是指向棧底的指針,在過程調用中不變,又稱為幀指針。ESP指向棧頂,程序執行時移動,ESP減小分配空間,ESP增大釋放空間,ESP又稱為棧指針。當然現在不理解沒關系(在堆棧中變量分布是從高地址到低地址分布)。
好了我們開始正式話題吧:
1.先看圖,下面我先貼出一個調用代碼。
# include <stdio.h>
int fun(int a, int b)
{
int c = 0;
c= a + b;
return c;
}
int main(void)
{
int a = 1;
int b = 3;
fun(a,b);
return 0;
}
反匯編后的代碼
--- 匯編代碼-----------------------------------------------------------------------
__CxxUnhandledExceptionFilter:
00A51113 jmp __CxxUnhandledExceptionFilter (0A525E0h)
___CxxSetUnhandledExceptionFilter:
00A51118 jmp __CxxSetUnhandledExceptionFilter (0A52660h)
_QueryPerformanceCounter@4:
00A5111D jmp _QueryPerformanceCounter@4 (0A53BB0h) 、
_fun: ;注意了 fun在這里
00A51122 jmp fun (0A513C0h) ;可以看出fun還要跳轉 這次跳到了0A513C0h
__unlock:
00A51127 jmp __unlock (0A536F4h)
_GetCurrentProcessId@0:
00A5112C jmp _GetCurrentProcessId@0 (0A53BB6h)
@_RTC_CheckStackVars2@12:
00A51131 jmp _RTC_CheckStackVars2 (0A51490h)
___set_app_type:
00A51136 jmp ___set_app_type (0A5269Eh)
--- 被調函數的真正地址 -----------------------------------------
1: # include <stdio.h>
2:
3: int fun(int a, int b)
4: {
00A513C0 push ebp ;壓棧 ebp 保護ebp
00A513C1 mov ebp,esp ;將現在的esp地址給ebp換句話說ebp現在指向了這里
;其實也就是棧幀的最下面
00A513C3 sub esp,0CCh
00A513C9 push ebx
00A513CA push esi
00A513CB push edi
00A513CC lea edi,[ebp-0CCh]
00A513D2 mov ecx,33h
00A513D7 mov eax,0CCCCCCCCh
00A513DC rep stos dword ptr es:[edi]
5: int c = 0;
00A513DE mov dword ptr [c],0
6: c= a + b;
00A513E5 mov eax,dword ptr [a]
00A513E8 add eax,dword ptr [b]
00A513EB mov dword ptr [c],eax
7: return c;
00A513EE mov eax,dword ptr [c]
8:
9: }
00A513F1 pop edi
00A513F2 pop esi
00A513F3 pop ebx
00A513F4 mov esp,ebp
00A513F6 pop ebp
00A513F7 ret
--- 主調函數 -----------------------------------------------------------------------
10: int main(void)
11: {
00A51A11 mov ebp,esp
00A51A13 sub esp,0D8h
00A51A19 push ebx
00A51A1A push esi
00A51A1B push edi
00A51A1C lea edi,[ebp-0D8h]
00A51A22 mov ecx,36h
00A51A27 mov eax,0CCCCCCCCh
00A51A2C rep stos dword ptr es:[edi]
12: int a = 1;
00A51A2E mov dword ptr [a],1 ;定義變量a
13: int b = 3;
00A51A35 mov dword ptr [b],3 ;定義變量b
14: fun(a,b);
00A51A3C mov eax,dword ptr [b] ;把變量b給eax
00A51A3F push eax ;eax壓棧 也就b壓棧
00A51A40 mov ecx,dword ptr [a] ;同上
00A51A43 push ecx
00A51A44 call _fun (0A51122h) ;匯編開始調用,在匯編中函數名前面加下划線當標號處理
;地址是0A51122h,現在我們去哪里
00A51A49 add esp,8
15:
16:
17: return 0;
00A51A4C xor eax,eax
18: }
00A51A4E pop edi
00A51A4F pop esi
00A51A50 pop ebx
00A51A51 add esp,0D8h
00A51A57 cmp ebp,esp
00A51A59 call __RTC_CheckEsp (0A5113Bh)
00A51A5E mov esp,ebp
00A51A60 pop ebp
00A51A61 ret
--- 無源文件 -----------------------------------------------------------------------
2.是不是看上面的已經懵逼了,沒關系了,我來介紹一下
上面我是在vs中進行了反匯編,原本准備gcc下搞,后來懶得折騰了。先講一下函數調用的過程,函數調用的時候其實也就是匯編中的地址的跳轉,匯編中的跳轉源於標號地址。其實這個也好理解,不知道地址,你讓我如何找你。但是在找的開始,我們需要先記錄一下回家地址,當前的一些寄存器狀態(這是因為調用到里面也可能用到這些寄存器)注意還要壓入一些函數調用參數。來張圖我們看看

我們從上面的圖可以看到,函數調用的時候依次壓棧從右到左。壓棧完畢調用call。call的作用有倆個,就是壓棧返回值,然后修改程序計數器eip,實現程序跳轉到被調函數。接着壓棧ebp里面的內容(是什么我們先不講)然后將esp賦值給ebp。也就是ebp里面的內容被改變,變為現在的esp內容,esp不就是棧頂,也就是說現在都指向了棧頂,然后壓棧結束了或者可能換有一些其他的參數,比如我們遞歸調用,那下面就是下一個函數的參數,返回地址等等等。現在我們討論的是ebp的作用是什么:那就是ebp指向了一個堆棧中一個棧幀的底部。而esp指向了頂部。我們可以利用ebp的偏移實現,局部變量和參數的訪問。下面我們要討論的就是如何返回。其實就是參數依次出棧,最后老ebp彈出到現在ebp。ebp指后到上一次的棧幀底部。但我們問一下參數是如何出棧的,難道是彈出,嗎?彈出還有什么用,因為局部變量用完后就沒用了呀,也沒必要彈出給寄存器,其實是ebp將值賦給esp,esp由以前的棧頂指向棧底也就是ebp的地方。然后老ebp彈出到ebp。ebp歸為到以前的ebp。esp再減4。esp回到返回地址處,然后在修改eip返回。然后esp再減4,回到新的棧頂。而返回的指令源於ret。
其實這個過程也不是很難,就是繁瑣。需要對着棧圖分析。需要理解局部變量的拋棄源於ebp對esp的修改。
