CPU阿甘:函數調用的祕密

原文:http://mp.weixin.qq.com/s?__biz=MzAxOTc0NzExNg==&mid=2665513039&idx=1&sn=381c1b8c7f86906c4838050b8c1db2bb#rd&utm_source=tuicool&utm_medium=referral


我是CPU阿甘, 上次我給大家承諾過,要講一講函數調用的祕密, 這個確實有點複雜, 想透徹的理解機器代碼層面的函數調用不容易。


我也是從無數的指令中悟出這個函數調用的祕密的,  所以慢慢來,不要急。 放鬆心情, 慢慢的品味, 你可能需要多看幾遍才能明白。


但是你一旦理解了,絕對物超所值,因爲你會瞭解到彙編,寄存器,指針,以及他們在一起到底是怎麼工作的。

首先, 一個程序一條一條的指令都的老老實實的放在內存的一個地方,這個地方是Linux老大分配的, 我干涉不了, 但是這些指令都是我打電話給硬盤, 讓他給運輸到內存的。 
然後Linux老大就會告訴我程序的入口點, 其實就是第一條指令的存放地址,  我就打電話問內存要這個指令, 取到指令以後就開始執行。這些指令當中無非有這麼幾類:1. 把數據從內存加載我的寄存器裏什麼? 你不知道啥是寄存器?  寄存器就是我內部的一個臨時的數據存儲空間了2. 對寄存器的數據進行運算, 例如把兩個寄存器的數加起來3. 把我寄存器的數據再寫到內存裏
但是我一旦遇到像這樣的指令。 "把寄存器ebp的值壓到棧裏去“我就知道好戲要上場了, 函數調用就會開始。 
我們這些x86體系的機器有個特點,就是每個函數調用都會創建一個所謂的“幀”
哈哈, 不要被這些術語嚇壞, 其實幀也就是我哥們內存中的一段連續的空間而已。像這樣: 640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=多個函數幀在內存裏排起來, 就像一個先進後出的棧一樣, 不過,這個棧不像我們常見的棧, 棧底在下面。相反,這個棧的棧底在上面, 是從上往下生長的 (或者說是從高地址向低地址生長的)
內存經常向我抱怨: "阿甘,你知道嗎, 每次我看到這個棧, 都有一種真氣逆行的感覺, 半天都調整不過來 " 
但內存不知道, 我有一個叫ebp的特殊寄存器, 一直會指向當前函數在一個棧的開始地址。 我還有另外一個特殊寄存器,叫做esp , 他會隨着指令的運行,指向函數幀的最後的地址, 像這樣:640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=現在這個指令來了:"把寄存器ebp的值壓到棧裏去“
"把esp的值賦給ebp"
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=你看看, 是不是新的函數幀生成了?只不過現在只有一行數據。 ebp和esp指向同一地址。函數幀的第一行的地址是800,  裏邊的內容是1000, 也就是上個函數幀的地址
注意, 我們每次操作的是4個字節,所以原來esp 的地址是804, 現在變成了800我又問內存要下一條指令:"把esp 的值減去24”
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=下面幾條指令是這樣的:“把10放到ebp 減去4的地址” (其實就是796嘛)“把20放到ebp減去8的地址” (其實就是792嘛)
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=你們知道這是幹什麼嗎? 我想了好久才明白這是幹嘛, 這其實就是在分配函數的局部變量啊我猜源代碼應該是這樣的:int x = 10;int y = 20;在我看來, x, y 只是變量, 他們叫什麼根本不重要, 重要的是他們的值和地址!下面幾條指令很有意思:" 把地址796作爲數據放到 esp指向的地址“ (其實就是776嘛)" 把地址792作爲數據放到 esp+4指向的地址" (其實就是780嘛)640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=這又是在幹嘛?

這其實就相當於把 x 的指針 &x和 y 的指針 &y ,放到了特定的地方, 準備着要做什麼事情 , 可能要調用函數了。

所以,所謂的指針就是地址而已。

我猜程序員寫的代碼應該是這樣:int x = 10;int y = 20;int sum= add(&x, &y); 接下來的指令是這樣:“調用函數 add”
我看到這樣的函數就需要特別小心, 因爲我必須要找到 add函數返回以後的那條指令的地址, 把它也壓到棧裏去。int x = 10;int y = 20;int sum = add(&x, &y); printf("the sum is %d\n",sum); 假設這條指令的地址是100640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=注意啊, 把函數調用結束的以後的返回地址100壓入棧以後, esp 也發生變化了, 指向了772的位置我會找到函數Add 的指令,繼續執行"把寄存器ebp的值壓到棧裏去“
"把esp的值賦給ebp""把寄存器ebx的值壓入棧”你看每個函數的開始指令都是這樣, 我猜這應該是一種約定吧這裏額外把ebx這個寄存器壓入棧, 是因爲ebx可能被上個函數使用, 但是在add函數中也會用 , 爲了不破壞之前的值, 只有先委屈一下暫時放到內存裏吧。640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=接下來的指令是:“把ebp 加8的數據取出來放到 edx 寄存器” (ebp+8 不就是地址776嘛, 其中存放的是&x的地址, 這就是取參數了)
“把ebp 加12的數據取出來放到 ecx 寄存器” (ebp+12 不就是地址780嘛, 其中存放的是&y的地址)

注意啊, 現在edx的值是796, ecx的值是792 , 但他們仍然不是真正的數據, 而是指針(地址)!
“把edx 指向的內存地址(796)的數據取出來,放到ebx 寄存器”
“把ecx 指向的內存地址(792)的數據取出來,放到eax寄存器” 
此時此刻, 終於取到了真正的值, ebx = 10, eax = 20你暈了沒有?  如果你到此已經暈了, 建議你再讀一遍。  我想源代碼應該非常的簡單,就是這樣:int add(int *xp , int *yp){    int x = *xp;    int y = *yp;    ....}“把ebx 和 eax 的值加起來,放到 eax寄存器中” 這個指令我最擅長做了。接下來的指令也很關鍵, add 函數已經調用完成, 準備返回了  “把esp 指向的數據彈出的ebx寄存器”“把esp 指向的數據彈出到ebp寄存器”
640?wx_fmt=png&tp=webp&wxfrom=5&wx_lazy=你看add 函數幀已經消失了, 或者換句話說, add 函數幀的數據還在內存裏, 只是我們不在關心了!接下來的指令非常的關鍵:"返回"我就會取出那個返回地址, 也就是 100, 去這裏找指令接着執行其實就是這條語句: printf("the sum is %d\n",sum);問你一個問題, sum的值在那裏保存着呢? 對, 是在eax寄存器裏 !
搞定了,看着很複雜, 其實看透了也挺簡單吧。 函數調用,關鍵就是(1)把參數和返回地址準備好, (2)然後大家都遵循約定, 每次新函數都要建立新的函數幀:   "把寄存器ebp的值壓到棧裏去“    "把esp的值賦給ebp"(3) 函數調用完了, 重置 ebp 和esp ,讓他們重新指向調用着的棧幀。
好了,今天就到此爲止 , 把我也累壞了,  主人又要關機了, 留一個問題吧: 
C語言編譯,鏈接以後直接就是機器碼, 那函數調用的操作都是上面講的。 但是對於Python, Ruby 這樣的解釋型語言, 或者對於java 這樣的有虛擬機的語言, 他們的函數調用是什麼樣的?  和上面講的有什麼關係?

640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy公衆號:碼農翻身“碼農翻身”公衆號由工作15年的前IBM架構師創建,分享編程和職場的經驗教訓。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章