找回密碼
 立即注冊(cè)

QQ登錄

只需一步,快速開始

搜索
查看: 2257|回復(fù): 0
打印 上一主題 下一主題
收起左側(cè)

C語(yǔ)言函數(shù)調(diào)用的底層機(jī)制

[復(fù)制鏈接]
跳轉(zhuǎn)到指定樓層
樓主
ID:107189 發(fā)表于 2016-3-6 00:13 | 只看該作者 回帖獎(jiǎng)勵(lì) |倒序?yàn)g覽 |閱讀模式
這是一篇介紹C語(yǔ)言中的函數(shù)調(diào)用是如何用實(shí)現(xiàn)的文章。寫給那些對(duì)C語(yǔ)言各種行為的底層實(shí)現(xiàn)感興趣人的入門級(jí)文章。如果你是C語(yǔ)言或者匯編、底層技術(shù)
的老鳥或是對(duì)這個(gè)問題不感興趣,那么這篇文章只會(huì)耽誤您的時(shí)間,您大可不必閱讀他。當(dāng)然如果前輩們?cè)敢鉃槲抑赋霾蛔,我將十分感謝您的指導(dǎo),并對(duì)耽誤您寶
貴的時(shí)間致歉。
好了,廢話少說!要研究這個(gè)問題,讓我們先打開VC++吧。最好是6.0的,:-P。(什么你沒有VC++,倒!....趕快裝一個(gè)!@#$,要快!)
首先,讓我們?cè)赩C++里建立一個(gè)Win32 Console Application項(xiàng)目,并建立主文件fun.c。并輸入以下內(nèi)容。
int fun(int a, int b) {
   a = 0x4455;
   b = 0x6677;
   return a + b;
}

int main() {
    fun(0x8899,0x1100);
    return 0;
}

后,最關(guān)鍵的是在項(xiàng)目設(shè)置里關(guān)閉優(yōu)化功能。也就是把Project->Setting->C/C++->Optimizations選
為Disabled。編譯器的優(yōu)化在分析底層實(shí)現(xiàn)時(shí)大多數(shù)情況不太受歡迎。 按鍵盤上的F10鍵,進(jìn)入單步調(diào)試模式(Step

Over)?吹侥愕膍ain函數(shù)左側(cè)有個(gè)黃色的小箭頭了嗎?那個(gè)就是程序即將執(zhí)行的語(yǔ)句。按Alt +

8。打開反編譯窗口,看到匯編語(yǔ)句了嗎?是不是想這個(gè)樣子
==> 00401078   push        1100h
    0040107D   push        8899h
    00401082   call        @ILT+5(fun) (0040100a)
    00401087   add         esp,8

到兩個(gè)PUSH指令了嗎?再看看后面的數(shù)字,不正是我們要傳遞的參數(shù)嗎。奇怪阿?我們明明是先傳遞的0x8899怎么反倒先push

1100h呢?呵呵,這個(gè)現(xiàn)象就叫Calling

conversion。究竟是何方神圣,我在后面會(huì)詳細(xì)的給你解釋的。先別著急。隨后的Call指令的作用就是開始調(diào)用函數(shù)了。
接下來關(guān)掉反匯編窗口,在源代碼窗口按F11(Step

Into)進(jìn)入函數(shù)體。當(dāng)看到那個(gè)黃色的小箭頭指向函數(shù)名的時(shí)候再調(diào)出反匯編窗口(Alt+8)。你會(huì)看到類似下面的代碼:
1:    int fun(int a, int b) {
00401000   push        ebp
00401001   mov         ebp,esp

00401003   sub         esp,40h
00401006   push        ebx
00401007   push        esi
00401008   push        edi
00401009   lea         edi,[ebp-40h]
0040100C   mov         ecx,10h
00401011   mov         eax,0CCCCCCCCh
00401016   rep stos    dword ptr [edi]
2:       a = 0x4455;
00401018   mov         dword ptr [ebp+8],4455h
3:       b = 0x6677;
0040101F   mov         dword ptr [ebp+0Ch],6677h
4:       return a + b;
00401026   mov         eax,dword ptr [ebp+8]
00401029   add         eax,dword ptr [ebp+0Ch]


5:    }
0040102C   pop         edi
0040102D   pop         esi
0040102E   pop         ebx
0040102F   mov         esp,ebp
00401031   pop         ebp
00401032   ret
VC++就是好,還在難懂的匯編語(yǔ)句前加入了C語(yǔ)言的源代碼。不過同時(shí)也有不少我們不需要的代碼。因此,你只需要關(guān)心紅色的部分就可以了。
奇怪阿?不是參數(shù)都用push傳遞了嗎?怎么沒看到被pop出來?問題其實(shí)是這樣,當(dāng)你調(diào)用Call進(jìn)入函數(shù)的時(shí)候Call背著你做了一件事。call把
它下一條語(yǔ)句的地址push進(jìn)了堆棧。(旁人:
什么!這是為什么?)原因很簡(jiǎn)單,因?yàn)楹瘮?shù)調(diào)用完了,要用ret返回。而ret怎么知道返回哪里呢?對(duì)了,

