分析WinXP下VC++6.0Debug的堆分配机制

首先来看这段程序:

我在里面进行了malloc操作也就是分配了堆空间。单步执行完malloc:

用黑色背景色标出的内存块即为刚刚分配的12字节长度堆内存。接下来来详细解析一下其组成部分:

一个堆分配完后内存分布大体如此:

来分别解释下:

  • 堆链前驱指针: 该指针指向了上一个分配的堆块的可用空间首地址。如果本堆块是第一个则前驱指针为NULL代表没有上一个
  • 堆链后继指针: 该指针指针了下一个分配的堆块的可用空间首地址。如果本堆块是最后一个则后继指针为NULL代表没有下一个
  • 所在文件: 标识分配堆的进程的文件的路径(可选参数),可以发现malloc就不会赋值这个字段
  • 所在函数: 表示分配本堆的malloc语句在哪一行
  • 分配大小: 指代可用空间的大小
  • 类型: 指代堆的类型
  • 编号: 一个编号
  • 上溢标志: 该字段一般为0xDFDFDFDF, 如果该字段被赋值,代表发生了上溢出
  • 分配的可用空间: 即malloc分配的内存块大小,其大小值在分配大小字段指出
  • 下溢标志: 与上溢标志一样,该字段一般为0xDFDFDFDF, 如果该字段被赋值,代表发生了下溢出

可以对照着上面来看一下:

首先分配大小为0xC,可以发现Hello world!的确是12字节大小

类型为1, 这里要仔细提一下,首先来进入malloc函数查看一下, 打开反汇编:

可以看到它调用了malloc, 进入该call:

可以发现内部调用了_nh_malloc_dbg这个函数,并且传入了nSize(很明显是大小)为)0xC。可以清晰看到eax为0xC并压栈了

 

_NORMAL_BLOCK应该是堆的类型

其他都基本上是0和NULL。还有一个_newnode发现值为0,不知道是什么不用管。

探明到这里就停了

可以知道的一点就是,在malloc内部其默认帮我们传递了非常多的参数,我们控制的只有大小这个变量而已。

接下来看一个非标准函数,这也是个堆分配函数但提供了给了更多的详细细节可供填写, 这个函数就是_malloc_dbg:

查看一下msdn发现如下信息:

现在有了这些后,我们把_malloc_dbg用一个宏封装成malloc:

再来看下调试结果, 找到_malloc_dbg后进入:

看到了吧,其内部还是使用了_nh_malloc_dbg函数,但是参数中的类型,文件名和行号是由我们自己指定了。

如果是同时分配多个堆内存会是什么情况? 一般会是这样:

堆会被通过双向链表串起来,来举一个具体的实例吧:

来看看这段代码, 经过第一次malloc后,发现前驱指针不为空,到0x00382208看一下

转到0x382208后发现了如下:

看见了把上一个堆块的后继指针指向了0x00382A50,这正是第一个malloc分配的堆块的起始位置。

发现了什么,当strcpy执行完后Hello world!被复制到堆内,但是下溢标志中一个字节被覆盖成了空字符,这种情况下就导致了错误:

一般在这种情况下我们会选择终止,但是仔细观察下错误信息的地址为0x00382A70对吧,很眼熟,这个位置其实就是第一个malloc分配的堆块可用内存的起始位置,其编号是41号普通堆块,来看一下第一个malloc分配的堆块的编号是0x29即十进制的41

这种情况下我们就通过了显示的错误信息就排查到了错误在哪里,即首先找到对应的位置后,发现下溢标志被覆盖了一位。现在我马上执行第二条malloc语句,观察后继指针的值本来是0,代表最后一块堆

完成后发现,新堆块被链入了堆链中,并且成了新的堆链尾部:

接下去执行第三条malloc语句:

看吧就成了一条完整的堆链了, 释放的过程就是双向链表的删除节点的过程。只不过最后的节点指回了上一个堆节点并把释放的内容填充成0xFEEE。注意这里并不是一定要填充,因为DEBUG版为了方便程序员调试才这么做的。

释放最后第二个堆节点:

全部释放完:

编写了一段代码可以查看所有堆链的节点, 效果如下:

代码:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main()
{
	char *p1 = NULL, *p2 = NULL, *p3 = NULL;
	int *pBlink = NULL, *pFlink = NULL, i = 0;
	unsigned char *pTmp = NULL, *pBackup = NULL;
	int iLen = 0;
	p1 = (char *)malloc(16);
	p2 = (char *)malloc(32);
	p3 = (char *)malloc(64);
	pBlink = (int *)((int)p3 - 0x20);
	while (*pBlink)
	{
		pBlink = (int *)*pBlink;
	}
	pFlink = (pBlink + 1);
	while (1)
	{	
		printf("前驱指针: 0x%08X  后继指针: 0x%08X  文件: %08X  行数: %08X\n", 
			*(pFlink - 1), *pFlink, *(pFlink + 1), *(pFlink + 2));	
		printf("分配大小: %dB  类型: 0x%08X  编号: 0x%08X  上溢出标志: %08X\n",
			*(pFlink + 3), *(pFlink + 4), *(pFlink + 5), *(pFlink + 6));

		iLen = *(pFlink + 3);
		if (iLen > 8)
		{
			iLen = 8;
		}
		pTmp = (char *)((pFlink + 7));

		for (i = 0; i < iLen; ++i)
		{
			printf("0x%X ", *pTmp);
			pTmp++;
		}
		if (*(pFlink + 3) > 8)
		{
			printf("... ");
		}
		pTmp = (int *)(((char *)(pFlink + 7)) + (int)*(pFlink + 3));
		printf("下溢出标志: 0x%08X\n\n", *(int *)pTmp);
		if (NULL == *pFlink)
		{
			break;
		}
		pFlink = (int *)(*pFlink) + 1;
	}

	system("pause");
	
	return(0);
}

堆的释放与分配很容易造成严重的错误,在高级语言中有异常处理机制比如: try catch之类的可以捕获对应异常,但在低级语言比如汇编语言, C语言中没有。当然了Windows平台为C语言提供了其内部自带的SEH结构化异常处理,但假设在其他平台下如何保证其安全性:

下面介绍一种安全的有关资源的申请和释放的编写原则:

  • 1. 定义指针或资源引用型变量时必须初始化为NULL
  • 2. 申请资源后必须检查成败并做对应处理,比如转到异常处理或者转到某个出口集中处理
  • 3. 使用资源
  • 4. 出口处无论资源是否有效都必须检查,如有效则释放,释放完成后必须置为NULL值

按照以上4步编写的代码可以认为是相对安全的。举一个具体的实例:

(完)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值