最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

函數(shù)運行時在內(nèi)存中是什么樣子?

2022-07-14 15:25 作者:補給站Linux內(nèi)核  | 我要投稿

在開始本篇的內(nèi)容前,我們先來思考幾個問題。

1. 我們先來看一段簡單的代碼:

你能看出這段代碼會有什么問題嗎?

你知道協(xié)程的本質(zhì)是什么嗎?有的同學可能會說是用戶態(tài)線程,那么什么是用戶態(tài)線程,這是怎么實現(xiàn)的?3. 函數(shù)運行起來后在內(nèi)存中是什么樣子?這幾個問題看似沒什么關(guān)聯(lián),但這背后都指向一樣東西,這就是所謂的函數(shù)運行時棧,run time stack。接下來我們就好好看看到底什么是函數(shù)運行時棧,為什么徹底理解函數(shù)運行時棧對程序員來說非常重要。

從進程、線程到函數(shù)調(diào)用

汽車在高速上行駛時有很多信息,像速度、位置等等,通過這些信息我們可以直觀的感受汽車的運行時狀態(tài)。

同樣的,程序在運行時也有很多信息,像有哪些程序正在運行、這些程序執(zhí)行到了哪里等等,通過這些信息我們可以直觀的感受系統(tǒng)中程序運行的狀態(tài)。

其中,我們創(chuàng)造了進程、線程這樣的概念來記錄有哪些程序正在運行,進程和線程的運行體現(xiàn)在函數(shù)執(zhí)行上,函數(shù)的執(zhí)行除了函數(shù)內(nèi)部執(zhí)行的順序執(zhí)行還有子函數(shù)調(diào)用的控制轉(zhuǎn)移以及子函數(shù)執(zhí)行完畢的返回。其中函數(shù)內(nèi)部的順序執(zhí)行乏善可陳,重點是函數(shù)的調(diào)用。因此接下來我們的視角將從宏觀的進程和線程拉近到微觀下的函數(shù)調(diào)用,重點來討論一下函數(shù)調(diào)用是怎樣實現(xiàn)的。

函數(shù)執(zhí)行的活動軌跡:棧

玩過游戲的同學應該知道,有時你為了完成一項主線任務不得不去打一些支線的任務,支線任務中可能還有支線任務,當一個支線任務完成后退回到前一個支線任務,這是什么意思呢,舉個例子你就明白了。假設(shè)主線任務西天取經(jīng)A依賴支線任務收服孫悟空B和收服豬八戒C,也就是說收服孫悟空B和收服豬八戒C完成后才能繼續(xù)主線任務西天取經(jīng)A;支線任務收服孫悟空B依賴任務拿到緊箍咒D,只有當任務D完成后才能回到任務B;整個任務的依賴關(guān)系如圖所示:

現(xiàn)在我們來模擬一下任務完成過程。首先我們來到任務A,執(zhí)行主線任務:

執(zhí)行任務A的過程中我們發(fā)現(xiàn)任務A依賴任務B,這時我們暫停任務A去執(zhí)行任務B:

執(zhí)行任務B的時候,我們又發(fā)現(xiàn)依賴任務D:

執(zhí)行任務D的時候我們發(fā)現(xiàn)該任務不再依賴任何其它任務,因此C完成后我們可以會退到前一個任務,也就是B:

任務B除了依賴任務C外不再依賴其它任務,這樣任務B完成后就可以回到任務A:

現(xiàn)在我們回到了主線任務A,依賴的任務B執(zhí)行完成,接下來是任務C:

和任務D一樣,C不依賴任何其它其它任務,任務C完成后就可以再次回到任務A,再之后任務A執(zhí)行完畢,整個任務執(zhí)行完成。讓我們來看一下整個任務的活動軌跡:

仔細觀察,實際上你會發(fā)現(xiàn)這是一個First In Last Out 的順序,天然適用于棧這種數(shù)據(jù)結(jié)構(gòu)來處理。再仔細看一下棧頂?shù)能壽E,也就是A、B、D、B、A、C、A,實際上你會發(fā)現(xiàn)這里的軌跡就是任務依賴樹的遍歷過程,是不是很神奇,這也是為什么樹這種數(shù)據(jù)結(jié)構(gòu)的遍歷除了可以用遞歸也可以用棧來實現(xiàn)的原因。


【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【891587639】整理了一些個人覺得比較好的學習書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。。。ê曨l教程、電子書、實戰(zhàn)項目及代碼)?