ret指令pop了call指令push給他的地址(搞清楚這個(gè)關(guān)系哦),然后返回到了這個(gè)地址。call和ret配合的如此絕妙,一個(gè)PUSH一個(gè)

POP肯定不會(huì)讓堆棧不平衡的(老外叫no stack unwinding),F(xiàn)在明白了,如果你來個(gè)pop

eax,那eax里面是什么?當(dāng)然是ret要用的返回地址了。好啦,你要是pop

eax就等于搶了ret要用的東西了。不論曾程序流程和道德標(biāo)準(zhǔn)上你做的都不對(duì) :-P。
可是怎么在函數(shù)體里使用參數(shù)呢?問題其實(shí)并不難,既然參數(shù)在堆棧里我們就可以使用esp(堆棧指針)來訪問了。不過,我相信你也想到了。esp是個(gè)經(jīng)常變
化的值。一旦,函數(shù)里出現(xiàn)pop或push他就會(huì)變化。這樣很不容易定位參數(shù)的于內(nèi)存中的位置。因此,我們需要一個(gè)不會(huì)變化的東西作為訪問參數(shù)的基準(zhǔn)。看
看函數(shù)體的開頭部分:
00401000   push        ebp
00401001   mov         ebp,esp

用push ebp保存了原來ebp的值再把esp的值給ebp。原來ebp就是用來做基準(zhǔn)的。也難怪他被稱為ebp(Base

Pointer)。很自然ret返回前的pop

ebp就是恢復(fù)原來ebp的數(shù)值嘍。當(dāng)然一定要恢復(fù),因?yàn)楹瘮?shù)里也可以調(diào)用函數(shù)嘛。每個(gè)函數(shù)都用ebp,自然要保證使用完后完璧歸趙了。現(xiàn)在當(dāng)函數(shù)執(zhí)行到

mov ebp, esp后堆棧應(yīng)該變成這個(gè)樣子了。
/-------------------\  Higher Address
| 參數(shù)2:  0x1100h |  
+-----------------+
| 參數(shù)1:  0x8899h |
+-----------------+
|   函數(shù)返回地址  |
|    0x00401087   |
+-----------------+
|       ebp       |
\-------------------/   Lower Address <== stack pointer
& ebp all point to here, now

于我們?cè)赩C++上使用的int類型是一個(gè)32位類型,ebp和函數(shù)返回值也是32位的。因此每個(gè)量要占去4個(gè)字節(jié)。另外還需要注意堆棧的擴(kuò)展方向是高地
址到低地址。有了這些指示。我們就可以分析出,第一個(gè)參數(shù)的地址是ebp + 08h,第二個(gè)參數(shù)就是ebp + 0ch?纯捶磪R編的代碼:
2:       a = 0x4455;
00401018   mov         dword ptr [ebp+8],4455h
3:       b = 0x6677;
0040101F   mov         dword ptr [ebp+0Ch],6677h
與我們的計(jì)算吻合。之后呢: 00401031   pop         ebp
00401032   ret
將ebp原來的數(shù)值完璧歸趙,調(diào)用ret指令,ret指令pop出返回地址,之后返回到調(diào)用函數(shù)的call指令的下一條語(yǔ)句。ret之后,堆棧應(yīng)該變成這個(gè)樣子了 /-------------------\  Higher Address
| 參數(shù)2:  0x1100h |  
+-----------------+
| 參數(shù)1:  0x8899h |
\-------------------/   Lower Address  <== stack pointer

哈,問題出現(xiàn)了,再函數(shù)返回后堆棧出現(xiàn)了不平衡的情況(Stack Unwinding)。怎么辦呢?好辦啊,直接 pop cx pop cx
把堆棧平衡過來就好了。幸好我們只有兩個(gè)參數(shù),要是有20個(gè)的話,那就要有20個(gè)pop

