C和C++安全编码笔记:动态内存管理

2023-10-30

4.1 C内存管理:

C标准内存管理函数:

(1).malloc(size_t size):分配size个字节,并返回一个指向分配的内存的指针。分配的内存未被初始化为一个已知值。

(2).aligned_alloc(size_t alignment, size_t size):为一个对象分配size个字节的空间,此对象的对齐方式是alignment指定的。alignment的值必须是实现支持的一种有效的对齐方式,size的值必须是alignment的整数倍,否则行为就是未定义的。此函数返回一个指向分配的空间的指针,如果分配失败,则返回一个空指针。

(3).realloc(void* p, size_t size):将p所指向的内存块的大小改为size个字节。新大小和旧大小中较小的值那部分内存所包含的内容不变,新分配的内存未作初始化,因此将有不确定的值。如果内存请求不能被成功分配,那么旧的对象保持不变,而且没有值被改变。如果p是一个空指针,则该调用等价于malloc(size);如果size等于0,则该调用等价于free(p),但这种释放内存的用法应该避免。

(4).calloc(size_t nmemb, size_t size):为数组分配内存,(该数组共有nmemb个元素,每个元素的大小为size个字节)并返回一个指向所分配的内存的指针。所分配的内存的内容全部被设置为0。

内存分配函数返回一个指向分配的内存的指针,这块内存是按照任何对象类型恰当地对齐的,如果请求失败,则返回一个空指针。连续调用内存分配函数分配的存储空间的顺序和邻接是不确定的。所分配的对象的生存期从分配开始,到释放时结束。返回的指针指向所分配的空间的起始地址(最低字节地址)。

free(void* p):释放由p指向的内存空间,这个p必须是先前通过调用aligned_alloc、malloc、calloc或realloc返回的。如果引用的内存不是被这些函数之一分配的或free(p)此前已经被调用过,将会导致未定义行为。如果p是一个空指针,则不执行任何操作

由C内存分配函数分配的对象有分配存储期限。存储期限是一个对象的属性,它定义了包含该对象存储的最低潜在生存期。这些对象的生存期并不限于创建它的范围内,因此,如果在一个函数内调用malloc,那么在该函数返回后,已分配的内存仍然存在。

对齐:完整的对象类型有对齐(alignment)要求,这种要求对可以分配该类型对象的地址施加限制。对齐是实现定义的整数值,它表示可以在一个连续的地址之间分配给指定的对象的字节数量。对象类型规定了每一个该类型对象的对齐要求。消除对齐要求,往往需要生成代码进行跨字边界域访问,或从更慢的奇数地址访问,从而减慢内存访问。

完整的对象(complete object):对象中可以包含其它对象,被包含的对象称为子对象(subobject)。子对象可以是成员子对象、基类子对象,或数组元素。如果一个对象不是任何其它对象的子对象,那么它被称为一个完整的对象。

对齐有一个从较弱的对齐到较强的对象(或更严格的对齐)的顺序。越严格的对齐,其对齐值越大。一个地址满足某一种对齐要求,也满足任何更弱的有效对齐要求。char、signed char、unsigned char类型的对齐要求最弱。对齐表示为size_t类型的值。每个有效对齐值都是2的一个非负整数幂。有效的对齐包括基本类型的对齐,加上额外的一组可选的实现定义的值。

基本对齐(fundamental alignment)小于或等于在所有上下文中由编译器支持的最大对齐。max_align_t类型的对齐与在所有上下文中由编译器支持的对齐大小相同。扩展对齐(extended alignment)大于max_align_t类型的对齐。一个具有扩展对齐要求的类型也称为超对齐(overaligned)类型。每个超对齐类型,要么是一个结构或联合类型,其中一个成员已应用扩展对齐,要么它包含这样的类型。如果实现支持,可以使用aligned_alloc函数分配比正常更严格的对齐的内存。如果一个程序要求比alignof(max_align_t)更大的对齐,那么这个程序是不可移植的,因为对超对齐类型的支持是可选的。

void test_aligned_alloc()
{
	const int arr_size = 11;
	// 分配16字节对齐的数据
#ifdef _MSC_VER
	float* array = (float*)_aligned_malloc(16, arr_size * sizeof(float));
#else
	float* array = (float*)aligned_alloc(16, arr_size * sizeof(float));
#endif
	auto addr = std::addressof(array);
	fprintf(stdout, "pointer addr: %p\n", addr);

	fprintf(stdout, "char alignment: %d, float alignment: %d, max_align_t alignment: %d\n",
		alignof(char), alignof(float), alignof(max_align_t));
}

在C标准中引入_Alignas关键字和aligned_alloc函数的主要理由是支持单指令多数据(SIMD)计算。

4.2 常见的C内存管理错误:常见的与内存管理相关的编程缺陷包括:初始化错误、未检查返回值、对空指针或无效指针解引用、引用已释放的内存、对同一块内存释放多次、内存泄漏和零长度分配。

