2 構造Stack Overflow
通過引入棧,我們可以看到,無論有多少層的函數調用,或者在函數A裡調用函數B,再在函數B裡調用A
這樣的遞歸調用,我們都隻需要通過維持rbp和rsp,這兩個維護棧頂所在位址的寄存器,就能管理好不同函數之間的跳轉
不過,棧的大小也是有限的。如果函數調用層數太多,我們往棧裡壓入它存不下的内容,程式在執行的過程中就會遇到棧溢出的錯誤,這就是stack overflow
構造一個棧溢出的錯誤
并不困難,最簡單的辦法,就是我們上面說的Infiinite Mirror Effect的方式,讓函數A調用自己,并且不設任何終止條件
這樣一個無限遞歸的程式,在不斷地壓棧過程中,将整個棧空間填滿,并最終遇上stack overflow。
int a()
{
return a();
}
int main()
{
a();
return 0;
}
除了無限遞歸,遞歸層數過深,在棧空間裡面建立非常占記憶體的變量(比如一個巨大的數組),這些情況都很可能給你帶來stack overflow
相信你了解了棧在程式運作的過程裡面是怎麼回事,未來在遇到stackoverflow這個錯誤的時候,不會完全沒有方向了。
3 利用函數内聯實作性能優化
上面我們提到一個方法,把一個實際調用的函數産生的指令,直接插入到的位置,來替換對應的函數調用指令。盡管這個通用的函數調用方案,被我們否決了,但是如果被調用的函數裡,沒有調用其他函數,這個方法還是可以行得通的。
事實上,這就是一個常見的編譯器進行自動優化的場景,我們通常叫函數内聯(Inline)
隻要在GCC編譯的時候,加上對應的一個讓編譯器自動優化的參數-O,編譯器就會在可行的情況下,進行這樣的指令替換。
案例
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLicmbw5iZmZWZzEjMhBTM3QTMwITO0YzN0MzN4M2NmVDZ5cjY18CX5d2bs92Yl1iclB3bsVmdlR2LcNWaw9CXt92Yu4GZjlGbh5yYjV3Lc9CX6MHc0RHaiojIsJye.png)
為了避免編譯器優化掉太多代碼,小小修改了一下function.c,讓參數x和y都變成了,通過随機數生成,并在代碼的最後加上将u通過printf列印
[hadoop@JavaEdge Documents]$ vim function.c
[hadoop@JavaEdge Documents]$ gcc -g -c -O function.c
[hadoop@JavaEdge Documents]$ objdump -d -M intel -S function.o
function.o: file format elf64-x86-64
Disassembly of section .text:
0000000000000000 <main>:
{
return a+b;
}
int main()
{
0: 53 push rbx
1: bf 00 00 00 00 mov edi,0x0
6: e8 00 00 00 00 call b <main+0xb>
b: 89 c7 mov edi,eax
d: e8 00 00 00 00 call 12 <main+0x12>
12: e8 00 00 00 00 call 17 <main+0x17>
17: 89 c3 mov ebx,eax
19: e8 00 00 00 00 call 1e <main+0x1e>
1e: 89 c1 mov ecx,eax
20: bf 67 66 66 66 mov edi,0x66666667
25: 89 d8 mov eax,ebx
27: f7 ef imul edi
29: d1 fa sar edx,1
2b: 89 d8 mov eax,ebx
2d: c1 f8 1f sar eax,0x1f
30: 29 c2 sub edx,eax
32: 8d 04 92 lea eax,[rdx+rdx*4]
35: 29 c3 sub ebx,eax
37: 89 c8 mov eax,ecx
39: f7 ef imul edi
3b: c1 fa 02 sar edx,0x2
3e: 89 d7 mov edi,edx
40: 89 c8 mov eax,ecx
42: c1 f8 1f sar eax,0x1f
45: 29 c7 sub edi,eax
47: 8d 04 bf lea eax,[rdi+rdi*4]
4a: 01 c0 add eax,eax
4c: 29 c1 sub ecx,eax
#include <time.h>
#include <stdlib.h>
int static add(int a, int b)
{
return a+b;
4e: 8d 34 0b lea esi,[rbx+rcx*1]
{
srand(time(NULL));
int x = rand() % 5;
int y = rand() % 10;
int u = add(x, y);
printf("u = %d\n", u);
51: bf 00 00 00 00 mov edi,0x0
56: b8 00 00 00 00 mov eax,0x0
5b: e8 00 00 00 00 call 60 <main+0x60>
60: b8 00 00 00 00 mov eax,0x0
65: 5b pop rbx
66: c3 ret
上面的function.c的編譯出來的彙編代碼,沒有把add函數單獨編譯成一段指令順序,而是在調用u = add(x, y)的時候,直接替換成了一個add指令。
除了依靠編譯器的自動優化,你還可以在定義函數的地方,加上inline的關鍵字,來提示編譯器對函數進行内聯。
内聯帶來的優化是,CPU需要執行的指令數變少了,根據位址跳轉的過程不需要了,壓棧和出棧的過程也不用了。
不過内聯并不是沒有代價,内聯意味着,我們把可以複用的程式指令在調用它的地方完全展開了。如果一個函數在很多地方都被調用了,那麼就會展開很多次,整個程式占用的空間就會變大了。
這樣沒有調用其他函數,隻會被調用的函數,我們一般稱之為葉子函數(或葉子過程)
3 總結
這一節,我們講了一個程式的函數間調用,在CPU指令層面是怎麼執行的。其中一定需要你牢記的,就是程式棧這個新概念。
我們可以友善地通過壓棧和出棧操作,使得程式在不同的函數調用過程中進行轉移。而函數内聯和棧溢出,一個是我們常常可以選擇的優化方案,另一個則是我們會常遇到的程式Bug。
通過加入了程式棧,我們相當于在指令跳轉的過程種,加入了一個“記憶”的功能,能在跳轉去運作新的指令之後,再回到跳出去的位置,能夠實作更加豐富和靈活的指令執行流程。這個也為我們在程式開發的過程中,提供了“函數”這樣一個抽象,使得我們在軟體開發的過程中,可以複用代碼和指令,而不是隻能簡單粗暴地複制、粘貼代碼和指令。
4 推薦閱讀
可以仔細讀一下《深入了解計算機系統(第三版)》的3.7小節《過程》,進一步了解函數調用是怎麼回事。
另外,我推薦你花一點時間,通過搜尋引擎搞清楚function.c每一行彙編代碼的含義,這個能夠幫你進一步深入了解程式棧、棧幀、寄存器以及Intel CPU的指令集。
參考
深入淺出計算機組成原理