當(dāng)前位置:首頁(yè) > 學(xué)習(xí)資源 > 講師博文 > 堆棧溢出的原因
一、棧(Stack)
1、概念和作用
棧是一種數(shù)據(jù)結(jié)構(gòu),在 Linux C 語(yǔ)言中用于存儲(chǔ)函數(shù)調(diào)用的相關(guān)信息。當(dāng)一個(gè)函數(shù)被調(diào)用時(shí),會(huì)在棧上創(chuàng)建一個(gè)棧幀(Stack Frame)。棧幀中包含了函數(shù)的參數(shù)、局部變量、返回地址等信息。棧的操作遵循后進(jìn)先出(LIFO)原則,這意味著最后壓入棧中的數(shù)據(jù)將最先被彈出。
2、存儲(chǔ)內(nèi)容
參數(shù)傳遞:在 C 語(yǔ)言中,函數(shù)參數(shù)通常是通過(guò)棧來(lái)傳遞的。
例如:對(duì)于函數(shù)int add(int a, int b);
當(dāng)調(diào)用add(3, 5)時(shí),a和b的值(3 和 5)可能會(huì)被壓入棧中。
局部變量存儲(chǔ):函數(shù)內(nèi)部定義的局部變量也存放在棧中。
例如:
返回地址保存:當(dāng)一個(gè)函數(shù)調(diào)用另一個(gè)函數(shù)時(shí),調(diào)用函數(shù)的下一條指令的地址(即返回地址)會(huì)被保存在棧中。這樣,當(dāng)被調(diào)用函數(shù)執(zhí)行完畢后,可以根據(jù)這個(gè)返回地址回到調(diào)用函數(shù)繼續(xù)執(zhí)行。
3、棧的大小
在 Linux 終端中,可以使用ulimit -s命令來(lái)查看棧的大小限制。ulimit是一個(gè)用于控制 shell 資源的工具,-s參數(shù)專門用于查看棧大小(以字節(jié)為單位)。
這個(gè)輸出結(jié)果8192表示當(dāng)前用戶的棧大小限制是 8192 字節(jié)。如果程序的棧使用超過(guò)了這個(gè)限制,就會(huì)導(dǎo)致棧溢出。
二、堆(Heap)
1、概念和作用
堆是用于動(dòng)態(tài)內(nèi)存分配的區(qū)域。在 C 語(yǔ)言中,通過(guò)函數(shù)如malloc、calloc和realloc來(lái)從堆中分配內(nèi)存,通過(guò)free函數(shù)來(lái)釋放內(nèi)存。堆用于存儲(chǔ)那些在程序運(yùn)行過(guò)程中需要?jiǎng)討B(tài)分配和釋放的內(nèi)存塊,這些內(nèi)存塊的生命周期通常由程序員控制,而不像棧中的數(shù)據(jù)在函數(shù)結(jié)束時(shí)自動(dòng)釋放。
2、內(nèi)存分配和管理
2.1、malloc 函數(shù):
void * ptr = malloc(size_t size),它會(huì)在堆中分配指定大小(size)的一塊內(nèi)存,并返回一個(gè)指向這塊內(nèi)存的指針(ptr)。如果內(nèi)存分配成功,ptr指向的內(nèi)存是未初始化的。
2.2、calloc 函數(shù):
void * ptr = calloc(size_t num, size_t size),它也會(huì)在堆中分配內(nèi)存。與malloc不同的是,calloc會(huì)將分配的內(nèi)存塊初始化為全 0。
2.3、realloc 函數(shù):
void * new_ptr = realloc(void * old_ptr, size_t new_size),用于重新調(diào)整已經(jīng)通過(guò)malloc或calloc分配的內(nèi)存塊的大小。
2.4、free 函數(shù):
free(void * ptr)用于釋放通過(guò)malloc、calloc或realloc分配的內(nèi)存。如果不釋放堆內(nèi)存,可能會(huì)導(dǎo)致內(nèi)存泄漏。
3、堆與棧的區(qū)別
3.1、內(nèi)存分配方式:
棧的內(nèi)存分配是由編譯器自動(dòng)完成的,在函數(shù)調(diào)用時(shí)自動(dòng)分配,函數(shù)結(jié)束時(shí)自動(dòng)釋放;而堆的內(nèi)存分配是由程序員通過(guò)函數(shù)調(diào)用手動(dòng)進(jìn)行的,并且需要手動(dòng)釋放,否則會(huì)導(dǎo)致內(nèi)存問(wèn)題。
3.2內(nèi)存增長(zhǎng)方向:
棧的內(nèi)存增長(zhǎng)方向通常是從高地址向低地址,而堆的內(nèi)存增長(zhǎng)方向通常是從低地址向高地址(這可能因操作系統(tǒng)和編譯器而略有不同)。
3.3內(nèi)存使用效率:
棧的內(nèi)存分配和釋放速度相對(duì)較快,因?yàn)樗亲詣?dòng)完成的;堆的內(nèi)存分配和釋放相對(duì)復(fù)雜,速度較慢,并且可能會(huì)產(chǎn)生內(nèi)存碎片。
三、堆棧溢出的原因
1、遞歸失控
在 Linux C 語(yǔ)言中,遞歸函數(shù)如果沒(méi)有正確的終止條件,就會(huì)不斷地進(jìn)行自身調(diào)用,導(dǎo)致棧空間被無(wú)限制地占用。
例如:
這個(gè)func函數(shù)會(huì)無(wú)限遞歸,每次調(diào)用都會(huì)將函數(shù)的返回地址、局部變量等信息壓入棧中。棧空間是有限的,最終就會(huì)導(dǎo)致棧溢出。
2、局部變量數(shù)組過(guò)大
如果在函數(shù)內(nèi)部定義了過(guò)大的局部變量數(shù)組,而棧空間不足以容納這些變量時(shí),就會(huì)出現(xiàn)棧溢出(棧大小限制是 8192 字節(jié))。
例如:
在這個(gè)例子中,func函數(shù)中定義的arr數(shù)組如果太大,超出了棧的容量,就會(huì)引發(fā)棧溢出。棧的大小在系統(tǒng)中是有限制的,一般由操作系統(tǒng)和編譯時(shí)的設(shè)置決定。
3、函數(shù)嵌套過(guò)深
當(dāng)有大量的函數(shù)嵌套調(diào)用時(shí),每一次函數(shù)調(diào)用都會(huì)在棧上創(chuàng)建一個(gè)新的棧幀來(lái)存儲(chǔ)函數(shù)的局部變量、參數(shù)和返回地址等信息。如果嵌套的層數(shù)過(guò)多,就會(huì)耗盡棧空間。
例如:
在這個(gè)代碼中,從func1到func100層層嵌套調(diào)用,可能會(huì)因?yàn)闂倪^(guò)度積累而導(dǎo)致棧溢出。
4、緩沖區(qū)溢出
當(dāng)程
序向一個(gè)緩沖區(qū)寫入數(shù)據(jù)時(shí),如果寫入的數(shù)據(jù)長(zhǎng)度超過(guò)了緩沖區(qū)的大小,就可能會(huì)覆蓋棧上的其他數(shù)據(jù),從而導(dǎo)致棧溢出。例如,在處理字符串復(fù)制操作時(shí):
在這個(gè)例子中,strcpy函數(shù)會(huì)將arr復(fù)制到buff中,但arr的長(zhǎng)度超過(guò)了buff的容量,就會(huì)導(dǎo)致緩沖區(qū)溢出,可能會(huì)覆蓋棧上相鄰的內(nèi)存區(qū)域,引發(fā)棧溢出。
四、防止堆棧溢出的方法
1、手動(dòng)記錄遞歸深度
當(dāng)使用遞歸函數(shù)時(shí),可以通過(guò)一個(gè)變量來(lái)記錄遞歸的深度。例如,在計(jì)算階乘的遞歸函數(shù)中:
在這里,depth變量用于記錄遞歸深度,當(dāng)depth超過(guò)預(yù)先定義的MAX時(shí),就會(huì)進(jìn)行相應(yīng)的錯(cuò)誤處理。
或者,定義一個(gè)全局變量,每次函數(shù)調(diào)用的時(shí)候就-1,當(dāng)超出限制的時(shí)候,就錯(cuò)誤處理結(jié)束調(diào)用。
2、估算局部變量空間需求,動(dòng)態(tài)分配空間
在函數(shù)設(shè)計(jì)時(shí),需要估算函數(shù)內(nèi)部局部變量所占用的棧空間。盡量避免定義大量占用空間的局部變量。如果必須使用較大的局部變量數(shù)組,可以考慮將其定義為全局變量或者動(dòng)態(tài)分配內(nèi)存(在堆上)。
例如,對(duì)于一個(gè)可能導(dǎo)致棧溢出的函數(shù):
可以將其修改為動(dòng)態(tài)分配內(nèi)存的方式:
這樣,數(shù)組的內(nèi)存是從堆上分配的,而不是棧,減少了棧溢出的風(fēng)險(xiǎn)。
3、優(yōu)化函數(shù)參數(shù)傳遞方式
如果函數(shù)參數(shù)是大型結(jié)構(gòu)體或者數(shù)組,可以考慮使用指針傳遞而不是值傳遞。值傳遞會(huì)復(fù)制整個(gè)參數(shù)對(duì)象到棧上,而指針傳遞只傳遞對(duì)象的地址,占用空間更小。
例如:
4、安全的字符串和緩沖區(qū)操作
1、使用安全的字符串處理函數(shù)
避免使用可能導(dǎo)致緩沖區(qū)溢出的函數(shù),如strcpy和gets。取而代之,使用安全的函數(shù),如strncpy和fgets。例如,對(duì)于strcpy可能導(dǎo)致的緩沖區(qū)溢出:
可以使用strncpy來(lái)安全地復(fù)制字符串:
這里strncpy會(huì)根據(jù)buff的大小來(lái)復(fù)制字符串,并且最后手動(dòng)添加字符串結(jié)束符\0,以確保字符串的完整性。
2、檢查緩沖區(qū)邊界
在對(duì)緩沖區(qū)進(jìn)行操作時(shí),無(wú)論是寫入還是讀取,都要明確知道緩沖區(qū)的邊界。例如,在循環(huán)向緩沖區(qū)寫入數(shù)據(jù)時(shí),要確保寫入的數(shù)據(jù)量不超過(guò)緩沖區(qū)的大小。可以通過(guò)比較寫入數(shù)據(jù)的索引和緩沖區(qū)大小來(lái)進(jìn)行控制。例如:
這個(gè)示例在從標(biāo)準(zhǔn)輸入讀取字符并寫入緩沖區(qū)buffer時(shí),通過(guò)比較i和sizeof(buff)-1來(lái)確保不會(huì)寫入超過(guò)緩沖區(qū)大小的數(shù)據(jù)。