09 年翻译的东西。
原文见: http://www.nobugs.org/developer/win32/debug_crt_heap.html
在DeviceStudio的Debug编译模式下, crt中的堆内存分配操作----包括malloc()和free()----使用一个特殊的, 便于调试的版本, 我们称之为crt debug堆(译注: 下面简称CDH). 相比于电光火石(译注: 原文blazingly, 我想不出更确切的说法)的运行效率, 调试版本更关注对于堆错误的定位, 它通过以下三种手法实现以上诉求:
1.用守护内存块包围新分配的内存, 这样就可以侦测到缓冲过载和欠载. 所谓守护内存块就是一系列被填充为0xfd的内存字节, 又被称为”无主之地”.
2.用一个特殊的值(0xcd)初始化新申请的内存.
3.同时用一个特殊的值填充(0xdd) 被释放的内存.
为了便于记忆, 可以这样理解以上用于填充的值:
1.0xcd意为Clean Memory.
2.0xdd意为Dead Memory.
3.0xfd意为Fences(译注: 栅栏).
CDH将大部分工作交由win32 api中的堆函数HeapAlloc()和HeapFree()完成, 每进程4Gb的虚地址空间的分块和管理是由kernel32.dl中的Win32堆自己完成的.
当你调用malloc(8)分配8字节的内存时, CDH会调用HeapAlloc()申请48字节的内存, 额外的40字节被用来存放内存块的额外信息----比如调用malloc()的源文件和行号, 以及指向上/下一个内存块的指针.在后面的列表中, 所有的crt调试信息均被标记为红色.
HeaoAlloc()本身也需要记录簿记(译注: 原文bookkeeping)信息, 事实上,一个HeaoAlloc()调用会在进程地址空间里保留80字节内存, 其中8字节的簿记信息出现在真正使用的40字节之前, 剩下的32字节在真正使用的40字节之后.在下面的列表里, Win32堆簿记信息被标记为灰色.最后, HeapAlloc()返回的内存总是被一4字节对齐初始化为0xBAADF00D.顺道说一句: 当你使用经由VirtualAlloc()管理内存的虚拟内存管理器申请内存页时, 获取的内存页会被初始化为0, 所以可以断定HeapAlloc()按照上述模式执行了额外的初始化工作.
crt取得40字节的内存块后会填入自己的簿记信息. 头两个字(译注: 即WORD)用来存放直向”前一个”和”后一个”crt堆内存块的指针. 这里的前后不能从字面去理解, 因为所谓指向”后一个”内存块的指针事实上指向的是时间顺序上紧邻本内存块之前分配的内存块, 相应的, 指向“前一个”的指针指向的是下一个将被分配的内存块. 之所以这样命名, 是因为内存块链表是从最后分配的内存块开始的. 同时, 为了使堆检查代码能遍历每个内存块, CDH还保存着第一块和最后一块内存的地址(_pFirstBlock和_pLastBlock).
如果调用malloc()代码所在的文件名和行号是已知的, 它们将被被保存在第三第四个字中, 紧接着下面一个字表示本块申请了多少字节内存. 再下面一个字是类型域, 等于1表示new或malloc()分配的普通块, 2表示crt分配的供内部使用的块. 0表示已经被用户释放但是还未归还给win32堆的块(详见下文). 通常来说, 新申请的内存块本位置等于1. 最后一块是计数器, 每执行一次内存分配计数器加1.
通过malloc()得到的8字节内存无用内存包围. 这些空内存被填充为0xfd, 当整个内存块被free()时, crt会检查这些空内存存放的值是否仍然是0xfd. 如果值改变了, 说明程序有错误存在. 不幸的是, 错误可能发生的远比检查到的早, 如果需要更精确的检查, 可以使用Purify或者Boundschecker, 他们能让程序停止在内存崩溃的点上, 如果你不想花钱, 你可以等我写篇文章告诉你怎样巧妙的实现这个功能.
真正被使用的8字节内存被初始化为0xcd, 如果你的对象中间出现连续的0xcd, 那么你一定是忘记了初始化一些东西.
当你调用free()释放上述8字节的内存时, crt首先会用0xdddddddd填充全部48字节的内存块(包括簿记信息), 这样就可以通过检查这块内存的值获知这块内存在释放后是否右被写入过(比如使用野指针写内存).
接下来, crt通常会调用HeapFree()函数将本内存块归还给win32堆, win32堆会将本内存块填充为0xFEEEFEEE. 注意, crt并不维护”空闲块列表”, 这些都由HeapFree()来做(译注: 也就是说空闲列表是由win32 堆来维护的).
但是, 你可以让crt不把被释放的内存块归还给win32堆(译注: 也就是不调用HeapFree()), 方法是将_CRTDBG_DELAY_FREE_MEM_DF传递给_CrtSetDbgFlag(), 这在你跟踪野指针错误时将会派上用场,在这种情况下, 内存不会被复用, 所以释放过的内存的值必然是0xDDDDDDDD, 处非你对释放过的内存执行了写操作. 你可以调用_CrtCheckMemory()检查释放过的内存是否被篡改.(译注: 这个函数缺省情况下需要显式调用, 你也可以将_CRTDBG_CHECK_ALWAYS_DF传递给_CrtSetDbgFlag(), 这样每次分配和释放内存时都会调用_CrtCheckMemory())
一个例子:
下面是调用p = malloc(8)然后调用free(p)的过程中内存的变化表, malloc(8)返回的指针为0x00321000, 我列出了偏移后的内存值, 以便你找到你自己分配的内存信息.
Address | Offset | After HeapAlloc() | After malloc() | During free() | After HeapFree() | Comments |
0x00320FD8 | -40 | 0x01090009 | 0x01090009 | 0x01090009 | 0x0109005A | Win32 heap info |
0x00320FDC | -36 | 0x01090009 | 0x00180700 | 0x01090009 | 0x00180400 | Win32 heap info |
0x00320FE0 | -32 | 0xBAADF00D | 0x00320798 | 0xDDDDDDDD | 0x00320448 | Ptr to next CRT heap block (allocated earlier in time) |
0x00320FE4 | -28 | 0xBAADF00D | 0x00000000 | 0xDDDDDDDD | 0x00320448 | Ptr to prev CRT heap block (allocated later in time) |
0x00320FE8 | -24 | 0xBAADF00D | 0x00000000 | 0xDDDDDDDD | 0xFEEEFEEE | Filename of malloc() call |
0x00320FEC | -20 | 0xBAADF00D | 0x00000000 | 0xDDDDDDDD | 0xFEEEFEEE | Line number of malloc() call |
0x00320FF0 | -16 | 0xBAADF00D | 0x00000008 | 0xDDDDDDDD | 0xFEEEFEEE | Number of bytes to malloc() |
0x00320FF4 | -12 | 0xBAADF00D | 0x00000001 | 0xDDDDDDDD | 0xFEEEFEEE | Type (0=Freed, 1=Normal, 2=CRT use, etc) |
0x00320FF8 | -8 | 0xBAADF00D | 0x00000031 | 0xDDDDDDDD | 0xFEEEFEEE | Request #, increases from 0 |
0x00320FFC | -4 | 0xBAADF00D | 0xFDFDFDFD | 0xDDDDDDDD | 0xFEEEFEEE | No mans land |
0x00321000 | +0 | 0xBAADF00D | 0xCDCDCDCD | 0xDDDDDDDD | 0xFEEEFEEE | The 8 bytes you wanted |
0x00321004 | +4 | 0xBAADF00D | 0xCDCDCDCD | 0xDDDDDDDD | 0xFEEEFEEE | The 8 bytes you wanted |
0x00321008 | +8 | 0xBAADF00D | 0xFDFDFDFD | 0xDDDDDDDD | 0xFEEEFEEE | No mans land |
0x0032100C | +12 | 0xBAADF00D | 0xBAADF00D | 0xDDDDDDDD | 0xFEEEFEEE | Win32 heap allocations are rounded up to 16 bytes |
0x00321010 | +16 | 0xABABABAB | 0xABABABAB | 0xABABABAB | 0xFEEEFEEE | Win32 heap bookkeeping |
0x00321014 | +20 | 0xABABABAB | 0xABABABAB | 0xABABABAB | 0xFEEEFEEE | Win32 heap bookkeeping |
0x00321018 | +24 | 0x00000010 | 0x00000010 | 0x00000010 | 0xFEEEFEEE | Win32 heap bookkeeping |
0x0032101C | +28 | 0x00000000 | 0x00000000 | 0x00000000 | 0xFEEEFEEE | Win32 heap bookkeeping |
0x00321020 | +32 | 0x00090051 | 0x00090051 | 0x00090051 | 0xFEEEFEEE | Win32 heap bookkeeping |
0x00321024 | +36 | 0xFEEE0400 | 0xFEEE0400 | 0xFEEE0400 | 0xFEEEFEEE | Win32 heap bookkeeping |
0x00321028 | +40 | 0x00320400 | 0x00320400 | 0x00320400 | 0xFEEEFEEE | Win32 heap bookkeeping |
0x0032102C | +44 | 0x00320400 | 0x00320400 | 0x00320400 | 0xFEEEFEEE | Win32 heap bookkeeping |