堆棧溢出的(de)原(yuan)因
時間(jian):2025-01-16 來源:華清遠見
一、棧(Stack)
1、概念(nian)和作用
棧(zhan)(zhan)是一(yi)(yi)種數(shu)據(ju)結構,在 Linux C 語言中用(yong)于存(cun)儲(chu)函(han)數(shu)調(diao)用(yong)的相關(guan)信息(xi)。當一(yi)(yi)個函(han)數(shu)被(bei)(bei)調(diao)用(yong)時,會在棧(zhan)(zhan)上創建一(yi)(yi)個棧(zhan)(zhan)幀(Stack Frame)。棧(zhan)(zhan)幀中包含了函(han)數(shu)的參數(shu)、局(ju)部變量(liang)、返回地址等信息(xi)。棧(zhan)(zhan)的操作遵循后(hou)進先出(LIFO)原則,這意味著最后(hou)壓入棧(zhan)(zhan)中的數(shu)據(ju)將最先被(bei)(bei)彈出。
2、存儲內容
參數傳遞(di):在 C 語言中,函數參數通(tong)常(chang)是通(tong)過棧來傳遞(di)的。
例如:對于函(han)數int add(int a, int b);
當(dang)調用add(3, 5)時,a和b的值(3 和 5)可能(neng)會被(bei)壓入(ru)棧中。
局部(bu)變量存儲:函數內部(bu)定義的局部(bu)變量也存放(fang)在棧(zhan)中。
例如:

返回(hui)地址保(bao)存:當一(yi)個(ge)(ge)函(han)數(shu)調(diao)用(yong)另一(yi)個(ge)(ge)函(han)數(shu)時,調(diao)用(yong)函(han)數(shu)的下一(yi)條指令的地址(即(ji)返回(hui)地址)會被(bei)(bei)保(bao)存在棧中。這樣,當被(bei)(bei)調(diao)用(yong)函(han)數(shu)執行完畢后,可以根據這個(ge)(ge)返回(hui)地址回(hui)到調(diao)用(yong)函(han)數(shu)繼(ji)續執行。
3、棧的大小
在(zai) Linux 終端中,可以使用(yong)(yong)ulimit -s命令來查(cha)看(kan)棧的大小限制。ulimit是一(yi)個用(yong)(yong)于控制 shell 資(zi)源的工具(ju),-s參數專門用(yong)(yong)于查(cha)看(kan)棧大小(以字節為單位)。

