【侯捷:C++内存管理】VC6内存分配
VC6内存分配
本节课通过解析VC6在调用main函数前的初始化行为来分析VC6的内存管理方法。
函数调用栈
上图展示了C++程序运行时函数调用栈的行为,该图表示程序运行时,函数调用的顺序。栈中的函数从下往上依次被调用,缩进表示该函数被上一级缩进的函数调用。该课程部分将着重于 _heap_init(...)
函数和 _ioinit()
的解析。
SBH(Small Block Heap) 小区块内存管理
在VC6中,当程序向内存管理索要内存的时候,需要进行判断,如果索要的内存大小 小于1016字节 时,将由SBH内存管理为其分配内存,否则内存管理调用操作系统的API函数 HeapAlloc
为其分配内存。
_heap_init 函数
// heapinit.c
int __cdecl _heap_init(int mtflag) {
// Initialize the "big-block" heap first.
if((crtheap =
HeapCreate(
mtflag ? 0 : HEAP_NO_SERIALIZE, BYTES_PER_PAGE, 0
)) == NULL) {
return 0;
}
// Initialize the small-block heap
if (__sbh_heap_init() == 0) {
HeapDestroy(_crtheap);
return 0;
}
return 1;
}
_heap_init
函数中,程序使用操作系统API函数 HeapCreate
向操作系统索要一块内存,并命名为 _crtheap
(即使用该指针变量指向这块内存的起始地址,这块内存的初始大小为4096,操作系统会按程序的需要扩张这块内存,所以这个初始大小没有什么意义。
成功获取这块内存后, _heap_init
函数调用__sbh_heap_init
函数对 _crtheap
内存进行扩张。
// sbheap.c
int_cdecl __sbh_heap_init (void) {
if(!(__sbh_pHeaderList =
HeapAlloc(_ertheap,0,16*sizeof(HEADER)))) {
return FALSE;
}
sbh_pHeaderScan =__sbh_pHeaderList;
sbh_pHeaderDefer = NULL;
sbh_cntHeaderList = 0;
_sbh_sizeHeaderList = 16;
return TRUE;
}
__sbh_heap_init
函数使用操作系统API函数 HeapAlloc
对 _crtheap
内存块进行扩张,获取了16块 HEADER
类型大小的内存块,这16个 HEADER
组成的内存块被叫做 __sbh_pHeaderList
。
HEADER 类型定义
typedef unsigned int BITVEC;
typedef struct tagHeader {
BITVEC bitvEntryHi;
BITVEC bitvEntryLo;
BITVEC bitvCommit;
void *pHeapData;
struct tagRegion *pRegion;
} HEADER, *PHEADER;
HEADER
是一个结构体,该结构体包括三个BITVEC类型变量和两个指针,该结构体内部各变量的作用暂时忽略,后续将对其进行讨论。
程序第一次内存分配(1)
在 _ioinit
函数中,程序进行了第一次内存分配,这个函数与IO操作有关,但是这里不进行讨论,这里只对这一条内存分配动作进行分析。
typedef struct {
long osfhnd;
char osfile;
char pipech;
}
void __cdecl _ioinit(void) {
...
if (
(pio = _malloc_crt(IOINFO_ARRAY_ELTS * sizeof(ioinfo))) == NULL
)
...
}
在 _ioinit
函数中,调用了 _malloc_crt
函数进行了内存分配,IOINFO_ARRAY_ELTS * sizeof(ioinfo)
大小为32*8(8是因为内存对齐,这里不展开),即函数请求了32*8大小的内存。
_malloc_crt 函数
#ifndef _DEBUG
#define _malloc_crt malloc
...
#else /*_DEBUG*/
#define _THISFILE __FILE__
#define _malloc_crt(s) _malloc_dbg
(s, _CRT_BLOCK,
_THISFILE, __LINE_)
#endif /*_DEBUG*/
可以看到, _malloc_crt
函数在非调试模式下就是 malloc
函数,而在调试模式下为 _malloc_dbg
函数,该函数除了为程序分配请求的内存空间外,在该内存上附加了调试信息,调试信息部分大小定义如下:
/*Memory block identification */
#define _FREE_BLOCK 0
#define _NORMAL_BLOCK 1
#define _CRT_BLOCK 2
#define _IGNORE_BLOCK 3
#define _CLIENT_BLOCK 4
#define _MAX_BLOCKS 5
_heap_alloc_dbg 函数
...
blockSize = sizeof(_CrtMemBlockHeader) + nSize + nNomanLandSize;
...
pHead = (_CrtMemBlockHeader*)_heap_alloc_base(blockSize);
nSize
为程序请求的内存大小,这个例子中,nSize为256,nNoManLandSize用于界定给程序分配的内存和调试信息。
- 指针,这里暂时不提;
- 指针,这里暂时不提,其实1和2两个指针使其构成了一个双向链表;
- 文件名,指明调用该内存的文件;
- 行数,指明调用该内存的代码在文件中的行号;
- 数据大小,指明分配内存大小,这里是256字节,十六进制就是0x100;
- 暂时不提;
- 流水号码,标注该内存块在内存管理中的编号;
- 两块gap将实际的Data包围,用于界定数据,防止在写内存的时候越界,两块gap用0xfd填充,如果其中的数据被修改,说明程序写越界了;
- data区域初始用0xcd填充,表示未被使用。
#define nNoMansLandSize 4
typedef struct _CrtMemBlockHeader {
struct _CrtMemBlockHeader *pBlockHeaderNext;
struct _CrtMemBlockHeader *pBlockHeaderPrev;
char *szFileName;
int nLine;
size_t nDataSize;
int nBlockUse;
long lRequest;
unsigned char gap[nNoMansLandSize];
/* followed by:
* unsigned chardata[nDataSize];
* unsigned char anotherGap[nNoMansLandSize];
*/
} _CrtMemBlockHeader;
如下图所示,所有分配的内存块会用链表串在一起,上面提到的指针1和2就是用来连接其他分配的内存块的:
其中,_pFirstBlock
和 _pLastBlock
是 _CrtMemBlockHeader
类型的指针,并且为静态全局变量,用来记录链表的头和尾,而新分配的内存块会成为链表头,这里新分配的 pHead
就会成为链表头节点。
_heap_alloc_base 函数
if (size <= __sbh_threshold) { //3F8, i.e. 1016
pvReturn = __sbh_alloc_block(size);
if(pvReturn)
return pvRetrurn;
}
if (size == 0)
size = 1;
size = (size + ...) & ~(...);
return HeapAlloc(_crtheap, 0, size);
在 _heap_alloc_base
函数中,对程序请求分配的内存大小进行判断,如果请求大小小于等于1016,就交由 __sbh_alloc_block
函数进行分配,否则交给操作系统进行分配。
__sbh_alloc_block 函数
// add 8 bytes entry overhead and round up to next para size
sizeEntry =
(nSize + 2 * sizeof(int) + (BYTES_PER_PARA-1))&
~(BYTES_PER_PARA - 1);
__sbh_alloc_block
函数给数据块加上头尾两个块,用来表示该数据块总大小(图中洋红色),并且将数据对齐到16字节,由于对齐后的数据最后一位一定为0,SBH将其作为标识位,如果为1,表示这块内存已经分配出去,反之表示该内存由SBH管理。
SBH 内存管理策略
我们回顾一下在 _heap_init
函数中申请的64个Headers:
typedef unsigned int BITVEC;
typedef struct tagHeader {
BITVEC bitvEntryHi;
BITVEC bitvEntryLo;
BITVEC bitvCommit;
void *pHeapData;
struct tagRegion *pRegion;
} HEADER, *PHEADER;
SBH使用HEADER对1MB的内存进行管理,64个HEADER也就是总共能管理64MB的内存,在每个HEADER中,bitvEntryHi
、bitvEntryLo
、bitvCommit
用于对管理的内存状态进行标识,这里不详细展开,pHeapData
指针指向一块1MB的虚拟内存空间,而 pRegion
指向一个REGION结构体,该结构体用于对1MB内存进行更精细化的管理。
REGION 结构体
typedef struct tagRegion {
int indGroupUse;
char cntRegionSize[64];
BITVEC bitvGroupHi[32];
BITVEC bitvGroupLo[32];
struct tagGroup grpHeadList[32];
} REGION, *PREGION;
1个HEADER管理的1MB内存在REGION中被分成了32个GROUP,每个GROUP管理32KB的内存,REGION中通过64对指针管理这些内存,每一对指针连接了一串双向链表。
REGION 初始状态
每一个GROUP又被分成了了8个PAGEs,一个PAGE管理4KB的内存,这8个PAGEs初始状态如下:
其中保留的8个字节是为了对齐16字节,两个黄色的 0xffffffff
是为了界定这一段内存,防止后续合并内存的时候越界,然后紧接用这一段内存的大小包围实际可用的空间,在内存的开始用两个指针将8个PAGE串在一起,形成双向链表,初始的双向链表挂在GROUP中64对指针的最后一对指针上。这里使用了64个双向链表应该是为了管理不同大小的空闲内存块,这样程序向SBH申请不同大小的内存时,SBH可以从不同大小的空闲内存进行切割,尽可能防止碎片的产生。
第一次内存分配(2)
_ioinit
申请了304个字节的内存(见 第一次内存分配 第一部分),SBH从第一个GROUP的第一个PAGE中的末端切出0x130大小的内存交给程序,如下图所示:
至此,将这块空间的首地址返回,完成了程序的第一次内存分配。