函數(shù)棧的實(shí)現(xiàn)原理

簡(jiǎn)介

編程語(yǔ)言離不開函數(shù)鞠评,函數(shù)是對(duì)一段代碼的封裝脱茉,往往實(shí)現(xiàn)了某個(gè)特定的功能剪芥,在程序中可以多次調(diào)用這個(gè)函數(shù)。稍有編程經(jīng)驗(yàn)的同學(xué)都知道琴许,函數(shù)是由棧實(shí)現(xiàn)的税肪,調(diào)用對(duì)應(yīng)入棧,退出對(duì)應(yīng)出棧虚吟。在寫遞歸函數(shù)的時(shí)候寸认,如果遞歸層次太深會(huì)出現(xiàn)棧溢出(StackOverFlow)的錯(cuò)誤。

"函數(shù)棧"包含了對(duì)函數(shù)調(diào)用的基本理解串慰,但是從細(xì)節(jié)來(lái)看偏塞,還有很多疑問(wèn),例如:

  • 函數(shù)的棧是如何開辟的邦鲫?
  • 如何傳入?yún)?shù)灸叼?
  • 返回值是如何得到的?

本文以 C 語(yǔ)言為例庆捺,從內(nèi)存布局古今、匯編代碼的角度來(lái)分析函數(shù)棧的實(shí)現(xiàn)原理。

Linux 進(jìn)程內(nèi)存布局

當(dāng)程序被執(zhí)行的時(shí)候滔以,Linux 會(huì)為其在內(nèi)存中分配相應(yīng)的空間以支撐程序的運(yùn)行捉腥,如下圖所示。

linux-memory.png
linux-memory.png

在虛擬內(nèi)存中你画,內(nèi)存空間被分為多個(gè)區(qū)域抵碟。代碼指令保存在文本段桃漾,已初始化的全局變量 global 保存在數(shù)據(jù)段,程序運(yùn)行中動(dòng)態(tài)申請(qǐng)的內(nèi)存malloc(10 * char())放在堆中拟逮,而函數(shù)執(zhí)行的時(shí)候則在棧中開辟空間運(yùn)行撬统。例如main函數(shù)便占有一個(gè)函數(shù)棧,其中的變量iip都保存在main的椂仄空間中恋追。

函數(shù)的棧空間有個(gè)名字叫做 棧幀罚屋,下面就具體了解一下棧幀苦囱。

棧幀

下圖是棧的結(jié)構(gòu)。圖中右側(cè)是椘⒚停空間沿彭,其中有多個(gè)棧幀。從上往下由較早的棧幀到較新的棧幀尖滚,由于棧是從高地址往低地址生長(zhǎng)的喉刘,所以最新的棧永遠(yuǎn)在最下面,即棧頂漆弄。

stack-frame.png
stack-frame.png

圖中有兩個(gè)畫出了具體結(jié)構(gòu)的棧幀睦裳,分別是函數(shù) A 和函數(shù) B。函數(shù) A 的棧幀最上面有一塊省略號(hào)標(biāo)識(shí)的區(qū)域撼唾,其中保存的是上一個(gè)棧幀的寄存器值以及函數(shù) A 自己內(nèi)部創(chuàng)建的局部變量廉邑。下面的參數(shù) n 到參數(shù) 1 則是函數(shù) A 要傳給函數(shù) B 的調(diào)用參數(shù)。那么函數(shù) B 如何獲鹊构取蛛蒙?答案是用寄存器。

CPU 計(jì)算時(shí)會(huì)把很多變量放在寄存器中渤愁,根據(jù)硬件體系的不同牵祟,寄存器數(shù)量和作用也不同。一般在 x86 32位中抖格,寄存器 %esp 保存了棧指針的值诺苹,也就是棧頂,而 %ebp 作為當(dāng)前棧幀的幀指針雹拄,也就是當(dāng)前棧幀的底部收奔,所以通過(guò) %esp%ebp 就可以知道當(dāng)前棧幀的頭跟尾。除了這兩個(gè)寄存器滓玖,還有其它一些通用寄存器(%eax坪哄、%edx等),用于保存程序執(zhí)行的臨時(shí)值。

了解了寄存器的基本知識(shí)后翩肌,下面我們就可以知道函數(shù) B 如何獲取到函數(shù) A 傳給它的參數(shù)了饰剥。參數(shù) 1 的地址是 %ebp + 8,參數(shù) 2 的地址是 %ebp + 12摧阅,參數(shù) n 的地址是 %ebp + 4 + 4 * n。相信大家已經(jīng)看明白绷蹲,通過(guò)幀指針往上找就可以取得這些參數(shù)棒卷,而這些參數(shù)之所以在這里當(dāng)然是函數(shù) A 預(yù)先準(zhǔn)備好的,關(guān)于這一點(diǎn)下文會(huì)有例子祝钢。

