[C/C++内存安全]_[中级]_[如何避免数组访问越界]

场景

  1. C/C++的标准在C++26以前还没支持内存安全的访问连续内存的类或特性。在开发分析内存数据或文件数据的程序时,经常需要把一段内存数据复制到另一个堆空间里。 这时目标内存空间由于起始地址的移动,剩余大小的计算错误,经常会导致访问越界错误。关键是C/C++的访问越界错误的行为是未定义的。 未定义的错误也不会立马导致程序崩溃,而是可能程序运行一段时间,某次访问越界操作访问了受保护的页面才会导致崩溃。那么,C/C++有办法防止这类访问越界操作吗?

说明

  1. 目前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();
  1. 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();
  1. 对于复制内存数据的函数,也就只有两个函数std::copymemcpy_s相对安全的函数。
  • std::copy在算法库<algorithm>里。它的作用是复制两个源枚举区间的数据到目标枚举。如果源枚举大小大于目标枚举所能容纳的大小,那么会在Debug模式时报断言错误cannot seek array iterator after end。缺点是Release模式并不会报错,而且不能设置目标枚举的长度。
     array<uint8_t, 3> arr1{}; // 初始化为0
    
     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);
    
  • memcpy_sC11添加的函数,多出了一个目标缓存的长度的参数。 但是这个函数对于源长度和目标长度实际上是否<=源缓存和目标缓存里有足够的长度并没有判断。即加入传错了大于实际长度的destszcount参数只会产生越界的未定义行为。
    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)
    
  1. 看完上边的说明,可以发现在上C++11上对数组越界行为并没有严格的保护,这样这些类和函数的安全性就降低很多,需要程序员自己花精力去计算数组长度。 实际上,如果一个数组能做好这两方面,就不会出现数组越界问题。一方面避免使用指针操作,使用方法和索引访问指定位置的内容;另一方面是对源数组和目标数组的传入长度进行越界判断后再进行复制操作。以下实现了一个安全数组SafeArray,可以避免数组访问越界问题。 使用它的内部复制方法,能记录已使用的数组空间和剩余的长度,避免越界。安全数组的目标是不会产生未定义的越界访问行为。
  • reset方法来重置已使用索引index_
  • copy方法可以判断destSize长度,当然传入的源sourceSize也是得使用SafeArray来管理可使用长度才不会越界。
  • begintotal方法可以获取数组的起始地址和长度。
  • currentremain是当前可用数组地址和剩余长度。

例子

#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

参考

  1. std::array

  2. memcpy, memcpy_s

  3. 如何编写内存安全的C++代码

  4. std::copy, std::copy_if

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Peter(阿斯拉达)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值