有讀者問題函數(shù)調用是如何實現(xiàn)的,今天就來聊聊這個比較簡單的問題。
大家都應該打包過東西吧,搬家之類的,通常都是找?guī)讉€箱子一股腦裝進去,為了不讓箱子占地方,你通常會把它們摞好,就像這樣:
最先被打包好的箱子被摞在最下方,剛打包好的箱子總是放在最上方,這就形成了一種first in last out的結構,也就是我們所說的棧,stack,上面的這些箱子就形成了棧。
如果你懂得用箱子打包東西,你就能明白函數(shù)調用是怎么一回事。
原來,在程序運行時每個被調用的函數(shù)都有自己的一個箱子,假設這段代碼是這樣寫的:
void D() {}
void C() {
D();
}
void B() {
C();
}
void A() {
B();
}
函數(shù)A調用函數(shù)B、B調用C、C調用D,那么當函數(shù)D在運行時內(nèi)存中就會有四個箱子,每個函數(shù)一個:
每個函數(shù)占據(jù)的這個箱子——也就是這塊內(nèi)存,就被稱為棧幀,stack frame,只不過由于引力的作用,我們摞箱子時是從下往上增長,而出于內(nèi)存布局的需要,函數(shù)調用時的棧是從高地址向低地址增長。
這些箱子中都裝有什么呢?你在函數(shù)中定義的局部變量就裝在這里,關于棧幀內(nèi)容更詳細的講解你可以參考這里《函數(shù)調用是在內(nèi)存中是什么樣子》,這些不是本文的重點,這里更關心的是這些棧幀是怎樣增長以及減少的。
仔細觀察上面這張圖,每個箱子最重要的信息有兩個, 你至少需要知道箱子的底部以及箱子的頂部在哪里 !
在計算機中,每個函數(shù)棧幀的“底部”和“頂部”的信息——也就是內(nèi)存地址,分別存放在兩個寄存器中:BasePointer(BP)寄存器以及StackPointer(SP)寄存器,即我們熟悉的rbp以及rsp,32位下為ebp以及esp,注意本文以x86_64為例。
只要確定了rbp和rsp你就能得到一塊棧區(qū),在這塊棧區(qū)上就可以進行函數(shù)調用:
讀到這里肯定有的同學可能會問,CPU中的寄存器不是有限的嗎?從這里的講解看每個棧幀都需要維護一個“棧頂”與“棧底”的信息,每個核心中的rbp以及rsp寄存器就一個,我們該怎樣確保函數(shù)運行時相應棧幀使用的rbp以及rsp是正確的呢?
方法非常簡單,調用函數(shù)時會創(chuàng)建新的棧幀,此時需要將原有rbp寄存器中的值保存在新的棧幀上,就像這樣:
上圖就是函數(shù)調用時第一件要完成的事情,把rbp的值push到棧上,rsp下移,然后呢?然后也很簡單,只需要把rsp指向的地址也賦值給rbp即可,這樣就開啟了一個新的棧幀:
完成上述操作的有兩條機器指令(gcc編譯器):
push %rbp
mov %rsp,%rbp
如果你去看編譯器為每個函數(shù)生成的機器指令,那么開頭幾乎都是這兩條指令,現(xiàn)在你應該明白這兩條指令的作用了吧。
這兩條指令就把上一個棧幀的rbp的保存到了新的棧幀,由于此時rsp已經(jīng)指向了新的棧幀棧頂,由于此時棧為空,因此棧頂和棧底的地址是一樣的,可以直接把rsp賦給rbp,這樣一個全新的棧幀就創(chuàng)建出來了。
如果我們在被調函數(shù)內(nèi)部創(chuàng)建一些局部變量:
void funcB() {
int a = 1;
int b = 2;
int c = 3;
...
}
那么此時棧會進一步擴大,并把局部變量存放在該函數(shù)的棧幀中:
現(xiàn)在我們的??梢噪S著函數(shù)調用而增長,可以看到,棧幀和你搬家時用的紙箱子還是不太一樣的,函數(shù)棧幀不會一開始就大小固定好,而是隨著指令的執(zhí)行動態(tài)增加,也就是如果你往棧上push一些數(shù)據(jù),棧幀就會相應的增大一點。
那么函數(shù)調用完成時該怎么辦呢?這也非常簡單,只需要一條機器指令:
leave
我們在上一篇棧區(qū)分配內(nèi)存快還是堆區(qū)分配內(nèi)存快中講解了一部分,leave指令的作用是將?;焚x值給rsp,這樣棧指針指向上一個棧幀的棧頂,然后pop出rbp,這樣rbp就指向上一個棧幀的棧底:
看到了吧,執(zhí)行完leave指令后rbp以及rsp就指向了上一個棧幀,這就相當于棧幀的彈出,這樣stack 1占用的內(nèi)存就無效了,沒有任何用處了,顯然這就是我們常說的內(nèi)存回收,因此簡單的一條leave指令即可把棧區(qū)中的內(nèi)存回收掉。
而在x86平臺,leave指令后往往跟上一條ret指令:
leave
ret
我們已經(jīng)了解了leave指令的作用,這條指令讓rbp以及rsp指向上一個棧幀,然后呢?顯然CPU應該從funcA調用函數(shù)funcB之后的一行代碼處繼續(xù)運行,那么這行代碼的地址在哪里呢?顯然就在funcA棧幀的棧頂:
當CPU執(zhí)行call指令時會把該函數(shù)的返回地址push到棧中,而ret指令的作用正是將棧頂彈出(pop)到rip寄存器,rip寄存器告訴CPU接下來該從哪里執(zhí)行機器指令,這個返回地址是funcA調用funcB時push到棧上的,這樣當從函數(shù)funcB()返回后我們就知道該從哪里繼續(xù)執(zhí)行機器指令了,這就是ret指令的作用,當然這里也是函數(shù)調用實現(xiàn)的基本原理。
-
程序
+關注
關注
117文章
3826瀏覽量
82960 -
函數(shù)
+關注
關注
3文章
4379瀏覽量
64832 -
函數(shù)調用
+關注
關注
0文章
19瀏覽量
2673
發(fā)布評論請先 登錄
C語言使用函數(shù)調用的知識點
C函數(shù)調用機制與棧幀原理詳解

如何查看及更改函數(shù)/函數(shù)塊的調用環(huán)境

如果使用FCALL調用函數(shù)而使用RET返回的話, 就會發(fā)生CSA泄露怎么解決?
內(nèi)聯(lián)函數(shù)和外聯(lián)函數(shù)有什么區(qū)別

評論