从 C# 崩溃异常 中研究页堆布局

   2023-02-09 学习力0
核心提示:一:背景1.讲故事最近遇到一位朋友的程序崩溃,发现崩溃点在富编辑器 msftedit 上,这个不是重点,重点在于发现他已经开启了 页堆 ,看样子是做了最后的挣扎。0:000 !analyze -vEXCEPTION_RECORD:(.exr -1)ExceptionAddress: 82779a9e (msftedit!CCallMgrCent

一:背景

1.讲故事

最近遇到一位朋友的程序崩溃,发现崩溃点在富编辑器 msftedit 上,这个不是重点,重点在于发现他已经开启了 页堆 ,看样子是做了最后的挣扎。


0:000> !analyze -v
EXCEPTION_RECORD:  (.exr -1)
ExceptionAddress: 82779a9e (msftedit!CCallMgrCenter::SendAllNotifications+0x00000123)
   ExceptionCode: c0000005 (Access violation)
  ExceptionFlags: 00000000
NumberParameters: 2
   Parameter[0]: 00000001
   Parameter[1]: 8351af28
Attempt to write to address 8351af28
...
STACK_TEXT:  
00ffe0dc 827bda2a 8351ae88 00000000 00ffe174 msftedit!CCallMgrCenter::SendAllNotifications+0x123
00ffe110 827bd731 00ffe324 00ffe174 00ffe300 msftedit!CCallMgrCenter::ExitContext+0xda
00ffe120 827bde71 8351ae88 827232dc 28112f80 msftedit!CCallMgr::~CCallMgr+0x17
00ffe300 8290281f 00000102 00000067 00220001 msftedit!CTxtEdit::TxSendMessage+0x201
00ffe374 7576110b 00f20268 00000102 00000067 msftedit!RichEditWndProc+0x9cf
00ffe3a0 757580ca 82901e50 00f20268 00000102 user32!_InternalCallWinProc+0x2b
...
SYMBOL_NAME:  system_windows_forms+1c45e7

MODULE_NAME: System_Windows_Forms

IMAGE_NAME:  System.Windows.Forms.dll

0:000> !heap -p

    Active GlobalFlag bits:
        vrf - Enable application verifier
        hpa - Place heap allocations at ends of pages

    StackTraceDataBase @ 04c20000 of size 01000000 with 00001b18 traces

    PageHeap enabled with options:
        ENABLE_PAGE_HEAP
        COLLECT_STACK_TRACES

    active heaps:

    + 5c20000
        ENABLE_PAGE_HEAP COLLECT_STACK_TRACES 
      NormalHeap - 5d90000
          HEAP_GROWABLE 
    + 5e90000
        ENABLE_PAGE_HEAP COLLECT_STACK_TRACES 
      NormalHeap - 4960000
          HEAP_GROWABLE HEAP_CLASS_1 
      ...

由于 页堆NT堆 的内存布局完全不一样,这一篇结合我的了解以及 windbg 验证来系统的介绍下 页堆

二:对 页堆 的研究

1. 案例演示

为了方便讲述,先上一段测试代码。


int main()
{
	HANDLE h = HeapCreate(NULL, 0, 100);

	int* ptr = (int*)HeapAlloc(h, 0, 9);

	printf("ptr= %x", ptr);

	DebugBreak();
}

接下来用 gflags 开启下页堆。


PS C:\Users\Administrator\Desktop> gflags -i ConsoleApplication1.exe +hpa
Current Registry Settings for ConsoleApplication1.exe executable are: 02000000
    hpa - Enable page heap

然后将程序跑起来,可以看到返回的 handle 句柄。

2. 页堆布局研究

接下来用 windbg 的 !heap -p 命令观察页堆。


0:000> !heap -p

    Active GlobalFlag bits:
        hpa - Place heap allocations at ends of pages

    StackTraceDataBase @ 042e0000 of size 01000000 with 0000000e traces

    PageHeap enabled with options:
        ENABLE_PAGE_HEAP
        COLLECT_STACK_TRACES

    active heaps:

    + 5b0000
        ENABLE_PAGE_HEAP COLLECT_STACK_TRACES 
      NormalHeap - 710000
          HEAP_GROWABLE 
    + 810000
        ENABLE_PAGE_HEAP COLLECT_STACK_TRACES 
      NormalHeap - 510000
          HEAP_GROWABLE HEAP_CLASS_1 
    + 56e0000
        ENABLE_PAGE_HEAP COLLECT_STACK_TRACES 
      NormalHeap - 5aa0000
          HEAP_CLASS_1 

稍微解读下上面的输出。

  1. + 56e0000**

表示 页堆 的堆句柄。

  1. NormalHeap - 5aa0000

表示 页堆 关联的 NT堆,可能有朋友要问了,既然都开启页堆了, 还要弄一个 ntheap 干嘛? 大家不要忘了,windows 的一些系统api会用到这个堆。

接下来有一个问题,如何观察这两个 heap 之间的关联关系呢? 要回答这个问题,需要了解 页堆 的布局结构,画个简图如下:

从图中可以看到,离句柄偏移 4k 的位置有一个 DPH_HEAP_ROOT 结构,它相当于 NTHEAP 的_HEAP,我们拿 56e0000 举个例子。


0:000> dt nt!_DPH_HEAP_ROOT 56e0000+0x1000
ntdll!_DPH_HEAP_ROOT
   ...
   +0x0b4 NormalHeap       : 0x05aa0000 Void
   +0x0b8 CreateStackTrace : 0x042f4d94 _RTL_TRACE_BLOCK
   +0x0bc FirstThread      : (null) 

上面输出的 NormalHeap: 0x05aa0000 就是它关联的 ntheap 句柄。

3. 堆块布局研究

页堆 有了一个整体认识,接下来继续研究堆块句柄,我们发现 ptr=0x56e5ff0 是落在 56e0000 这个页堆上,接下来我们导出这个页堆的详细信息。


0:000> !heap -p -h 56e0000
    _DPH_HEAP_ROOT @ 56e1000
    Freed and decommitted blocks
      DPH_HEAP_BLOCK : VirtAddr VirtSize
    Busy allocations
      DPH_HEAP_BLOCK : UserAddr  UserSize - VirtAddr VirtSize
        056e1f70 : 056e5ff0 00000009 - 056e5000 00002000
          unknown!fillpattern
    _HEAP @ 5aa0000
      No FrontEnd
      _HEAP_SEGMENT @ 5aa0000
       CommittedRange @ 5aa04a8
      HEAP_ENTRY Size Prev Flags    UserPtr UserSize - state
        05aa04a8 0167 0000  [00]   05aa04b0    00b30 - (free)
      * 05aa0fe0 0004 0167  [00]   05aa0fe8    00018 - (busy)
       VirtualAllocdBlocks @ 5aa009c

上面的信息如何解读呢?我们逐一来聊一下吧。

  1. _DPH_HEAP_ROOT @ 56e1000

这个已经和大家聊过了,它和 _HEAP 结构是一致的。

  1. DPH_HEAP_BLOCK :

从字面意思就能看出来和 ntheapheap_entry 是一致的,都是用来描述堆块信息, 不过有一点要注意,这个堆块是落在上图中的 DPH_HEAP_BLOCK Pool 池链表结构中的,言外之意就是它不会作为 heap_entry 的头部附加信息,接下来我们 dt 导出来看看。


0:000> dt ntdll!_DPH_HEAP_BLOCK 056e1f70 
   +0x000 pNextAlloc       : 0x056e1020 _DPH_HEAP_BLOCK
   +0x000 AvailableEntry   : _LIST_ENTRY [ 0x56e1020 - 0x0 ]
   +0x000 TableLinks       : _RTL_BALANCED_LINKS
   +0x010 pUserAllocation  : 0x056e5ff0  "???"
   +0x014 pVirtualBlock    : 0x056e5000  "???"
   +0x018 nVirtualBlockSize : 0x2000
   +0x01c nVirtualAccessSize : 0x20
   +0x020 nUserRequestedSize : 9
   +0x024 nUserActualSize  : 0x56e1f60
   +0x028 UserValue        : 0x056e1fc8 Void
   +0x02c UserFlags        : 0x3f18
   +0x030 StackTrace       : 0x042f4dcc _RTL_TRACE_BLOCK
   +0x034 AdjacencyEntry   : _LIST_ENTRY [ 0x56e1010 - 0x56e1010 ]
   +0x03c pVirtualRegion   : (null) 

从字段信息看,它记录了堆块的分配首地址,栈信息等等,比如用 dds 观察一下 StackTrace。


0:000> dds 0x042f4dcc 
042f4dcc  00000000
042f4dd0  00006001
042f4dd4  000d0000
042f4dd8  78aba8b0 verifier!AVrfDebugPageHeapAllocate+0x240
042f4ddc  77e0ef8e ntdll!RtlDebugAllocateHeap+0x39
042f4de0  77d76150 ntdll!RtlpAllocateHeap+0xf0
042f4de4  77d757fe ntdll!RtlpAllocateHeapInternal+0x3ee
042f4de8  77d753fe ntdll!RtlAllocateHeap+0x3e
042f4dec  00ad1690 ConsoleApplication1!main+0x30 [D:\net6\ConsoleApp1\ConsoleApplication1\DisplayGreeting.cpp @ 14]
042f4df0  00ad1bc3 ConsoleApplication1!invoke_main+0x33 [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 78]
042f4df4  00ad1a17 ConsoleApplication1!__scrt_common_main_seh+0x157 [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 288]
042f4df8  00ad18ad ConsoleApplication1!__scrt_common_main+0xd [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl @ 331]
042f4dfc  00ad1c48 ConsoleApplication1!mainCRTStartup+0x8 [D:\a\_work\1\s\src\vctools\crt\vcstartup\src\startup\exe_main.cpp @ 17]
042f4e00  7646fa29 KERNEL32!BaseThreadInitThunk+0x19
042f4e04  77d975f4 ntdll!__RtlUserThreadStart+0x2f
042f4e08  77d975c4 ntdll!_RtlUserThreadStart+0x1b
...

接下来再回答一个问题,页堆的堆块有没有头部附加信息呢?当然是有的,叫做 DPH_BLOCK_INFORMATION ,即在 UserPtr-0x20 的位置,我们可以用 dt 显示一下。


0:000> ?? sizeof(ntdll!_DPH_BLOCK_INFORMATION)
unsigned int 0x20

0:000> dt ntdll!_DPH_BLOCK_INFORMATION 056e5ff0-0x20
   +0x000 StartStamp       : 0xabcdbbbb
   +0x004 Heap             : 0x056e1000 Void
   +0x008 RequestedSize    : 9
   +0x00c ActualSize       : 0x1000
   +0x010 FreeQueue        : _LIST_ENTRY [ 0x0 - 0x0 ]
   +0x010 FreePushList     : _SINGLE_LIST_ENTRY
   +0x010 TraceIndex       : 0
   +0x018 StackTrace       : 0x042f4dcc Void
   +0x01c EndStamp         : 0xdcbabbbb
   ...

根据上面两个输出,在脑海中应该可以绘出如下图:

这里要稍微解释下 栅栏页 的概念。

4. 栅栏页

每一个 heap_entry 都会占用 8k 的空间,第一个 4k 是用户区,第二个 4k 是栅栏区,为了就是当代码越界时访问了这个 栅栏页 会立即报错,因为栅栏页是禁止访问的,我们可以提取 UserAddr 附近的内存,看看 056e6000= 056e5000+0x1000 后面是不是都是问号。


0:000> dp 056e5ff0 
056e5ff0  c0c0c0c0 c0c0c0c0 d0d0d0c0 d0d0d0d0
056e6000  ???????? ???????? ???????? ????????
056e6010  ???????? ???????? ???????? ????????
056e6020  ???????? ???????? ???????? ????????
056e6030  ???????? ???????? ???????? ????????
056e6040  ???????? ???????? ???????? ????????
056e6050  ???????? ???????? ???????? ????????
056e6060  ???????? ???????? ???????? ????????

0:000> !address 056e5000+0x1000

Usage:                  PageHeap
Base Address:           056e6000
End Address:            057e0000
Region Size:            000fa000 (1000.000 kB)
State:                  00002000          MEM_RESERVE
Protect:                <info not present at the target>
Type:                   00020000          MEM_PRIVATE
Allocation Base:        056e0000
Allocation Protect:     00000001          PAGE_NOACCESS
More info:              !heap -p 0x56e1000
More info:              !heap -p -a 0x56e6000


Content source: 0 (invalid), length: fa000

三:总结

这就是对 页堆 的一个研究,总的来说 页堆 是一种专用于调试的堆,优缺点如下:

  • 优点:

因为 栅栏页 紧邻 用户页,一旦代码越界进入了 栅栏页,会立即报 访问违例 异常,这样我们就可以获取第一现场错误。

  • 缺点:

对空间造成了巨大浪费,即使 1byte 的内存分配,也需要至少 2 个内存页 的内存占用 (8k)。

哈哈,对调试程序崩溃类问题,非常值得一试!

图片名称
 
反对 0举报 0 评论 0
 

免责声明:本文仅代表作者个人观点,与乐学笔记(本网)无关。其原创性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容、文字的真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
    本网站有部分内容均转载自其它媒体,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责,若因作品内容、知识产权、版权和其他问题,请及时提供相关证明等材料并与我们留言联系,本网站将在规定时间内给予删除等相关处理.

  • 使用C#编写一个.NET分析器(一) 使用csv模块的什么方法可以一次性将一行数据写入文件
    使用C#编写一个.NET分析器(一) 使用csv模块的
    译者注这是在Datadog公司任职的Kevin Gosse大佬使用C#编写.NET分析器的系列文章之一,在国内只有很少很少的人了解和研究.NET分析器,它常被用于APM(应用性能诊断)、IDE、诊断工具中,比如Datadog的APM,Visual Studio的分析器以及Rider和Reshaper等等。之前
    03-08
  • 跨语言调用C#代码的新方式-DllExport 跨语言调用本质
    跨语言调用C#代码的新方式-DllExport 跨语言调
    简介上一篇文章使用C#编写一个.NET分析器文章发布以后,很多小伙伴都对最新的NativeAOT函数导出比较感兴趣,今天故写一篇短文来介绍一下如何使用它。在以前,如果有其他语言需要调用C#编写的库,那基本上只有通过各种RPC的方式(HTTP、GRPC)或者引入一层C++
    03-08
  • 我比较了 Go 和 C# 的速度
    我比较了 Go 和 C# 的速度
    我在 Go 和 C# 之间进行了速度比较。我通常使用 C#,但我有机会使用 Go,并且由于传闻 Go 速度很快,所以我实际测量了它。测量内容我在 Go 和 C# 中执行了一个简单的循环和判断过程,以查看整数 2 到 N 是否为质数。来源是Github参考。测量模式 逻辑内核 8 Wi
    03-08
  • [C#]使用 AltCover 获得代码覆盖率 - E2E Test 和 Unit Test
    [C#]使用 AltCover 获得代码覆盖率 - E2E Test
    背景在 CI/CD 流程当中,测试是 CI 中很重要的部分。跟开发人员关系最大的就是单元测试,单元测试编写完成之后,我们可以使用 IDE 或者 dot cover 等工具获得单元测试对于业务代码的覆盖率。不过我们需要一个独立的 CLI 工具,这样我们才能够在 Jenkins 的 CI
  • C#中LINQ的Select与SelectMany函数如何使用 c反应蛋白高说明什么
    C#中LINQ的Select与SelectMany函数如何使用 c反
    本篇内容主要讲解“C#中LINQ的Select与SelectMany函数如何使用”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“C#中LINQ的Select与SelectMany函数如何使用”吧!LINQ的Select与SelectMany函数使用Select扩展函
    02-09 linqselect
  • PerfView专题 (第三篇):如何寻找 C# 中的 VirtualAlloc 内存泄漏
    PerfView专题 (第三篇):如何寻找 C# 中的 Virt
    一:背景上一篇我们聊到了如何用 PerfView 去侦察 NTHeap 的内存泄漏,这种内存泄漏往往是用 C 的 malloc 或者 C++ 的 new 分配而不释放所造成的,这一篇我们来聊一下由 VirtualAlloc 方法造成的泄漏如何去甄别?了解 VirtualAlloc 的朋友肯定说, C# 这种高
    02-09
  • Blazor和Vue对比学习(知识点杂锦3.04):Blazor中C#和JS互操作(超长文)
    Blazor和Vue对比学习(知识点杂锦3.04):Blazo
    C#和JS互操作的基本语法是比较简单的,但小知识点特别多,同时,受应用加载顺序、组件生命周期以及参数类型的影响,会有比较多坑,需要耐心的学习。在C#中调用JS的场景会比较多,特别是在WASM模式下,由于WebAssembly的限制,很多时候,还是需要借助JS去控制D
    02-09
  • 的键">C#怎么使用struct类型作为泛型Dictionary
    本文小编为大家详细介绍“C#怎么使用struct类型作为泛型DictionaryTKey,TValue的键”,内容详细,步骤清晰,细节处理妥当,希望这篇“C#怎么使用struct类型作为泛型DictionaryTKey,TValue的键”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学
  • C#如何实现折半查找算法 彩票查询
    C#如何实现折半查找算法 彩票查询
    本篇内容主要讲解“C#如何实现折半查找算法”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“C#如何实现折半查找算法”吧!折半查找,也叫二分查找,当在一个数组或集合中查找某个元素时,先定位出中间位置元素
    02-09
  • C#如何实现选择排序 c罗
    C#如何实现选择排序 c罗
    本篇内容主要讲解“C#如何实现选择排序”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“C#如何实现选择排序”吧!选择排序是一种低效的排序算法,大致过程是:遍历数组的每一个元素,先假设0号位置上的元素是最
    02-09
点击排行