我的感言:真正的知識是深刻淺出的,碼農翻身」 公共號將苦澀難懂的計算機知識,用形象有趣的生活中實例呈現給咱們,讓咱們更好地理解。感謝「碼農翻身」 公共號,感謝大家的成果,謝謝大家的分享。css
我是CPU阿甘,今天要講一講函數調用的祕密,這個確實有點複雜,想透徹的理解機器代碼層面的函數調用不容易。 我也是從無數的指令中悟出這個函數調用的祕密的, 因此慢慢來,不要急。 放鬆心情,慢慢的品味,你可能須要多看幾遍才能明白。 可是你一旦理解了,絕對物超所值,由於你會了解到彙編,寄存器,指針,以及他們在一塊兒究竟是怎麼工做的。
首先, 一個程序的有指令都老老實實的放在內存的一個地方,這個地方是Linux老大分配的,我干涉不了,可是這些指令都是我打電話通知硬盤,讓他給運輸到內存的。
而後Linux老大就會告訴我程序的入口點,其實就是第一條指令的存放地址,我就打電話問內存要這個指令,取到指令之後就開始執行。這些指令當中無非有這麼幾類:編程
可是我一旦遇到像這樣的指令。架構
當把寄存器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架構師建立,分享編程和職場的經驗教訓。
長按二維碼, 關注碼農翻身