1 引言
“緩沖區溢出”對現代作業系統與編譯器來講已經不是什麼大問題,但是作為一個合格的 C/C++ 程式員,還是完全有必要了解它的整個細節。
計算機程式一般都會使用到一些記憶體,這些記憶體或是程式内部使用,或是存放使用者的輸入資料,這樣的記憶體一般稱作緩沖區。簡單的說,緩沖區就是一塊連續的計算機記憶體區域,它可以儲存相同資料類型的多個執行個體,如字元數組。而緩沖區溢出則是指當計算機向緩沖區内填充資料位數時超過了緩沖區本身的容量,溢出的資料覆寫在合法資料上。
2 C/C++中記憶體配置設定
任何一個源程式通常都包括靜态的代碼段(或者稱為文本段)和靜态的資料段,為了運作程式,作業系統首先負責為其建立程序,并在程序的虛拟位址空間中為其代碼段和資料段建立映射。但是隻有靜态的代碼段和資料段是不夠的,程序在運作過程中還要有其動态環境。
一般說來,預設的動态存儲環境通過堆棧機制建立。所有局部變量及所有按值傳遞的函數參數都通過堆棧機制自動配置設定記憶體空間,配置設定同一資料類型相鄰塊的記憶體區域被稱為
緩沖區
。如下圖。

- 棧區(stack):由編譯器自動配置設定與釋放,存放為運作時函數配置設定的局部變量、函數參數、傳回資料、傳回位址等。其操作類似于資料結構中的棧。
- 堆區(heap):一般由程式員自動配置設定,如果程式員沒有釋放,程式結束時可能有OS回收。其配置設定類似于連結清單。
- 全局區(靜态區static):資料段,程式結束後由系統釋放。全局區分為已初始化全局區(data),用來存放儲存全局的和靜态的已初始化變量和未初始化全局區(bss),用來儲存全局的和靜态的未初始化變量。
- 常量區(文字常量區):資料段,存放常量字元串,程式結束後有系統釋放。
- 代碼區:存放函數體(類成員函數和全局區)的二進制代碼,這個段在記憶體中一般被标記為隻讀,任何對該區的寫操作都會導緻段錯誤(Segmentation Fault)。
需要特别注意的是,堆(Heap)和棧(Stack)是有差別的,很多程式員混淆堆棧的概念,或者認為它們就是一個概念。簡單來說,它們之間的主要差別可以表現在如下五個方面。
配置設定和管理方式不同
堆是動态配置設定的,其空間的配置設定和釋放都由程式員控制。也就是說,堆的大小并不固定,可動态擴張或縮減,其配置設定由
malloc()
等這類實時記憶體配置設定函數來實作。當程序調用
malloc
等函數配置設定記憶體時,新配置設定的記憶體就被動态添加到堆上(堆被擴張);當利用
free
等函數釋放記憶體時,被釋放的記憶體從堆中被剔除(堆被縮減)。
而棧由編譯器自動管理,其配置設定方式有兩種:靜态配置設定和動态配置設定。靜态配置設定由編譯器完成,比如局部變量的配置設定。動态配置設定由
alloca()
函數進行配置設定,但是棧的動态配置設定和堆是不同的,它的動态配置設定是由編譯器進行釋放,無需手工控制。
申請的大小限制不同
棧是向低位址擴充的資料結構,是一塊連續的記憶體區域,棧頂的位址和棧的最大容量是系統預先規定好的,能從棧獲得的空間較小。
堆是向高位址擴充的資料結構,是不連續的記憶體區域,這是由于系統是由連結清單在存儲空閑記憶體位址,自然堆就是不連續的記憶體區域,且連結清單的周遊也是從低位址向高位址周遊的,堆得大小受限于計算機系統的有效虛拟記憶體空間,
由此空間,堆獲得的空間比較靈活,也比較大。在 32 位平台下,VC6 下預設為 1M,堆最大可以到 4G;
申請效率不同
- 棧由系統自動配置設定,速度快,但是程式員無法控制。
- 堆是有程式員自己配置設定,速度較慢,容易産生碎片,不過用起來友善。
産生碎片不同
對堆來說,頻繁執行malloc或free勢必會造成記憶體空間的不連續,形成大量的碎片,使程式效率降低;而對棧而言,則不存在碎片問題。
記憶體位址增長的方向不同
- 堆是向着記憶體位址增加的方向增長的,從記憶體的低位址向高位址方向增長;
- 棧的增長方向與之相反,是向着記憶體位址減小的方向增長,由記憶體的高位址向低位址方向增長。
假設一個程式的函數調用順序為:主函數
main
調用函數
func1
,函數
func1
調用函數
func2
。當這個程式被作業系統調入記憶體運作時,其對應的程序在記憶體中的映射結果如下圖所示
程序的棧是由多個棧幀構成的,其中每個棧幀都對應一個函數調用。當調用函數時,新的棧幀被壓入棧;當函數傳回時,相應的棧幀從棧中彈出。由于需要将函數傳回位址這樣的重要資料儲存在程式員可見的堆棧中,是以也給系統安全帶來了極大的隐患。
當程式寫入超過緩沖區的邊界時,就會産生所謂的
“緩沖區溢出”
。發生緩沖區溢出時,就會覆寫下一個相鄰的記憶體塊,導緻程式發生一些不可預料的結果:也許程式可以繼續,也許程式的執行出現奇怪現象,也許程式完全失敗或者崩潰等。
緩沖區溢出
對于緩沖區溢出,一般可以分為4種類型,即棧溢出、堆溢出、BSS溢出與格式化串溢出。其中,棧溢出是最簡單,也是最為常見的一種溢出方式。
沒有保證足夠的存儲空間存儲複制過來的資料
void function(char *str)
{
char buffer[10];
strcpy(buffer,str);
}
上面的
strcpy()
将直接把
str
中的内容
copy
到
buffer
中。這樣隻要
str
的長度大于 10 ,就會造成
buffer
的溢出,使程式運作出錯。存在象
strcpy
這樣的問題的标準函數還有
strcat(),sprintf(),vsprintf(),gets(),scanf()
等。對應的有更加安全的函數,即在函數名後加上
_s
,如
scanf_s()
函數。
- 嚴格檢查輸入長度和緩沖區長度。
- 常見的高危函數
ble
dat
a-draft-node="block" data-draft-type="table" data-size="normal" data-row-style="normal">函數嚴重性防範手段
整數溢出
- 寬度溢出:把一個寬度較大的操作數賦給寬度較小的操作數,就有可能發生資料截斷或符号位丢失
#include<stdio.h>
int main()
{
signed int value1 = 10;
usigned int value2 = (unsigned int)value1;
}
- 算術溢出,該程式即使在接受使用者輸入的時候對a、b的指派做安全性檢查,a+b依舊可能溢出:
#include<stdio.h>
int main()
{
int a;
int b;
int c=a*b;
return 0;
}
數組索引不在合法範圍内
enum {TABLESIZE = 100};
int *table = NULL;
int insert_in_table(int pos, int value) {
if(!table) {
table = (int *)malloc(sizeof(int) *TABLESIZE);
}
if(pos >= TABLESIZE) {
return -1;
}
table[pos] = value;
return 0;
}
其中:
pos
為
int
類型,可能為負數,這會導緻在數組所引用的記憶體邊界之外進行寫入,可以将
pos
類型改為
size_
t避免
空字元錯誤
例如:
//錯誤
char array[]={'0','1','2','3','4','5','6','7','8'};
//正确的寫法應為:
char array[]={'0','1','2','3','4','5','6','7','8',’0’};
//或者
char array[11]={'0','1','2','3','4','5','6','7','8','9’};