-
Notifications
You must be signed in to change notification settings - Fork 11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
在C C++应用程序中检测内存泄漏 Detecting Memory Leaks in CC++ Applications[进行中] #220
Comments
01 - 介绍002 进程内存布局内存管理基础在本节中,我将讨论内存管理的基础知识。我会介绍不同类型的存储区域,如栈、数据段和堆,并且还会解释如何在 C 和 C++ 应用程序中进行动态内存分配。具体来说,我将展示 C 语言中的内存分配函数,如 进程基础但首先,我想谈谈进程的基本概念。当程序启动时,操作系统会为该程序分配内存,这块内存被称为虚拟地址空间。虚拟地址空间为可执行文件提供了运行的环境。这块地址空间的大小取决于平台。在 32 位平台上,地址空间的大小通常是 4GB。在其他平台上,地址空间的大小可能会有所不同。 这块地址空间被划分为不同的部分,一些部分用于存储数据,比如全局数据、静态数据、局部数据和运行时数据。我们来看看从存储的角度看进程的不同部分。 内存分区
从内存布局中可以看到,栈和堆之间有一个间隔。这里的箭头表示栈和堆在需要时会增长。堆会向低地址方向增长,而栈会向高地址方向增长。 局部变量的存储我稍后会演示,函数中的局部变量是如何从高内存地址到低内存地址存储的。这是因为栈是从顶部向下增长的。 数据存储示例以下是不同类型数据存储在虚拟地址空间中的示例:
对于静态变量来说,其作用域仅限于定义它的函数内部。举例来说, 局部变量局部变量 代码段最后,代码段存储程序的可执行代码,即程序的各个语句。 内存布局的变化需要注意的是,这些部分在虚拟地址空间中的位置并非总是固定的。许多现代操作系统实现了一种叫做“地址空间布局随机化”(ASLR, Address Space Layout Randomization)技术。ASLR 会使操作系统每次执行程序时将这些段放置在随机的位置。 在没有地址空间布局随机化的老旧系统中,程序的各个部分会在内存中固定位置存储,因此攻击者可能知道特定数据的地址,并能够将恶意负载注入到这些已知地址。而采用地址空间布局随机化后,攻击者很难知道数据或程序可执行文件的存储位置,这增加了攻击的难度。 总结本节介绍了虚拟地址空间的不同部分以及它们在内存中的分布情况,尤其是栈、堆和数据段。接下来,我们将深入探讨动态内存分配,并了解如何在 C 和 C++ 中进行内存管理。 这就是本节的所有内容,下一个视频见! 003 指针指针简介欢迎回来!在本视频中,我将快速介绍指针。如果你已经熟悉指针的概念,可以跳过这个视频。 指针就像一个变量,但它不是存储一个值,而是存储一个内存地址。指针的语法与普通变量不同。一个指针可以持有一个变量的地址,也可以持有在堆上分配的内存地址,这部分内存被称为动态内存。 指针的用途指针可以保存以下几种类型的地址:
示例以下是一个关于指针存储不同类型内存地址的示例:
总结指针是一个非常强大的工具,可以让你访问和操作内存中的地址。无论是指向变量、指针、对象还是函数,指针都提供了灵活的内存访问能力。接下来,我们将通过 Visual Studio 查看一个例子,进一步加深理解。 谢谢收看,本节内容完毕! 004 使用 Visual Studio在 Visual Studio 中创建 C++ 项目在本视频中,我们将创建一个 C++ 项目,并简单介绍如何使用 Visual Studio 进行开发。Visual Studio 不仅可以用来开发 C++ 应用程序,还支持开发其他类型的项目,比如 C# 应用程序。 创建项目
添加 C++ 文件
#include <iostream>
using namespace std;
int main() {
int x{5}; // 使用 C++ 的统一初始化语法
cout << "Value of x: " << x << endl;
cout << "Address of x: " << &x << endl; // 输出 x 的内存地址
return 0;
} 构建和运行项目
输出和分析在控制台输出中,你会看到两个信息:
栈和内存地址
下一步,我们将使用调试功能进一步查看 005 在 Visual Studio 中调试Visual Studio 调试基础在本视频中,我将向您展示如何使用 Visual Studio 进行基本的调试。 设置断点
调试命令
记住这些快捷键:
停止调试
逐行调试对于这个简单的程序,我们不需要设置断点,直接使用 Step Over (F10) 来逐行调试代码。在调试过程中,Visual Studio 会显示一些额外的窗口,这些窗口是用于调试的窗口。 调试窗口
您可以根据需要调整这些窗口的位置,将它们拖动到适合的位置。比如,我将这些窗口垂直排列,方便观察。 查看变量值
查看内存地址
字节顺序当您在 Memory 窗口 中查看 通过右键点击 Memory 窗口,选择 Hexadecimal Display,您可以查看内存中的值的十六进制表示。 总结通过调试功能,您可以逐行执行代码,查看变量的值,修改变量的值,甚至直接访问内存地址。这对于了解程序的执行流程、查找错误非常有帮助。关于字节顺序的问题,我们将在下一个视频中详细讨论。 下次见! 006 字节顺序字节顺序(Byte Ordering)在计算机的内存中,数据是以特定的顺序存储的,这个顺序被称为 字节顺序。字节顺序定义了字节在内存中存储的排列顺序。常见的字节顺序有两种:大端序(Big Endian) 和 小端序(Little Endian)。下面我们将通过示例来了解这两种字节顺序。 大端序(Big Endian)与小端序(Little Endian)
示例:字节顺序假设我们声明了一个整数变量,并赋了一个很大的数字。这个数字将不会直接以这种方式存储在内存中,而是转换为等效的二进制格式。当你调试程序时,调试器通常以十六进制格式显示该数字,因为十六进制比二进制更易于阅读。 例如,假设整数的十六进制值为
在大端序(Big Endian)中:在大端序格式中,字节的存储顺序如下:
在小端序(Little Endian)中:在小端序格式中,字节的存储顺序如下:
处理字节顺序的硬件架构
在 Visual Studio 中查看字节顺序现在,让我们切换到 Visual Studio,查看该数字在内存中的表示,并通过调试器来识别字节顺序。
小端序的实际应用通过这个实验,我们可以确认当前机器使用的是小端序字节顺序,并且我的计算机使用的是 Intel 处理器。了解字节顺序对于后续编写内存泄漏检测器非常有帮助。我们在调试时经常需要查看内存中的数据,了解字节顺序能帮助我们更好地理解和分析内存中的值。 总结
下次课我们将继续学习如何处理内存泄漏检测。再见! 007 C 内存分配函数概述动态内存分配基础动态内存分配是指在 堆 上分配的内存。与在编译时分配的静态内存不同,动态内存是在 运行时 分配的。动态内存的作用范围与全局变量相同,这意味着只要你拥有该内存的地址,就可以在程序的任何地方访问该内存。接下来,我们将深入了解动态内存的生命周期、使用场景以及如何在 C++ 中进行动态内存分配。 动态内存的生命周期动态内存的生命周期由程序员管理,而不是由系统自动管理。当堆上的内存不再需要时,程序员必须手动释放它。这与栈内存不同,栈内存的生命周期是由函数的作用范围决定的,函数返回时栈上的内存会自动释放。 为什么需要动态内存分配?在程序中,某些情况下我们无法预先知道需要多少内存。比如:
如何在 C++ 中进行动态内存分配在 C++ 中,我们可以通过以下几种方式在运行时分配内存:
示例:如何在 Visual Studio 中使用这些函数让我们通过一个简单的例子来演示如何在 C++ 中使用这些动态内存分配函数。 #include <iostream>
#include <cstdlib> // 包含 malloc, calloc, realloc 和 free 的头文件
int main() {
// 使用 malloc 分配内存
int *arr = (int*) malloc(5 * sizeof(int)); // 分配 5 个整数大小的内存
if (arr == nullptr) {
std::cerr << "Memory allocation failed!" << std::endl;
return 1;
}
// 使用 calloc 分配内存,并初始化为 0
int *arr2 = (int*) calloc(5, sizeof(int)); // 分配 5 个整数并初始化为 0
if (arr2 == nullptr) {
std::cerr << "Memory allocation failed!" << std::endl;
free(arr); // 释放之前分配的内存
return 1;
}
// 使用 realloc 修改已分配内存的大小
arr = (int*) realloc(arr, 10 * sizeof(int)); // 重新分配内存,扩展为 10 个整数大小
if (arr == nullptr) {
std::cerr << "Memory reallocation failed!" << std::endl;
free(arr2); // 释放之前的内存
return 1;
}
// 初始化数据
for (int i = 0; i < 10; ++i) {
arr[i] = i + 1; // 为数组元素赋值
}
// 打印数据
std::cout << "Array values: ";
for (int i = 0; i < 10; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;
// 释放内存
free(arr);
free(arr2);
return 0;
} 代码解析
总结
理解和掌握动态内存分配对于编写高效的 C++ 程序是至关重要的,特别是在处理大量数据或构建复杂数据结构时。 008 C 内存分配函数 - malloc, calloc使用
|
02 - C++ 中的动态内存分配001 C++ 分配运算符C++ 动态内存分配:使用
|
03 - 内存管理问题001 内存管理问题内存管理中的问题及其避免方法欢迎回来!在本节课程中,我将讨论在内存管理过程中可能遇到的一些问题,并教你如何避免这些问题。本视频将简要概述由于内存管理引发的不同类型的问题。手动内存管理如果没有正确处理,可能会引发错误。导致这些问题的原因有很多,我们将深入探讨这些原因。 1. 指针的初始化问题指针用于存储内存地址,但在使用指针之前,我们必须确保它指向一个有效的内存地址。如果没有正确初始化指针,指针可能包含一个未知的值,这个值可能是一个有效的内存地址,也可能是无效的地址。
2. 编译器和运行时行为
因此,为了避免这种风险,你应该始终初始化指针。我们将在下一节视频中通过示例进一步展示未初始化指针的问题。 感谢收看,我们下次见! 002 未初始化指针 - 第一部分未初始化指针的示例在本视频中,我们将演示一个未初始化指针的问题。假设有一个函数用于为某些数据分配内存。我们将从用户处获取输入,并编写一个简单的条件语句。如果输入的值大于 100,我们就为一个整数分配内存,并将该地址存储在指针 示例代码:int* GetData() {
int* p_value;
int input;
std::cin >> input;
if (input > 100) {
p_value = new int; // 动态分配内存
*p_value = input; // 存储数据
return p_value; // 返回指针
}
return nullptr;
} 你可能会好奇,为什么我不直接返回值?假设这些数据不能通过值返回,所以我们为其分配内存并返回指向该数据的指针。你可能已经注意到这里有一个潜在的 bug,但我们先不处理,后面会再讲到。 接下来,我将创建一个 序列化函数:void Serialize(int* data) {
std::ofstream file("data.txt");
if (!file.is_open()) {
std::cout << "Error opening file!" << std::endl;
return;
}
file << *data; // 写入指针指向的值
file.close(); // 自动关闭文件
} 在 主函数代码:int main() {
int* p_value = GetData();
if (p_value) {
Serialize(p_value);
std::cout << "Data serialized." << std::endl;
delete p_value; // 释放内存
}
return 0;
} 问题描述在这个示例中,有几个问题,但是我们先运行程序看看会发生什么。请注意,我们在调试模式下编译代码,调试模式下,编译器会启用标准库的检查,这有助于开发者捕获错误。发布模式下,这些检查是关闭的。 首先,我们正常运行程序,输入一个大于 100 的值(例如 250):
接下来,我们运行程序,输入一个小于 100 的值(例如 50):
调试模式与发布模式的差异
在发布模式下,程序崩溃但没有详细错误提示,这意味着程序没有正常退出,退出代码不是零。由于发布模式下没有启用内存安全检查,你只能依赖其他手段进行调试和错误排查。 总结
我们下次见! 003 未初始化指针 - 第二部分确保程序不崩溃的措施在本视频中,我们将继续讨论未初始化指针的问题,并探讨如何确保程序不崩溃。为了解决这个问题,我们可以添加一个条件来检查指针的地址。问题是我们应该与什么值进行比较? 比较指针与 null 指针如果我们将指针与空指针进行比较,可能会抛出异常。为此,我们可以编写一个 程序行为示例现在,我们先来看一下程序在这种实现下的行为。我们在调试构建下进行编译,并给出一个小于 100 的值。程序再次崩溃。为什么没有抛出异常?我们没有看到异常,但我们确实编写了代码来比较指针值与空指针。问题是,当值小于 100 时, 在 Visual Studio 中,未初始化的内存会被赋予一个特殊的值。让我们在此处设置一个断点并调试代码。然后,我们可以查看指针的值。 调试和查看未初始化指针在调试时,执行到下一行代码之前,我们将 在继续执行代码时,我们可以看到,尽管我们给了用户输入并初始化了变量,但未初始化的指针 关闭运行时检查为了演示关闭运行时检查后的行为,我们将关闭对未初始化变量的检查。我们可以在 项目属性 中找到 代码生成(Code Generation),并关闭 基本运行时检查(Basic Runtime Checks)。设置为默认值之后,程序将不再在运行时崩溃。 我们再次设置断点并调试程序,这次输入一个小于 100 的值。你会看到,关闭运行时检查后,指针 程序崩溃的原因此时,如果程序继续执行,它可能会在尝试读取该无效内存地址时崩溃。因为该地址是无效的,因此行为是未定义的。 确保指针初始化为了避免崩溃,我们必须确保在使用指针之前对其进行初始化。我们可以使用统一初始化语法(uniform initialization)来初始化指针和变量。 初始化后的程序行为通过将指针和变量初始化后,我们运行程序,并输入一些值。程序运行时,我们会捕获到抛出的异常,确保程序不会崩溃,而是优雅地处理了错误情况。 总结
在下一视频中,我们将讨论缓冲区溢出和下溢问题。再见! 004 缓冲区溢出 - 栈内存覆盖问题在本节中,我们将讨论在处理内存时可能遇到的另一个问题,即 内存覆盖。这种情况通常发生在你为数据分配的内存不足时。如果数据存储在内存中,它可能会溢出到周围的内存区域。这种情况被称为 缓冲区溢出,因为数据会溢出到周围的内存区域。这个区域可能包含一些重要的程序代码,导致数据丢失,甚至可能导致程序崩溃。因此,缓冲区溢出 在运行时会导致 未定义行为。 缓冲区溢出的常见情况缓冲区溢出通常发生在数组和字符串处理中,特别是当你自己手动管理内存时。为了避免这种问题,建议使用 标准库容器,因为这些容器会自动管理内存,并根据需要自动扩展。 缓冲区溢出示例接下来,我将展示一个简单的缓冲区溢出示例。我们将使用静态数组来演示缓冲区溢出。 创建静态数组首先,我们创建一个字符数组来存储一个人的地址,假设地址最多有 15 个字符。然后,我们还创建一个字符数组来存储该人的名字。我故意创建一个较小的名字缓冲区,以展示缓冲区溢出的效果。程序会提示用户输入数据,以帮助用户理解期望的输入格式。 char address[15]; // 用来存储地址
char name[4]; // 用来存储名字(故意给它一个较小的大小) 程序会要求用户输入地址和名字,并将其打印出来。 输入数据和缓冲区溢出如果输入的数据符合预期,例如地址为 "Park Lane" 和名字为 "Bob",程序将按预期正常工作,打印出正确的地址和名字。示例输出如下:
但是,如果在输入名字时,输入的名字长度超过了 4 个字符(例如,输入 "Robert"),程序将出现错误,提示缓冲区溢出。具体的错误信息是:
调试缓冲区溢出在调试时,我们可以看到地址和名字数组的内存位置非常接近。当我们输入一个超长的名字时,名字的内容会覆盖地址的内存区域。这就是为什么程序输出的内容不正确,甚至导致了堆栈内存的损坏。 通过将 发布模式下的表现当我们在 发布模式(release build)下编译代码时,可能不会看到错误提示。因为编译器不会为这些局部变量添加运行时检查,这与我们之前讨论的未初始化变量类似。在发布模式下,编译器不会进行这些检查,因此不会显示类似的运行时错误。尽管程序没有崩溃,但由于内存被覆盖,输出仍然是不正确的。 解决缓冲区溢出问题缓冲区溢出会导致程序行为不可预测,因此要避免这种情况。为此,我们可以采取以下措施:
下一节预告在下一节中,我将展示如果地址和名字是在堆上创建的情况,以及它们在堆上时会发生什么变化。再见! 005 缓冲区溢出 - 堆堆内存中的缓冲区溢出在上一节视频中,我们讨论了在基于栈的内存中发生缓冲区溢出时会发生什么情况。Visual Studio 在栈内存中进行了检查,并在缓冲区溢出时通知用户。现在,在这一节视频中,我们将讨论如果缓冲区溢出发生在堆内存中时,会发生什么情况。 代码改动首先,让我们回顾一下上一节中的代码。我将把 char* address = new char[15]; // 在堆上为地址分配内存
char* name = new char[4]; // 在堆上为名字分配内存 正常情况当程序运行时,数据会被存储在堆内存中而不是栈内存中。程序运行正常,没有问题。 造成缓冲区溢出接下来,我们尝试引发缓冲区溢出。这时,程序会出现问题,退出码也不会是零。让我们在调试模式下运行程序。 堆损坏错误当我们调试程序时,会弹出错误消息,提示堆损坏。具体的错误信息是:
这个错误的意思是,应用程序写入了超出堆缓冲区末尾的内存。这就像往玻璃杯里倒水,如果不断倒水,最终水会溢出并洒到周围的区域。类似地,程序向缓冲区写入了数据,但这些数据太大,超出了分配的内存范围,导致了缓冲区溢出。 Visual Studio的堆调试功能Visual Studio 在调试模式下使用调试堆库。这些库提供了很多帮助我们管理堆内存的功能。例如,调试堆库会在调用 如果没有弹出消息框,您可以在 输出窗口 中查看相关的错误消息。 缓冲区下溢(Buffer Underflow)除了缓冲区溢出,还有一种情况是 缓冲区下溢。我们可以直接在 name[-1] = 'A'; // 在分配的堆内存之前的内存位置写入数据 运行程序时,错误消息并不会弹出消息框,而是在 输出窗口 中显示:
这就是 缓冲区下溢 的情况,它会导致堆内存中分配区域之前的数据被覆盖。 使用
|
04 - 检测堆损坏002 字符串类 - 第一部分动态内存管理中的问题诊断大家好,欢迎来到本课程的下一个部分。在这一部分中,我们将学习如何诊断与动态内存管理相关的问题,包括内存覆盖和内存泄漏等问题。为了帮助我们解释这些问题以及它们的解决方案,我将创建一个名为 创建
|
05 - 检测内存泄漏001 _CrtDumpMemoryLeaks() 函数使用 C 运行时函数检测内存泄漏在本节中,我们将学习如何使用 Visual Studio 的 C 运行时库来检测内存泄漏。Visual Studio 的 C 运行时库提供了一个名为 1.
|
06 - 自定义泄漏检测器002 泄漏检测内部实现自定义内存泄漏与堆损坏检测库的实现在本节课程中,我们将实现一个自定义的库,帮助我们在 C 和 C++ 应用程序中检测内存泄漏和堆损坏。你可能会问,为什么要这样做,毕竟 Visual Studio 已经提供了相关的功能。原因在于,Visual Studio 提供的 Debug Heap 库是 C 运行时库的一部分,仅能在 Visual Studio 中使用。这个库无法在其他编译器上使用,也不能在 Mac 或 Linux 等平台上运行,因为它是专门为 Windows 设计的。因此,我们希望创建一个自己的库,利用标准的 C 和 C++ 特性。这样,该库就可以在支持标准 C 或 C++ 编译器的任何平台上使用。 创建自定义库的优势:
Visual Studio 如何实现内存泄漏和堆损坏检测在我们开始实现自定义库之前,首先让我们了解一下 Visual Studio 是如何实现内存泄漏和堆损坏检测的。以下是一些核心概念。 内存分配过程当用户通过
当有多个内存分配时,这些内存块会形成一个双向链表。 内存块结构示例让我们用一个例子来理解这个过程。如果用户请求分配 4 字节的内存,则系统会分配比 4 字节更大的内存块。比如,分配的内存块可能是 16 字节,其中包括以下几个部分:
用户请求的内存指针指向的是用户数据区,而不是包含分配信息的块头区域。块头区域包含的两个指针分别指向链表中的前一个块和下一个块。 处理内存删除当用户通过 内存泄漏检测原理总结
实现我们的自定义库在接下来的视频中,我们将根据上述的内存分配机制,开始实现我们自己的内存泄漏和堆损坏检测库。我们的库将模仿 Visual Studio 中的实现方式,通过分配一个更大的内存块,并在块头存储分配信息来进行检测。 结语通过了解 Visual Studio 如何实现内存泄漏和堆损坏检测的机制,我们为实现自定义的检测库奠定了基础。在下一节课中,我们将开始编写这个库的代码,并逐步实现内存泄漏和堆损坏的检测功能。希望你能在这次实践中更深入地理解内存管理的工作原理。我们下节课见! 003 内存块头和 ptmalloc() 函数实现自定义内存泄漏检测库在本节中,我们将开始实现一个自定义的内存泄漏检测库,命名为 PD Leak Detector。我已经创建了项目,接下来的实现将主要使用 C 语言,这样我们可以确保这个库能被 C 和 C++ 应用程序同时使用。如果我们使用 C++ 特性来实现代码,那么 C 语言应用程序将无法使用这个库。因此,为了最大程度地提高跨平台的兼容性,我们将使用 C 来实现。 设计内存块头结构我们需要设计一个结构体来存储分配的内存块的相关信息。这个信息将包括:
为了实现这一点,我们将在头文件中定义一个结构体,命名为 Memory Block Header。为了保持平台的独立性,我们将避免使用 // 定义内存块头结构
typedef struct MemoryBlockHeader {
const char* function_name; // 分配发生的函数名称
const char* file_name; // 文件名
int line_number; // 行号
size_t size; // 分配的内存大小
struct MemoryBlockHeader* prev_block; // 指向前一个块的指针
struct MemoryBlockHeader* next_block; // 指向下一个块的指针
} MemoryBlockHeader; 分配与释放内存的函数实现接下来,我们将实现分配和释放内存的函数。由于 C 不支持函数重载,我们不能直接使用
|
07 - 堆损坏支持001 检测堆损坏 - 内部实现添加堆内存损坏检测功能在本节中,我们将为内存泄漏检测库增加堆内存损坏(heap corruption)检测功能。这将模仿 Visual Studio 的内存损坏检测机制,通过监控分配内存周围的特定值来识别潜在的内存覆盖错误。 1. Visual Studio 的堆内存损坏检测原理Visual Studio 的堆内存损坏检测方法主要依赖于 "No Man's Land"(无人区)的概念。具体来说,用户分配的内存块会被已知的值包围,这些值会被用来检测内存是否被篡改。如果这些已知值发生变化,就表示分配的内存区域可能被意外覆盖,导致堆内存损坏。 1.1 No Man's Land 和内存检查
2. 实现内存损坏检测为了实现类似的堆内存损坏检测机制,我们需要修改现有的内存分配器,使其能够在用户分配的内存前后添加额外的内存区域,并检查这些区域的值。 2.1 分配更大内存块我们已经为检测内存泄漏而分配了一个比用户请求的内存更多的内存块。在这个内存块中,我们将额外分配两个区域,这两个区域就是 No Man's Land:
这样,内存块的结构将会像这样:
2.2 堆块头结构为了能够在内存块的前后存储 No Man's Land 区域,我们需要修改内存块头的结构。可以在内存块头中增加额外的空间来保存这些区域。假设我们使用一个固定的 16 字节作为 No Man's Land 区域的大小,那么每个内存块的总大小将是:
2.3 内存检查函数我们将实现一个类似于 3. 在 Visual Studio 中实现堆内存损坏检测3.1 修改内存分配逻辑我们需要在内存分配时增加额外的空间,用于 No Man's Land 区域。假设我们为每个内存块的前后添加 16 字节作为 No Man's Land 区域。 // 修改后的内存块头结构
struct BlockHeader {
size_t size; // 用户请求的内存大小
char noMan'sLandBefore[16]; // No Man's Land - 前
void* userMemory; // 用户的内存块
char noMan'sLandAfter[16]; // No Man's Land - 后
};
// 重写分配内存的函数
void* pt_malloc(size_t size) {
// 为了包含 No Man's Land 区域,我们增加额外的内存空间
size_t totalSize = size + 2 * sizeof(char[16]); // 用户内存和前后 No Man's Land 区域
BlockHeader* header = (BlockHeader*)malloc(totalSize);
if (!header) {
return nullptr; // 内存分配失败
}
// 初始化 No Man's Land 区域
memset(header->noMan'sLandBefore, 0xAA, sizeof(header->noMan'sLandBefore));
memset(header->noMan'sLandAfter, 0xAA, sizeof(header->noMan'sLandAfter));
// 设置用户内存的指针
header->userMemory = (void*)(header + 1); // 用户内存位于 BlockHeader 之后
return header->userMemory;
} 3.2 实现内存损坏检测函数接下来,我们实现一个函数来检查所有已分配内存块的 No Man's Land 区域的值。 void checkMemory() {
// 遍历所有内存块,检查 No Man's Land 区域
for (auto& block : allocatedBlocks) {
// 检查前后 No Man's Land 区域的值是否被修改
if (memcmp(block->noMan'sLandBefore, "\xAA\xAA\xAA\xAA", 16) != 0) {
std::cout << "Heap corruption detected before normal block" << std::endl;
}
if (memcmp(block->noMan'sLandAfter, "\xAA\xAA\xAA\xAA", 16) != 0) {
std::cout << "Heap corruption detected after normal block" << std::endl;
}
}
} 3.3 内存块的释放释放内存时,我们不需要特别注意 No Man's Land 区域,因为它们只用于检查堆内存损坏。在释放内存时,仍然按照原有方式操作: void pt_free(void* ptr) {
BlockHeader* header = (BlockHeader*)ptr - 1; // 获取内存块头
free(header); // 释放整个内存块(包括 No Man's Land 区域)
} 4. 测试内存损坏检测功能
int main() {
// 分配内存并进行内存损坏测试
char* ptr = (char*)pt_malloc(100);
pt_free(ptr);
// 故意修改 No Man's Land 区域
memcpy(ptr - 16, "corruption", 10); // 修改前 No Man's Land 区域
checkMemory(); // 应该能检测到堆内存损坏
} 5. 总结我们已经完成了以下几项工作:
通过这些改进,我们的内存检测库现在不仅可以检测内存泄漏,还能够有效地检测堆内存损坏,模拟 Visual Studio 的内存检测功能。 002 PtCheckMemory() 实现 - 第一部分堆内存损坏检测功能实现在本节中,我们将为内存检测库增加堆内存损坏(heap corruption)检测功能。我们会修改内存块头结构,并添加一个“Guard”区域(防护区)来帮助检测内存是否被覆盖。通过这个功能,我们能够模拟类似于 Visual Studio 的堆内存损坏检测机制。 1. 修改内存块头我们需要为每个内存块添加一个新的属性——Guard(防护区)。这个属性将是一个大小为 4 字节的无符号字符数组。其主要功能是防止用户内存被意外覆盖,通过在内存块的前后填充已知值来检测这些区域是否被修改。 1.1 添加 Guard 数组首先,我们定义一个 Guard 数组,并且用一个已知值填充它。我们定义一个常量 God fill,并将其值设置为 我们还需要添加一个宏 God size 来规定该防护区的大小,设置为 4 字节。 // 定义 Guard 区域大小为 4 字节
#define GOD_SIZE 4
// 定义 Guard 填充值为 0xFD
static const unsigned char GOD_FILL = 0xFD; 接下来,我们会修改内存块头结构,加入 Guard 数组。 1.2 修改内存块头结构我们将 BlockHeader 结构体修改,增加一个 Guard 属性,并确保计算内存块总大小时,考虑到 Guard 区域。 // 修改后的内存块头结构
struct BlockHeader {
size_t size; // 用户请求的内存大小
char noMan'sLandBefore[16]; // No Man's Land - 前
void* userMemory; // 用户的内存块
char noMan'sLandAfter[16]; // No Man's Land - 后
unsigned char guard[GOD_SIZE]; // Guard 区域
}; 1.3 更新内存分配函数我们需要修改内存分配函数,将 Guard 区域包含到内存块中,同时用已知值填充这些区域。 void* pt_malloc(size_t size) {
// 增加 Guard 区域和 No Man's Land 区域的空间
size_t totalSize = size + 2 * sizeof(char[16]) + GOD_SIZE; // 用户内存 + 前后 No Man's Land 区域 + Guard 区域
BlockHeader* header = (BlockHeader*)malloc(totalSize);
if (!header) {
return nullptr; // 内存分配失败
}
// 初始化 No Man's Land 区域
memset(header->noMan'sLandBefore, 0xAA, sizeof(header->noMan'sLandBefore));
memset(header->noMan'sLandAfter, 0xAA, sizeof(header->noMan'sLandAfter));
// 填充 Guard 区域
memset(header->guard, GOD_FILL, GOD_SIZE);
// 设置用户内存的指针
header->userMemory = (void*)(header + 1); // 用户内存位于 BlockHeader 之后
return header->userMemory;
} 2. 填充 No Man's Land 后的 Guard 区域在上面的代码中,我们已经处理了内存块前面的 No Man's Land 区域,并成功将 Guard 区域填充到内存块中。接下来,我们需要在内存块的尾部(用户内存之后)填充 Guard 区域。 2.1 计算内存块结束位置为了填充 No Man's Land 后的 Guard 区域,我们需要计算内存块结束的位置。通过以下方式,我们可以得到内存块的结尾位置,并填充防护区。 void* pt_malloc(size_t size) {
size_t totalSize = size + 2 * sizeof(char[16]) + GOD_SIZE;
BlockHeader* header = (BlockHeader*)malloc(totalSize);
if (!header) {
return nullptr; // 内存分配失败
}
// 初始化前后 No Man's Land 区域
memset(header->noMan'sLandBefore, 0xAA, sizeof(header->noMan'sLandBefore));
memset(header->noMan'sLandAfter, 0xAA, sizeof(header->noMan'sLandAfter));
// 填充 Guard 区域
memset(header->guard, GOD_FILL, GOD_SIZE);
// 获取指向内存块结束位置的指针
unsigned char* pEnd = (unsigned char*)(header + 1) + size;
// 填充 Guard 区域在用户内存后的部分
memset(pEnd, GOD_FILL, GOD_SIZE);
header->userMemory = (void*)(header + 1);
return header->userMemory;
} 3. 实现内存损坏检查为了检测内存是否被覆盖,我们需要创建一个函数,遍历所有已分配的内存块,并检查每个内存块的 Guard 区域是否被修改。这个函数类似于 3.1 堆损坏检查函数void p_checkMemory() {
// 遍历所有已分配的内存块,检查 Guard 区域是否被修改
for (auto& block : allocatedBlocks) {
// 检查前后 No Man's Land 区域是否被篡改
if (memcmp(block->noMan'sLandBefore, "\xAA\xAA\xAA\xAA", 16) != 0) {
std::cout << "Heap corruption detected before normal block" << std::endl;
}
if (memcmp(block->noMan'sLandAfter, "\xAA\xAA\xAA\xAA", 16) != 0) {
std::cout << "Heap corruption detected after normal block" << std::endl;
}
// 检查 Guard 区域是否被修改
if (memcmp(block->guard, &GOD_FILL, GOD_SIZE) != 0) {
std::cout << "Heap corruption detected in Guard area" << std::endl;
}
}
} 4. 堆损坏信息转储为了方便调试,我们还可以实现一个辅助函数 void dumpHeapCorruptionInfo(BlockHeader* block) {
if (memcmp(block->noMan'sLandBefore, "\xAA\xAA\xAA\xAA", 16) != 0) {
std::cout << "Heap corruption detected before normal block at address: " << block << std::endl;
}
if (memcmp(block->noMan'sLandAfter, "\xAA\xAA\xAA\xAA", 16) != 0) {
std::cout << "Heap corruption detected after normal block at address: " << block << std::endl;
}
if (memcmp(block->guard, &GOD_FILL, GOD_SIZE) != 0) {
std::cout << "Heap corruption detected in Guard area at address: " << block << std::endl;
}
} 5. 总结通过上述修改,我们已经为内存泄漏检测库添加了堆内存损坏检测的功能。具体步骤如下:
通过这些改进,我们的内存检测库现在不仅可以检测内存泄漏,还能够有效检测堆内存损坏,提供更强大的内存管理支持。 003 PtCheckMemory() 实现 - 第二部分堆内存损坏检测实现继续在上一部分中,我们已经为内存泄漏检测库添加了堆内存损坏检测的基本框架。现在,我们继续实现 1. 准备 Guard 区域的预期值首先,我们为 Guard 区域创建一个缓冲区,初始化为 1.1 定义初始化函数为了提高代码的可读性,我们将创建两个函数来帮助计算内存区域的偏移量,从而确定需要比较的内存区域。
// 获取用户内存前的 Guard 区域
unsigned char* getBeginGuard(BlockHeader* block) {
return (unsigned char*)(&block->guard);
}
// 获取用户内存后的 Guard 区域
unsigned char* getEndGuard(BlockHeader* block) {
unsigned char* pEnd = (unsigned char*)block->userMemory + block->size;
return pEnd;
} 2. 比较 Guard 区域我们将使用 2.1 实现
|
在C C++应用程序中检测内存泄漏 Detecting Memory Leaks in CC++ Applications
01 - 介绍
002 进程内存布局
003 指针
004 使用 Visual Studio
005 在 Visual Studio 中调试
006 字节顺序
007 C 内存分配函数概述
008 C 内存分配函数 - malloc, calloc
009 C 内存分配函数 - realloc
02 - C++ 中的动态内存分配
001 C++ 分配运算符
002 new 的工作原理 - 第一部分
003 new 的工作原理 - 第二部分
004 处理 new 分配失败 - 异常
005 处理 new 分配失败 - 处理程序
006 处理 new 分配失败 - nothrow
007 无抛出 new 示例 - 第一部分
008 无抛出 new 示例 - 第二部分
010 Placement new - 第一部分
011 Placement new - 第二部分
012 Placement new - 第三部分
013 Placement new - 第四部分
014 Placement new - 第五部分
015 运算符 new 和 delete 函数
03 - 内存管理问题
001 内存管理问题
002 未初始化指针 - 第一部分
003 未初始化指针 - 第二部分
004 缓冲区溢出 - 栈
005 缓冲区溢出 - 堆
006 悬空指针 - 第一部分
007 悬空指针 - 第二部分
008 内存泄漏 - 第一部分
009 内存泄漏 - 第二部分
04 - 检测堆损坏
002 字符串类 - 第一部分
003 字符串类 - 第二部分
004 字符串类 - 第三部分
005 字符串类 - 第四部分
006 检测字符串类中的堆损坏
007 堆检查器类 - 第一部分
008 堆检查器类 - 第二部分
009 堆检查器类 - 第三部分
010 堆检查器类 - 第四部分
011 堆检查器类 - 第五部分
05 - 检测内存泄漏
001 _CrtDumpMemoryLeaks() 函数
002 _CrtDumpMemoryLeaks() 代码示例
003 泄漏检测标志
004 new 的详细泄漏转储
006 内存快照
007 内存快照 - 代码示例
008 内存检查点助手类
009 检查点问题及解决方案
010 Visual Studio 中的快照 - 第一部分
011 Visual Studio 中的快照 - 第二部分
013 报告模式和类型
014 报告模式和类型 - 代码示例
015 报告模式文件
06 - 自定义泄漏检测器
002 泄漏检测内部实现
003 内存块头和 ptmalloc() 函数
004 ptfree() 函数的实现
005 PtDumpLeaks() 函数的实现
006 添加 C++ 支持 - 第一部分
007 添加 C++ 支持 - 第二部分
008 在 Linux 上编译
07 - 堆损坏支持
001 检测堆损坏 - 内部实现
002 PtCheckMemory() 实现 - 第一部分
003 PtCheckMemory() 实现 - 第二部分
004 对齐与结构填充
005 对齐内存块头
01 - Introduction
002 Process Memory Layout
003 Pointers
004 Using Visual Studio
005 Debugging In Visual Studio
006 Byte Ordering
007 C Allocation Functions Overview
008 C Allocation Functions - malloc, calloc
009 C Allocation Functions - realloc
02 - Dynamic Memory Allocation in C++
001 C++ Allocation Operators
002 How new works - Part I
003 How new works - Part II
004 Handling new Failure - Exception
005 Handling new Failure - Handler
006 Handling new Failure - nothrow
007 Non-throwing new Example - Part I
008 Non-throwing new Example - Part II
010 Placement new - I
011 Placement new - II
012 Placement new - III
013 Placement new - IV
014 Placement new - V
015 Operator new & delete Functions
03 - Memory Management Issues
001 Memory Management Issues
002 Uninitialized Pointers - I
003 Uninitialized Pointers - II
004 Buffer Overflow - Stack
005 Buffer Overflow - Heap
006 Dangling Pointers - I
007 Dangling Pointers - II
008 Memory Leaks - I
009 Memory Leaks - II
04 - Detecting Heap Corruption
002 String class - I
003 String class - II
004 String class - III
005 String class - IV
006 Detecting Heap Corruption in String Class
007 Heap Checker Class - I
008 Heap Checker Class - II
009 Heap Checker Class - III
010 Heap Checker Class - IV
011 Heap Checker Class - V
05 - Detecting Memory leaks
001 _CrtDumpMemoryLeaks() function
002 _CrtDumpMemoryLeaks() Code Example
003 Leak Detection Flags
004 Detailed Leak Dump For new
006 Memory Snapshots
007 Memory Snapshots - Code Example
008 Memory Checkpoint Helper Class
009 Issues With Checkpoints (& Resolution)
010 Snapshots in Visual Studio - I
011 Snapshots in Visual Studio - II
013 Report Mode & Type
014 Report Mode & Type - Code Example
015 Report Mode File
06 - Custom Leak Detector
002 Leak Detection Internals
003 Memory Block Header & ptmalloc() Function
004 Implementation Of ptfree() Function
005 Implementation Of PtDumpLeaks() Function
006 Adding C++ Support - I
007 Adding C++ Support - Part II
008 Compiling on Linux
07 - Heap Corruption Support
001 Detecting Heap Corruption - Internal Implementation
002 PtCheckMemory() Implementation - I
003 PtCheckMemory() Implementation - II
004 Alignment & Structure Padding
005 Aligning Memory Block Header
The text was updated successfully, but these errors were encountered: