首先来看这段程序:
我在里面进行了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步编写的代码可以认为是相对安全的。举一个具体的实例:
(完)