初始化错误:由malloc函数返回的空间中的值是不确定的。一个常见的错误是不正确地假设malloc把分配的内存的所有位都初始化为零。

// 读取未初始化的内存
void test_memory_init_error()
{
	// 初始化大的内存块可能会降低性能并且不总是必要的.
	// C标准委员会决定不需要malloc来初始化这个内存,而把这个决定留给程序员
	int n = 5;
	int* y = static_cast<int*>(malloc(n * sizeof(int)));
	int A[] = {1, 2, 3, 4, 5};

	for (int i = 0; i < n; ++i) {
		y[i] += A[i];
	}

	std::for_each(y, y+n, [](int v) { fprintf(stdout, "value: %d\n", v); });
	free(y);
}

不要假定内存分配函数初始化内存。不要引用未初始化的内存

清除或覆写内存通常是通过调用C标准的memset函数来完成的。遗憾的是,如果不在写后访问内存,编译器优化可能会默默地删除对memset函数的调用。

未检查返回值:内存分配函数的返回值表示分配失败或成功。如果请求的内存分配失败,那么aligned_alloc、calloc、malloc和realloc函数返回空指针

// 检查malloc的返回值
int* test_memory_return_value()
{
	// 如果不能分配请求的空间,那么C内存分配函数将返回一个空指针
	int n = 5;
	int* ptr = static_cast<int*>(malloc(sizeof(int) * n));
	if (ptr != nullptr) {
		memset(ptr, 0, sizeof(int) * n);
	} else {
		fprintf(stderr, "fail to malloc\n");
		return nullptr;
	}

	return ptr;
}

Null或无效指针解引用:用一元操作符”*”解引用的指针的无效值包括:空指针、未按照指向的对象类型正确对齐的地址、生存期结束后的对象的地址。

空指针的解引用通常会导致段错误,但并非总是如此。许多嵌入式系统有映射到地址0处的寄存器,因此覆写它们会产生不可预知的后果。在某些情况下,解引用空指针会导致任意代码的执行

引用已释放内存:除非指向某块内存的指针已设置为NULL或以其它方式被覆写,否则就有可能访问已被释放的内存。

// 引用已释放内存
void test_memory_reference_free()
{
	int* x = static_cast<int*>(malloc(sizeof(int)));
	*x = 100;
	free(x);
	// 从已被释放的内存读取是未定义的行为
	fprintf(stderr, "x: %d\n", *x);
	// 写入已经被释放的内存位置,也不大可能导致内存故障,但可能会导致一些严重的问题
	*x = -100;
	fprintf(stderr, "x: %d\n", *x);
}

从已被释放的内存读取是未定义的行为,但在没有内存故障时几乎总能成功,因为释放的内存是被内存管理器回收的。然而,并不保证内存的内容没有被篡改过。虽然free函数调用通常不会擦除内存,但内存管理器可能使用这个空间的一部分来管理释放或未分配的内存。如果内存块已被重新分配,那么其内容可能已经被全部替换。其结果是,这些错误可能检测不出来,因为内存中的内容可能会在测试过程中被保留,但在运行过程中被修改。

写入已经被释放的内存位置,也不太可能导致内存故障,但可能会导致一些严重的问题。如果该块内存已被重新分配,程序员就可以覆写此内存,一个内存块是专门(dedicated)为一个特定的变量分配的,但在现实中,它是被共享(shared)的。在这种情况下,该变量中包含最后一次写入的任何数据。如果那块内存没有被重新分配,那么写入已释放的块可能会覆写并损坏内存管理器所使用的数据结构。

多次释放内存:最常见的场景是两次释放(double-free)。这个错误是危险的,因为它会以一种不会立即显现的方式破坏内存管理器中的数据结构。

// 多次释放内存
void test_memory_multi_free()
{
	int* x = static_cast<int*>(malloc(sizeof(int)));
	free(x);
	// 多次释放相同的内存会导致可以利用的漏洞
	free(x);
}

内存泄漏:当动态分配的内存不再需要后却没有被释放时,就会发生内存泄漏。

零长度分配:C标准规定:如果所要求的空间大小是零,其行为是实现定义的:要么返回一个空指针,要么除了不得使用返回的指针来访问对象以外,行为与大小仿佛是某个非零值。

// 零长度分配:不要执行零长度分配
void test_memory_0_byte_malloc()
{
	char* p1 = static_cast<char*>(malloc(0));
	fprintf(stderr, "p1 pointer: %p\n", std::addressof(p1)); // 是不确定的
	free(p1);

	p1 = nullptr;
	char* p2 = static_cast<char*>(realloc(p1, 0));
	fprintf(stderr, "p2 pointer: %p\n", std::addressof(p2)); // 是不确定的
	free(p2);
	
	int nsize = 10;
	char* p3 = static_cast<char*>(malloc(nsize));
	char* p4 = nullptr;
	// 永远不要分配0个字节
	if ((nsize == 0) || (p4 = static_cast<char*>(realloc(p3, nsize))) == nullptr) {
		free(p3);
		p3 = nullptr;
		return;
	}

	p3 = p4;
	free(p3);
}

