调试HeapAlloc虚拟分配时的发现(xp sp3+vc++6)

63 篇文章 4 订阅

    <软件调试>第23章中提到:当请求分配的内存超过Heap!VirtualMemoryThreshold指定的阈值时,堆管理器会调用ZwAllocateVirtualMemory分配空间.

    作者给出的样例HiHeap!TestVirtualAlloc会引起堆管理器做如此动作.不同的OS版本/不同的编译器Debug启动和Attach附加到程序对堆管理器的调试都有很大的差别,因此本文只讨论在Xp sp3下以Attach进程的方式调试vc++6.0编译的HiHeap,因此需要在main入口处添加int 3,以便windbg附加.

    当我首次调试,试图分配0x1000000B的内存空间,但HeapAlloc返回时得到的堆块大小远远没有达到这个数量:


0:000> dd pMem L4
0012fef4  00530020 01000000 0012ff80 0040194e
0:000> dt _HEAP_ENTRY 00530020-8
ntdll!_HEAP_ENTRY
   +0x000 Size             : 0x1000
   +0x002 PreviousSize     : 0
   +0x000 SubSegmentCode   : 0x00001000 Void
   +0x004 SmallTagIndex    : 0 ''
   +0x005 Flags            : 0xb ''
   +0x006 UnusedBytes      : 0 ''
   +0x007 SegmentIndex     : 0 ''

    按书上的内容,HeapAlloc返回的用户堆块是0x00530020 ,因此0x00530020-8就是虚拟分配的堆块的HEAP_ENTRY结构。由HEAP_ENTRY!Size可知,整个堆块大小是0x1000*8B<<0x1000000B,而Flag字段明确的指出这是虚拟分配的堆块并且该块处于占用状态.看情形好像HeapAlloc并没有分配指定大小的内存。于是,我开始怀疑是不是HeapAlloc的参数有问题,于是开始了新的一轮调试:

0:000> dv
  dwGranularity = 8
         ulSize = 0x1000000
           pMem = 0x00530020
从堆栈上的变量来看,传递给HeapAlloc的参数是正确的。那只能怀疑是不是调用ZwAllocateVirtualMemory时出错了?在调用ZwAllocateVirtualMemory前下断点,查看函数参数:
0:000> bp ZwAllocateVirtualMemory
0:000> g
Breakpoint 2 hit
ntdll!ZwAllocateVirtualMemory:
0:000> kb
ChildEBP RetAddr  Args to Child              
0012fc54 7c9311cf ffffffff 0012fe68 00000000 ntdll!ZwAllocateVirtualMemory
0012fe94 004014f3 00140000 00000000 01000000 ntdll!RtlAllocateHeap+0xc4a
0012fefc 0040194e 00000008 31f97f38 01d1c791 HiHeap!TestVirtualAlloc+0x43 [C:\doc\debug\src\chap23\HiHeap\HiHeap.cpp @ 100]
0012ff80 00404269 00000002 00430ea0 00430cd0 HiHeap!main+0x8e [C:\doc\debug\src\chap23\HiHeap\HiHeap.cpp @ 167]
0012ffc0 7c817067 31f97f38 01d1c791 7ffd9000 HiHeap!mainCRTStartup+0xe9 [crt0.c @ 206]
0012fff0 00000000 00404180 00000000 78746341 kernel32!BaseProcessStart+0x23
最近一个栈帧frame 0:0012fc54显示了刚进入ZwAllocateVirtualMemory时,函数的栈顶(esp),dd转储出来:

0:000> dd 0012fc54 L8
0012fc54  00000000 7c9311cf ffffffff 0012fe68
0012fc64  00000000 0012fe70 00001000 00000004
参照MSDN上ZwAllocateVirtualMemory的接口形式
NTSTATUS 
  ZwAllocateVirtualMemory(
    __in HANDLE  ProcessHandle,
    __inout PVOID  *BaseAddress,
    __in ULONG_PTR  ZeroBits,
    __inout PSIZE_T  RegionSize,
    __in ULONG  AllocationType,
    __in ULONG  Protect
    ); 
