Loading

【侯捷:C++内存管理】VC6内存分配

VC6内存分配

本节课通过解析VC6在调用main函数前的初始化行为来分析VC6的内存管理方法。

函数调用栈

image

上图展示了C++程序运行时函数调用栈的行为,该图表示程序运行时,函数调用的顺序。栈中的函数从下往上依次被调用,缩进表示该函数被上一级缩进的函数调用。该课程部分将着重于 _heap_init(...) 函数和 _ioinit() 的解析。

SBH(Small Block Heap) 小区块内存管理

image

在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用于界定给程序分配的内存和调试信息。

image

  1. 指针,这里暂时不提;
  2. 指针,这里暂时不提,其实1和2两个指针使其构成了一个双向链表;
  3. 文件名,指明调用该内存的文件;
  4. 行数,指明调用该内存的代码在文件中的行号;
  5. 数据大小,指明分配内存大小,这里是256字节,十六进制就是0x100;
  6. 暂时不提;
  7. 流水号码,标注该内存块在内存管理中的编号;
  8. 两块gap将实际的Data包围,用于界定数据,防止在写内存的时候越界,两块gap用0xfd填充,如果其中的数据被修改,说明程序写越界了;
  9. 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就是用来连接其他分配的内存块的:

image

其中,_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);

image

__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中,bitvEntryHibitvEntryLobitvCommit 用于对管理的内存状态进行标识,这里不详细展开,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 初始状态

image

每一个GROUP又被分成了了8个PAGEs,一个PAGE管理4KB的内存,这8个PAGEs初始状态如下:

image

其中保留的8个字节是为了对齐16字节,两个黄色的 0xffffffff 是为了界定这一段内存,防止后续合并内存的时候越界,然后紧接用这一段内存的大小包围实际可用的空间,在内存的开始用两个指针将8个PAGE串在一起,形成双向链表,初始的双向链表挂在GROUP中64对指针的最后一对指针上。这里使用了64个双向链表应该是为了管理不同大小的空闲内存块,这样程序向SBH申请不同大小的内存时,SBH可以从不同大小的空闲内存进行切割,尽可能防止碎片的产生。

第一次内存分配(2)

_ioinit 申请了304个字节的内存(见 第一次内存分配 第一部分),SBH从第一个GROUP的第一个PAGE中的末端切出0x130大小的内存交给程序,如下图所示:

image

至此,将这块空间的首地址返回,完成了程序的第一次内存分配。

posted @ 2024-01-14 00:52  杨谖之  阅读(47)  评论(0编辑  收藏  举报