另外在所有參數(shù)的最下面保存著 返回地址比规,這個(gè)是在函數(shù) B 返回之后接下來(lái)要執(zhí)行的指令的地址。

看了函數(shù) A 之后拦英,再看看函數(shù) B蜒什。在函數(shù) B 的棧幀最上面是 被保存的 %ebp,這個(gè)指的是函數(shù) A 的幀指針疤估,畢竟 %ebp 這個(gè)寄存器就一個(gè)灾常,所以新的函數(shù)入棧的時(shí)候要先把老的保存起來(lái),等函數(shù)出棧再恢復(fù)铃拇。在這個(gè)老的幀指針下面則是其它需要保存的寄存器變量以及函數(shù) B 自己內(nèi)部用到的局部變量钞瀑。再往下是 參數(shù)構(gòu)造區(qū)域,也就是函數(shù) B 即將調(diào)用另一個(gè)函數(shù)慷荔,在這里先把參數(shù)準(zhǔn)備好雕什。可以看出显晶,函數(shù) B 與函數(shù) A 的棧幀結(jié)構(gòu)是類似的贷岸。

了解了棧幀的理論之后,大家可能會(huì)覺得很抽象磷雇,下面結(jié)合具體實(shí)例來(lái)看棧幀從產(chǎn)生到消亡的過(guò)程偿警。

函數(shù)調(diào)用實(shí)例

下面圖是函數(shù) caller 的具體執(zhí)行過(guò)程,左邊是 C 代碼唯笙,中間是匯編碼户敬,右邊是對(duì)應(yīng)的棧幀。

caller-frame.png
caller-frame.png

我們一行一行的來(lái)分析睁本,看中間匯編碼尿庐,上面三行綠色的:

pushl %ebp // 保存舊的 %ebp
movl %esp, %ebp // 將 %ebp 設(shè)置為 %esp
subl $24, %esp // 將 %esp 減 24 開辟棧空間

這三行其實(shí)是為棧幀做準(zhǔn)備工作呢堰。第一行保存舊的 %ebp抄瑟,此時(shí)新的棧空間還沒有創(chuàng)建,但保存舊的 %ebp 的這一行空間將作為新棧幀的棧底皮假,也就是幀指針鞋拟,因此第二行將棧指針 %esp(永遠(yuǎn)指向棧頂)的值設(shè)置到 %ebp 上。 第三行將 %esp 下移 24 個(gè)字節(jié)惹资,這一行其實(shí)就是為函數(shù) caller 開辟椇馗伲空間了。從圖中可以看出褪测,下面的空間用于保存 caller 中的變量以及傳給下個(gè)函數(shù)的參數(shù)猴誊。有部分空間未使用,這個(gè)是為了地址對(duì)齊侮措,不影響我們的分析懈叹,可以忽略。

在開辟了棧幀之后分扎,就開始執(zhí)行 caller 內(nèi)部的邏輯了澄成,caller 首先創(chuàng)建了兩個(gè)局部變量(arg1arg2)。對(duì)應(yīng)的匯編代碼為 movl $534, -4(%ebp); movl $1057, -8(%ebp)畏吓,其中 -4(%ebp) 表示 %ebp - 4 的位置墨状,也就是圖中 arg1 所在的位置, arg2 的位置則是 %ebp - 8 的位置菲饼。這兩行是把 5341057 保存到傳送到這兩個(gè)位置上歉胶。

繼續(xù)往下是這幾行:

leal -8(%ebp), %eax // 把 %ebp - 8 這個(gè)地址保存到 %eax 
movl %eax, 4(%esp)  // 把 %eax 的值保存到 %esp + 4 這個(gè)位置上
leal -4(%ebp), %eax  // 把 %ebp - 4 這個(gè)地址保存到 %eax 
movl %eax, ($esp)  // 把 %eax 的值保存到 %esp 這個(gè)位置上

第一行把 %ebp - 8 這個(gè)地址保存到 %eax 中,而 %ebp - 8arg2 的地址巴粪,下一行把這個(gè)地址放到 %esp + 4 這個(gè)位置上通今,也就是圖中 &arg2 的那個(gè)區(qū)域塊。其實(shí)這一行是在為函數(shù) swap_add 準(zhǔn)備參數(shù) &arg2肛根,而下面兩行則是準(zhǔn)備參數(shù) &arg1辫塌。

再下面一行是 call swap_add。這一行就是調(diào)用函數(shù) swap_add 了派哲,不過(guò)在這之前還需要把返回地址壓到棧上臼氨,這里的返回地址是函數(shù) swap_add 返回后要接著執(zhí)行的代碼的地址,也就是 int diff = arg1 - arg2 地址芭届。

在調(diào)用 swap_add 后用到了其返回值 sum 繼續(xù)進(jìn)行計(jì)算储矩,我們還不知道返回值是怎么拿到的。在這之前褂乍,我們先進(jìn)入 swap_add 函數(shù)持隧,下面是對(duì)應(yīng)的代碼執(zhí)行圖:

swap_add-frame.png
swap_add-frame.png

swap_add 對(duì)應(yīng)的匯編代碼的前三行與 caller 類似,同樣是保存舊的幀指針逃片,但是因?yàn)?swap_add 不需要保存額外的變量屡拨,只需要多用一個(gè)寄存器 %ebx,所以這里保存了這個(gè)寄存器的舊值,但是沒有將 %esp 直接下移一段長(zhǎng)度的操作呀狼。

接下來(lái)綠色的兩行就是關(guān)鍵了:

movl 8(%ebp), %edx // 從 %ebp + 8 取值保存到 %edx
movl 12(%ebp), %ecx // 從 %ebp + 12 取值保存到 %ecx

這兩行分別是從 caller 中保存參數(shù) &arg1&arg2 的地方取得地址值裂允,并根據(jù)地址取得 arg1arg2 的實(shí)際數(shù)值。

接下來(lái)的 4 行是交換操作哥艇,這里就不具體看每一行的邏輯了绝编。

再下面一行 addl %ebx, %eax 是將返回值保存到寄存器 %eax 中,這里非常關(guān)鍵貌踏,函數(shù) swap_add 的返回值保存在 %eax十饥,一會(huì)兒 caller 就是從這個(gè)寄存器獲取的。

swap_add 的最后幾行是出棧操作哩俭,將 %ebx%ebp 分別恢復(fù)為 caller 中的值。最后執(zhí)行 ret 返回到 caller 中拳恋。

下面我們繼續(xù)回到 caller 中凡资,剛才執(zhí)行到 call swap_add,下面幾行是執(zhí)行 int diff = arg1 - arg2谬运,結(jié)果保存在 %edx 中隙赁。

最后一行是計(jì)算 sum * diff,對(duì)應(yīng)的匯編代碼為 imull %edx, %eax梆暖。這里是把 %edx%eax 的值相乘并且把結(jié)果保存到 %eax 中伞访。在上面的分析中,我們知道 %eax 保存著 swap_add 的返回值轰驳,這里還是從 %eax 中取出返回值進(jìn)行計(jì)算厚掷,并且把結(jié)果繼續(xù)保存到 %eax 中,而這個(gè)值又是 caller 的返回值级解,這樣調(diào)用 caller 的函數(shù)也可以從這個(gè)寄存器中獲取返回值了冒黑。

caller 函數(shù)的最后一行匯編代碼是 ret,這會(huì)銷毀 caller 的棧幀并且恢復(fù)相應(yīng)寄存器的舊值勤哗。到此抡爹,callerswap_add 這個(gè)函數(shù)的調(diào)用過(guò)程就全部分析完了。

總結(jié)

本文詳細(xì)分析了函數(shù)調(diào)用過(guò)程中棧幀變化的過(guò)程芒划,對(duì)于開頭提出的幾個(gè)疑問(wèn)也都有了解答冬竟。函數(shù)棧的實(shí)現(xiàn)在常規(guī)的開發(fā)中幾乎不會(huì)涉及到,但是學(xué)習(xí)其中的原理有利于更深入地理解內(nèi)存以及編程語(yǔ)言的奧秘民逼。

參考

  • 《深入理解計(jì)算機(jī)系統(tǒng)》
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末泵殴,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子拼苍,更是在濱河造成了極大的恐慌袋狞,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異苟鸯,居然都是意外死亡同蜻,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門早处,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)湾蔓,“玉大人,你說(shuō)我怎么就攤上這事砌梆∧穑” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵咸包,是天一觀的道長(zhǎng)桃序。 經(jīng)常有香客問(wèn)我,道長(zhǎng)烂瘫,這世上最難降的妖魔是什么媒熊? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮坟比,結(jié)果婚禮上芦鳍,老公的妹妹穿的比我還像新娘。我一直安慰自己葛账,他們只是感情好柠衅,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著籍琳,像睡著了一般菲宴。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上趋急,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天裙顽,我揣著相機(jī)與錄音,去河邊找鬼宣谈。 笑死愈犹,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的闻丑。 我是一名探鬼主播漩怎,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼嗦嗡!你這毒婦竟也來(lái)了勋锤?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤侥祭,失蹤者是張志新(化名)和其女友劉穎叁执,沒想到半個(gè)月后茄厘,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡谈宛,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年次哈,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吆录。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡窑滞,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出恢筝,到底是詐尸還是另有隱情哀卫,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布撬槽,位于F島的核電站此改,受9級(jí)特大地震影響侄柔,放射性物質(zhì)發(fā)生泄漏共啃。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一勋拟、第九天 我趴在偏房一處隱蔽的房頂上張望勋磕。 院中可真熱鬧妈候,春花似錦敢靡、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至幔虏,卻和暖如春纺念,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背想括。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工陷谱, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人瑟蜈。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓烟逊,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親铺根。 傳聞我的和親對(duì)象是個(gè)殘疾皇子宪躯,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345