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