此外,要求分配0字节时,成功调用内存分配函数分配的存储量是不确定的。在内存分配函数返回一个非空指针的情况下,读取或写入分配的内存区域将导致未定义的行为。通常情况下,指针指向一个完全由控制结构组成的零长度的内存块。覆写这些控制结构损害内存所使用的数据结构。

realloc函数将释放旧对象,并返回一个指针,它指向一个具有指定大小的新对象。然而,如果不能为新对象分配内存,那么它就不释放旧对象,而且旧对象的值是不变的。正如malloc(0),realloc(p, 0)的行为是实现定义的。

不要执行零长度分配

4.3 C++的动态内存管理:在C++中,使用new表达式分配内存并使用delete表达式释放内存。C++的new表达式分配足够的内存来保存所请求类型的对象,并可以初始化所分配的内存中的对象。

new表达式是构造一个对象的唯一方法,因为不可能显示地调用构造函数。分配的对象类型必须是一个完整的对象类型,并且不可以(例如)是一个抽象类类型或一个抽象类数组。对于非数组对象,new表达式返回一个指向所创建的对象的指针;对于数组,它返回一个指向数组初始元素的指针。new表达式分配的对象有动态存储期限(dynamic storage duration)。存储期限定义了该对象包含的存储的生存期。使用动态存储的对象的生存期,不局限于创建该对象所在的范围。

如果提供了初始化参数(即类的构造函数的参数,或原始整数类型的合法值),那么由new操作符所分配的内存被初始化。只有一个空的new-initializer()存在时,”普通的旧数据”(POD)类型的对象是new默认初始化(清零)的。这包括所有的内置类型。

void test_memory_new_init()
{
	// 包括所有的内置类型
	int* i1 = new int(); // 已初始化
	int* i2 = new int; // 未初始化
	fprintf(stdout, "i1: %d, i2: %d\n", *i1, *i2);

	// 就地new没有实际分配内存,所以该内存不应该被释放
	int* i3 = new (i1) int;
	fprintf(stdout, "i3: %d\n", *i3);

	delete i1;
	delete i2;

	// 通常情况下,分配函数无法分配存储时抛出一个异常表示失败
	int* p1 = nullptr;
	try {
		p1 = new int;
	} catch (std::bad_alloc) {
		fprintf(stderr, "fail to new\n");
		return;
	}
	delete p1;

	// 用std::nothrow参数调用new,当分配失败时,分配函数不会抛出一个异常,它将返回一个空指针
	int* p2 = new(std::nothrow) int;
	if (p2 == nullptr) {
		fprintf(stderr, "fail to new\n");
		return;
	}
	delete p2;
}

就地new(placement new)是另一种形式的new表达式,它允许一个对象在任意内存位置构建。就地new需要在指定的位置有足够的内存可用。因为就地new没有实际分配内存,所以该内存不应该被释放。

分配函数:必须是一个类的成员函数或全局函数,不能在全局范围以外的命名空间范围中声明,并且不能把它在全局范围内声明为静态的。分配函数的返回类型是void*。

分配函数试图分配所请求的存储量。如果分配成功,则它返回存储块的起始地址,该块的长度(以字节为单位)至少为所要求的大小。分配函数返回的分配的存储空间内容没有任何限制。连续调用分配函数分配的存储的顺序、连续性、初始值都是不确定的。返回的指针是适当地对齐的,以便它可以被转换为任何具有基本对齐要求的完整对象类型的指针,并在之后用于访问所分配的存储中的对象或数组(直到调用一个相应的释放函数显示释放这块存储)。即使所请求的空间大小为零,请求也可能会失败。如果请求成功,那么返回值是一个非空指针值。如果一个指针是大小为零的请求返回的,那么对它解引用的效果是未定义的。C++在发起一个为零的请求时行为与C不同,它返回一个非空指针

通常情况下,分配函数无法分配存储时抛出一个异常表示失败,这个异常将匹配类型为std::bad_alloc的异常处理器。

如果用std::nothrow参数调用new,当分配失败时,分配函数不会抛出一个异常。相反,它将返回一个空指针

当一个异常被抛出时,运行时机制首先在当前范围内搜索合适的处理器。如果当前范围内没有这样的处理程序存在,那么控制权将由当前范围转移到调用链中的一个更高的块。这个过程一直持续,直到找到一个合适的处理程序为止。如果在任何级别中都没有处理程序捕获该异常,那么std::terminate函数被自动调用。默认情况下,terminate调用标准C库函数abort,它会突然退出程序。当abort被调用时,没有正常的程序终止函数调用发生,这意味着全局和静态对象的析构函数不执行。

