遍历GC堆

了解对象在gc堆中的布局是很有用的。在垃圾收集期间,有效的对象是通过递归访问对象来标记的,这些对象从堆栈的根和句柄开始。但是,从每个堆段的开始到结束,对象的位置以一种有组织的方式排列也很重要。!DumpHeap命令依赖于这个逻辑组织来正确地遍历堆,如果它报告了一个错误,可以打赌您的堆出了问题。

下面是代码

using System;
using System.Reflection;
using System.Reflection.Emit;
using System.Threading;
using System.Runtime.InteropServices;

public class EmitHelloWorld
{

      [DllImport("kernel32")]
public static extern void DebugBreak();

      static void Main(string[] args)
      {
            // create a dynamic assembly and module
            AssemblyName assemblyName = new AssemblyName();
            assemblyName.Name = "HelloWorld";
            AssemblyBuilder assemblyBuilder = Thread.GetDomain().DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndSave);
            ModuleBuilder module;
            module = assemblyBuilder.DefineDynamicModule("HelloWorld.exe");
      
       // create a new type to hold our Main method
            TypeBuilder typeBuilder = module.DefineType("HelloWorldType", TypeAttributes.Public | TypeAttributes.Class);
            
            // create the Main(string[] args) method
            MethodBuilder methodbuilder = typeBuilder.DefineMethod("Main", MethodAttributes.HideBySig | MethodAttributes.Static | MethodAttributes.Public, typeof(void), new Type[] { typeof(string[]) });
            
            // generate the IL for the Main method
            ILGenerator ilGenerator = methodbuilder.GetILGenerator();
            ilGenerator.EmitWriteLine("hello, world");
            ilGenerator.Emit(OpCodes.Ret);

            // bake it
            Type helloWorldType = typeBuilder.CreateType();

            // run it
            helloWorldType.GetMethod("Main").Invoke(null, new string[] {null});

DebugBreak();

            // set the entry point for the application and save it
            assemblyBuilder.SetEntryPoint(methodbuilder, PEFileKinds.ConsoleApplication);
            assemblyBuilder.Save("HelloWorld.exe");
      }
}

将程序另存为example.cs,编译并运行“cdb-g示例.exe"
当您到达断点时,加载sos并运行“!eeheap-gc”。它列出了存储对象的堆段:

0:000> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x00aa1b78
generation 1 starts at 0x00a5100c
generation 2 starts at 0x00a51000
ephemeral segment allocation context: none
segment begin allocated size
00a50000 00a51000 00ae4000 0x00093000(602112)
Large object heap starts at 0x01a51000
segment begin allocated size
01a50000 01a51000 01a54060 0x00003060(12384)
Total Size 0x96060(614496)
------------------------------
GC Heap Size 0x96060(614496)

这是一个小的gc堆,只有一个普通对象段和一个大对象段(对于80K以上的对象)。对我们来说没问题。正常大小的对象从地址00a51000开始,到00ae4000结束。一般来说,我们有一个简单的模式:

|---------| segment.begin = 00a51000
|object 1 |
|_________|
|object 2 |
|_________|
| ... |
|_________|
|object N |
|_________| segment.allocated = 00ae4000

每个对象有多大?你可以运行!dumpobj就知道。有趣的是,每个对象都有一个4字节的头,对象2的头大小包含在对象1的大小中。另一点是堆中存在一种称为“Free”对象的特殊类型的对象。这是用来填补有效对象之间的洞。这些自由对象是临时的,因为如果发生压缩gc,它们将消失。

我们开始走吧。

