17_堆
#include <stddef.h>
#include <tchar.h>
#include <string.h>
#include <crtdbg.h>
#include <stdlib.h>
#ifdef _DEUBG
#DEFINE malloc(n) _malloc_dbg(n, _NORMAL_BLOCK, __FILE__, __LINE__)
#endif
int main(){
int *p = (int *)malloc(4);
*p = 999;
char *psz = (char *)malloc(20);
strcpy(psz, "Hello");
char *psz2 = (char *)realloc(p, 1);
psz2 = "a";
p = (int *)realloc(psz2, 80);
p[0] = ox6666;
p[1] = 0x8888;
free(p);
free(psz);
return 0;
}
可以自己调试试试
在程序开发中,发布版(Release)和调试版(Debug)对于内存管理和堆的结构处理有显著的不同。以下是两者在堆结构上的差异:
1. 内存分配策略
调试版(Debug):为了帮助开发者调试,调试版通常会在堆分配的内存块周围增加额外的字节(称为哨兵或保护字节),这些字节用来检测缓冲区溢出和未初始化的内存。调试堆会在每个内存块的前后添加特定的字节标记(例如
0xFD
或0xCD
),以便监控内存溢出或非法内存访问行为。举例来说,在调试版中,C++ 标准库中的
malloc()
或new
会在堆内存块的前后加上保护字节,防止溢出,并且会记录分配的调用栈,方便在出现问题时定位内存泄漏等问题。发布版(Release):为了提高性能,发布版通常会省去这些额外的检测机制,仅分配程序实际需要的内存。堆分配和释放的效率更高,但不会进行内存溢出保护或未初始化内存的检查。
2. 内存对齐
调试版(Debug):调试版堆在内存分配时可能会额外对齐内存,以便更容易调试某些问题,并为调试器提供额外信息。例如,分配的内存块可能会被对齐到更大的边界。
发布版(Release):发布版通常会采用更高效的内存对齐策略,避免浪费过多的内存空间,以获得最佳的性能和内存使用率。
3. 内存管理函数的额外开销
调试版(Debug):在调试版本中,通常会使用带有调试信息的内存管理函数。例如,某些库会提供
malloc_dbg()
和free_dbg()
这类带有调试功能的分配和释放函数,用来记录分配堆内存时的文件和行号,以及调用堆栈信息。这可以帮助开发者追踪内存泄漏和内存非法访问。发布版(Release):发布版使用标准的内存分配函数,如
malloc()
或free()
,不记录额外的调试信息。内存管理开销更低,但失去了调试功能。
4. 内存清理和初始化
调试版(Debug):在调试版本中,分配的内存块通常会被初始化为某些预定义的值,例如
0xCDCDCDCD
或0xCCCCCCCC
,以便在调试过程中发现未初始化内存访问。这些值的存在帮助开发者更容易定位未初始化的内存问题。发布版(Release):发布版中,分配的内存通常不会被自动初始化(除非程序明确地进行初始化),以减少性能开销。这意味着内存块中的数据可能包含之前程序的残留数据。
5. 内存释放的处理
调试版(Debug):在调试版中,释放的内存块可能不会立即被重新分配,以帮助开发者检测释放后继续使用(use-after-free)的问题。有时,调试堆会将释放的内存标记为某种特定值(例如
0xDDDDDDDD
),防止已释放内存被误用。发布版(Release):在发布版中,内存块一旦被释放,就会立即返回到堆以供后续分配使用。这种策略提升了内存使用效率,但在某些情况下可能导致调试难度增加。
调试版的堆结构更加冗余,设计上更多关注内存安全和调试信息,例如:
增加内存溢出保护字节
提供调用栈信息
初始化未使用的内存
延迟内存释放
这些特性在调试时非常有帮助,但会带来性能损失。
之后写的都是debug版的堆
调试版堆里面FE EE,或DD DD表示堆空间空闲
未初始化的堆空间,初始化之后变为CD CD
堆的附加数据,在得到返回指针-0x20的位置,这出CD CD返回的是指针位置
一共分为十个字段
0x8205E0这个是前一个堆的地址,如果为0就是第一块堆
第二个红框是后一个堆的地址,如果为0就是最后一块堆
申请堆的文件的信息
申请的堆的代码的那行的行数
堆的体积,是用户用的体积,不包含附加数据的体积
堆的类型,0x01是normal堆
堆的编号,但是不能表示堆的总数,可能中间有被释放的
这第八个框的四个FD是上溢标志,如果这个四个FD不在了,表示程序上溢
堆的正文
这第十个框的四个FD是下溢标志,如果这个四个FD不在了,表示程序下溢
0x8205E0是前一个堆的地址,第三个黑方块里0x422F20是调用文件的路径_file.c
堆块类型定义
从24行运行到25行,发现后一个堆块的地址变成了0x8538B8,这就是新开辟的堆的地址。这就是新开辟的,我们执行下一步
从27到28,realloc重新分配,我们发现变成了大小变成01,再执行下一步,我们那一个位置分配成a,也就是ascii 61。这里我们没有行和文件路径,是因为没用_realloc_dbg
执行30行,第一步开辟空间,第二步把原数据复制过去,第三步释放空间
新空间如下: 一共有0x50 也就是80个
再看刚刚的那个psz2的空间,发现已经释放了
执行完31,32。已经赋值完毕
执行free(p)
因为是p在psz后面,释放p,psz的指向下一个堆的地址清零。
free(psz),最后这片地方都被清除了,变为FE EE。只有调试版才会清除,发布版会有残留值。
要强调其实堆的生长方向不确定,一般情况下,如果是连续分配堆块,是低地址往高地址,和栈对立生长。但是还是要看堆算法,是从前到后搜索空闲区域,还是从后向前搜索空闲区域,有空闲的地方就可以放。
其次就是这些附加数据只有debug版本有,release版本只有分配表和代码数据。
下面举个例子,怎么调试以及下断点
#include <stddef.h>
#include <tchar.h>
#include <string.h>
#include <crtdbg.h>
#include <stdlib.h>
#ifdef _DEUBG
#DEFINE malloc(n) _malloc_dbg(n, _NORMAL_BLOCK, __FILE__, __LINE__)
#endif
int main(){
//puts("test")表示有若干代码的意思
puts("test");
puts("test");
puts("test");
puts("test");
puts("test");
puts("test");
puts("test");
char *psz = (char *)malloc(strlen("Hello World!"));
puts("test");
puts("test");
puts("test");
puts("test");
puts("test");
puts("test");
puts("test");
strcpy(psz, "Hello World!")
puts("test");
puts("test");
puts("test");
puts("test");
puts("test");
puts("test");
puts("test");
free(psz);
return 0;
}
这个程序会崩溃,如果这个环境代码很多,没有时间单步调试,我们需要使用各种断点,各种调试工具定位。
首先阅读提示信息
告诉你63号堆,出现了问题
第二步看栈窗口,然后看我们的位置,我们可能出错的就是我们有控制权的位置,就是这个main
我们点击main函数跳转过去,然后我们检查我们的堆,发现psz的后四个FD没了
此刻我们因为有若干代码,所以我们采用折半查找,最后定位到是strcpy那行代码出的问题,我们分配的时候能看到第一个图片是没问题的。第二个图片发现,他溢出了。所以我们应该给空间变大。
找到解决方法,给malloc + 1就好