zoukankan      html  css  js  c++  java
  • 遍历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:WINDOWSMicrosoft.NETFrameworkv2.0.x86dbgmscorlib.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:WINDOWSMicrosoft.NETFrameworkv2.0.x86dbgmscorlib.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:WINDOWSMicrosoft.NETFrameworkv2.0.x86dbgmscorlib.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溢出就是原因。

  • 相关阅读:
    使用Astah画UML类图经验总结
    Qt的四个常见的图像叠加模式
    获取Linux时间函数
    DBus学习网站
    线程属性pthread_attr_t简介
    Secure CRT 自动记录日志log配置
    MySQL的group_concat()函数合并多行数据
    MySQL的Limit详解
    异步查询json传日期格式到前台,变成了时间戳的格式
    启动studio报错Gradle error
  • 原文地址:https://www.cnblogs.com/yilang/p/13501035.html
Copyright © 2011-2022 走看看