緩沖區(qū)溢出
1 引言
“緩沖區(qū)溢出”對(duì)現(xiàn)代操作系統(tǒng)與編譯器來(lái)講已經(jīng)不是什么大問(wèn)題,但是作為一個(gè)合格的 C/C++ 程序員,還是完全有必要了解它的整個(gè)細(xì)節(jié)。
計(jì)算機(jī)程序一般都會(huì)使用到一些內(nèi)存,這些內(nèi)存或是程序內(nèi)部使用,或是存放用戶的輸入數(shù)據(jù),這樣的內(nèi)存一般稱作緩沖區(qū)。簡(jiǎn)單的說(shuō),緩沖區(qū)就是一塊連續(xù)的計(jì)算機(jī)內(nèi)存區(qū)域,它可以保存相同數(shù)據(jù)類(lèi)型的多個(gè)實(shí)例,如字符數(shù)組。而緩沖區(qū)溢出則是指當(dāng)計(jì)算機(jī)向緩沖區(qū)內(nèi)填充數(shù)據(jù)位數(shù)時(shí)超過(guò)了緩沖區(qū)本身的容量,溢出的數(shù)據(jù)覆蓋在合法數(shù)據(jù)上。
2 C/C++中內(nèi)存分配
任何一個(gè)源程序通常都包括靜態(tài)的代碼段(或者稱為文本段)和靜態(tài)的數(shù)據(jù)段,為了運(yùn)行程序,操作系統(tǒng)首先負(fù)責(zé)為其創(chuàng)建進(jìn)程,并在進(jìn)程的虛擬地址空間中為其代碼段和數(shù)據(jù)段建立映射。但是只有靜態(tài)的代碼段和數(shù)據(jù)段是不夠的,進(jìn)程在運(yùn)行過(guò)程中還要有其動(dòng)態(tài)環(huán)境。
一般說(shuō)來(lái),默認(rèn)的動(dòng)態(tài)存儲(chǔ)環(huán)境通過(guò)堆棧機(jī)制建立。所有局部變量及所有按值傳遞的函數(shù)參數(shù)都通過(guò)堆棧機(jī)制自動(dòng)分配內(nèi)存空間。如下圖。
-
棧區(qū)(stack):由編譯器自動(dòng)分配與釋放,存放為運(yùn)行時(shí)函數(shù)分配的局部變量、函數(shù)參數(shù)、返回?cái)?shù)據(jù)、返回地址等。其操作類(lèi)似于數(shù)據(jù)結(jié)構(gòu)中的棧。 -
堆區(qū)(heap):一般由程序員自動(dòng)分配,如果程序員沒(méi)有釋放,程序結(jié)束時(shí)可能有OS回收。其分配類(lèi)似于鏈表。 -
全局區(qū)(靜態(tài)區(qū)static):數(shù)據(jù)段,程序結(jié)束后由系統(tǒng)釋放。全局區(qū)分為已初始化全局區(qū)(data),用來(lái)存放保存全局的和靜態(tài)的已初始化變量和未初始化全局區(qū)(bss),用來(lái)保存全局的和靜態(tài)的未初始化變量。 -
常量區(qū)(文字常量區(qū)):數(shù)據(jù)段,存放常量字符串,程序結(jié)束后有系統(tǒng)釋放。 -
代碼區(qū):存放函數(shù)體(類(lèi)成員函數(shù)和全局區(qū))的二進(jìn)制代碼,這個(gè)段在內(nèi)存中一般被標(biāo)記為只讀,任何對(duì)該區(qū)的寫(xiě)操作都會(huì)導(dǎo)致段錯(cuò)誤(Segmentation Fault)。
需要特別注意的是,堆(Heap)和棧(Stack)是有區(qū)別的,很多程序員混淆堆棧的概念,或者認(rèn)為它們就是一個(gè)概念。簡(jiǎn)單來(lái)說(shuō),它們之間的主要區(qū)別可以表現(xiàn)在如下五個(gè)方面。
分配和管理方式不同
堆是動(dòng)態(tài)分配的,其空間的分配和釋放都由程序員控制。也就是說(shuō),堆的大小并不固定,可動(dòng)態(tài)擴(kuò)張或縮減,其分配由malloc()
等這類(lèi)實(shí)時(shí)內(nèi)存分配函數(shù)來(lái)實(shí)現(xiàn)。當(dāng)進(jìn)程調(diào)用malloc
等函數(shù)分配內(nèi)存時(shí),新分配的內(nèi)存就被動(dòng)態(tài)添加到堆上(堆被擴(kuò)張);當(dāng)利用free
等函數(shù)釋放內(nèi)存時(shí),被釋放的內(nèi)存從堆中被剔除(堆被縮減)。
而棧由編譯器自動(dòng)管理,其分配方式有兩種:靜態(tài)分配和動(dòng)態(tài)分配。靜態(tài)分配由編譯器完成,比如局部變量的分配。動(dòng)態(tài)分配由alloca()
函數(shù)進(jìn)行分配,但是棧的動(dòng)態(tài)分配和堆是不同的,它的動(dòng)態(tài)分配是由編譯器進(jìn)行釋放,無(wú)需手工控制。
申請(qǐng)的大小限制不同
棧是向低地址擴(kuò)展的數(shù)據(jù)結(jié)構(gòu),是一塊連續(xù)的內(nèi)存區(qū)域,棧頂?shù)牡刂泛蜅5淖畲笕萘渴窍到y(tǒng)預(yù)先規(guī)定好的,能從棧獲得的空間較小。
堆是向高地址擴(kuò)展的數(shù)據(jù)結(jié)構(gòu),是不連續(xù)的內(nèi)存區(qū)域,這是由于系統(tǒng)是由鏈表在存儲(chǔ)空閑內(nèi)存地址,自然堆就是不連續(xù)的內(nèi)存區(qū)域,且鏈表的遍歷也是從低地址向高地址遍歷的,堆的大小受限于計(jì)算機(jī)系統(tǒng)的有效虛擬內(nèi)存空間,
由此空間,堆獲得的空間比較靈活,也比較大。在 32 位平臺(tái)下,VC6 下默認(rèn)為 1M,堆最大可以到 4G;
申請(qǐng)效率不同
-
棧由系統(tǒng)自動(dòng)分配,速度快,但是程序員無(wú)法控制。 -
堆是有程序員自己分配,速度較慢,容易產(chǎn)生碎片,不過(guò)用起來(lái)方便。
產(chǎn)生碎片不同
對(duì)堆來(lái)說(shuō),頻繁執(zhí)行malloc或free勢(shì)必會(huì)造成內(nèi)存空間的不連續(xù),形成大量的碎片,使程序效率降低;而對(duì)棧而言,則不存在碎片問(wèn)題。
內(nèi)存地址增長(zhǎng)的方向不同
-
堆是向著內(nèi)存地址增加的方向增長(zhǎng)的,從內(nèi)存的低地址向高地址方向增長(zhǎng); -
棧的增長(zhǎng)方向與之相反,是向著內(nèi)存地址減小的方向增長(zhǎng),由內(nèi)存的高地址向低地址方向增長(zhǎng)。
假設(shè)一個(gè)程序的函數(shù)調(diào)用順序?yàn)椋褐骱瘮?shù)main
調(diào)用函數(shù)func1
,函數(shù)func1
調(diào)用函數(shù)func2
。當(dāng)這個(gè)程序被操作系統(tǒng)調(diào)入內(nèi)存運(yùn)行時(shí),其對(duì)應(yīng)的進(jìn)程在內(nèi)存中的映射結(jié)果如下圖所示
進(jìn)程的棧是由多個(gè)棧幀構(gòu)成的,其中每個(gè)棧幀都對(duì)應(yīng)一個(gè)函數(shù)調(diào)用。當(dāng)調(diào)用函數(shù)時(shí),新的棧幀被壓入棧;當(dāng)函數(shù)返回時(shí),相應(yīng)的棧幀從棧中彈出。由于需要將函數(shù)返回地址這樣的重要數(shù)據(jù)保存在程序員可見(jiàn)的堆棧中,因此也給系統(tǒng)安全帶來(lái)了極大的隱患。
當(dāng)程序?qū)懭氤^(guò)緩沖區(qū)的邊界時(shí),就會(huì)產(chǎn)生所謂的“緩沖區(qū)溢出”
。發(fā)生緩沖區(qū)溢出時(shí),就會(huì)覆蓋下一個(gè)相鄰的內(nèi)存塊,導(dǎo)致程序發(fā)生一些不可預(yù)料的結(jié)果:也許程序可以繼續(xù),也許程序的執(zhí)行出現(xiàn)奇怪現(xiàn)象,也許程序完全失敗或者崩潰等。
緩沖區(qū)溢出
對(duì)于緩沖區(qū)溢出,一般可以分為4種類(lèi)型,即棧溢出、堆溢出、BSS溢出與格式化串溢出。其中,棧溢出是最簡(jiǎn)單,也是最為常見(jiàn)的一種溢出方式。
沒(méi)有保證足夠的存儲(chǔ)空間存儲(chǔ)復(fù)制過(guò)來(lái)的數(shù)據(jù)
void function(char *str)
{
char buffer[10];
strcpy(buffer,str);
}
上面的strcpy()
將直接把str
中的內(nèi)容copy
到buffer
中。這樣只要str
的長(zhǎng)度大于 10 ,就會(huì)造成buffer
的溢出,使程序運(yùn)行出錯(cuò)。存在象strcpy
這樣的問(wèn)題的標(biāo)準(zhǔn)函數(shù)還有strcat(),sprintf(),vsprintf(),gets(),scanf()
等。對(duì)應(yīng)的有更加安全的函數(shù),即在函數(shù)名后加上_s
,如scanf_s()
函數(shù)。
-
嚴(yán)格檢查輸入長(zhǎng)度和緩沖區(qū)長(zhǎng)度。 -
常見(jiàn)的高危函數(shù)
函數(shù) | 嚴(yán)重性 | 防范手段 |
---|---|---|
gets() | 最危險(xiǎn) | 使用 fgets(buf, size, stdin) |
strcpy() | 很危險(xiǎn) | 改為使用 strncpy() |
strcat() | 很危險(xiǎn) | 改為使用 strncat() |
sprintf() | 很危險(xiǎn) | 改為使用snprintf(),或者使用精度說(shuō)明符 |
scanf() | 很危險(xiǎn) | 使用精度說(shuō)明符,或自己進(jìn)行解析 |
sscanf() | 很危險(xiǎn) | 使用精度說(shuō)明符,或自己進(jìn)行解析 |
fscanf() | 很危險(xiǎn) | 使用精度說(shuō)明符,或自己進(jìn)行解析 |
vfscanf() | 很危險(xiǎn) | 使用精度說(shuō)明符,或自己進(jìn)行解析 |
vfscanf() | 很危險(xiǎn) | 改為使用 vsnprintf(),或者使用精度說(shuō)明符 |
vscanf() | 很危險(xiǎn) | 使用精度說(shuō)明符,或自己進(jìn)行解析 |
vsscanf() | 很危險(xiǎn) | 使用精度說(shuō)明符,或自己進(jìn)行解析 |
streadd() | 很危險(xiǎn) | 使用精度說(shuō)明符,或自己進(jìn)行解析 |
整數(shù)溢出
-
寬度溢出:把一個(gè)寬度較大的操作數(shù)賦給寬度較小的操作數(shù),就有可能發(fā)生數(shù)據(jù)截?cái)嗷蚍?hào)位丟失
#include<stdio.h>
int main()
{
signed int value1 = 10;
usigned int value2 = (unsigned int)value1;
}
-
算術(shù)溢出,該程序即使在接受用戶輸入的時(shí)候?qū)、b的賦值做安全性檢查,a+b 依舊可能溢出:
#include<stdio.h>
int main()
{
int a;
int b;
int c=a*b;
return 0;
}
數(shù)組索引不在合法范圍內(nèi)
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
類(lèi)型,可能為負(fù)數(shù),這會(huì)導(dǎo)致在數(shù)組所引用的內(nèi)存邊界之外進(jìn)行寫(xiě)入,可以將pos
類(lèi)型改為size_
t避免
空字符錯(cuò)誤
例如:
//錯(cuò)誤
char array[]={'0','1','2','3','4','5','6','7','8'};
//正確的寫(xiě)法應(yīng)為:
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’};
點(diǎn)【在看】是最大的支持
免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問(wèn)題,請(qǐng)聯(lián)系我們,謝謝!