在C++中,处理分配和分配失败的标准惯用法是资源获取初始化(Resource Acquisition Is Initializatiton, RAII)。RAII运用C++的对象生存期概念控制程序资源,如内存、文件句柄、网络连接、审计跟踪等。要保持对资源的跟踪,只要创建一个对象,并把资源的生存期关联到对象的生存期即可。这使你可以使用C++对象管理设施来管理资源。其最简单的形式是,创建一个对象,它在构造函数获得资源,并在其析构函数中释放资源。

class intHandle {
public:
	explicit intHandle(int* anInt) : i_(anInt) {} // 获取资源
	~intHandle() { delete i_; } // 释放资源

	intHandle& operator=(const int i)
	{
		*i_ = i;
		return *this;
	}

	int* get() { return i_; } // 访问资源

private:
	intHandle(const intHandle&) = delete;
	intHandle& operator=(const intHandle&) = delete;
	int* i_;
};

// 资源获取初始化(Resource Acquisition Is Initialization, RAII)
void test_memory_arii()
{
	intHandle ih(new int);
	ih = 5;
	fprintf(stdout, "value: %d\n", *ih.get());

	// 使用std::unique_ptr能完成同样的事情,而且更简单
	std::unique_ptr<int> ip(new int);
	*ip = 5;
	fprintf(stdout, "value: %d\n", *ip.get());
}

如果发生下列任何情况,new表达式抛出std::bad_array_new_length异常,以报告无效的数组长度:(1).数组的长度为负;(2).新数组的总大小超过实现定义的最大值;(3).在一个大括号初始化列表中的初始值设定子句数量超过要初始化的元素数量(即声明的数组的大小)。只有数组的第一个维度可能会产生这个异常,除第一个维度外的维度都是常量表达式,它们在编译时检查。

// 抛出std::bad_array_new_length的三种情况
void test_memory_bad_array_new_length()
{
	try {
		int negative = -1;
		new int[negative]; // 大小为负
	} catch(const std::bad_array_new_length& e) {
		fprintf(stderr, "1: %s\n", e.what());
	}

	try {
		int small = 1;
		new int[small]{1, 2, 3}; // 过多的初始化值设定
	} catch(const std::bad_array_new_length& e) {
		fprintf(stderr, "2: %s\n", e.what());
	}

	try {
		int large = INT_MAX;
		new int[large][1000000]; // 过大
	} catch(const std::bad_alloc& e) {
		fprintf(stderr, "3: %s\n", e.what());
	}
}

释放函数:是类的成员函数或全局函数,在一个全局范围以外的命名空间范围声明释放函数或全局范围内声明静态的释放函数都是不正确的。每个释放函数都返回void,并且它的第一个参数是void*。对于这些函数的两个参数形式,第一个参数是一个指向需要释放的内存块的指针,第二个参数是要释放的字节数。这种形式可能被用于从基类中删除一个派生类对象。提供给一个释放函数的第一个参数的值可以是一个空指针值,如果是这样的话,并且如果释放函数是标准库提供的,那么该调用没有任何作用

如果提供给标准库中的一个释放函数的参数是一个指针,且它不是空指针值,那么释放函数释放该指针所引用的存储,引用指向已释放存储(deallocated storage)的任何部分的所有指针无效。使用无效的指针值(包括将它传递给一个释放函数)产生的影响是未定义的。

垃圾回收:在C++中,垃圾回收(自动回收不再被引用的内存区域)是可选的,也就是说,一个垃圾回收器(Garbage Collector, GC)不是必须的。一个垃圾回收器必须能够识别动态分配的对象的指针,以便它可以确定哪些对象可达(reachable),不应该被回收,哪些对象不可达(unreachable),并可以回收。

4.4 常见的C++内存管理错误:常见的与内存管理相关的编程缺陷,包括未能正确处理分配失败、解引用空指针、写入已经释放的内存、对相同的内存释放多次、不当配对的内存管理函数、未区分标量和数组,以及分配函数使用不当。

未能正确检查分配失败:new表达式要么成功要么抛出一个异常。new操作符的nothrow形式在失败时返回一个空指针,而不是抛出一个异常。

// 未能正确检查分配失败
void test_memory_new_wrong_usage()
{
	// new表达式,要么成功,要么抛出一个异常
	// 意味着,if条件永远为真,而else子句永远不会被执行
	int* ip = new int;
	if (ip) { // 条件总是为真
		
	} else {
		// 将永远不执行
	}
	delete ip;

	// new操作符的nothrow形式在失败时返回一个空指针,而不是抛出一个异常
	int* p2 = new(std::nothrow)int;
	if (p2) {
		delete p2;
	} else {
		fprintf(stderr, "fail to new\n");
	}
}