函數(shù)調(diào)用也是同樣的道理,你把上面的ABCD換成函數(shù)ABCD,本質(zhì)不變。因此,現(xiàn)在我們知道了,使用棧這種結(jié)構(gòu)就可以用來保存函數(shù)調(diào)用信息。和游戲中的每個任務一樣,當函數(shù)在運行時每個函數(shù)也要有自己的一個“小盒子”,這個小盒子中保存了函數(shù)運行時的各種信息,這些小盒子通過棧這種結(jié)構(gòu)組織起來,這個小盒子就被稱為棧幀,stack frames,也有的稱之為call stack,不管用什么命名方式,總之,就是這里所說的小盒子,這個小盒子就是函數(shù)運行起來后占用的內(nèi)存,這些小盒子構(gòu)成了我們通常所說的棧區(qū)。那么函數(shù)調(diào)用時都有哪些信息呢?

控制轉(zhuǎn)移

我們知道當函數(shù)A調(diào)用函數(shù)B的時候,控制從A轉(zhuǎn)移到了B,所謂控制其實就是指CPU執(zhí)行屬于哪個函數(shù)的機器指令,CPU從開始執(zhí)行屬于函數(shù)A的指令切換到執(zhí)行屬于函數(shù)B的指令,我們就說控制從函數(shù)A轉(zhuǎn)移到了函數(shù)B??刂茝暮瘮?shù)A轉(zhuǎn)移到函數(shù)B,那么我們需要有這樣兩個信息:

  • 我從哪里來 (返回)

  • 要到去哪里 (跳轉(zhuǎn))

是不是很簡單,就好比你出去旅游,你需要知道去哪里,還需要記住回家的路。函數(shù)調(diào)用也是同樣的道理。當函數(shù)A調(diào)用函數(shù)B時,我們只要知道:

  • 函數(shù)A對于的機器指令執(zhí)行到了哪里 (我從哪里來,返回)

  • 函數(shù)B第一條機器指令所在的地址 (要到哪里去,跳轉(zhuǎn))

有這兩條信息就足以讓CPU開始執(zhí)行函數(shù)B對應的機器指令,當函數(shù)B執(zhí)行完畢后跳轉(zhuǎn)回函數(shù)A。那么這些信息是怎么獲取并保持的呢?現(xiàn)在我們就可以打開這個小盒子,看看是怎么使用的了。假設(shè)函數(shù)A調(diào)用函數(shù)B,如圖所示:

當前,CPU執(zhí)行函數(shù)A的機器指令,該指令的地址為0x400564,接下來CPU將執(zhí)行下一條機器指令也就是:

call 0x400540

這條機器指令是什么意思呢?這條機器指令對應的就是我們在代碼中所寫的函數(shù)調(diào)用,注意call后有一條機器指令地址,注意觀察上圖你會看到,該地址就是函數(shù)B的第一條機器指令,從這條機器指令后CPU將跳轉(zhuǎn)到函數(shù)B。現(xiàn)在我們已經(jīng)解決了控制跳轉(zhuǎn)的“要到哪里去”問題,當函數(shù)B執(zhí)行完畢后怎么跳轉(zhuǎn)回來呢?原來,call指令除了給出跳轉(zhuǎn)地址之外還有這樣一個作用,也就是把call指令的下一條指令的地址,也就是0x40056a push到函數(shù)A的棧幀中,如圖所示:

現(xiàn)在,函數(shù)A的小盒子變大了一些,因為裝入了返回地址:

現(xiàn)在CPU開始執(zhí)行函數(shù)B對應的機器指令,注意觀察,函數(shù)B也有一個屬于自己的小盒子(棧幀),可以往里面扔一些必要的信息。

如果函數(shù)B中又調(diào)用了其它函數(shù)呢?道理和函數(shù)A調(diào)用函數(shù)B是一樣的。讓我們來看一下函數(shù)B最后一條機器指令ret,這條機器指令的作用是告訴CPU跳轉(zhuǎn)到函數(shù)A保存在棧幀上的返回地址,這樣當函數(shù)B執(zhí)行完畢后就可以跳轉(zhuǎn)到函數(shù)A繼續(xù)執(zhí)行了。至此,我們解決了控制轉(zhuǎn)移中“我從哪里來”的問題。

傳遞參數(shù)與獲取返回值

函數(shù)調(diào)用與返回使得我們可以編寫函數(shù),進行函數(shù)調(diào)用。但調(diào)用函數(shù)除了提供函數(shù)名稱之外還需要傳遞參數(shù)以及獲取返回值,那么這又是怎樣實現(xiàn)的呢?