由此解释dd转储的内容为:
<pre name="code" class="cpp">7c9311cf:ZwAllocateVirtualMemory返回到RtlAllocateHeap函数的地址
以下都是 ZwAllocateVirtualMemory的参数
ffffffff:进程堆句柄,即GetProcessHeap的返回值
0012fe68:接收函数返回地址的指针
00000000
0012fe70:传入和接收分配大小的指针
00001000:

 如果此时转储RegionSize的内容,可以看到堆管理器(HeapAlloc)向内存管理器(ZwAllocateVirtualMemory)请求分配的内存大小: 

0:000> dd 0012fe70 L4
0012fe70  01000020 00011b60 00140000 0012fc74
这里*RegionSize的值为0x01000020,这跟前面frame 2中ulSize的值相差无几(多出来的0x20B可能是堆块元数据)。

当然,这时ZwAllocateVirtualMemory还没有返回,不能确定内存管理器到底分配了多少内存,因此需要在RtlAllocateHeap调用ZwAllocateVirtualMemory后的返回地址下断点以查看结果,返回地址就保存在堆栈frame 0中:

0:000> .frame 0
00 0012fc54 7c9311cf ntdll!ZwAllocateVirtualMemory
0x7c9311cf 就是返回地址,应该在这个地方下断点,观察*RegionSize的返回值

0:000> bp 7c9311cf 

可以返汇编查看这个地址附近的内容,确定断点没有下错:

0:000> ub 7c9311cf L5
ntdll!RtlAllocateHeap+0xc3e:
7c9311c3 56              push    esi
7c9311c4 8d45d4          lea     eax,[ebp-2Ch]
7c9311c7 50              push    eax
7c9311c8 6aff            push    0FFFFFFFFh
7c9311ca e881bdffff      call    ntdll!ZwAllocateVirtualMemory (7c92cf50)
0:000> u 7c9311cf L5
ntdll!RtlAllocateHeap+0xc4a:
7c9311cf 8945a8          mov     dword ptr [ebp-58h],eax
7c9311d2 3bc6            cmp     eax,esi
7c9311d4 0f8cc6a10200    jl      ntdll!RtlAllocateHeap+0xcb4 (7c95b3a0)
7c9311da 8b45dc          mov     eax,dword ptr [ebp-24h]
7c9311dd 2b4510          sub     eax,dword ptr [ebp+10h]
下图是调用ZwAllocateVirtualMemory前后HiHeap的虚拟内存在任务管理器中的变化:

前:

后:


前后内存变化了0x100A000B。而windbg验证RegionSize这个值为0x01001000,两者之间差别不是很大。

0:000> dd 0012fe70 L4
0012fe70  01001000 00011b60 00140000 0012fc74

不过新问题来了,HeapAlloc只请求0x01000000B,多出来的0x1000B是什么情况?暂时不得而知。继续收集其他信息。

ZwAllocateVirtualMemory的BaseAddress返回值为:0x00530000

0:000> dd 0012fe68 l4
0012fe68  00530000 004329b0 01001000 00011b60

这个地址比堆块的用户空间0x00530020低了0x20B,还记得前面调用ZwAllocateVirtualMemory时传入的RegionSize的值吗?正是0x01000020,当时认为这多出来的0x20B是堆块元数据,按目前的调试结果,可以确认这个猜测了。

    软件调试书中只提到了非虚拟分配的堆块元结构是_HEAP_ENTRY。起初我以为对于虚拟分配也是这样的结构,不过现在看来并不是这样。那会是什么结构?进一步,查看0x00530000的内存:

0:000> dd 00530000 
00530000  00140050 00140050 00000000 00000000
00530010  01001000 01001000 00001000 00000b00
00530020  00000000 00000000 00000000 00000000
00530030  00000000 00000000 00000000 00000000
00530040  00000000 00000000 00000000 00000000
1.首先看在0x530010处,隐约可以看到虚拟分配的堆块元数据记录了这次内存分配的虚拟空间为0x01001000B,这个比HeapAlloc请求的内存多出了一个页面,为什么会多一整个页面?

首先,有0x20B的堆块元数据,另外不排除堆尾数据。这些内存在HeapAlloc请求的内存数量之外。其次,MSDN也明确的指出:

The <strong>ZwAllocateVirtualMemory</strong> routine reserves, commits, or both, a region of pages within the user-mode 
virtual address space of a specified process
RegionSize 
A pointer to a variable that will receive the actual size, in bytes, of the allocated region of pages. 
The initial value of this parameter specifies the size, in bytes, of the region and is rounded up to the next 
host page size boundary. *RegionSize cannot be zero on input. 
RegionSize的返回值是页面对齐的,因此,对于多出来的额外堆管理数据需要用一个页面来容纳。

2.在看0x530000处QDWORD值:

0:000> dd 00530000 
00530000  00140050 00140050 00000000 00000000
感觉是一个LIST_ENTRY结构。因为这是进程堆第一次进行虚拟分配,因此LIST_ENTRY队列中只有一个元素。因此,可以从0x530000处QDWORD反推LIST_ENTRY队列头就是0x00140050。软件调试中提到过堆HEAP结构中有这样的一个队列记录,因此,我打算通过搜索进程堆的HEAP结构来验证0x00140050就是进程堆中HEAP!VirtualAllocdBlocks域

0:000> dt _PEB @$peb
ntdll!_PEB
 +0x018 ProcessHeap      : 0x00140000 Void
0:000> !heap v 0x00140000 
Index   Address  Name      Debugging options enabled
  1:   00140000 
    Segment at 00140000 to 00240000 (00008000 bytes committed)
0:000> dt _HEAP 00140000 
ntdll!_HEAP
   +0x000 Entry            : _HEAP_ENTRY
   +0x008 Signature        : 0xeeffeeff
   +0x050 VirtualAllocdBlocks : _LIST_ENTRY [ 0x530000 - 0x530000 ]

果然HEAP!VirtualAllocdBlocks指向了刚才虚拟分配的内存

后记:

昨天在软件调试书上看到虚拟分配时的HEAP_VIRTUAL_ALLOC_ENTRY结构,不过书上没有详细列出,在bing上找到xp sp3上这个结构的原型:

typedef struct _HEAP_VIRTUAL_ALLOC_ENTRY {
    /*0x000*/     struct _LIST_ENTRY Entry;
    /*0x008*/     struct _HEAP_ENTRY_EXTRA ExtraStuff;
    /*0x010*/     ULONG32      CommitSize;
    /*0x014*/     ULONG32      ReserveSize;
    /*0x018*/     struct _HEAP_ENTRY BusyBlock;
    }HEAP_VIRTUAL_ALLOC_ENTRY, *PHEAP_VIRTUAL_ALLOC_ENTRY;


操作系统实际内存分配演示是一个比较复杂的过程,下面我会简单地介绍一下。 在Linux平台下,内存分配主要由以下几个部分组成: 1. 物理内存管理 2. 虚拟内存管理 3. 进程内存管理 在物理内存管理中,Linux将物理内存分为许多大小相等的页,通常为4KB。物理内存管理的任务是跟踪哪些页被使用,哪些页没有被使用。当需要申请新的内存,Linux会先检查物理内存的可用情况,如果没有足够的空闲页,则会使用虚拟内存。 虚拟内存管理允许Linux将进程的逻辑地址空间映射到物理地址空间中。在Linux中,每个进程都有自己的虚拟地址空间,该空间是相互独立的。当进程需要访问内存,它使用虚拟地址进行访问,而不必关心物理地址。 进程内存管理包括分配和释放进程的虚拟地址空间。当进程需要申请新的内存,它调用malloc()函数来分配内存。当进程不再需要这些内存,它调用free()函数来释放它们。 在Windows平台下,内存管理主要由以下几个部分组成: 1. 物理内存管理 2. 虚拟内存管理 3. 进程内存管理 Windows使用页面文件来扩展物理内存。页面文件是一个特殊的文件,用于保存进程不常用的内存页面。当物理内存不足,Windows将这些页面从物理内存中换出,并将它们写入页面文件中。当需要使用这些页面,Windows会从页面文件中读取它们,并将它们放回到物理内存中。 虚拟内存管理允许Windows将进程的逻辑地址空间映射到物理地址空间中。在Windows中,每个进程都有自己的虚拟地址空间,该空间是相互独立的。当进程需要访问内存,它使用虚拟地址进行访问,而不必关心物理地址。 进程内存管理包括分配和释放进程的虚拟地址空间。当进程需要申请新的内存,它调用HeapAlloc()函数来分配内存。当进程不再需要这些内存,它调用HeapFree()函数来释放它们。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值