CPU阿甘:函數調用的祕密

我的感言:真正的知識是深刻淺出的,碼農翻身」 公共號將苦澀難懂的計算機知識,用形象有趣的生活中實例呈現給咱們,讓咱們更好地理解。感謝「碼農翻身」 公共號,感謝大家的成果,謝謝大家的分享。css

本文源地址:http://mp.weixin.qq.com/s?__biz=MzAxOTc0NzExNg==&mid=2665513039&idx=1&sn=381c1b8c7f86906c4838050b8c1db2bb&scene=21#wechat_redirect程序員

我是CPU阿甘,今天要講一講函數調用的祕密,這個確實有點複雜,想透徹的理解機器代碼層面的函數調用不容易。

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

可是你一旦理解了,絕對物超所值,由於你會了解到彙編,寄存器,指針,以及他們在一塊兒究竟是怎麼工做的。

首先, 一個程序的有指令都老老實實的放在內存的一個地方,這個地方是Linux老大分配的,我干涉不了,可是這些指令都是我打電話通知硬盤,讓他給運輸到內存的。 
而後Linux老大就會告訴我程序的入口點,其實就是第一條指令的存放地址,我就打電話問內存要這個指令,取到指令之後就開始執行。這些指令當中無非有這麼幾類:編程

  1. 把數據從內存加載個人寄存器裏什麼?(寄存器就是CPU內部的一個臨時的數據存儲空間了);
  2. 對寄存器的數據進行運算,例如把兩個寄存器的數加起來;
  3. 把我寄存器的數據再寫到內存裏。

可是我一旦遇到像這樣的指令。架構

當把寄存器ebp的值壓到棧裏去,我就知道好戲要上場了,函數調用就會開始。 函數

咱們這些x86體系的機器有個特色,就是每一個函數調用都會建立一個所謂的「幀」
哈哈, 不要被這些術語嚇壞, 其實幀也就是我哥們內存中的一段連續的空間而已。像這樣: spa

多個函數幀在內存裏排起來, 就像一個先進後出的棧同樣,不過,這個棧不像咱們常見的棧,棧底在下面。相反,這個棧的棧底在上面,是從上往下生長的 (或者說是從高地址向低地址生長的)。
內存常常向我抱怨:"阿甘,你知道嗎,每次我看到這個棧,都有一種真氣逆行的感受,半天都調整不過來 " 
但內存不知道,我有一個叫ebp的特殊寄存器,一直會指向當前函數在一個棧的開始地址。我還有另一個特殊寄存器,叫作esp。他會隨着指令的運行,指向函數幀的最後的地址,像這樣:如今這個指令來了:3d

「把寄存器ebp的值壓到棧裏去」指針

「把esp的值賦給ebp」
你看看,是否是新的函數幀生成了?只不過如今只有一行數據。ebp和esp指向同一地址。函數幀的第一行的地址是800,裏邊的內容是1000,也就是上個函數幀的地址。
注意,咱們每次操做的是4個字節,因此原來esp 的地址是804,如今變成了800我又問內存要下一條指令:code

「把esp 的值減去24」
blog

下面幾條指令是這樣的:

「把10放到ebp 減去4的地址」 (其實就是796嘛)

「把20放到ebp減去8的地址」 (其實就是792嘛)

大家知道這是幹什麼嗎? 我想了很久才明白這是幹嗎, 這其實就是在分配函數的局部變量啊我猜源代碼應該是這樣的:int x = 10;int y = 20;在我看來, x, y 只是變量, 他們叫什麼根本不重要, 重要的是他們的值和地址!下面幾條指令頗有意思:

「把地址796做爲數據放到 esp指向的地址」 (其實就是776嘛)

「把地址792做爲數據放到 esp+4指向的地址」 (其實就是780嘛)

這又是在幹嗎?

這其實就至關於把 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);// 假設這條指令的地址是100

注意啊, 把函數調用結束的之後的返回地址100壓入棧之後, esp 也發生變化了, 指向了772的位置我會找到函數Add 的指令,繼續執行

「把寄存器ebp的值壓到棧裏去」

「把esp的值賦給ebp」

「把寄存器ebx的值壓入棧」

你看每一個函數的開始指令都是這樣, 我猜這應該是一種約定吧這裏額外把ebx這個寄存器壓入棧, 是由於ebx可能被上個函數使用, 可是在add函數中也會用 , 爲了避免破壞以前的值, 只有先委屈一下暫時放到內存裏吧。

接下來的指令是:

「把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寄存器」

你看add 函數幀已經消失了,或者換句話說,add 函數幀的數據還在內存裏,只是咱們不在關心了!

接下來的指令很是的關鍵:

「返回」

我就會取出那個返回地址,也就是 100,去這裏找指令接着執行其實就是這條語句:

 printf("the sum is %d\n",sum);

問你一個問題,sum的值在那裏保存着呢? 對,是在eax寄存器裏 !
搞定了,看着很複雜,其實看透了也挺簡單吧。

函數調用,關鍵就是:

(1)把參數和返回地址準備好;

(2)而後你們都遵循約定, 每次新函數都要創建新的函數幀:

「把寄存器ebp的值壓到棧裏去」

「把esp的值賦給ebp」

(3) 函數調用完了,重置 ebp 和esp,讓他們從新指向調用着的棧幀。

「碼農翻身」 公共號 : 由工做15年的前IBM架構師建立,分享編程和職場的經驗教訓。

長按二維碼, 關注碼農翻身

相關文章
相關標籤/搜索