[翻译] 深入解析win32 crt 调试堆(上)
这两天老得同时开好几个VC和BCB调试程序, 机器慢的要命, 每次构建都得要好一会, 而且BCB6在我手里老莫名其妙的出现不能单步的情况, 重新编译一遍就好了. 老盯着电脑看编译器的output或者盯着浏览器刷renren.com也不是办法. 就从网上找了一篇关于crt调试堆的文章, 里面说得比较详细的一篇文章, 也浅显易懂.
我发现拿零碎的时间翻译或者整理点小东西蛮不错, 不然平时两三分钟的时间根本不知道该拿来干什么.
原文见: 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, 那么你一定是忘记了初始化一些东西.
(未完待续)