一步步编写操作系统 62 函数调用约定
由于我們要將c語言和匯編語言結合編程啦,所以一定會存在匯編代碼和c代碼相互調用的問題,有些事情還是要提前交待給大家的,本節就是要給大家說下函數調用規約中的那些事兒。
函數調用約定是什么?
調用約定,calling conventions,從字面上理解,它是調用函數時的一套約定,是被調用代碼的接口,它體現在:
- 參數的傳遞方式,是放在寄存器中?棧中?還是兩者混合;
- 參數的傳遞順序,是從左到右傳遞?還是從右到左;
- 是調用者保存寄存器環境還是被調用者保存?保存哪些寄存器呢?
我估計,我這么解釋調用約定的話,之前對此不懂的同學還是不懂,所以咱們得從頭說起啦。沒例子還真說不清楚,咱們還是拿例子來說事吧。
比如在c語言中我們有這樣的代碼
int subtract(int a, int b) {return a-b; }我們可以用這樣的形式調用它:
int sub = subtract(3,2);
這樣sub的值就變成了1。這是我們司空見慣的用法,但大家有沒有想過,計算機是如何確定參數3和2在哪里的呢?這是有關參數存儲的問題。
計算機中可沒有專門存儲參數的硬件,即使有的話,我想也不太容易確定該硬件的容量,畢竟參數的個數是不定的。而且還有個致命的問題,若在剛剛傳入參數之后,函數執行之前被換下了cpu,新的進程上cpu后,也要調用函數,也要傳遞參數呢,還是會引出參數覆蓋的問題。不過咱們之前說過,參數可以放在寄存器中,也可以放在內存中。
寄存器數量是有限的,假設將參數放在寄存器中傳遞的話,主調函數必然要考慮保存寄存器現場的問題,一是用哪些寄存器傳參數,二是用于傳遞參數的寄存器,其原來的值如果要保留的話,往哪里保存呢?估計大家也是這么想的,內存足夠大,肯定是往內存中轉儲啦,那既然是還要在內存中折騰,不如直接把參數放在內存中更直接省事。
說到用內存來傳遞參數,還要考慮內存地址,用哪塊內存來存儲參數呢?為了避免多進程的參數覆蓋問題,每個進程的參數得單獨存儲在不同地址,得在內存中再為每個進程規劃出一塊存儲參數的內存區域,想想就很麻煩。或許您早已經迫不及待想說出答案啦:棧也是位于內存中的啊,最好的方式就是在棧中來保存。這有兩個好處:
好啦,參數存儲的問題解決了,我們決定在進程自己的棧空間中保存參數, 一種可行的方案是,調用者在調用函數時,先把所有參數壓棧,然后再調用函數。被調用函數在棧中獲取到參數后進行處理。
以上方案如果不細想的話似乎還挺好,其實解決了一個問題后,又引入了兩個新的問題:
內心深處傳來了小齊的《傷心太平洋》:一波還未過去,一波又來侵襲……
上面提到的回收棧空間或者清理棧空間,并不是把參數在棧中所占據的內存清0,而是回收參數所在的內存空間,也就是指讓棧頂恢復到棧中參數所在的位置之前,即讓棧指針往高地址處回退。這樣一來,參數原本占用的空間又變得可用了,下次再有入棧操作時,push指令可以直接將其覆蓋。
也許有部分同學并未意識到這兩個問題,心想,我自己寫的函數,我自己調用,難道我自己還不知道怎么處理嗎?您看,這里用了三個“我自己”來強調問題的關鍵所在,自己調用自己的代碼確實可以避免以上兩個問題,只要自己協調好了就一切ok,可保不準您會調用其他同事寫的函數。
調用約定是為解決匯編語言的問題才提出的,不像咱們平時所用的高級語言,直接用實參往函數中一代入就算調用完成了,高級語言中本身不存在這兩個問題,高級語言編譯器為了方便程序員,默默承擔了這些,這兩個問題是高級語言在被編譯為底層匯編語言時才有的,所以高級語言中不涉及調用約定。
在c語言中,咱們不用考慮這些問題,還是拿前面說過的減法函數舉例:
subtract(int a, int b) {??//被調用者return a-b; } int sub = subtract(3,2);??//主調用者函數subtract是返回a減b的差,這里只要代入實參3和2即可完成調用。可是,在其被編譯為匯編語言時,參數是要壓入棧中的,現在問題來了……我們模擬一下這種情況,以上c代碼中的調用方和被調用方對應的匯編代碼如下:
主調用者:
?1?push 2????;壓入參數b2?push 3????;壓入參數a3?call subtract???;調用函數subtract被調用者:
?1?push ebp???;備份ebp,為以后用ebp做為基址來尋址參數2?mov ebp, esp??;將當前棧頂賦值給ebp3?mov eax, [ebp+8]?;得到被減數,參數a4?sub eax,[ebp+12]?;得到減數,參數b5?pop ebp????;恢復ebp的值目前棧中的情況如圖
?
如果調用者和被調用者(subtract函數)都是同一個程序員寫的,他很清楚自己壓入棧中參數的順序,所以他在subtract函數中,明確的知道棧中[ebp+8]處的內容是被減數a,[ebp+12]處的是減數b。其實,這個程序員在潛意識中自己跟自己建立了個約定,先被壓入棧的是減數b,后被壓入的是被減數a,這樣他才能確信從容地在subtract函數體中獲取到正確的參數。其實他也可以反著來,先把被減數a壓入棧,再把減數b壓入棧,這樣在subtract函數中通過[ebp+8]得到的是參數b(減數),[ebp+12]得到的是參數a(被減數)。總之參數很多的情況下就會涉及到參數傳遞的順序問題,即使是自己負責傳遞參數的話,也很少有人會今天一個“這樣”的順序傳遞參數,明天一個“那樣”的順序傳遞參數,這不得搞得人格分裂嗎^_^,因此參數傳遞的順序應該是一始如終的,要么從左到右,要么從右到左,只能選擇一種。
以上是自己調用自己代碼的情況,怎么說都比較方便。可萬一,被調用函數subtract不是自己寫的,咱們不知道在subtract把[ebp+8]是當做被減數a還是減數b,咱們該以怎樣的順序將參數壓入棧中呢?這得跟人家商量了,雙方得協調個大家認同的參數入棧順序,這就是最初調用約定的由來。
我們要解決的不只是參數壓棧順序問題,還有棧空間的清理工作呢。其實問題倒也不難解決,這都是屬于調用方和被調用方之間協調的問題,只要雙方提前商量好傳入參數的順序和由誰來負責清理棧空間就行。
一步步編寫操作系統 64 常見的函數調用約定
在高級語言中,這兩個問題是通過調用約定來解決的,調用約定就是調用方和被調用方就以上問題達成一致解決方案的約定,雙方按照這種約定合作就不會發生問題。我們按照由誰來清理棧空間分類,目前的調用約定見表1
?
?
總結
以上是生活随笔為你收集整理的一步步编写操作系统 62 函数调用约定的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 一步步编写操作系统 58 门、调用门与R
- 下一篇: 上汽、阿里打造!智己L7开启全国交付:3