這個(ge)輸出(chu)結果(guo)(guo)8192表示當前(qian)用戶(hu)的(de)棧(zhan)大小限(xian)制是 8192 字節。如果(guo)(guo)程序的(de)棧(zhan)使用超過了這個(ge)限(xian)制,就會導致棧(zhan)溢出(chu)。
二、堆(Heap)
1、概念和(he)作用
堆(dui)(dui)是用于(yu)動態(tai)內(nei)存(cun)分配的(de)區域。在 C 語(yu)言中(zhong),通(tong)(tong)過函(han)數如malloc、calloc和realloc來從(cong)堆(dui)(dui)中(zhong)分配內(nei)存(cun),通(tong)(tong)過free函(han)數來釋(shi)放內(nei)存(cun)。堆(dui)(dui)用于(yu)存(cun)儲那(nei)些(xie)在程(cheng)(cheng)序運(yun)行過程(cheng)(cheng)中(zhong)需要(yao)動態(tai)分配和釋(shi)放的(de)內(nei)存(cun)塊,這些(xie)內(nei)存(cun)塊的(de)生(sheng)命周期通(tong)(tong)常由程(cheng)(cheng)序員(yuan)控制(zhi),而不像(xiang)棧中(zhong)的(de)數據在函(han)數結(jie)束(shu)時自動釋(shi)放。
2、內存(cun)分(fen)配(pei)和管(guan)理
2.1、malloc 函數:
void * ptr = malloc(size_t size),它(ta)會在堆中分(fen)配指定大小(size)的一(yi)塊內(nei)存(cun),并(bing)返回一(yi)個指向這塊內(nei)存(cun)的指針(zhen)(ptr)。如(ru)果內(nei)存(cun)分(fen)配成功,ptr指向的內(nei)存(cun)是未(wei)初始化的。
2.2、calloc 函(han)數:
void * ptr = calloc(size_t num, size_t size),它也會(hui)在堆中分配(pei)內存(cun)。與malloc不(bu)同的是,calloc會(hui)將(jiang)分配(pei)的內存(cun)塊初始(shi)化為全 0。
2.3、realloc 函數(shu):
void * new_ptr = realloc(void * old_ptr, size_t new_size),用于重新調整已經(jing)通過(guo)malloc或(huo)calloc分配的內存塊的大小。
2.4、free 函數(shu):
free(void * ptr)用于釋放通過malloc、calloc或realloc分配的內存。如果不釋放堆內存,可(ke)能會導致內存泄漏。
3、堆與(yu)棧的區別
3.1、內存分(fen)配方式:
棧的內(nei)存(cun)分配(pei)是(shi)由編(bian)譯器自動(dong)(dong)(dong)完成的,在函數調用時自動(dong)(dong)(dong)分配(pei),函數結束時自動(dong)(dong)(dong)釋放;而堆的內(nei)存(cun)分配(pei)是(shi)由程序員通過函數調用手動(dong)(dong)(dong)進行的,并且需要(yao)手動(dong)(dong)(dong)釋放,否則會(hui)導致(zhi)內(nei)存(cun)問題。
3.2內存(cun)增長方向:
棧的內存增長方向通常(chang)是從高地(di)址(zhi)向低地(di)址(zhi),而(er)堆的內存增長方向通常(chang)是從低地(di)址(zhi)向高地(di)址(zhi)(這可能因操作系(xi)統和編譯器而(er)略有不同)。
3.3內存(cun)使用(yong)效率:
棧(zhan)的(de)內(nei)存(cun)(cun)分配和(he)釋放速度(du)相對較快,因為它是(shi)自動完成(cheng)的(de);堆(dui)的(de)內(nei)存(cun)(cun)分配和(he)釋放相對復(fu)雜,速度(du)較慢,并且可能會產生內(nei)存(cun)(cun)碎片。
三、堆棧溢出的原因
1、遞歸失控
在 Linux C 語言中,遞歸函(han)數(shu)如果(guo)沒有正確的終止(zhi)條件,就會不斷(duan)地(di)(di)進行自身調用,導(dao)致棧空間(jian)被無限(xian)制地(di)(di)占用。
例如:

這個func函數會無限遞歸,每(mei)次(ci)調用都會將函數的返回地址、局部變量(liang)等信息壓(ya)入(ru)棧中。棧空間是有限的,最終就會導(dao)致棧溢出(chu)。
2、局部變量數組過大
如果在函數內部(bu)(bu)定(ding)義(yi)了過大的局部(bu)(bu)變量(liang)數組,而棧空間不(bu)足以容納這(zhe)些(xie)變量(liang)時,就會(hui)出現棧溢出(棧大小限制是 8192 字節(jie))。
例如:

在(zai)這個例子(zi)中(zhong),func函數中(zhong)定義的arr數組如果太大,超出了棧的容量,就會引(yin)發(fa)棧溢出。棧的大小在(zai)系統(tong)中(zhong)是(shi)有限制的,一般(ban)由(you)操作系統(tong)和(he)編譯時(shi)的設置決定。
3、函數嵌(qian)套過(guo)深
當有大量(liang)(liang)的(de)(de)函(han)數(shu)(shu)(shu)嵌套調用時(shi),每一次函(han)數(shu)(shu)(shu)調用都(dou)會在棧上創(chuang)建一個新的(de)(de)棧幀來存儲函(han)數(shu)(shu)(shu)的(de)(de)局部變(bian)量(liang)(liang)、參數(shu)(shu)(shu)和返(fan)回地址等(deng)信息。如果嵌套的(de)(de)層數(shu)(shu)(shu)過多,就會耗盡棧空(kong)間。
例如:

在這個代碼(ma)中,從func1到func100層層嵌(qian)套調用,可能會因為棧幀的過度積累而(er)導致棧溢出。
4、緩沖(chong)區溢出
當程
序向一個緩沖區寫入數據(ju)時(shi)(shi),如果寫入的數據(ju)長(chang)度超過(guo)了緩沖區的大小,就(jiu)可能會覆蓋棧上(shang)的其他(ta)數據(ju),從(cong)而導(dao)致棧溢出。例(li)如,在處理(li)字符串復制操作時(shi)(shi):