cx。不說影響美觀,程序效率也會(huì)很低。所以VC++使用了這個(gè)辦法解決問題:
00401082   call        @ILT+5(fun) (0040100a)
00401087   add         esp,8
看紅色的語(yǔ)句,直接將esp的值加8,讓堆棧變成 /-------------------\  Higher Address <== stack pointer
| 參數(shù)2:  0x1100h |  
+-----------------+
| 參數(shù)1:  0x8899h |
\-------------------/   Lower Address
通過改變esp從根本上解決了Stack unwinding。(push,pop指令本質(zhì)上不就是通過改變esp來實(shí)現(xiàn)堆棧平衡的嗎) 現(xiàn)在,明白了函數(shù)如何傳遞參數(shù),如何調(diào)用,如何返回。下一個(gè)問題就是看看函數(shù)如何傳遞返回值了。相信你早就注意到了 4:       return a + b;
00401026   mov         eax,dword ptr [ebp+8]
00401029   add         eax,dword ptr [ebp+0Ch]

見,函數(shù)正式用eax寄存器來保存返回值的。如果你想使用函數(shù)的返回值,那么一定要在函數(shù)一返回就把eax寄存器的值讀出來。至于為什么不用ebx,

ecx...,這個(gè)雖然沒有規(guī)定,但是習(xí)慣上大家都是用eax的。而且windows程序中也明確指出了,函數(shù)的返回值必須放入eax內(nèi)。

OK,現(xiàn)在來解決什么是calling

conversion這個(gè)歷史遺留問題。如果認(rèn)真思考過,你一定想函數(shù)的參數(shù)為什么偏用堆棧轉(zhuǎn)遞呢,寄存器不也可以傳遞嗎?而且很快阿。參數(shù)的傳遞順序不
一定要是由后到前的,從前到后傳遞也不會(huì)出現(xiàn)任何問題?再有為什么一定要等到函數(shù)返回了再處理堆棧平衡的問題呢,能否在函數(shù)返回前就讓堆棧平衡呢?
所有上述提議都是絕對(duì)可行的,而他們之間不同的組合就造就了函數(shù)不同的調(diào)用方法。也就是你常看到或聽到的stdcall,pascal,

fastcall,WINAPI,cdecl等等。這些不同的處理函數(shù)調(diào)用方式就叫做calling convention。
默認(rèn)情況下C語(yǔ)言使用的是cdecl方式,也就是上面提到的。參數(shù)由右到左進(jìn)棧,調(diào)用函數(shù)者處理堆棧平衡。如果你在我們剛才的程序中fun函數(shù)前加入

__stdcall,再來用上面的方法分析一下。
8:        fun(0x8899,0x1100);
00401058   push        1100h  ; <== 參數(shù)仍然是由右到左傳遞的
0040105D   push        8899h   
00401062   call        fun (00401000)
;<== 這里沒有了 add esp, 08h

1:    int __stdcall fun(int a, int b) {
00401000   push        ebp
00401001   mov         ebp,esp
00401003   sub         esp,40h
00401006   push        ebx
00401007   push        esi
00401008   push        edi
00401009   lea         edi,[ebp-40h]
0040100C   mov         ecx,10h
00401011   mov         eax,0CCCCCCCCh
00401016   rep stos    dword ptr [edi]
2:       a = 0x4455;
00401018   mov         dword ptr [ebp+8],4455h
3:       b = 0x6677;
0040101F   mov         dword ptr [ebp+0Ch],6677h
4:       return a + b;
00401026   mov         eax,dword ptr [ebp+8]
00401029   add         eax,dword ptr [ebp+0Ch]
5:    }
0040102C   pop         edi
0040102D   pop         esi
0040102E   pop         ebx
0040102F   mov         esp,ebp
00401031   pop         ebp
00401032   ret         8; <== ret 取出返回地址后,
                       ; 給esp加上 8?!堆棧平衡在函數(shù)內(nèi)完成了。
                       ; ret指令這個(gè)語(yǔ)法設(shè)計(jì)就是專門用來實(shí)現(xiàn)函數(shù)
                       ; 內(nèi)完成堆棧平衡的

是得出結(jié)論,stdcall是由右到左傳遞參數(shù),被調(diào)用函數(shù)恢復(fù)堆棧的calling convention. 其他幾種calling

convention的修飾關(guān)鍵詞分別是__pascal,__fastcall,

WINAPI(這個(gè)要包含windows.h才可以用)。現(xiàn)在,你可以用上面說的方法自己分析一下他們各自的特點(diǎn)了。

分享到:  QQ好友和群QQ好友和群 QQ空間QQ空間 騰訊微博騰訊微博 騰訊朋友騰訊朋友
收藏收藏 分享淘帖 頂 踩
回復(fù)

使用道具 舉報(bào)

本版積分規(guī)則

小黑屋|51黑電子論壇 |51黑電子論壇6群 QQ 管理員QQ:125739409;技術(shù)交流QQ群281945664

Powered by 單片機(jī)教程網(wǎng)

快速回復(fù) 返回頂部 返回列表