定義
棧溢出就是緩衝區溢出的一種。 由於緩衝區溢出而使得有用的存儲單元被改寫,往往會引發不可預料的後果。程式在運行過程中,為了臨時存取數據的需要,一般都要分配一些記憶體空間,通常稱這些空間為緩衝區。如果向緩衝區中寫入超過其本身長度的數據,以致於緩衝區無法容納,就會造成緩衝區以外的存儲單元被改寫,這種現象就稱為緩衝區溢出。緩衝區長度一般與用戶自己定義的緩衝變數的類型有關。
棧溢出就是緩衝區溢出的一種。
在pascal語言中,棧溢出的錯誤代碼為202號錯誤。
在Python中,函式調用是通過棧(stack)這種數據結構實現的,每當進入一個函式調用,棧就會加一層棧幀,每當函式返回,棧就會減一層棧幀。由於棧的大小不是無限的,所以,遞歸調用的次數過多,會導致棧溢出。
例子如下:
def fact(n):
if n==1:
return 1
return n * fact(n - 1)
>>> fact(1000)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 4, in fact
...
File "<stdin>", line 4, in fact
RuntimeError: maximum recursion depth exceeded in comparison
解決方法
解決遞歸調用棧溢出的方法是通過尾遞歸最佳化,事實上尾遞歸和循環的效果是一樣的,所以,把循環看成是一種特殊的尾遞歸函式也是可以的。
尾遞歸是指,在函式返回的時候,調用自身本身,並且, return語句不能包含表達式。這樣,編譯器或者解釋器就可以把尾遞歸做最佳化,使遞歸本身無論調用多少次,都只占用一個棧幀,不會出現棧溢出的情況。
如上棧溢出例子,由於fact(n)函式return n * fact(n - 1)引入了乘法表達式,所以就不是尾遞歸了。要改成尾遞歸方式,需要多一點代碼,主要是要把每一步的乘積傳入到遞歸函式中:
def fact(n):
return fact_iter(n, 1)
def fact_iter(num, product):
if num == 1:
return product
return fact_iter(num - 1, num * product)
可以看到,return fact_iter(num - 1, num * product)僅返回遞歸函式本身,num - 1和num * product在函式調用前就會被計算,不影響函式調用。尾遞歸調用時,如果做了最佳化,棧不會增長,因此,無論多少次調用也不會導致棧溢出。
性質
由於緩衝區溢出而使得有用的存儲單元被改寫,往往會引發不可預料的後果。向這些單元寫入任意的數據,一般只會導致程式崩潰之類的事故,對這種情況我們也至多說這個程式有bug。但如果向這些單元寫入的是精心準備好的數據,就可能使得程式流程被劫持,致使不希望的代碼被執行,落入攻擊者的掌控之中,這就不僅僅是bug,而是漏洞(exploit)了。