提示:前面几篇笔记基本都是侧重理论,下面用2个示例进行讲解:一个是堆块的分配和释放,另一个是
FreeLists
的作用(可以直接看下面的图,很容易理解,画了一个多小时…)
示例1:堆块分配和释放
用一个示例来说明堆块的分配和释放,侧重点主要是对堆块分配后,返回的地址是哪里?以及堆块释放后,申请的内存变化,被谁回收了?
//说明:为了说明3个函数的作用,省略了函数是否调用成功的返回值判断逻辑
#include "stdafx.h"
#pragma optimize("", off) //关闭优化,为了更好的观察局部变量和栈帧
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
HANDLE h_heap = HeapCreate(HEAP_GENERATE_EXCEPTIONS, 0x2001, 0xfe001); //创建私有堆
LPVOID p_heap_block = HeapAlloc(h_heap, HEAP_GENERATE_EXCEPTIONS, 0x123); //私有堆上分配一块内存
bool free_ok = HeapFree(h_heap, HEAP_NO_SERIALIZE, p_heap_block); //释放
bool destory_ok = HeapDestroy(h_heap); //销毁私有堆
return 0;
}
下面对源码中处理堆块的2个函数HeapAlloc
和HeapFree
重点进行讲解
1.HeapAlloc
执行完 HeapAlloc(h_heap, HEAP_GENERATE_EXCEPTIONS, 0x123)
,在新创建的堆上想要获取一个大小是0x123的堆块
#查看局部变量(返回地址)
0:000> dv /i
prv local h_heap = 0x01200000 #私有堆的地址(句柄)
prv local p_heap_block = 0x012004b0 #返回地址是0x012004b0
说明:分配给应用程序内存区(用户数据区)的起始地址
0x012004b0
= 第一个用户堆块地址0x012004a8
(FirstEntry
指定的值,后面有介绍) +_HEAP_ENTRY
结构大小(8bytes)
查看堆段和堆块的相关信息,主要关注012004b0
和012004a8
是怎么得到的?
###[1].查询 堆段 相关信息
#私有堆的地址(句柄)是0x01200000,直接查看_HEAP_SEGMENT结构
0:000> dt _HEAP_SEGMENT 0x01200000
ntdll!_HEAP_SEGMENT
+0x000 Entry : _HEAP_ENTRY
+0x008 SegmentSignature : 0xffeeffee
+0x018 Heap : 0x01200000 _HEAP
+0x01c BaseAddress : 0x01200000 Void
+0x024 FirstEntry : 0x012004a8 _HEAP_ENTRY #指向第一个用户堆块
+0x028 LastValidEntry : 0x012ff000 _HEAP_ENTRY
#第一个用户堆 + 8bytes(_HEAP_ENTRY长度)就是HeapAlloc函数返回的用户数据区的起始地址
? 0x012004a8+8
Evaluate expression: 18875568 = 012004b0 = p_heap_block,HeapAlloc函数返回的用户数据区的起始地址
###[2].查询 堆块 相关信息
#前8个字节是HEAP_ENTRY结构
0:000> dt _HEAP_ENTRY 0x012004b0-8
ntdll!_HEAP_ENTRY
+0x000 Size : 0x4018 #单位是分配粒度8bytes
+0x002 Flags : 0x32 '2'
+0x003 SmallTagIndex : 0xca '' #堆块的标记序号
+0x000 SubSegmentCode : 0xca324018
+0x004 PreviousSize : 0x9ac5 #前一个堆块的大小
+0x006 SegmentOffset : 0 ''
+0x006 LFHFlags : 0 ''
+0x007 UnusedBytes : 0x1d '' #用户数据区后未使用的字节数
0:000> dd p_heap_block l1 #p_heap_block是一个指针
00d8fd68 012004b0
#查看堆块的内容
0:000> dd poi(p_heap_block) l8
012004b0 baadf00d baadf00d baadf00d baadf00d #填充为bad food(调试器中运行,系统自动启动对堆的调试支持)
012004c0 baadf00d baadf00d baadf00d baadf00d
2.HeapFree
执行完 HeapFree(h_heap, HEAP_NO_SERIALIZE, p_heap_block);
,释放刚才申请的堆内存
#执行前
0:000> dd poi(p_heap_block) l8
012004b0 baadf00d baadf00d baadf00d baadf00d #默认都是baadf00d
012004c0 baadf00d baadf00d baadf00d baadf00d
#执行后
0:000> dd poi(p_heap_block) l8
012004b0 012000c0 012000c0 feeefeee feeefeee #除了前8个字节,其他都被填充为feeefeee(相当于free)
012004c0 feeefeee feeefeee feeefeee feeefeee
#说明:
#1.填充为feeefeee,这正是堆管理器对已经释放堆块所填充的内容
#2.前8个字节实际上构造成_LIST_ENTRY结构体Flink和Blink的指向都是012000c0,下面是详细解释:
#扩展:已经释放的堆块,堆管理器用一个专门的数据结构来描述,这个结构的前8个字节与HEAP_ENTRY结构一样,多增加了
# 8个字节的空闲链表节点
0:000> dt _HEAP_FREE_ENTRY 0x012004b0-8
ntdll!_HEAP_FREE_ENTRY
+0x000 HeapEntry : _HEAP_ENTRY
+0x000 UnpackedEntry : _HEAP_UNPACKED_ENTRY
+0x000 Size : 0x4557
+0x002 Flags : 0x31 '1'
+0x003 SmallTagIndex : 0x83 ''
...
+0x008 FreeList : _LIST_ENTRY [ 0x12000c0 - 0x12000c0 ] #空闲链表节点
3.!heap
命令
使用!heap
命令观察堆块信息,下面是释放内存后的堆信息
#!heap简单介绍
0:000> !heap 0x01200000 -hf
Index Address Name Debugging options enabled
3: 01200000 #堆句柄
Segment at 01200000 to 012ff000 (00003000 bytes committed) #0号段
Flags: 40001064
Granularity: 8 bytes #分配粒度
Segment Reserve: 00100000
Segment Commit: 00002000
DeCommit Block Thres: 00000200
DeCommit Total Thres: 00002000
Total Free Size: 00000567 #空闲链表中堆块的总大小(以分配粒度为单位)
FreeList[ 00 ] at 012000c0: 012004b0 . 012004b0 #00号空闲链表
012004a8: 004a8 . 02b38 [104] - free #空闲堆块
Heap entries for Segment00 in Heap 01200000 #00号段中的堆块
#堆块起始地址 前一个堆块的字节数 本堆块字节数 状态(用户数据区字节数)(堆块的标记序号)
address: psize . size flags state (requested size)
01200000: 00000 . 004a8 [101] - busy (4a7) #包含段结构,尺寸004a8正好是第一个用户堆块地址
012004a8: 004a8 . 02b38 [104] free fill #等于空闲堆块总和
01202fe0: 02b38 . 00020 [111] - busy (1d) #段中最后一个块,空闲块
01203000: 000fc000 - uncommitted bytes. #未提交区域
#说明:堆块起始地址就是HEAP_ENTRY的起始地址,用户数据区字节数不包含未使用的字节
0:000> dd 01200000+004a8
012004a8 83314557 00009ac5 012000c0 012000c0
012004b8 feeefeee feeefeee feeefeee feeefeee
012004c8 feeefeee feeefeee feeefeee feeefeee
如果感兴趣可以观察一下堆块释放前后的堆结构对比,会发现结构是一样的,唯一的区别:
- 释放前:堆块用户数据区是用
baadfood
和必要的对齐信息填充 - 释放后:堆块开头用16 bytes构造一个
_HEAP_FREE_ENTRY
结构,然后剩下的内容用feeefeee
填充
4.问题:申请大小是0x123个字节,在哪里可以看到?
WinDbg中用
!heap
加参数-hf
可以查看申请和释放内存前后heap的变化
#注意:示例中堆句柄与上面不同,因为是另一次调试的结果,不影响说明内存申请和释放影响
#1.栈上申请内存
#LPVOID p_heap_block = HeapAlloc(h_heap, HEAP_GENERATE_EXCEPTIONS, 0x123);
0:000> !heap 01320000 -hf
3: 01320000
Heap entries for Segment00 in Heap 01320000
address: psize . size flags state (requested size)
01320000: 00000 . 004a8 [101] - busy (4a7) #包含段结构,尺寸004a8正好是第一个用户堆块地址
013204a8: 004a8 . 00140 [107] - busy (123), tail fill #申请的内存0x123在这里,有填充,总大小0x140
013205e8: 00140 . 029f8 [104] free fill #空闲堆块的总和:0x029f8
01322fe0: 029f8 . 00020 [111] - busy (1d)
01323000: 000fc000 - uncommitted bytes.
#2.栈上释放内存
#bool free_ok = HeapFree(h_heap, HEAP_NO_SERIALIZE, p_heap_block);
0:000> !heap 01320000 -hf
Index Address Name Debugging options enabled
3: 01320000
Heap entries for Segment00 in Heap 01320000
address: psize . size flags state (requested size)
01320000: 00000 . 004a8 [101] - busy (4a7)
013204a8: 004a8 . 02b38 [104] free fill #申请的内存大小0x123不见了,归还给空闲堆块了
01322fe0: 02b38 . 00020 [111] - busy (1d)
01323000: 000fc000 - uncommitted bytes.
#3.对上释放后的空闲堆块大小 - 释放前的空闲堆块大小 = HeapAlloc申请的0x123内存所在的堆块的大小0x00140
0:000> ? 02b38 - 029f8
Evaluate expression: 320 = 00000140
下面针对内存回收中使用的FreeLists重点进行介绍
示例2:内存分配中FreeLists作用
FreeLists
链表里面保存了空闲内存,当程序员申请一个堆块时,会先在FreeLists
里面找看看是否能提供要求大小的内存?
- 能提供:就直接用空闲堆块构造一个符合要求的堆块
- 不能提供:commit一部分内存,在新内存中构造一个新堆块
堆块构造完,会将这个堆块的用户数据区首地址返回,同时也会更新FreeLists
链表
#1.FreeLists记录指定堆中的所有空闲堆块,起始地址 = 堆句柄 + FreeLists偏移(0x0c0)
#2.双向链表
typedef struct _LIST_ENTRY
{
struct _LIST_ENTRY *Flink; //指向前一个堆块(的用户数据区)
struct _LIST_ENTRY *Blink; //执行后一个堆块(的用户数据区)
} LIST_ENTRY, *PLIST_ENTRY;
#注意:一定要注意Flink和Blink并不是指向一个完整堆块的起始地址,而是指向用户数据区
源码:
#include "stdafx.h"
#include <windows.h>
int main(int argc, char* argv[])
{
HANDLE hProcessHeap = GetProcessHeap(); //获取默认堆句柄
BYTE* pBlock1 = (BYTE*) HeapAlloc(hProcessHeap, 0, 16);//分配
HeapFree(hProcessHeap, 0, pBlock1); //释放
return 0;
}
WinDbg调试
- 1.运行完
GetProcessHeap()
,查看默认堆的相关信息
#1.默认堆的句柄
0:000> dv
hProcessHeap = 0x012a0000
#2.查看默认堆的详细信息
0:000> !heap 012a0000 -hf
Index Address Name Debugging options enabled
1: 012a0000
Segment at 012a0000 to 0139f000 (00008000 bytes committed)
FreeList[ 00 ] at 012a00c0: 012a5e30 . 012a4c08 #空闲链表
012a4c00: 00040 . 00010 [104] - free
012a48e8: 00040 . 00038 [104] - free
012a5e28: 00458 . 021b8 [104] - free
Heap entries for Segment00 in Heap 012a0000 #段中每个堆块的详细信息
address: psize . size flags state (requested size)
012a0000: 00000 . 004a8 [101] - busy (4a7)
012a04a8: 004a8 . 00118 [107] - busy (117), tail fill Internal
...
012a48a8: 00028 . 00040 [107] - busy (28), tail fill
012a48e8: 00040 . 00038 [104] free fill #第一个空闲块
012a4920: 00038 . 000c0 [107] - busy (a8), tail fill
...
012a4bc0: 00138 . 00040 [107] - busy (24), tail fill
012a4c00: 00040 . 00010 [104] free fill #第二个空闲块
012a4c10: 00010 . 00028 [107] - busy (10), tail fill
...
012a59d0: 00060 . 00458 [107] - busy (440), tail fill
012a5e28: 00458 . 021b8 [104] free fill #第三个空闲块
012a7fe0: 021b8 . 00020 [111] - busy (1d)
012a8000: 000f7000 - uncommitted bytes.
#3.空闲列表信息
0:000> dt _HEAP 012a0000
ntdll!_HEAP #Flink #Blink
+0x0c0 FreeLists : _LIST_ENTRY [ 0x12a4c08 - 0x12a5e30 ]
#4.使用dl查看链表,dt和!list命令使用相对难一点,dl会很简单
#dl语法:dl 起始地址 要显示的节点数 节点结构的长度(单位是指针长度)
#说明:起始地址必须是LIST_ENTRY(双向链表) 或 SINGLE_LIST_ENTRY(单向链表)结构
0:000> dl 0x12a00c0 5 2
012a00c0 012a4c08 012a5e30
012a4c08 012a48f0 012a00c0
012a48f0 012a5e30 012a4c08
012a5e30 012a00c0 012a48f0
- 2.下面是运行
HeapAlloc(hProcessHeap, 0, 16);
使用WinDbg解析出的FreeLists相关内容重新整理的示意图
关注点:
-
FreeLists
链表中Flink和Blink
是指向用户数据区的首地址,而使用!heap
命令显示的FreeList
中地址是堆块的首地址#堆块首地址 + HEAP_ENTRY结构体大小(8bytes)= 用户数据区首地址
-
申请的16 bytes内存是在3个空闲链表中的一个构造的;空闲堆块构造一个堆块相当于将
HEAP_FREE_ENTRY
结构向高地址平移 -
HeapAlloc返回的是堆块中用户数据区的首地址
-
申请16 bytes内存空间,内存空间所属的堆块消耗了40个bytes
参考
- 1.《软件调试》第二版,卷2的第23章
- 2.《Windows高级调试》,第6章
- 3.《深入解析Windows操作系统》第七版,第五章
- 4.《Windows编程调试技术内幕》