函式呼叫底層實現 zz

2022-01-25 03:55:18 字數 4504 閱讀 9140

int fun(int a, int b) 

int main()

之後,最關鍵的是在專案設定裡關閉優化功能。也就是把project->setting->c/c++->optimizations選為disabled。編譯器的優化在分析底層實現時大多數情況不太受歡迎。按鍵盤上的f10鍵,進入單步除錯模式(step over)。看到你的main函式左側有個黃色的小箭頭了嗎?那個就是程式即將執行的語句。按alt + 8。開啟反編譯視窗,看到彙編語句了嗎?是不是想這個樣子
==> 00401078   push        1100h

0040107d push 8899h

00401082 call @ilt+5(fun) (0040100a)

00401087 add esp,8

看到兩個push指令了嗎?再看看後面的數字,不正是我們要傳遞的引數嗎。奇怪阿?我們明明是先傳遞的0x8899怎麼反倒先push 1100h呢?呵呵,這個現象就叫calling conversion。究竟是何方神聖,我在後面會詳細的給你解釋的。先別著急。隨後的call指令的作用就是開始呼叫函式了。接下來關掉反彙編視窗,在源**視窗按f11(step into)進入函式體。當看到那個黃色的小箭頭指向函式名的時候再調出反彙編視窗(alt+8)。你會看到類似下面的**:
1:    int fun(int a, int b) 

0040102c pop edi

0040102d pop esi

0040102e pop ebx

0040102f mov esp,ebp

00401031 pop ebp

00401032 ret

vc++就是好,還在難懂的彙編語句前加入了c語言的源**。不過同時也有不少我們不需要的**。因此,你只需要關心紅色的部分就可以了。奇怪阿?不是引數都用push傳遞了嗎?怎麼沒看到被pop出來?問題其實是這樣,當你呼叫call進入函式的時候call揹著你做了一件事。call把它下一條語句的位址push進了堆疊。(旁人: 什麼!這是為什麼?)原因很簡單,因為函式呼叫完了,要用ret返回。而ret怎麼知道返回**呢?對了, ret指令pop了call指令push給他的位址(搞清楚這個關係哦),然後返回到了這個位址。call和ret配合的如此絕妙,乙個push乙個pop肯定不會讓堆疊不平衡的(老外叫no stack unwinding)。現在明白了,如果你來個pop eax,那eax裡面是什麼?當然是ret要用的返回位址了。好啦,你要是pop eax就等於搶了ret要用的東西了。不論曾程式流程和道德標準上你做的都不對 :-p。可是怎麼在函式體裡使用引數呢?問題其實並不難,既然引數在堆疊裡我們就可以使用esp(堆疊指標)來訪問了。不過,我相信你也想到了。esp是個經常變化的值。一旦,函式裡出現pop或push他就會變化。這樣很不容易定位引數的於記憶體中的位置。因此,我們需要乙個不會變化的東西作為訪問引數的基準。看看函式體的開頭部分:
00401000   push        ebp

00401001 mov ebp,esp

先用push ebp儲存了原來ebp的值再把esp的值給ebp。原來ebp就是用來做基準的。也難怪他被稱為ebp(base pointer)。很自然ret返回前的pop ebp就是恢復原來ebp的數值嘍。當然一定要恢復,因為函式裡也可以呼叫函式嘛。每個函式都用ebp,自然要保證使用完後完璧歸趙了。現在當函式執行到 mov ebp, esp後堆疊應該變成這個樣子了。
/-------------------\  higher address

| 引數2: 0x1100h |

+-----------------+

| 引數1: 0x8899h |

+-----------------+

| 函式返回位址 |

| 0x00401087 |

+-----------------+

| ebp |

\-------------------/ lower address <== stack pointer

& ebp all point to here, now

由於我們在vc++上使用的int型別是乙個32位型別,ebp和函式返回值也是32位的。因此每個量要占去4個位元組。另外還需要注意堆疊的擴充套件方向是高位址到低位址。有了這些指示。我們就可以分析出,第乙個引數的位址是ebp + 08h,第二個引數就是ebp + 0ch。看看反彙編的**:
2:       a = 0x4455;

00401018 mov dword ptr [ebp+8],4455h

3: b = 0x6677;

0040101f mov dword ptr [ebp+0ch],6677h

與我們的計算吻合。之後呢:
00401031   pop         ebp

00401032 ret

將ebp原來的數值完璧歸趙,呼叫ret指令,ret指令pop出返回位址,之後返回到呼叫函式的call指令的下一條語句。ret之後,堆疊應該變成這個樣子了
/-------------------\  higher address

| 引數2: 0x1100h |

+-----------------+

| 引數1: 0x8899h |

\-------------------/ lower address <== stack pointer

哈哈,問題出現了,再函式返回後堆疊出現了不平衡的情況(stack unwinding)。怎麼辦呢?好辦啊,直接 pop cx pop cx 把堆疊平衡過來就好了。幸好我們只有兩個引數,要是有20個的話,那就要有20個pop cx。不說影響美觀,程式效率也會很低。所以vc++使用了這個辦法解決問題:
00401082   call        @ilt+5(fun) (0040100a)

00401087 add esp,8

看紅色的語句,直接將esp的值加8,讓堆疊變成
/-------------------\  higher address <== stack pointer

| 引數2: 0x1100h |

+-----------------+

| 引數1: 0x8899h |

\-------------------/ lower address

通過改變esp從根本上解決了stack unwinding。(push,pop指令本質上不就是通過改變esp來實現堆疊平衡的嗎) 現在,明白了函式如何傳遞引數,如何呼叫,如何返回。下乙個問題就是看看函式如何傳遞返回值了。相信你早就注意到了
4:       return a + b;

00401026 mov eax,dword ptr [ebp+8]

00401029 add eax,dword ptr [ebp+0ch]

可見,函式正式用eax暫存器來儲存返回值的。如果你想使用函式的返回值,那麼一定要在函式一返回就把eax暫存器的值讀出來。至於為什麼不用ebx,ecx...,這個雖然沒有規定,但是習慣上大家都是用eax的。而且windows程式中也明確指出了,函式的返回值必須放入eax內。 ok,現在來解決什麼是calling conversion這個歷史遺留問題。如果認真思考過,你一定想函式的引數為什麼偏用堆疊轉遞呢,暫存器不也可以傳遞嗎?而且很快阿。引數的傳遞順序不一定要是由後到前的,從前到後傳遞也不會出現任何問題啊?再有為什麼一定要等到函式返回了再處理堆疊平衡的問題呢,能否在函式返回前就讓堆疊平衡呢?所有上述提議都是絕對可行的,而他們之間不同的組合就造就了函式不同的呼叫方法。也就是你常看到或聽到的stdcall,pascal,fastcall,winapi,cdecl等等。這些不同的處理函式呼叫方式就叫做calling convention。預設情況下c語言使用的是cdecl方式,也就是上面提到的。引數由右到左進棧,呼叫函式者處理堆疊平衡。如果你在我們剛才的程式中fun函式前加入__stdcall,再來用上面的方法分析一下。8: fun(0x8899,0x1100);

00401058 push 1100h ; <== 引數仍然是由右到左傳遞的

0040105d push 8899h

00401062 call fun (00401000)

;<== 這裡沒有了 add esp, 08h

1: int __stdcall fun(int a, int b)

0040102c pop edi

0040102d pop esi

0040102e pop ebx

0040102f mov esp,ebp

00401031 pop ebp

00401032 ret 8; <== ret 取出返回位址後,

; 給esp加上 8。看!堆疊平衡在函式內完成了。

; ret指令這個語法設計就是專門用來實現函式

; 內完成堆疊平衡的

函式呼叫的底層機制

int fun int a,int b int main 之後,最關鍵的是在專案設定裡關閉優化功能。也就是把project setting c c optimizations選為disabled。編譯器的優化在分析底層實現時大多數情況不太受歡迎。按鍵盤上的f10鍵,進入單步除錯模式 step ove...

函式呼叫的底層機制

int fun int a,int b int main 之後,最關鍵的是在專案設定裡關閉優化功能。也就是把project setting c c optimizations選為disabled。編譯器的優化在分析底層實現時大多數情況不太受歡迎。按鍵盤上的f10鍵,進入單步除錯模式 step ove...

zz 系統呼叫和庫函式

1.11 系統呼叫和庫函式 所有作業系統都提供多種服務的入口點,由此程式向系統核請求服務。各種版本的unix都 提供經良好定義的有限數目的入口點,經過這些入口點進入系統核,這些入口點被稱之為 系統呼叫 system call 系統呼叫是我們不能更改的一種unix特徵。unix版本7提供了約 50個系...