不正确配对的内存管理函数:使用new和delete而不是原始的内存分配和释放。C内存释放函数std::free不应该被用于由C++内存分配函数分配的资源,C++内存释放操作符和函数也不应该被用于由C内存分配函数分配的资源。C++的内存分配和释放函数分配和释放内存的方式可能不同于C内存分配和释放函数分配和释放内存的方式。因此,在同一资源上混合调用C++内存分配和释放函数及C内存分配和释放函数是未定义的行为,并可能会产生灾难性的后果。

class Widget {};

// 不正确配对的内存管理函数
void test_memory_new_delete_unpaired()
{
	int* ip = new int(12);
	free(ip); // 错误,应使用delete ip

	int* ip2 = static_cast<int*>(malloc(sizeof(int)));
	*ip2 = 12;
	delete ip2; // 错误,应使用free(ip2)

	// new和delete操作符用于分配和释放单个对象
	Widget* w = new Widget();
	delete w;

	// new[]和delete[]操作符用于分配和释放数组
	Widget* w2 = new Widget[10];
	delete [] w2;

	// operator new()分配原始内存,但不调用构造函数
	std::string* sp = static_cast<std::string*>(operator new(sizeof(std::string)));
	//delete sp; // 错误
	operator delete (sp); // 正确
}

在C++的new表达式分配的对象上调用free,因为free不会调用对象的析构函数。这样的调用可能会导致内存泄漏、不释放锁或其它问题,因为析构函数负责释放对象所使用的资源。

new和delete操作符用于分配和释放单个对象;new[]和delete[]操作符用于分配和释放数组。

当分配单个对象时,先调用operator new()函数来分配对象的存储空间,然后调用其构造函数来初始化它。当一个对象被删除时,首先调用它的析构函数,然后调用相应的operator delete()函数来释放该对象所占用的内存。当分配一个对象数组时,先调用operator new[]()对整个数组分配存储空间,随后调用对象的构造函数来初始化数组中的每个元素。当删除一个对象数组时,首先调用数组中的每个对象的析构函数,然后调用operator delete[]()释放整个数组所占用的内存。要将operator delete()与operator new()一起使用,并将operator delete[]()与operator new[]()一起使用。如果试图使用operator delete()删除整个数组,只有数组的第一个元素所占用的内存将被释放,一个明显的内存泄漏可能会导致被利用。

new和operator new():可以直接调用operator new()分配原始内存,但不调用构造函数

函数operator new()、operator new[]()、operator delete()和operator delete[]()都可以被定义为成员函数。它们是隐藏继承的或命名空间范围中同名函数的静态成员函数。与其它内存管理函数一样,重要的是让它们正确配对。如果对象所使用的内存不是通过调用operator new()获得的,同时对其使用operator delete(),就可能发生内存损坏。

多次释放内存:智能指针是一个类类型(class type),它具有重载的”->”和”*”操作符以表现得像指针。比起原始指针,智能指针往往是一种更安全的选择,因为它们可以提供原始指针中不存在的增强行为,如垃圾回收、检查空,而且防止使用在特定情况下不合适或危险的原始指针操作(如指针算术和指针复制)。引用计数智能指针对它们所引用的对象的引用计数进行维护。当引用计数为零时,该对象就被销毁。

释放函数抛出一个异常:如果释放函数通过抛出一个异常终止,那么该行为是未定义的。释放函数,包括全局的operator delete()函数,它的数组形式与其用户定义的重载,经常在销毁类类型的对象时被调用,其中包括作为某个异常结果的栈解开。允许栈解开期间抛出异常导致逃避了调用std::terminate,而导致std::abort函数调用的默认效果。这种情况可能被利用为一种拒绝服务攻击的机会。因此,释放函数必须避免抛出异常

4.5 内存管理器:既管理已分配的内存,也管理已释放的内存。在大多数操作系统中,包括POSIX系统和Windows,内存管理器作为客户进程的一部分运行。分配给客户进程的内存,以及供内部使用而分配的内存,全部位于客户进程的可寻址内存空间内。操作系统通常提供内存管理器作为它的一部分(通常是libc的一部分)。在较不常见的情况下,编译器也可以提供替代的内存管理器。内存管理器可以被静态链接在可执行文件中,也可以在运行时确定。

4.6 Doug Lea的内存分配器:GNU C库和大多数Linux版本(例如Red Hat、Debian)都是将Doug Lea的malloc实现(dlmalloc)作为malloc的默认原生版本。Doug Lea独立地发布了dlmalloc,其他一些人对其作了修改并用作GNU libc的分配器。

动态分配的内存也可能遭遇缓冲区溢出。例如,缓冲区溢出可被用于破坏内存管理器所使用的数据结构从而能够执行任意的代码。

解链(unlink)技术:最早由Solar Designer提出。unlink技术被用于利用缓冲区溢出来操纵内存块的边界标志,以欺骗unlink()宏向任意位置写入4字节数据。

4.7 双重释放漏洞:Doug Lea的malloc还易于导致双重释放漏洞。这种类型的漏洞是由于对同一块内存释放两次造成的(在这两次释放之间没有对内存进行重新分配)。要成功地利用双重释放漏洞,有两个条件必须满足:被释放的内存块必须在内存中独立存在(也就是说,其相邻的内存块必须是已分配的,这样就不会发生合并操作了),并且该内存所被放入的筐(在dlmalloc中,空闲块被组织成环形双链表,或筐(bin))必须为空。

写入已释放的内存:一个常见的安全缺陷。

RtlHeap:并非只有使用dlmalloc开发的应用程序才可能存在基于堆的漏洞。使用微软RtlHeap开发的应用程序在内存管理API被误用时也有可能被利用。与大多数软件一样,RtlHeap也在不断地进化,不同的Windows版本通常都有不同的RtlHeap实现,它们的行为稍有不同。

4.8 缓解策略:有很多缓解措施可以用来消除或减少基于堆的漏洞。

空指针:一个明显的可以减少C和C++程序中漏洞数量的技术就是在指针所引用的内存被释放后,将此指针设置为NULL。空悬指针(执行已释放内存的指针)可能导致赋写已释放内存和双重释放漏洞。将指针置为NULL后,任何企图解引用该指针的操作都会导致致命的错误,这样就增加了在实现和测试过程中发现问题的几率。并且,如果指针被设置为NULL,内存可以被”释放”多次而不会导致不良后果

一致的内存管理约定:(1).使用同样的模式分配和释放内存;(2).在同一个模块中,在同一个抽象层次中分配和释放内存;(3).让分配和释放配对。

随机化:传统的malloc函数调用返回的内存分配地址在很大程度上是可预测的。通过让内存管理程序返回的内存块地址随机化,可以使对基于堆的漏洞利用变得更加困难。

运行时分析工具:Valgrind、Purify、Insure++、Application Verifier。

GitHubhttps://github.com/fengbingchun/Messy_Test

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

C和C++安全编码笔记:动态内存管理 的相关文章

  • C++11中enum class的使用

    枚举类型 enumeration 使我们可以将一组整型常量组织在一起 和类一样 每个枚举类型定义了一种新的类型 枚举属于字面值常量类型 C 包含两种枚举 限定作用域的和不限定作用域的 这里主要介绍限定作用域的 不限定作用域的使用可以参考 h
  • 关于C/C++动态申请空间释放和内存泄漏问题介绍

    1 动态申请空间 1 1 基本内容 动态申请的空间没有具体名称 只能通过指针间接访问 无论new还是malloc方式 动态申请空间都是存放在堆中 有别于系统自动分配的空间是存放在堆栈中 即栈 栈中系统分配的空间 在使用结束会自动释放 而程序
  • C和C++安全编码笔记:整数安全

    5 1 整数安全导论 整数由包括0的自然数 0 1 2 3 和非零自然数的负数 1 2 3 构成 5 2 整数数据类型 整数类型提供了整数数学集合的一个有限子集的模型 一个具有整数类型的对象的值是附着在这个对象上的数学值 一个具有整数类型的
  • C语言中的弱符号与强符号介绍

    弱符号 Weak symbol 是链接器 ld 在生成ELF Executable and Linkable Format 缩写为ELF 可执行和可链接格式 是一种用于可执行文件 目标文件 共享库和核心转储的标准文件格式 ELF文件有两种索
  • C++中namespace detail或namespace internal的使用

    在很多开源代码中偶尔会使用名字为 detail 或 internal 的命名空间 如OpenCV的modules目录中 有些文件中使用了namespace detail 有些文件中使用了namespace internal 名为detail
  • 在Ubuntu 18.04上支持C++17的std::filesystem的方法

    在Ubuntu 18 04上通过命令sudo apt install gcc g 安装的gcc g 版本为7 5 此版本并不直接支持filesystem 如下图所示 Ubuntu 18 04上的g 7 5支持experimental的fil
  • C++11容器中新增加的emplace相关函数的使用

    C 11中 针对顺序容器 如vector deque list 新标准引入了三个新成员 emplace front emplace和emplace back 这些操作构造而不是拷贝元素 这些操作分别对应push front insert和p
  • log库spdlog简介及使用

    spdlog是一个开源的 快速的 仅有头文件的C 11 日志库 code地址在 https github com gabime spdlog 目前最新的发布版本为0 14 0 它提供了向流 标准输出 文件 系统日志 调试器等目标输出日志的能
  • 提高C++性能的编程技术笔记:多线程内存池+测试代码

    为了使多个线程并发地分配和释放内存 必须在分配器方法中添加互斥锁 全局内存管理器 通过new 和delete 实现 是通用的 因此它的开销也非常大 因为单线程内存管理器要比多线程内存管理器快的多 所以如果要分配的大多数内存块限于单线程中使用
  • 提高C++性能的编程技术笔记:引用计数+测试代码

    引用计数 reference counting 基本思想是将销毁对象的职责从客户端代码转移到对象本身 对象跟踪记录自身当前被引用的数目 在引用计数达到零时自行销毁 换句话说 对象不再被使用时自行销毁 引用计数和执行速度之间的关系是与上下文紧
  • 程序员的自我修养--链接、装载与库笔记:Linux共享库的组织

    共享库 Shared Library 概念 其实从文件结构上来讲 共享库和共享对象没什么区别 Linux下的共享库就是普通的ELF共享对象 由于共享对象可以被各个程序之间共享 所以它也就成为了库的很好的存在形式 很多库的开发者都以共享对象的
  • 内存检测工具Dr. Memory的使用

    Dr Memory是一个内存调试工具 它是一个开源免费的内存检测工具 它能够及时发现内存相关的编程错误 比如未初始化访问 内存非法访问 数组越界读 写 以及内存泄露等 它可以在Linux Windows Mac OS和Android操作系统
  • json11库的使用

    JSON JavaScript Object Notation 是一种轻量级的文本数据交换格式 易于让人阅读 同时也易于机器解析和生成 尽管JSON是Javascript的一个子集 但JSON是独立于语言的文本格式 并且采用了类似于C语言家
  • C++14中返回类型推导的使用

    使用C 14中的auto返回类型 编译器将尝试自动推导 deduce 返回类型 namespace int xx 1 auto f return xx return type is int const auto f3 return xx r
  • CSV文件简介及C++实现

    逗号分隔值 Comma Separated Values CSV 有时也称为字符分隔值 因为分隔字符也可以不是逗号 其文件以纯文本形式存储表格数据 数字和文本 纯文本意味着该文件是一个字符序列 不含必须象二进制数字那样被解读的数据 CSV文
  • 程序员的自我修养--链接、装载与库笔记:目标文件里有什么

    编译器编译源代码后生成的文件叫做目标文件 目标文件从结构上讲 它是已经编译后的可执行文件格式 只是还没有经过链接的过程 其中可能有些符号或有些地址还没有被调整 其实它本身就是按照可执行文件格式存储的 只是跟真正的可执行文件在结构上稍有不同
  • C/C++中#pragma once的使用

    在C C 中 为了避免同一个文件被include多次 有两种方式 一种是 ifndef方式 一种是 pragma once方式 在头文件的最开始加入 ifndef SOME UNIQUE NAME HERE define SOME UNIQ
  • C++/C++11中头文件iterator的使用

  • C++中std::sort/std::stable_sort/std::partial_sort的区别及使用

    某些算法会重排容器中元素的顺序 如std sort 调用sort会重排输入序列中的元素 使之有序 它默认是利用元素类型的 lt 运算符来实现排序的 也可以重载sort的默认排序 即通过sort的第三个参数 此参数是一个谓词 predicat
  • 提高C++性能的编程技术笔记:单线程内存池+测试代码

    频繁地分配和回收内存会严重地降低程序的性能 性能降低的原因在于默认的内存管理是通用的 应用程序可能会以某种特定的方式使用内存 并且为不需要的功能付出性能上的代价 通过开发专用的内存管理器可以解决这个问题 对专用内存管理器的设计可以从多个角度

