电脑故障

位置:IT落伍者 >> 电脑故障 >> 浏览文章

平坦内存空间中的层次结构:Heap和Stack


发布日期:2021/3/3
 

《Windows 用户态程序高效排错》市场价元 特价元 购买>>

本小结主要介绍Heap相关的崩溃和内存洩漏和如何使用pageheap来排错首先介绍heap的原理不同层面的内存分配接下来通过例子代码举例演示heap问题的严重性和欺骗性最后介绍如何使用pageheap工具高效地对heap问题排错

Heap是对平坦空间的高效管理和利用

内存是容纳代码和资料的空间无论是stackheap还是DLL都是生长在内存上的代码的执行效果其实是对内存上的资料进行转化内存是导致问题最多的地方比如内存不足内存访问违例内存洩漏等都是常见的问题

关于内存的详细信息Programming Applications for Microsoft Windows书中有详细介绍这里针对排错作一些补充

Windows API中有两类内存分配函数分别是VirtualAlloc和HeapAlloc前一种是向操作系统申请KB为边界的整块内存后者是分配任意大小的内存块区别在于后者的实现依赖于前者换句话说操作系统管理内存的最小单位是KB这个粒度是固定的(其实根据芯片是可以做调整的这里只讨论最普遍的情况)但用户的资料不可能恰好都是KB大小KB作单位难免会产生很多浪费解决办法是依靠用户态代码的薄计工作实现比KB单位更小的分配粒度换句话说用户态的程序需要实现一个Memory Manager通过自身的管理KB为粒度的基础上提供以字节为粒度的内存分配释放功能并且能够平衡好时间利用率和空间利用率

Windows提供了Heap Manager完成上述功能HeapAlloc函数是Heap Manager的分配函数Heap Manager的工作方式大概是这样首先分配足够的大的 KB倍数的连续内存空间然后在这块内存上开辟一小块区域用来做薄计接下来把这连续的大块内存分割成很多尺寸不等的小块把每一小块的信息记录到薄计里面薄计记录了每一小块的起始地址和长度以及是否已经分配

当内存请求发生的时候HeapManager根据请求的长度在薄计信息里面找到大小最合适的一块空间把这块空间标记成已经分配然后把这块空间的地址返回这样就完成了一次内存分配如果找不到长度足够大的空闲小块Heap Manager继续以 KB为粒度向系统申请更多的内存

当用户需要释放内存的时候调用HeapFree同时传入起始地址HeapManager在薄计信息中找到这块地址把这块地址的信息由已经分配改回没有分配当Heap Manager发现有大量的连续空闲空间的时候也会调用VirtualFree来把这些内存归还给操作系统在实现上面这些基本功能的情况下 HeapManager还需要考虑到

分配的内存最好是在字节边界上这样可以提高内存访问效率

做好线程同步保证多个线程同时分配内存的时候不会出现错误

尽可能节省维护薄计的开销提高性能避免不必要的计算和检查所以HeapManager假设用户的代码没有bug比如用户代码永远不会越界对内存块进行存取这样就可以省去检查核对的开销

优化内部的内存块管理比如灵活地合并连续的内存小块以便满足长尺寸的内存申请或者拆分连续内存块提高小尺寸的内存使用率

有了上面的理解后看下面一些情况

如果首先用HeapAlloc分配了一块空间然后用HeapFree释放了这块空间但是在释放后继续对这块空间做操作程序会发生访问违例错误吗?答案是不会除非HeapManager恰好把那块地址用VirtualFree返还给操作系统了但是带来的结果是什么?是非预期结果也就是说谁都无法保证最后会产生什么情况程序可能不会有什么问题也可能会格式化整个硬盘出现得最多的情况是这块内存后来被Heap Manager重新分配出去导致两个本应指向不同地址的指针指向同一个地址伴随而来的是资料损坏或者访问违例等等

如果用HeapAlloc分配了KB的空间但是访问的长度超过了KB会怎么样?如果KB恰好在KB内存边界上而且恰好后面的内存地址并没有被映像上来程序不会崩溃的这时越界的写操作要么写到别的内存块上要么就写入薄计信息中破坏了薄计导致的结果是 HeapManager维护的数据损坏导致非预期结果

其他错误的代码比如对同一个地址HeapFree了两次多线程访问的时候忘记在调用HeapAllocate的第二个参数中传入SERIALIZE bit等等都会导致非预期结果

总的来说上面这些情况都会导致非预期结果如果问题发生后程序立刻崩溃或者抛出异常则可以在第一时间截获这个错误但是现实的情况是这些错误不会有及时的效果错误带来的后果会暂时隐藏起来在程序继续执行几个小时后突然在一些看起来绝对不可能出现错误的地方崩溃比如在调用HeapAllocate/HeapFree的时候崩溃比如访问一个刚刚分配好的地址的时候崩溃这个时候哪怕抓到了崩溃的详细信息也无济于事因为问题根源潜伏在很久以前这种根源在前现象在后的情况会给调试带来极大的困难

仔细考虑这种难于调试的情况错误之所以没有在第一时间暴露在于下面两点

Heap每一块内存的界限是Heap Manager定义的而内存访问无效的界限是操作系统定义的哪怕访问越界如果越界的地方已经有映像上来的KB为粒度的内存页程序就不会立刻崩溃

为了提高效率Heap Manager不会主动检查自身的数据结构是否被破坏

所以为了方便检查Heap上的错误让现象尽早表现出来Heap Manager应该这样管理内存

把所有的Heap内存都分配到KB页的结尾然后把下一个KB页面标记为不可访问越界访问发生时候就会访问到无效地址程序就立刻崩溃

每次调用Heap相关函数的时候Heap Manager主动去检查自身的数据结构是否被破坏如果检查到这样的情况就主动报告出来

上一篇:保护Windows更新服务

下一篇:日志秘密Windows登录类型知多少