C語言緩衝區溢出詳解

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() 函數。
  • 嚴格檢查輸入長度和緩衝區長度。
  • 常見的高危函數
函數
嚴重性
防範手段



gets()
最危險
使用 fgets(buf, size, stdin)
strcpy()
很危險
改成使用 strncpy()
strcat()
很危險
改成使用 strncat()
sprintf()
很危險
改成使用snprintf(),或者使用精度說明符
scanf()
很危險
使用精度說明符,或本身進行解析
sscanf()
很危險
使用精度說明符,或本身進行解析
fscanf()
很危險
使用精度說明符,或本身進行解析
vfscanf()
很危險
使用精度說明符,或本身進行解析
vfscanf()
很危險
改成使用 vsnprintf(),或者使用精度說明符
vscanf()
很危險
使用精度說明符,或本身進行解析
vsscanf()
很危險
使用精度說明符,或本身進行解析
streadd()
很危險
使用精度說明符,或本身進行解析

整數溢出

  1. 寬度溢出:把一個寬度較大的操做數賦給寬度較小的操做數,就有可能發生數據截斷或符號位丟失
   
#include<stdio.h>

int main()
{
  signed int value1 = 10;
  usigned int value2 = (unsigned int)value1;
}
  1. 算術溢出,該程序即便在接受用戶輸入的時候對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’};

點【在看】是最大的支持 

本文分享自微信公衆號 - C語言入門到精通(c_printf)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。程序員

相關文章
相關標籤/搜索