随机推荐

  • 记mysql-connector-java:8.0.28的bug排查,你可能也踩坑了

    前言 如标题 最终查明问题是因为 mysql connector java 8 0 28 的一个 bug 导致的 但是在真相未浮出之前 整个问题可谓扑朔迷离 博主好久没有排查过如此得劲的 bug 随着一层层的 debug 深入 真相也随之浮
  • PCB布线线宽和过孔孔径设置为多少合适?

    作者 李大闯 2017 08 19 22 16 对于很多新入行的人来说 不清楚PCB的线宽应该设置为多少 这里作一下解释 对于PCB布线线宽的设置 主要要考虑两个问题 一是流过的电流大小 比如对于电源线来说 需要考虑电路工作时流过的电流 如
  • ios开发App的图标背景色不能是透明

    在ios开发中 App的图标背景不能是透明的 否则打包出来以后 在界面上的显示会是一个带有把黑色背景的图标 换成白色背景后
  • C++ 解析Json——jsoncpp

    C 解析Json
  • SKU 模块丨前端uniapp微信小程序项目

    小兔鲜儿 SKU 模块 学会使用插件市场 下载并使用 SKU 组件 实现商品详情页规格展示和交互 存货单位 SKU SKU 概念 存货单位 Stock Keeping Unit 库存管理的最小可用单元 通常称为 单品 SKU 常见于电商领域
  • 【2023】java多线程——锁的使用及分类

    锁简介 java多线程的锁都是基于对象的 每个对象都可以作为一个锁 类锁也是对象锁 java6以后 一个对象分为了4种锁的状态 级别由低到高依次是 无锁状态 偏向锁状态 轻量级锁状态 重量级锁状态 java对象头 每个Java对象都有对象头
  • SpringBoot 如何使用 @RequestBody 进行数据校验

    SpringBoot 如何使用 RequestBody 进行数据校验 在 Web 开发中 前台向后台发送数据是非常常见的场景 而在 SpringBoot 框架中 我们通常使用 RequestBody 注解来接收前台发送的 JSON 数据 并
  • PDF 的各种操作,我用 Python 来实现(附网站和操作指导)

    导言 PDF 处理是日常工作中的常见需求 包括 PDF 合并 删除 提取等 更复杂的任务如 将 PDF 转换成 图像 下面通过几个简单的例子和一份代码 帮助大家解决上面的需求 操作非常简单 在文末我会提供一份源码和一个神奇的 PDF 处理网
  • outside of class is not definition

    有一种可能的情况 You have semicolons at the end of all your function definitions making the compiler think they re declarations
  • 解决Base64报java.lang.IllegalArgumentException: Illegal base64 character 20

    报错 java lang IllegalArgumentException Illegal base64 character 20 原因 base64编码时使用加号 在URL传递时加号会被当成空格让base64字符串更改 服务器端解码出错
  • ROS主从机配置,并实现远程登陆

    第一步 主从机配置 首先确保主从机在同一个局域网中 1 编辑主机的bashrc文件 机器人平台 gedit bashrc 主机的bashrc文件添加如下的内容 export ROS MASTER URI http 主机的ip 11311 e
  • stm32F4 IAP实现原理讲解以及中断向量表的偏移

    一 IAP原理 IAP即是在应用编程 IAP 是用户自己的程序在运行过程中对User Flash 的部分区域进行烧写 目的是为了在产品发布后可以方便地通过预留的通信口对产 品中的固件程序进行更新升级 通常实现IAP 功能时 即用户程序运行中
  • 生命在于磨炼——连续两年参加4C大赛心得

    一 4C大赛简介 1 大赛简介 中国大学生计算机设计大赛 下面简称 大赛 是由教育部高等学校计算机类专业教学指导委员会 教育部高等学校软件工程专业教学指导委员会 教育部高等学校大学计算机课程教学指导委员会 教育部高等学校文科计算机基础教学指
  • 操作系统笔记五(Linux存储管理)

    1 Buddy内存管理算法 内部碎片就是已经被分配出去 能明确指出属于哪个进程 却不能被利用的内存空间 外部碎片指的是还没有被分配出去 不属于任何进程 但由于太小了无法分配给申请内存空间的新进程的内存空闲区域 目的 努力让内存分配与相邻内存
  • Task2_MySQL_basic

    MySQL表数据类型 用SQL语句创建表 创建MySQL数据表需要以下信息 表名 表字段名 定义每个表字段 语句解释 设定列类型 大小 约束 设定主键 用SQL语句向表中添加数据 语句解释 多种添加方式 指定列名 不指定列名 用SQL语句删
  • Ubuntu16.04下搭建LAMP环境

    Ubuntu16 04下搭建LAMP环境 Ubuntu16 04下搭建LAMP环境 1 安装 Apache2 2 重启 apache2 3 测试apache2是否安装成功 4 安装php7 5 测试php是否安装成功 6 安装mysql数据
  • 序列化与反序列化之Flatbuffers(一):初步使用

    序列化与反序列化之Flatbuffers 一 初步使用 一 前言 在MNN中 一个训练好的静态模型是经过Flatbuffers序列化之后保存在硬盘中的 这带来两个问题 1 为什么模型信息要序列化不能直接保存 2 其他框架如caffe和onn
  • 深度学习在目标视觉检测中的应用进展与展望

    前言 文章综述了深度学习在目标视觉检测中的应用进展与展望 首先对目标视觉检测的基本流程进行总结 并介绍了目标视觉检测研究常用的公共数据集 然后重点介绍了目前发展迅猛的深度学习方法在目标视觉检测中的最新应用进展 最后讨论了深度学习方法应用于目
  • ORAN专题系列-0: O-RAN快速索引

    专题一 O RAN的快速概述 ORAN专题系列 1 什么是开放无线接入网O RAN ORAN专题系列 1 什么是开放无线接入网O RAN 文火冰糖的硅基工坊的博客 CSDN博客 什么是oran ORAN专题系列 2 O RAN的系统架构 O
  • C和C++安全编码笔记:动态内存管理

    4 1 C内存管理 C标准内存管理函数 1 malloc size t size 分配size个字节 并返回一个指向分配的内存的指针 分配的内存未被初始化为一个已知值 2 aligned alloc size t alignment siz