1

VC6_SBH 概述

SBH 是 Small Block Heap 的缩写,进行操作系统之上的小区块内存的管理。在 VC6 中可找到源码实现,升级版本 VC10 中统一使用系统API进行内存申请, SBH 被整合到了操作系统内部。

VC6 部分实现

image.png

VC10 部分实现

image.png

注意: __heap_init__ioinit 在 VC6 和 VC10 中都存在

VC6_SBH 分解

heap_init(...)

_heap_init(...)__sbh_heap_init() 说明 :

CRT 会先为自己创建一个 _ctrheap "独特"内存(HeapCreate), 然后从中配置 SBH 所需的 headers, regions(__sbh_heap_init).应用程序内存申请时如果 size > threshold 就以 HeapAlloc() 从 _crtheap 取.若size <= thread 就从 SBH 取(实际区块来自 VirtualAlloc)

image.png

1. 操作系统内存管理的一些概念:可创建一块独特的空间并给其命名,之后相关操作所需的内存都取自这里【逻辑上分类】
1.1 HeapCreate 要求操作系统分配一块内存
1.2 __crtheap 全局指针变量,指向分配的内存空间,专为 CRT(C Run Time) 使用

2. __sbh_heap_init,到 __ctrheap 指向的内存获取 16 * sizeof(Header) 空间 
2.1 每个 SBH 中还有 16 个HEADER (当16 个HEADER用光后,会再进行分配)
header 结构分析

image.png

1. 在 _sbh_heap_init() 中含有对 Header 的配置,包括对所属的 region 的配置
2. pHeadData 指向实际所管理的可分配的内存区块
3. pRegin 指向管理 Region 区块,其中存储了所管理内存的控制信息 

_heap_init() 和 __sbh_heap_init() 调用完成之后就可以进行下一步 _ioinit(), 使用 SBH 对小区块的内存请求进行响应和分配了

_ioinit()

_ioinit() 完成了应用程序启动后的“第一次”内存申请

_malloc_dbg()

示例中按照 dbg 模式进行

image.png

_heap_alloc_dbg(nSize)

首先了解 SBH 对所分配的内存块的结构设计,可分为三部分:
_CrtMemBlockHeader、data[nDateSize]、anotherGap[nNoMansLandSize]。
由此可以知道当应用程序申请 data[nSize] 实际消耗的内存空间大小为 blockSize = sizeof(_CrtMemBlickHeader) + nSize + nNoMansLandSize;
由此又可知 _heap_alloc_dbg(nSize) 的主要任务,调整申请的内存大小,增加DebugHeader,并进行值填充和初始化。

image.png

1. pBlockHeaderNext, 指向链表后内存块
2. pBlockHeaderPre,  指向链表前内存块
3. szFileName, 记录申请使用该内存空间所在的文件
4. nLine,      记录申请使用该内存空间所在文件的行号
5. nDataSize,  实际数据区的空间大小(应用程序实际使用到的内存块)
6. nBlockUse,  表示当前内存块的类型, 如 _CRT_BLOCK、_NORMAL_BLOCK 等...
7. IRequest,   操作流水号
8. gap[nNoMansLandSize], 上下两处“栏杆”,保护应用程序实际使用到的内存,到发生内存越界的情况,调试器可以检查到

image.png

1. 上图主要对分配的空间进行数据填充和值初始化,以后续对内存块管理
2. 在调试模式下,malloc 分配的内存都被管理在链表中(及时已被客户使用),因此调试器可以实现复杂的功能
3. _pFirstBlock、_pLastBlock 全局指针

_heap_alloc_base(size) 说明 :

完成对实际内存的分配和管理
image.png

1. 当应用程序进行内存申请时,内存相关底层实现会根据申请空间的大小选择不同的接口
1.1 当申请量小于等于 1016Bytes 时调用 __sbh_alloc_block
1.2 当申请量大于 1016Bytes 时调用操作系统提供的API HeapAlloc() 从 __crtheap(全局变量) 中取内存