在x86-64中,多數(shù)情況下參數(shù)的傳遞與獲取返回值是通過寄存器來實現(xiàn)的。假設(shè)函數(shù)A調(diào)用了函數(shù)B,函數(shù)A將一些參數(shù)寫入相應的寄存器,當CPU執(zhí)行函數(shù)B時就可以從這些寄存器中獲取參數(shù)了。同樣的,函數(shù)B也可以將返回值寫入寄存器,當函數(shù)B執(zhí)行結(jié)束后函數(shù)A從該寄存器中就可以讀取到返回值了。我們知道寄存器的數(shù)量是有限的,當傳遞的參數(shù)個數(shù)多于寄存器的數(shù)量該怎么辦呢?這時那個屬于函數(shù)的小盒子也就是棧幀又能發(fā)揮作用了。原來,當參數(shù)個數(shù)多于寄存器數(shù)量時剩下的參數(shù)直接放到棧幀中,這樣被調(diào)函數(shù)就可以從前一個函數(shù)的棧幀中獲取到參數(shù)了?,F(xiàn)在棧幀的樣子又可以進一步豐富了,如圖所示:


從圖中我們可以看到,調(diào)用函數(shù)B時有部分參數(shù)放到了函數(shù)A的棧幀中,同時函數(shù)A棧幀的頂部依然保存的是返回地址。

局部變量

我們知道在函數(shù)內(nèi)部定義的變量被稱為局部變量,這些變量在函數(shù)運行時被放在了哪里呢?原來,這些變量同樣可以放在寄存器中,但是當局部變量的數(shù)量超過寄存器的時候這些變量就必須放到棧幀中了。因此,我們的棧幀內(nèi)容又一步豐富了。


細心的同學可能會有這樣的疑問,我們知道寄存器是共享資源可以被所有函數(shù)使用,既然可以將函數(shù)A的局部變量寫入寄存器,那么當函數(shù)A調(diào)用函數(shù)B時,函數(shù)B的局部變量也可以寫到寄存器,這樣的話當函數(shù)B執(zhí)行完畢回到函數(shù)A時寄存器的值已經(jīng)被函數(shù)B修改過了,這樣會有問題吧。這樣的確會有問題,因此我們在向寄存器中寫入局部變量之前,一定要先將寄存器中開始的值保存起來,當寄存器使用完畢后再恢復原值就可以了。那么我們要將寄存器中的原始值保存在哪里呢?有的同學可能已經(jīng)猜到了,沒錯,依然是函數(shù)的棧幀中。


最終,我們的小盒子就變成了如圖所示的樣子,當寄存器使用完畢后根據(jù)棧幀中保存的初始值恢復其內(nèi)容就可以了?,F(xiàn)在你應該知道函數(shù)在運行時到底是什么樣子了吧,以上就是問題3的答案。

Big Picture

需要再次強調(diào)的一點就是,上述討論的棧幀就位于我們常說的棧區(qū)。棧區(qū),屬于進程地址空間的一部分,如圖所示,我們將棧區(qū)放大就是圖左邊的樣子。


最后,讓我們回到文章開始的這段簡單代碼:


想一想這段代碼會有什么問題?原來,棧區(qū)是有大小限制的,當超過限制后就會出現(xiàn)著名的棧溢出問題,顯然上述代碼會導致這一問題的出現(xiàn)。因此:

  1. 不要創(chuàng)建過大的局部變量

  2. 函數(shù)棧幀,也就是調(diào)用層次不能太多


總結(jié)

本章我們從幾個看似沒什么關(guān)聯(lián)的問題出發(fā),詳細講解了函數(shù)運行時棧是怎么一回事,為什么我們不能創(chuàng)建過多的局部變量。細心的同學會發(fā)現(xiàn)第2個問題我們沒有解答,這個問題的講解放到下一篇,也就是協(xié)程中講解。



函數(shù)運行時在內(nèi)存中是什么樣子?的評論 (共 條)

分享到微博請遵守國家法律
尼木县| 连江县| 油尖旺区| 临西县| 高碑店市| 克拉玛依市| 余庆县| 玛多县| 乐亭县| 大渡口区| 六安市| 乳源| 将乐县| 中江县| 阳原县| 榕江县| 弋阳县| 石河子市| 竹山县| 同江市| 凤冈县| 颍上县| 高陵县| 吉水县| 铜川市| 白朗县| 溧水县| 大足县| 彭水| 南昌市| 锦屏县| 平谷区| 诸暨市| 泸溪县| 峨山| 巴彦淖尔市| 玉林市| 博野县| 凤城市| 天长市| 九江市|