在這個(ge)例子(zi)中(zhong),strcpy函數(shu)會(hui)將arr復制(zhi)到buff中(zhong),但arr的(de)(de)長度超過了buff的(de)(de)容量,就(jiu)會(hui)導致緩(huan)沖區(qu)溢(yi)出,可能會(hui)覆蓋棧上相鄰的(de)(de)內存區(qu)域(yu),引發棧溢(yi)出。
四、防止堆棧溢出的方法
1、手(shou)動記錄遞歸深度(du)
當使用遞歸函數(shu)時,可以通過(guo)一個變(bian)量來記錄遞歸的(de)深度。例如,在計(ji)算階乘的(de)遞歸函數(shu)中(zhong):

在這里,depth變量用于記錄遞歸(gui)深度,當(dang)depth超過(guo)預先定(ding)義(yi)的MAX時,就會進行(xing)相應的錯誤處(chu)理(li)。
或(huo)者(zhe),定義一個全局變量,每次(ci)函數調用(yong)的(de)時(shi)候就(jiu)-1,當超出(chu)限制的(de)時(shi)候,就(jiu)錯誤處理(li)結束調用(yong)。
2、估算局部變(bian)量空間(jian)需求,動態分配空間(jian)
在函數(shu)(shu)設計時,需(xu)要(yao)估(gu)算函數(shu)(shu)內部(bu)局部(bu)變(bian)量(liang)(liang)所占用(yong)的(de)(de)棧(zhan)空間(jian)。盡(jin)量(liang)(liang)避免(mian)定義大量(liang)(liang)占用(yong)空間(jian)的(de)(de)局部(bu)變(bian)量(liang)(liang)。如果必須使用(yong)較大的(de)(de)局部(bu)變(bian)量(liang)(liang)數(shu)(shu)組,可以(yi)考慮將其(qi)定義為全(quan)局變(bian)量(liang)(liang)或者動態(tai)分配內存(在堆(dui)上(shang))。
例如,對于一(yi)個可能(neng)導致棧(zhan)溢出的(de)函數:

可(ke)以將其(qi)修改為動態分配內存的方式(shi):

這樣,數組的內存是從堆上分(fen)配(pei)的,而(er)不是棧,減少了(le)棧溢出的風險。
3、優化函數參數傳遞方(fang)式(shi)
如果(guo)函數參(can)數是大(da)型結構體或(huo)者數組,可(ke)以(yi)考慮使用指針(zhen)傳(chuan)(chuan)遞而不(bu)是值(zhi)傳(chuan)(chuan)遞。值(zhi)傳(chuan)(chuan)遞會復制整個參(can)數對象到棧上(shang),而指針(zhen)傳(chuan)(chuan)遞只傳(chuan)(chuan)遞對象的(de)地址,占用空間更(geng)小。

例如:
4、安全的字符串和緩沖區操作(zuo)
1、使用安全(quan)的字符串處理函數(shu)
避(bi)免使用可能(neng)導致緩沖區溢出的(de)函(han)數(shu)(shu),如(ru)strcpy和gets。取而(er)代(dai)之,使用安全的(de)函(han)數(shu)(shu),如(ru)strncpy和fgets。例如(ru),對(dui)于strcpy可能(neng)導致的(de)緩沖區溢出:
可以使用strncpy來安(an)全地復制字符(fu)串:
這里strncpy會根(gen)據buff的大小來復(fu)制字符(fu)串,并且(qie)最后手(shou)動添加字符(fu)串結(jie)束符(fu)\0,以確(que)保字符(fu)串的完整性。
2、檢查緩沖區邊界
在(zai)對(dui)緩(huan)沖(chong)(chong)區進行操作時(shi),無(wu)論是寫(xie)(xie)入還是讀(du)取(qu),都要明確知道緩(huan)沖(chong)(chong)區的(de)邊界。例(li)如(ru),在(zai)循環(huan)向緩(huan)沖(chong)(chong)區寫(xie)(xie)入數(shu)據(ju)(ju)時(shi),要確保寫(xie)(xie)入的(de)數(shu)據(ju)(ju)量不(bu)超過(guo)緩(huan)沖(chong)(chong)區的(de)大(da)小(xiao)。可以通過(guo)比較(jiao)寫(xie)(xie)入數(shu)據(ju)(ju)的(de)索引和緩(huan)沖(chong)(chong)區大(da)小(xiao)來(lai)進行控(kong)制。例(li)如(ru):
這個示例(li)在從標準輸入(ru)讀取字符并寫(xie)入(ru)緩沖區buffer時,通過比(bi)較i和sizeof(buff)-1來確(que)保不會寫(xie)入(ru)超過緩沖區大小的數據。