2. __sbh_threshold = 1016 = 1024 - (2 * 4). 其中 2 * 4Bytes 对应两个 cookies 的开销

_sbh_alloc_block(...)

image.png

1. (应用程序实际申请量 + DebugHeader + cookie * 2) 向上调整 16 字节 【类似于 std::alooc 中的 ROUNDUP()】
2. 0x100 + 0x24 + 4 * 2 = 0x12C = 130; 因为是16的倍数,所以上下cookie最后四位都为0,借用最后 1 bit来标记当前内存是否空闲(1用户使用,0空闲)

__sbh_alloc_new_region()

之前的_ioinit()内部调用细节,主要完成实际分配内存大小的调整,自此函数开始真正的内存管理动作(可以看到真正的内存管理结构)

image.png

1. __sbh_heap_init 创建的 16 个header, 每个 header 负责管理 1M 字节内存(调用系统virtual alloc获得),未来应用程序的小区块内存申请都从这里切割(管理中心成本 16k 字节)。
2. bitvGroupHi, bitvGroupLow 对应 32 * (32 * 2) = 32 * 64bit 的空间,对应32个Group中的64个双向循环链表,记录其中每个链表是否已有空闲节点(1表示有,0表示无)
3. listHeader[64] 表示每个 Group 中的 64 个双向循环链表

__sbh_alloc_new_group()

image.png

1. 一个 Header 对应管理 1MB 内存,其被逻辑分为 32 块(1MB / 32 = 32kB) 对应到 32 个 Group 
2. 每块内存又被分为 8 page(32KB / 8 = 4KB)
3. 每个 page 被组织到 Group 的最后一条链表中
4. 当有内存请求时,便从此链表中进行切割提供。当 Group0 再无内存可用,Group 1 便再去管理对应逻辑分块的第二块内存进行分配
Group中 listHead 管理规则:

listHead[0] 管理 16 字节内存块,依次叠加 16 字节增长至 listHead[63] (1024)。其中所有大于等于 1KB 的内存块都被 listHead[63] 管理

image.png

1. 上图黄色标注 0xffffffff (-1) 可理解为“阻隔器”,在内存回收合并时使用,保证相邻的page总是被分离的(不会交融合并)
2. 上图红色标注三个区块是 ENTRY 结构,4080 对应 sizeFont (表示空闲字节数), 是阻隔器之间空间大小
3. 已知 1page 是 4096 字节, 减去两个”阻隔器“ 4096 - (2 * 4) = 4088,预留(浪费)8字节以进行 16 对齐 4088 - 8 = 4080
4. 在上图 Group 结构中,只有 64 对指针,并没有看到 sizeFont。这因为 Group 只是自由链表的辅助节点,并未对应实际的page空间,因此为了节省内存 sizeFont 借用了上一个 Entry 的pEntryPrev

-----------------
EntryNext
EntryPrev | sizefont 
-----
EntryNext
EntryPrev
-----------------

经过以上步骤,SBH 已经将一定数量的内存空间纳入自己的管理之下,并能够依照自己的规则向应用程序提供内存

image.png

函数调用栈返回,返回应用程序申请得到的内存地址
1. ioint 进行了第一次 0x100 Bytes 的内存申请
2. 经过调整为 0x130 Bytes  [(应用程序实际申请量 + DebugHeader + cookie * 2) 向上调整 16 字节对齐]
3. 调整 sizeFont 为 0xEC0 (0xFF0 - 0x130 = 0xECO)
--
4. 切割 0x130 大小内存,填充上下 Cookie 空间为 0x131 (130 - > 131 前面已解释)
5. 返回申请得到的内存空间(不包含 Cookie 地址)
6. 填充构造 DebugHeader 
7. 返回用用程序实际申请大小的内存空间 (不包含 DebugHeader)

TianSong
737 声望139 粉丝

阿里山神木的种子在3000年前已经埋下,今天不过是看到当年注定的结果,为了未来的自己,今天就埋下一颗好种子吧