堆(heap)系列_0x03:堆块 + malloc/new底层 + LFH(WinDbg分析)

  • 前言:一般情况下,我们使用的mallocnew分配堆上内存,我们直接操作返回的指针,用完这块内存在合适的时机进行释放

  • 异常:上面操作流程看似没有什么不妥,但是当出现堆内存被破坏时,不像栈被破坏是会直接影响附近的函数栈帧,程序立刻异常(shellcode破坏除外),堆内存被破坏往往很难在内存被破坏的第一时间发现问题,因此找到bug的难度更大

  • 建议:为了在堆溢出时,能有定位思路和使用相应的工具,在平时就要多积累堆内存的相关知识(出现bug一般都是紧急问题,基本没有学习时间的)

1.堆块

下面先介绍一下常规编程中基本不会接触到的术语:堆块
什么是堆块?
简单理解,就是你调用一次malloc或者new,操作系统就会自动分配一个堆块,然后将可操作部分(即用户数据区)的首地址返回给你,它是学习堆结构的基础,下面是堆块的示意图,先有一个直观的印象

在这里插入图片描述

堆创建完成后,相当于有一片大的连续的内存可以给应用程序服务;应用程序要使用堆中的内容,就涉及到堆块的分配和释放

  • 分配:应用程序调用堆管理器函数申请内存后,堆管理器会从自己维护的内存区中划分出一块满足要求的内存块(chunk,堆块),将这个内存块中可以被用户访问的起始地址返回给应用程序HeapAlloc函数的返回值)
  • 释放:应用程序用完,调用堆管理器函数释放即可(程序会进行合并堆块等操作)

注意:堆块的分配和释放涉及到堆的内存分割和合并等操作暂时不介绍

下面从两个维度对我们常用的堆分配函数进行介绍;堆管理器对HeapAlloc函数的支持,以及CRT堆对mallocnew的支持

2.堆管理器中的相关函数

直接从堆中分配空间可以使用HeapAlloc函数,对应的释放函数是HeapFree

//声明
DECLSPEC_ALLOCATOR LPVOID HeapAlloc(
  [in] HANDLE hHeap,			//一个堆的内存句柄
  [in] DWORD  dwFlags,		
  [in] SIZE_T dwBytes			//需要的内存块的字节数
);

