相关函数
标准库函数
C语言标准对堆内存的申请提供了三个函数:
void *malloc( size_t size );
void *calloc( size_t num, size_t size );
void *realloc( void *memblock, size_t size );
释放堆内存提供一个函数:
void free( void *memblock );
系统调用
C标准函数落地到具体的操作系统,需要系统调用,Windows提供了如下5个系统调用:
HANDLE HeapCreate(DWORD flOptions, DWORD dwInitialSize, DWORD dwMaximumSize);
LPVOID HeapAlloc(HANDLE hHeap, DWORD dwFlags, DWORD dwBytes);
LPVOID HeapReAlloc(HANDLE hHeap, DWORD dwFlags, LPVOID lpMem, DWORD dwBytes);
BOOL HeapFree(HANDLE hHeap, DWORD dwFlags, LPVOID lpMem);
BOOL HeapDestroy(HANDLE hHeap);
Debug版函数
在VC++平台,除了C标准库函数,还提供了相对应的调试版函数:
void *_malloc_dbg( size_t size, int blockType, const char *filename, int linenumber );
void *_calloc_dbg( size_t num, size_t size, int blockType, const char *filename, int linenumber );
void *_realloc_dbg( void *userData, size_t newSize, int blockType, const char *filename, int linenumber );
源码调试
先准备一段C代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <crtdbg.h>
int main(int argc, char *argv[], char *env[])
{
char *psz = (char *)malloc(5);
strcpy(psz, "xxyy");
int *pn = (int *)calloc(sizeof(int), 3);
*pn = 999;
psz = (char *)realloc(psz, 2);
psz = (char *)realloc(psz, 20);
free(pn);
pn = (int *)malloc(sizeof(int));
*pn = 666;
free(pn);
free(psz);
return 0;
}
在VC++6中,main函数是由mainCRTStartup函数调用的,可以通过栈帧窗口定位到该函数,然后在堆初始化处打一个断点:
步入_heap_init方法,这里调用了系统调用HeapCreate:
单步往下执行,可以看到有调用HeapDestroy的地方,不过该分支并没有执行到:
在main函数中调用malloc的地方打断点:
层层步入:
最终执行到这里,调用了HeapAlloc系统调用:
接下来跟进calloc函数:
步入_calloc_dbg,这里将入参nSize和nNum相乘并将乘积赋值给nSize,并且再用nSize调用_malloc_dbg:
继续跟进的话,发现最终也调用了HeapAlloc:
再跟进realloc函数:
最终调用了HeapReAlloc:
最后跟进free函数:
最终调用了HeapFree:
在main函数返回后,会调用exit函数结束进程,exit最终会跟进到一个ExitProcess调用,里面会调用HeapDestroy销毁堆,但由于这是一个系统函数,所以无法步入。
总结:
进程启动时调用HeapCreate创建堆;
malloc底层调用了HeapAlloc;
calloc先计算num*size,再调用malloc;
realloc底层调用了HeapReAlloc;
free底层调用了HeapFree;
进程退出时,调用HeapDestroy。
堆内存结构分析
VC++平台提供的调试版函数可以在堆内存中添加更多附加信息,方便调试,所以我们在代码中定义几个宏:
#ifdef _DEBUG
#define malloc(size) _malloc_dbg(size, _NORMAL_BLOCK, __FILE__, __LINE__);
#define calloc(num, size) _calloc_dbg(num, size, _NORMAL_BLOCK, __FILE__, __LINE__);
#define realloc(p, size) _realloc_dbg(p, size, _NORMAL_BLOCK, __FILE__, __LINE__);
#endif
这样在使用C标准函数时会替换为调试版函数,以_malloc_dbg为例解释入参含义:
#define _FREE_BLOCK 0
#define _NORMAL_BLOCK 1
#define _CRT_BLOCK 2
#define _IGNORE_BLOCK 3
#define _CLIENT_BLOCK 4
#define _MAX_BLOCKS 5
对于我们在程序中申请的内存块,通常传_NORMAL_BLOCK即可,也就是1 。
运行代码至调用malloc之后,打开内存窗口,跳转到psz所指向的地址(0x00382a80):
图中蓝框区域属于刚申请的堆区,每个蓝格子代表一个附加信息。
0x00382a80地址处有5个连续的CD,VC++平台Debug版本会将已申请的堆内存初始化为0xCDCD(刚好对应GBK编码中的”屯“),由于我们申请了5字节,所以这里有5个CD。
申请的堆内存还有很多附加信息,在向前偏移32字节,也就是0x00382a60处,图中第一个蓝格子框中的4字节,按小端序读出为0x00382218,这是上一个堆块的地址,如果是第一个堆块,该值为0 ;第二个蓝格子是下一个堆块地址,如果是最后一个堆块则为0;第三个蓝格子保存了一个地址,该地址指向一个字符串,也就是源文件全路径,按小端序读出为0x00422024,跳转到该地址可见:
第四个蓝格子是行号0x0E,说明是在代码中第14行申请的堆块;第五个蓝格子是申请的字节数;第六个蓝格子是blockType,我们传入的_NORMAL_BLOCK也就是1;第七个蓝格子是编号0x29,编号从1开始,当前是第0x29个堆块;第八个蓝格子是四个连续的0xFD,这是堆块的上溢标志;第九个蓝格子是5个连续的0xCD,也正是我们申请的5字节;接下来又是连续的4个0xFD,是堆块的下溢标志。后面的字节无需关注。往下两行有很多0xFEEE填充的字节,这是调试状态下尚未分配的堆标志。
单步指行strcpy之后:
可见"xxyy"连同末尾的'\0'已经被复制到申请的5字节中。
再单步执行一行,使用calloc申请12字节内存:
此时,上一个堆块中,第二个四字节保存了新堆块的地址0x00382aa8,下面的蓝框区域就是新申请的堆块,同理第一个四字节0x00382a60指向前一个堆块;第二个四字节是0代表这是最后一个堆块;接下来是源文件路径指针,行号,申请的字节数,blockType,堆块编号(上一个是0x29,这次+1是0x2A),上溢标志,接下来是连续12个0字节代表申请了12字节的内存,这是calloc与malloc不同之处,calloc会将申请内存初始化为0,而不是0xCDCD,最后是下溢标志。
接下来执行realloc将psz指向的堆内存缩减为2字节:
此时原堆块中的变化:行号变为0x14,申请字节数变为2,编号变为0x2B,原先的xxyy\0被截断为xx,紧接着是下溢标志。
接下来执行realloc将psz指向的内存扩展为20字节:
此时原堆块区域以及不够用了,所以需要新开辟一块内存,并且将原堆块释放掉,所以原堆块区域变为了0xFEEE填充的字节;
pn指向的堆块,前一个堆块指向了0x00382218,后一个堆块指向了0x00382af0,也就是扩容之后的新psz,新psz前一个堆块指向0x00382aa8,是目前最后一个堆块,接下来依次包含信息:源文件路径,行号,申请字节数0x14,blockType,编号0x2C,上溢标志,从原堆块复制来的xx,以及剩余用0xCDCD填充的初始化字节,下溢标志。
接下来执行free(pn),pn执行的堆块被释放,用0xFEEE填充。
接下来,再申请4字节内存给pn:
此时原psz指向的堆块是足够容纳新堆块的,所以新堆块地址还是在0x00382a60处,新psz堆块中指向下一个堆块的区域变为了0x00382a60,新pn堆块前一个堆块区域是0x00382af0,是目前最后一个堆块,接下来依次包含信息:源文件路径,行号,申请字节数4,blockType,编号0x2D,上溢标志,用0xCDCD填充的初始化字节,下溢标志。
最后释放pn,psz,所有区域都变为0xFEEE填充:
最后于 4小时前
被米龙·0xFFFE编辑
,原因: