#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):为了帮助开发者调试,调试版通常会在堆分配的内存块周围增加额外的字节(称为哨兵或保护字节),这些字节用来检测缓冲区溢出和未初始化的内存。调试堆会在每个内存块的前后添加特定的字节标记(例如 0xFD0xCD),以便监控内存溢出或非法内存访问行为。

    举例来说,在调试版中,C++ 标准库中的 malloc()new 会在堆内存块的前后加上保护字节,防止溢出,并且会记录分配的调用栈,方便在出现问题时定位内存泄漏等问题。

  • 发布版(Release):为了提高性能,发布版通常会省去这些额外的检测机制,仅分配程序实际需要的内存。堆分配和释放的效率更高,但不会进行内存溢出保护或未初始化内存的检查。

2. 内存对齐

  • 调试版(Debug):调试版堆在内存分配时可能会额外对齐内存,以便更容易调试某些问题,并为调试器提供额外信息。例如,分配的内存块可能会被对齐到更大的边界。

  • 发布版(Release):发布版通常会采用更高效的内存对齐策略,避免浪费过多的内存空间,以获得最佳的性能和内存使用率。

3. 内存管理函数的额外开销

  • 调试版(Debug):在调试版本中,通常会使用带有调试信息的内存管理函数。例如,某些库会提供 malloc_dbg()free_dbg() 这类带有调试功能的分配和释放函数,用来记录分配堆内存时的文件和行号,以及调用堆栈信息。这可以帮助开发者追踪内存泄漏和内存非法访问。

  • 发布版(Release):发布版使用标准的内存分配函数,如 malloc()free(),不记录额外的调试信息。内存管理开销更低,但失去了调试功能。

4. 内存清理和初始化

  • 调试版(Debug):在调试版本中,分配的内存块通常会被初始化为某些预定义的值,例如 0xCDCDCDCD0xCCCCCCCC,以便在调试过程中发现未初始化内存访问。这些值的存在帮助开发者更容易定位未初始化的内存问题。

  • 发布版(Release):发布版中,分配的内存通常不会被自动初始化(除非程序明确地进行初始化),以减少性能开销。这意味着内存块中的数据可能包含之前程序的残留数据。

5. 内存释放的处理

  • 调试版(Debug):在调试版中,释放的内存块可能不会立即被重新分配,以帮助开发者检测释放后继续使用(use-after-free)的问题。有时,调试堆会将释放的内存标记为某种特定值(例如 0xDDDDDDDD),防止已释放内存被误用。

  • 发布版(Release):在发布版中,内存块一旦被释放,就会立即返回到堆以供后续分配使用。这种策略提升了内存使用效率,但在某些情况下可能导致调试难度增加。

调试版的堆结构更加冗余,设计上更多关注内存安全和调试信息,例如:

  • 增加内存溢出保护字节

  • 提供调用栈信息

  • 初始化未使用的内存

  • 延迟内存释放

    这些特性在调试时非常有帮助,但会带来性能损失。

之后写的都是debug版的堆

调试版堆里面FE EE,或DD DD表示堆空间空闲

未初始化的堆空间,初始化之后变为CD CD

堆的附加数据,在得到返回指针-0x20的位置,这出CD CD返回的是指针位置

一共分为十个字段

  1. 0x8205E0这个是前一个堆的地址,如果为0就是第一块堆

  2. 第二个红框是后一个堆的地址,如果为0就是最后一块堆

  3. 申请堆的文件的信息

  4. 申请的堆的代码的那行的行数

  5. 堆的体积,是用户用的体积,不包含附加数据的体积

  6. 堆的类型,0x01是normal堆

  7. 堆的编号,但是不能表示堆的总数,可能中间有被释放的

  8. 这第八个框的四个FD是上溢标志,如果这个四个FD不在了,表示程序上溢

  9. 堆的正文

  10. 这第十个框的四个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;
}

这个程序会崩溃,如果这个环境代码很多,没有时间单步调试,我们需要使用各种断点,各种调试工具定位。

  1. 首先阅读提示信息

告诉你63号堆,出现了问题

  1. 第二步看栈窗口,然后看我们的位置,我们可能出错的就是我们有控制权的位置,就是这个main

  1. 我们点击main函数跳转过去,然后我们检查我们的堆,发现psz的后四个FD没了

  1. 此刻我们因为有若干代码,所以我们采用折半查找,最后定位到是strcpy那行代码出的问题,我们分配的时候能看到第一个图片是没问题的。第二个图片发现,他溢出了。所以我们应该给空间变大。

  1. 找到解决方法,给malloc + 1就好