调用成功时,返回指向所分配堆块中用户可以使用的区域的指针(简单理解就是我们可以使用的内存块的起始地址

#HeapAlloc函数实际上就是RtlAllocateHeap函数简单的包装
#查看栈回溯,HeapAlloc函数调用没有在栈帧里显示出来,而是直接调用RtlAllocateHeap
0:000> k L
 # ChildEBP RetAddr  
00 006ff6d4 77076e3c ntdll!RtlpAllocateHeap
01 006ff770 77075dde ntdll!RtlpAllocateHeapInternal+0x104c
02 006ff78c 00851078 ntdll!RtlAllocateHeap+0x3e
03 006ff7b0 0085124d test_heap!WinMain+0x38

3.CRT分配函数

编译器的运行时库在初始化阶段会创建CRT堆(通常是系统模式:CRT堆建立在Win32堆上,即最终会调用HeapAlloc函数),这个堆主要是给我们常用的mallocnew等内存分配函数或运算符使用的

注意:下面2个栈回溯示例是VS2010编译器的Release版本(Debug版本会加入一些中间函数)

  • C语言:最常使用的是malloc函数,对应的释放函数是free
#源码:int *p_malloc = (int*)malloc(0x100);
#malloc调用HeapAlloc(即RtlAllocateHeap)的过程
0:000> k L
 # ChildEBP RetAddr  
00 00a0f898 009010ea ntdll!RtlAllocateHeap				#HeapAlloc(即RtlAllocateHeap)
01 00a0f8b8 0090105a test_malloc!malloc+0x4b
02 00a0f8c0 009012ae test_malloc!main+0xa
03 00a0f908 76f4fa29 test_malloc!__tmainCRTStartup+0x10b
04 00a0f918 77097a9e KERNEL32!BaseThreadInitThunk+0x19
05 00a0f974 77097a6e ntdll!__RtlUserThreadStart+0x2f
06 00a0f984 00000000 ntdll!_RtlUserThreadStart+0x1b
  • C++:最常使用new运算符来创建对象和分配内存(可以简单理解成是malloc的包装),对应的释放操作符是delete
;int *p = new int[35];					;35 * 4bytes = 140 = 0x8c
;反汇编(Release版本)
00ed1280 688c000000      push    8Ch	;参数
00ed1285 e8fa030000      call    test_new!operator new (00ed1684)

#1.WinDbg查看的堆栈信息
0:000> kb L
 # ChildEBP RetAddr  Args to Child              
00 00e0f938 00ed409c 00ec0000 00000000 0000008c ntdll!RtlAllocateHeap
01 00e0f958 00ed16a3 0000008c 00ed50a6 00e0f954 test_new!malloc+0x4b		#内存大小(8c)
02 00e0f974 00ed128a 0000008c 00ed3ff4 00ed4008 test_new!operator new+0x1f	#内存大小(8c)
03 00e0f998 00ed188e 00000001 00ec1ef8 00ec1f48 test_new!main+0x1a
04 00e0f9e0 77406359 011fd000 77406340 00e0fa4c test_new!__tmainCRTStartup+0x10b

#2.再次看一次参数的十进制和转成int类型的大小
0:000> ? 8c;? 8c/4
Evaluate expression: 140 = 0000008c		
Evaluate expression: 35 = 00000023		#转成int类型的大小是35


;delete p;
;3.反汇编(Release版本)
00ed128a 50              push    eax
00ed128b e874030000      call    test_new!operator delete (00ed1604)
00ed1290 83c408          add     esp,8 

#4.WinDbg查看的堆栈信息
0:000> k L
 # ChildEBP RetAddr  
00 0137fd1c 00ed3b4c ntdll!RtlFreeHeap+0x9
01 0137fd30 00ed1290 test_new!free+0x1c
02 0137fd58 00ed188e test_new!main+0x20
03 0137fda0 77406359 test_new!__tmainCRTStartup+0x10b

共性:C/C++的调用栈回溯可以看出,最后都会间接调用ntdll!RtlAllocateHeap函数在堆上分配内存

4.分配和释放简单总结

  • 1、区分好对应关系:new/delete是一对,malloc/free是一对,HeapAlloc/HeapFree是一对

  • 2、真正的调用顺序:new/delete -> malloc/free -> HeapAlloc/HeapFree -> RtlAllocateHeap/RtlFreeHeap

    具体采用哪对函数看实际需求,越底层效率越高

  • 3、细节区别:在Release版本中,new/delete通常直接被编译为跳转指令;在Debug版本中,会加入很多内存检查功能

  • 4、解除提交:freedelete释放的堆上内存不一定会归还给系统,要同时满足下面2个条件才会调用NtFreeVitrualMemory函数释放堆内存给系统

    • 1.本次释放的堆块大小 > HeapDeCommitFreeBlockThreshold
    • 2.累积起来的总空闲内存 > HeapDeCommitTotalFreeThreshold

    上面的2个参数是哪里来的?创建堆时,会在PEB结构中记录HeapDeCommitFreeBlockThresholdHeapDeCommitTotalFreeThreshold的初始值,WinDbg查看如下

    #[01]查看PEB相关结构中解除提交的相关信息
    0:000> dt _PEB @$peb
       +0x018 ProcessHeap      : 0x00b50000 Void
       +0x080 HeapDeCommitTotalFreeThreshold : 0x10000				#64KB
       +0x084 HeapDeCommitFreeBlockThreshold : 0x1000				#4KB
    #解释:
    #1.本次释放堆空间大小超过4KB,且堆上的总空闲空间超过64KB,堆管理器会向系统的内存管理发出解除提交操作来真正释放内存
    #2.分配粒度:堆管理器内部使用 分配粒度 来表示HeapDeCommitFreeBlockThreshold和HeapDeCommitTotalFreeThreshold;
    #  且用GetSystemInfo函数可以查看分配粒度(通常是8bytes)
    
    #[02]!heap -v 观察分配粒度和解除提交阈值
    0:000> !heap 0x00b50000 -v										#0x00b50000是进程的默认堆
    Index   Address  Name      Debugging options enabled			#没有启动任何调试选项
      1:   00b50000 
        Segment at 00b50000 to 00c4f000 (0000e000 bytes committed)	#内存范围和提交字节数
        Flags:                40000062			#堆标志字段
        Granularity:          8 bytes			#分配粒度是8bytes
        Segment Reserve:      00100000			#堆的保留空间,1MB
        Segment Commit:       00002000			#每次提交的堆内存大小,8KB
        DeCommit Block Thres: 00000200			#解除提交的单块阈值,0x200 * 分配粒度8bytes = 0x1000,4KB
        DeCommit Total Thres: 00002000			#解除提交的总空闲阈值,64KB
        Total Free Size:      000004d2			#堆中空闲块的大小
    

5.扩展:低碎片堆(LFH)

提示:堆被反复操作,释放又不是很及时,就一定会出现碎片化的现象,为了解决这个问题,就一定知道LFH的概念

  • 堆碎片引入的解决什么问题?

堆函数申请的内存必须是一块连续的区间;如果堆内存被不断申请和释放后,有可能再也找不到一块连续的空间满足申请内存的要求,即使堆空闲空间满足要求,分配内存也会失败,这种现象被成为堆碎片(Heap Fragmentation

  • 操作系统支持

引入低碎片堆LFH(Low Fragmentation Heap),通过叫做桶(buckets)的单元来管理分配的堆块

  • LFH如何降低碎片化?

LFH将已经分配的内存块映射为事先确定了不同大小范围的桶,LFH将可用空间划分成128个桶,每个桶的空间依次递增;桶中分配粒度和范围如下:

在这里插入图片描述

当需要从LFH上分配空间,对管理器会根据堆函数的参数,找到满足条件的最小可用桶分配出去;如分配5个bytes,1号桶有空闲,就将1号桶分配出去;没有空闲就查找2号桶,直到找到合适的桶为止

提示:分配粒度可用理解为最小的分配单位;如果分配大于16384 bytes,LFH将会直接转发到底层堆后端

  • 是否生效?

HeapSetInformation函数控制是否支持LFHHeapQueryInformation可以查询一个堆是否支持LFH;堆管理器实现了一种自动调整算法,在特定的条件下默认启动LFH

注意:启用堆调试选项会影响进程中所有堆,因此启动任何堆调试选项都会使LFH被自动禁用且转为使用核心堆;不可扩展堆也不会使用LFH

  • _LFH_HEAP结构:描述LFH的结构
0:000> dt _LFH_HEAP
ntdll!_LFH_HEAP
   +0x00c Heap             : Ptr32 Void				#LFH的父堆的句柄(或指向父堆的指针)
   +0x038 UserBlockCache   : [12] _USER_MEMORY_CACHE_ENTRY
   +0x1bc Buckets          : [129] _HEAP_BUCKET

6.参考

  • 1.《软件调试》第二版,卷2的第23章
  • 2.《Windows高级调试》,第6章
  • 3.《深入解析Windows操作系统》第七版,第五章
  • 4.《Windows编程调试技术内幕》
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值