函數調用本質

函數調用的本質

從反彙編角度窺探平時開發調用的函數或者方法的本質。平時咱們編寫的高級語言最終經過編譯器、連接生成機CPU執行的機器指令。 不一樣的CPU對應着不一樣着機器指令,而且每一條機器指令對應着一條彙編。linux

先看一個最簡單的C語言函數,這裏主要經過C++來反編譯分析彙編指令。windows

<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207104946022-295278539.png" width=600/>函數

能夠經過反彙編看到調用func函數的彙編指令,當前環境是8086彙編。spa

<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207105149822-1871992655.png" width=600/>指針

經過最終的彙編指令能夠看出,在執行調用一個函數:本質就是經過call指令調用函數在代碼段的地址進行直接調用。code

注意:在上面的彙編指令能夠看到當函數執行完畢,執行ret彙編指令退出函數。其實一個完整的函數調用一定包含call和ret指令。blog

那麼只有瞭解了call和ret才能完全從最根本瞭解函數的調用過程。內存

call 標號
1.將下一條指令的偏移地址入棧
2.轉到標號出執行指令
ret
將棧頂的值出棧,賦值給IP

下面經過彙編代碼調用printf函數標號打印HelloWorld執行驗證上面的結論。作用域

<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207105343691-1758314410.png" width=600/>開發

<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207111011221-68861063.png" width=600/>

在即將執行執行printf函數以前棧頂指針SP指向內存單元的數據。

<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207105447268-321117954.png" width=600/>

上面說到執行函數前會將下一條指令的偏移地址入棧,上圖能夠看出的下一條CPU執行的指令偏移地址IP爲:000D。開始執行,看下棧頂指針SP的指向和指向內存單元的數據。

<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207105522887-757981176.png" width=600/>

函數printf執行完畢後,執行ret指令,棧頂偏移地址出棧賦值給IP中,棧頂指針向上移動兩個字節。

<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207105637159-2025675130.png" width=600/>

無論什麼開發語言最終都會轉成二進制彙編指令,對應着相應的彙編指令,本質都是一致的。這裏是經過C++反彙編窺探函數調用本質。

上述介紹只是最簡單函數調用,一說到函數首先就會想到函數的三要素,函數的返回值、函數的參數、局部變量。窺探下函數返回值的實現。

若是調用函數想拿到函數返回值,就得有容器來存放返回值,咱們能夠想到用棧、數據區、寄存器來保存。

首先棧段不能夠的,以下圖,函數內部push返回值,棧頂存儲的是CPU函數執行完畢後的IP的偏移地址。

<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207105750295-560968837.png" width=600/>

能夠考慮將返回值放入數據段,這個須要與調用者約好協議,好比越好將返回值放在ds:[0]

<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207110133049-1764170239.png" width=600/>

這樣側面證實了數據段裏的數據是全局,全局區的數據是做用域是全局的。上面的實例代碼比如下面的C++代碼。

<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207110214345-1433373661.png" width=600/>

在實際中,大多數平臺,windows、linux、Android等一般的作法是將方法返回值放在寄存器ax。其實這樣的效率比上面返回值放在全局區效率高,CPU從寄存器中讀取數據要快,放在全局區須要從內存先讀取到寄存器。

<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207110307278-1974910310.png" width=600/>

下面在X86環境下寫一段代碼看下彙編指令

<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207110343526-394389412.png" width=600/>

對於函數的返回值本質清楚以後,接下來看函數的第二個要素-函數的形參。

一樣咱們先考慮將參數放入數據段來實現一個求和的函數。

<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207110427514-209968254.png" width=600/>

放在數據段是能夠的,在咱們概念中形參的做用因而數據函數內部,函數執行完畢形參所佔用的內存空間會被回收。這樣就很明顯了,一般,形參是放在棧中的。

<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207110509159-902437971.png" width=600/>

注意:在函數調用完畢後,必定要保證棧平衡,否者會致使棧的空間會被用完,一般保持棧平衡有兩種方式:內平棧和外平棧。

上面的案例是使用了外平棧方式,也就是在函數調用完畢後,對棧頂指針進行回覆到函數調用前的位置。

<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207110541461-2041452995.png" width=600/>

對於函數的封裝性跟人覺的棧內平衡的方式會好一些,讓函數調用者不用關心內部細節。函數的形參本質瞭解後,接下來窺探最後一個函數的局部變量本質,這個相對複雜一些。

函數的內部須要定義局部變量,C語言特別簡單,那麼在彙編中怎麼分配內存空間給局部變量呢,局部變量的做用域只是當前函數,函數執行完畢後局部所棧中的空間被回收,所以局部變量空間分配仍是經過棧來實現。

<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207110620935-537749251.png" width=600/>

上面開始沒有問題,惟一缺陷是在函數內部調用函數時,因爲咱們沒有對bp進行恢復,一旦對函數內部在調用函數就會存存在問題, 所以須要對bp進行記錄和恢復。

<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207110719153-1623126868.png" width=600/>

<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207110744056-955025758.png" width=600/>

<img src="https://img2018.cnblogs.com/blog/1375651/201812/1375651-20181207110807313-176724709.png" width=600/>

相關文章
相關標籤/搜索