0:000> !dumpobj 00a51000
Free Object
Size 12(0xc) bytes
0:000> !dumpobj 00a51000+c
Free Object
Size 12(0xc) bytes
0:000> !dumpobj 00a51000+c+c
Free Object
Size 12(0xc) bytes
0:000> !dumpobj 00a51000+c+c+c
Name: System.OutOfMemoryException
MethodTable: 03077e9c
EEClass: 03064050
Size: 68(0x44) bytes
(C:\WINDOWS\Microsoft.NET\Framework\v2.0.x86dbg\mscorlib.dll)
Fields:
MT Field Offset Type Attr Value Name
03076b7c 40000a5 4 CLASS instance 00000000 _className
03076b7c 40000a6 8 CLASS instance 00000000 _exceptionMe
thod
03076b7c 40000a7 c CLASS instance 00000000 _exceptionMe
thodString
03076b7c 40000a8 10 CLASS instance 00000000 _message
03076b7c 40000a9 14 CLASS instance 00000000 _data
03076b7c 40000aa 18 CLASS instance 00000000 _innerExcept
ion
03076b7c 40000ab 1c CLASS instance 00000000 _helpURL
03076b7c 40000ac 20 CLASS instance 00000000 _stackTrace
03076b7c 40000ad 24 CLASS instance 00000000 _stackTraceS
tring
03076b7c 40000ae 28 CLASS instance 00000000 _remoteStack
TraceString
03076b7c 40000af 30 System.Int32 instance 0 _remoteStack
Index
03076b7c 40000b0 34 System.Int32 instance -2147024882 _HResult
03076b7c 40000b1 2c CLASS instance 00000000 _source
03076b7c 40000b2 38 System.IntPtr instance 0 _xptrs
03076b7c 40000b3 3c System.Int32 instance -532459699 _xcode

哇,花了点时间才找到有趣的东西。你可以继续这样,直到你得到一个缓冲区溢出,因为所有“+c+44+68+12+…”你也可以让!DumpHeap帮你做这个。它给出了一个相当稀疏的printers对象。让我们将输出限制在我们关心的段(注意大小是十进制):

0:000> !dumpheap 00a51000 00ae4000
Address MT Size
00a51000 0015c260 12 Free
00a5100c 0015c260 12 Free
00a51018 0015c260 12 Free
00a51024 03077e9c 68
00a51068 030782cc 68
00a510ac 030786fc 68
00a510f0 03078b5c 68
00a51134 030f7b54 20
00a51148 0308b06c 108
00a511b4 030fa5bc 32
00a511d4 0305bbf8 28
00a511f0 030592e0 80
00a51240 0015c260 72 Free
...

我们怎么知道每个物体的大小?看看MethodTable,对象的第一个DWORD。你可以在上面运行!dumpmt

0:000> !dumpmt 03077e9c
EEClass: 03064050
Module: 0016b118
Name: System.OutOfMemoryException
mdToken: 02000038 (C:\WINDOWS\Microsoft.NET\Framework\v2.0.x86dbg\mscorlib.dll)
BaseSize: 44
Number of IFaces in IFaceMap: 2
Slots in VTable: 21
 这里的BaseSize是十六进制的。数组呢,我们怎么知道它们的大小?让我们列出该段中的所有数组来计算:
0:000> !dumpheap -type [] 00a51000 00ae4000
Address MT Size
00a511f0 030592e0 80
00a5129c 03115b68 56
00a51348 03135ca0 76
00a513a8 030592e0 16
00a51434 0313b1c0 144
00a51634 0313c234 100
00a51698 0313c620 56
00a51cc4 030592e0 16
00a51e8c 0313b1c0 144
00a52008 0313b1c0 144
00a52244 0313b1c0 144
00a52308 0313b1c0 144
00a523cc 0313b1c0 144
00a52620 0313b1c0 144
00a526e4 0313b1c0 144
00a52a14 031e23f8 36
00a52b7c 0313b1c0 144
00a52c0c 0315778c 1084
00a53048 0315778c 1628
00a536a4 0315778c 824
...
随机挑选一个:
0:000> !dumpobj 00a52c0c
Name: System.Int32[]
MethodTable: 0315778c
EEClass: 03157708
Size: 1084(0x43c) bytes
Array: Rank 1, Type Int32
Element Type: System.Int32
Fields:
None

确定数组大小的公式为:

MethodTable.BaseSize + (MethodTable.ComponentSize * Object.Components)

!dumpmt会告诉你前两个:
0:000> !dumpmt 315778c
EEClass: 03157708
Module: 0016b118
Name: System.Int32[]
mdToken: 02000000 (C:\WINDOWS\Microsoft.NET\Framework\v2.0.x86dbg\mscorlib.dll)
BaseSize: 0xc
ComponentSize: 0x4
Number of IFaces in IFaceMap: 4
Slots in VTable: 25

您可以通过以下方式找到数组中的项数:

0:000> dd 00a52c0c+4 l1
00a52c10 0000010C
0:000>

所以我们了解物体的大小,以及它们是如何排列的。但是有一件事遗漏了,这就是在整个堆中存在称为分配上下文的零填充区域。为了提高效率,可以为每个托管线程指定这样一个区域,以便将新的分配定向到该区域。这允许多线程应用程序在没有昂贵的锁定操作的情况下进行分配。对于包含第0代和第1代的堆段(也称为临时段),还有一个分配上下文。这个!dumpheap命令知道这些区域,并在它们上面轻轻地执行步骤。您可以使用获取线程分配上下文地址!thread命令:

0:000> !threads
ThreadCount: 2
UnstartedThread: 0
BackgroundThread: 1
PendingThread: 0
DeadThread: 0
PreEmptive GC Alloc Lock
ID OSID ThreadOBJ State GC Context Domain Count APT Exception
0 1 16ac 00155da8 a020 Enabled 00ae2e1c:00ae3ff4 0014a890 0 MTA
2 2 169c 001648f8 b220 Enabled 00000000:00000000 0014a890 0 MTA (Finalizer)

线程0(主线程)具有从00ae2e1c到00ae3ff4的分配上下文。如果我们看一下内存,我们会看到所有的零:

0:000> dd 00ae2e1c
00ae2e1c 00000000 00000000 00000000 00000000
00ae2e2c 00000000 00000000 00000000 00000000
00ae2e3c 00000000 00000000 00000000 00000000
00ae2e4c ...

至于短暂的段分配上下文,我们没有。看下!eeheap-gc输出:

0:000> !eeheap -gc
Number of GC Heaps: 1
generation 0 starts at 0x00aa1b78
generation 1 starts at 0x00a5100c
generation 2 starts at 0x00a51000
ephemeral segment allocation context: none
segment begin allocated size
00a50000 00a51000 00ae4000 0x00093000(602112)
...

有一天可能会出现缓冲区溢出,并在StrongBad fan club成员数组之后删除对象的MethodTable。下一次GC发生时,您的程序将崩溃。让我们模拟一下那可怕的发生,看看怎么办!dumpheap输出:

0:000> ed adf7f8 00650033 (I'm overwriting the MethodTable of the array we've been enjoying)
0:000> !dumpheap 00a51000 00ae4000
...
00adf7ac 03135ca0 76
object 00adf7f8: does not have valid MT
curr_object : 00adf7f8
Last good object: 00adf7ac

这会让你对最后一个好东西,00adf7ac产生怀疑。我们当然知道他没事,他对发生的事情不负责任。但在现实世界中,需要积极的回应!
最后的好东西是什么?

0:000> !dumpobj adf7ac
Name: System.Byte[]
MethodTable: 03135ca0
EEClass: 03135c1c
Size: 76(0x4c) bytes
Array: Rank 1, Type Byte
Element Type: System.Byte
Fields:
None

谁在乎他?如果我能在堆栈上找到这个对象的根,我可能接近覆盖下一个对象的代码:

0:000> !gcroot adf7ac
Note: Roots found on stacks may be false positives. Run "!help gcroot" for
more info.
Scan Thread 0 OSTHread 16ac
ESP:12ea9c:Root:00ad4914(System.Reflection.Emit.MethodBuilder)->00ad4448(System.
Reflection.Emit.TypeBuilder)->00adf52c(System.Reflection.Emit.MethodBuilder)->00
adf74c(System.Reflection.Emit.ILGenerator)->00adf7ac(System.Byte[])
Scan Thread 2 OSTHread 169c

线程0,是吗?他受雇于ILGenerator,是吗?他们店里正在进行什么邪恶的行动!好吧,我停下来。但这是真的,最后一个好的对象通常是有责任的,而PInvoke溢出就是原因。

posted on 2020-08-21 08:18  活着的虫子  阅读(263)  评论(0编辑  收藏  举报

导航