場景
C/C++
的標準在C++26
以前還沒支持內存安全的訪問連續內存的類或特性。在開發分析內存數據或文件數據的程序時,經常需要把一段內存數據復制到另一個堆空間里。 這時目標內存空間由于起始地址的移動,剩余大小的計算錯誤,經常會導致訪問越界錯誤。關鍵是C/C++
的訪問越界錯誤的行為是未定義的。 未定義的錯誤也不會立馬導致程序崩潰,而是可能程序運行一段時間,某次訪問越界操作訪問了受保護的頁面才會導致崩潰。那么,C/C++
有辦法防止這類訪問越界操作嗎?
說明
- 目前
C++11
的類std::array
靜態數組可以使用at()方法或者[]操作符
來訪問指定索引的內容,如果下標超過數組的長度會拋出std::out_of_range
異常。 也可以通過data()
訪問連續的內存,但是如果通過這種指針const T*
越界訪問時,不會拋出異常,是未定義行為。
array<uint8_t, 3> arr1{}; // 初始化為0
arr1[4] = 0; // 運行時拋出out_of_range異常
auto p1 = arr1.data();
- 而
C++11
的動態數組可以使用std::vector<uint8_t>
和std::string
來代替malloc()
函數。這兩個類都是支持at()
方法和下標[]
操作符,同樣也是支持訪問越界拋異常。 也支持data()
方法訪問連續內存,這種也是訪問越界時,未定義行為,不會拋出異常。
vector<uint8_t> as;
auto asData = as.data();string data(8, 0);
cout << data.size() << endl;string buf(3, 0);
buf.at(0) = 'a';
buf.at(1) = 'b';
buf.at(2) = 'c';auto myData = buf.data();
- 對于復制內存數據的函數,也就只有兩個函數
std::copy
和memcpy_s
相對安全的函數。
std::copy
在算法庫<algorithm>
里。它的作用是復制兩個源枚舉區間的數據到目標枚舉。如果源枚舉大小大于目標枚舉所能容納的大小,那么會在Debug
模式時報斷言錯誤cannot seek array iterator after end
。缺點是Release
模式并不會報錯,而且不能設置目標枚舉的長度。array<uint8_t, 3> arr1{}; // 初始化為0string buf(3, 0);buf.at(0) = 'a';buf.at(1) = 'b';buf.at(2) = 'c';auto myData = buf.data();// Debug運行時拋出"cannot seek array iterator after end"斷言錯誤。std::copy(buf.begin(), buf.end(), arr1.begin() + 1);
memcpy_s
是C11
添加的函數,多出了一個目標緩存的長度的參數。 但是這個函數對于源長度和目標長度實際上是否<=
源緩存和目標緩存里有足夠的長度并沒有判斷。即加入傳錯了大于實際長度的destsz
和count
參數只會產生越界的未定義行為。void* memcpy( void *dest, const void *src, size_t count );(until C99) errno_t memcpy_s( void *restrict dest, rsize_t destsz,const void *restrict src, rsize_t count );(since C11)
- 看完上邊的說明,可以發現在上
C++11
上對數組越界行為并沒有嚴格的保護,這樣這些類和函數的安全性就降低很多,需要程序員自己花精力去計算數組長度。 實際上,如果一個數組能做好這兩方面,就不會出現數組越界問題。一方面避免使用指針操作,使用方法和索引訪問指定位置的內容;另一方面是對源數組和目標數組的傳入長度進行越界判斷后再進行復制操作。以下實現了一個安全數組SafeArray
,可以避免數組訪問越界問題。 使用它的內部復制方法,能記錄已使用的數組空間和剩余的長度,避免越界。安全數組的目標是不會產生未定義的越界訪問行為。
reset
方法來重置已使用索引index_
。copy
方法可以判斷destSize
長度,當然傳入的源sourceSize
也是得使用SafeArray
來管理可使用長度才不會越界。begin
和total
方法可以獲取數組的起始地址和長度。current
和remain
是當前可用數組地址和剩余長度。
例子
#include <iostream>#include <array>
#include <assert.h>
#include <vector>
#include <string>
#include <functional>
#include <stdint.h>using namespace std;class SafeArray
{
public:SafeArray(int size) {buf_ = (uint8_t*)malloc(size+1);if (buf_) {memset(buf_, 0, size+1);size_ = size;}else {throw "Error allocate size memory.";}}~SafeArray() {free(buf_);}public:uint8_t* begin() {return buf_;}int total() {return size_;}void clear() {memset(buf_, 0, size_);}public:uint8_t& at(int index) {if (index < size_)return *(buf_ + index);string message("Index exceeds maximum limit!");message.append(" -> ").append(to_string(index));throw std::out_of_range(message);}uint8_t& operator [](int index) {return at(index);}operator uint8_t*(){return current();}uint8_t* current() {if (index_ >= size_)return NULL;return buf_ + index_;}int remain() {return size_ - index_;}int remain(int maxSize) {return min(remain(), maxSize);}void reset() {index_ = 0;}uint8_t* add(int number) {if ((index_ + number) < 0)return NULL;if ((index_ + number) > size_)return NULL;index_ += number;return buf_ + index_;}bool full() {return (index_ + 1) == size_;}int copy(uint8_t* dest, int destSize, uint8_t* source, int sourceSize) {if (!destSize || !sourceSize)return 0;auto lSize = remain();if (!lSize)return 0;if (destSize > lSize)destSize = lSize;if (sourceSize > destSize)sourceSize = destSize;if (memcpy_s(dest, destSize, source, sourceSize) == 0) {auto count = min(destSize, sourceSize);return (add(count))?count:0;}return 0;}int index() {return index_;}const char* c_str() {if (index_ == 0)return "";return (const char*)buf_;}private:uint8_t *buf_ = NULL;int size_ = 0;int index_ = 0;
};void TestDynamicArray()
{array<uint8_t, 3> arr1{}; // 初始化為0//arr1[4] = 0; // 運行時拋出異常auto p1 = arr1.data();vector<uint8_t> as;auto asData = as.data();string data(8, 0);cout << data.size() << endl;string buf(3, 0);buf.at(0) = 'a';buf.at(1) = 'b';buf.at(2) = 'c';auto myData = buf.data();// Debug運行時拋出"cannot seek array iterator after end"斷言錯誤。// std::copy(buf.begin(), buf.end(), arr1.begin() + 1);// 非安全方式1: 復制內存數據到動態數組,如果越界,會拋出out_of_range異常。memcpy_s(&data.at(0),3,buf.data(),buf.size());cout << data.c_str() << endl;// 安全方式auto sPos = 3;auto sizeSouce = &buf.at(sPos - 1) - buf.data() + 1;memcpy_s(&data.at(sPos),data.size() - sPos,buf.data(),sizeSouce);cout << data.c_str() << endl;}void TestDynamicArray2()
{SafeArray data(8);cout << data.total() << endl;SafeArray buf(3);buf.at(0) = 'a';buf.at(1) = 'b';buf.at(2) = 'c';// 1. 安全方式data.copy(data, data.remain(), buf, buf.total());cout << data.c_str() << endl;data[3] = 'd';cout << data.c_str() << endl;// 2. 繼續復制,用完剩余空間data.copy(data, data.remain(), buf, 3);cout << data.c_str() << endl;data.copy(data, data.remain(), buf, 3);cout << data.c_str() << endl;// 3. 目標空間已滿,不會再復制。data.copy(data, data.remain(), buf, 3);cout << data.c_str() << endl;// 4. 直接指定目標剩余空間超出最大容量,超過剩余大小,會使用剩余大小代替指定容量。data.copy(data, 100, buf, 3);cout << data.c_str() << endl;// 5. 重置緩存,重新使用; 目標長度如果超出剩余長度,會只使用剩余長度。data.reset();data.clear();data.copy(data, 100, buf, 3);cout << data.c_str() << endl;// 6. 越界訪問,會拋出異常try {data[100] = 'A';}catch (const std::out_of_range& e) {std::cerr << "Error: " << e.what() << std::endl;}}int main()
{std::cout << "Hello World!\n";TestDynamicArray();TestDynamicArray2();
}
輸出
Hello World!
8
abc
abcabc
8
abc
abcd
abcabc
abcabcab
abcabcab
abcabcab
abc
Error: Index exceeds maximum limit! -> 100
參考
-
std::array
-
memcpy, memcpy_s
-
如何編寫內存安全的C++代碼
-
std::copy, std::copy_if