7.3 堆变量
堆变量是所有变量表现形式中最容易识别的。在C\C++中,使用malloc与new实现堆空间的申请,返回的数据便是申请的堆空间地址。相对应的,使用free与delete完成堆空间释放,但需要申请堆空间时得到的首地址。如果这个首地址丢失将无法释放堆空间,从而导致内存泄漏。
保存堆空间首地址的变量大小为4字节的指针类型,其访问方式按作用域来分,和之前所介绍过的全局、局部以及静态的表现形式相同,故不再讲解。
C++中的new与delete属于运算符,在没有定义重载的情况下,它们的执行过程与malloc、free类似。我们以malloc与new为例来进行介绍,如代码清单7-4所示。
代码清单7-4 new与malloc的区别
//C++源码说明(Debug编译选项):new与malloc
//malloc内部实现
char*pCharMalloc=(char*)malloc(10);
_CRTIMP void*__cdecl malloc(
size_t nSize
){
//使用_nh_malloc_dbg申请堆空间
return_nh_malloc_dbg(nSize,_newmode,_NORMAL_BLOCK, NULL,0);
}
//new内部实现
char*pCharNew=new char[10];
void*operator new(unsigned int cb){
return_nh_malloc(cb,1);
}
void*__cdecl_nh_malloc(
size_t nSize,
int nhFlag
){
//使用_nh_malloc_dbg申请堆空间
return_nh_malloc_dbg(nSize, nhFlag,_NORMAL_BLOCK, NULL,0);
}
代码清单7-4为malloc和new的内部实现,可见,使用new申请堆空间最终也会使用到“_nh_malloc_dbg”和malloc。当它们被执行后,将会返回所申请堆空间的首地址。(calloc、realloc与malloc类似,本节只对malloc进行讲解。)
堆空间的分配类似于商场中的商铺管理,malloc是从商场的空地中划分出一块作为商铺,而new则可以将划分好的商铺直接租用。由于malloc缺少商铺的营业范围规定,因此需要将申请好的堆强制转换以说明其类型方可使用,而new则无需这种操作,直接可以使用。
当不再使用堆内存时,需要调用free与delete释放对应的堆。相当于退租时将商铺归还给商场,商场将商铺回收,用于下次出租。
那么这个出租、回收、再出租的过程如何实现呢?物业部门利用表格将每次租出的商铺进行记录,商铺被回收后,修改表格中对应的记录,对应铺位的状态置为“空闲”。当再次租用时,便会检查空闲的商铺是否符合要求,然后再次分配出租。堆空间的管理也是如此,通过表格记录每次申请的堆空间的信息。
确定变量空间属于堆空间只要找到两个关键点即可。
空间申请:malloc与new等;
空间释放:free与delete等。
在使用IDA分析反汇编代码时,需要安装对应的SIG符号文件,这样才可以在反汇编代码中快速识别出malloc与new(高版本的IDA中默认装有此符号文件,可直接识别)。如图7-9所示。
图 7-9 malloc与new的识别
通过图7-9中对malloc与new的识别,即可得知此处在申请堆空间,得到堆空间的首地址。知道了堆空间的起始处,如何找到其销毁处呢?与malloc和new对应的有free和delete,只要确定free与delete所释放的地址和malloc与new所申请的堆空间地址一致,即可确定该堆空间的生命周期,如代码清单7-5所示。
代码清单7-5 堆空间的生命周期
//C++源码说明:堆空间生命周期识别
char*pCharMalloc=(char*)malloc(10);//申请堆空间
char*pCharNew=new char[10];//申请堆空间
if(pCharNew!=NULL){
delete[]pCharNew;//释放堆空间
pCharNew=NULL;
}
if(pCharMalloc!=NULL){
free(pCharMalloc);//释放堆空间
pCharMalloc=NULL;
}
代码清单7-5分别使用了delete与free来释放new和malloc所申请的堆空间。free与delete的识别原理和malloc与new相同,都需要装有对应的SIG符号文件,如图7-10所示。
图 7-10 delete与free的识别
通过对图7-9与图7-10的比较和分析可得知所申请的堆空间的生命周期。在分析的过程中,关于堆空间的释放不能只看delete与free,还需要结合new和malloc确认所操作的是同一个堆空间。
了解了堆空间的产生与销毁,那么堆空间中都存储哪些信息呢?编译器又是如何管理它们的呢?申请堆空间的过程中调用了函数_heap_alloc_dbg,其中使用_CrtMemBlockHeader结构描述了堆空间中的各个成员。在内存中,堆结构的每个节点都是使用双向链表的形式存储的,在_CrtMemBlockHeader结构中定义了前指针pBlockHeaderPrev和后指针pBlockHeaderNext,通过这两个指针就可遍历程序中申请的所有堆空间。成员lRequest记录了当前堆是第几次申请的,例如第10次申请堆操作对应的数值为0x0A;成员gap为保存堆数据的数组,在Debug版下,这个数据的前后4个字节被初始化为0xFD,用于检测堆数据访问过程中是否有越界访问。_CrtMemBlockHeader结构的原型如下:
typedef struct_CrtMemBlockHeader{
struct_CrtMemBlockHeader*pBlockHeaderNext;//下一块堆空间首地址(实际上指向的是前//一次申请的堆信息)
struct_CrtMemBlockHeader*pBlockHeaderPrev;//上一块堆空间首地址(实际上指向的是后
//一次申请的堆信息)
char*szFileName;
int nLine;
size_t nDataSize;//堆空间数据大小
int nBlockUse;
long lRequest;//堆申请次数
unsigned char gap[nNoMansLandSize];//堆空间数据
}_CrtMemBlockHeader;
以上结构定义在VC安装目录下的“\VC98\CRT\SRC\DBGINT.H”文件中。
CrtMemBlockHeader便是调试版堆空间管理的每一项数据,有了此结构,就可以管理所申请的堆空间。在释放过程中,根据堆数据的首地址将所释放的堆从链表中脱链,完成堆释放操作。
学习了堆结构的理论知识,接下来让我们来实践一下,分析堆结构在内存中的表现形式,通过将图7-11与_CrtMemBlockHeader结构进行对比,解析出堆结构中的重要数据。
图 7-11 堆空间数据说明
在图7-11中,内存监视窗口的数据为使用malloc后申请的堆空间数据。new或malloc函数返回的地址为堆数据地址0x00431BF0,堆数据地址减4后,其数据为0xFDFDFDFD,这是往上越界的检查标志。堆数据地址减8后数据为0x2A,表示此堆空间为第0x2A次申请堆操作,说明在其之前多次申请过堆空间。堆数据空间的容量存储在地址0x00431BE0处,该堆空间占10个字节大小。地址0x00431BD0处为上一个堆空间首地址。地址0x00431BD4处的数据为0,表示没有下个一堆空间。在堆数据的末尾也加入了0xFDFDFDFD,这是往下越界的检查标志,这是程序编译方式为Debug版的重要特征之一。
当某个堆空间被释放后,再次申请堆空间时会检查这个被释放的堆空间是否能满足用户要求。如果能满足,则再次申请的堆空间地址将会是刚释放过的堆空间地址,这就形成了回收空间的再次利用。
通过以上讨论可以得到堆空间的基本信息,但是对于堆空间中存放的数据类型,则需要进一步分析对该堆空间的使用方式,结合各种数据类型的特征以得到对应的数据类型,综合以前所学知识即可,这里不再赘述。