We have an improved SOS.DLL with many bug fixes and enhancements. Tom Christian in Product Support maintains it, and gave me permission to post it here under the name PSSCOR.DLL.
[update June 2005 - For some time now, PSSCOR.DLL has been included with the Windows Debugger package, although it is renamed to SOS.DLL. I've removed this old link because you get it just by installing the debugger].
It works on V1.0 and V1.1 of the CLR. Load it in the same way you'd load sos.dll in the Windows Debugger, with “.load psscor.dll“. The good thing about PSSCOR.DLL is that we can fix bugs and enhance functions without going through a lengthy QFE process. If you've found bugs in SOS, it's likely that many were fixed already in PSSCOR.DLL.
The code examples below use psscor.dll to explore the gc heap. You'll want to use it in lieu of SOS, because some commands like !DumpMT and !EEHeap have additional useful output.
It's useful to know how objects are laid out in the gc heap. During garbage collection, valid objects are marked by recursively visiting objects starting from roots on stacks and in handles. But it's also important that the location of the objects sit in an organized way from the beginning to end of each heap segment. The psscor !DumpHeap command counts on this logical organization to walk the heap properly, and if it reports an error you can bet something is wrong with your heap (and will bite you later with a perplexing application violation). So to understand what !dumpheap is talking about, here is your guide to walking these objects by hand, hopping from one stone to another across a vast lake.
First you need a program. I have taken this program from Joel Pobar's Reflection Emit example, and inserted a PInvoke to DebugBreak so you can easily stop in the Windows Debugger. (You could use Visual Studio for these illustrations too, but the Windows Debugger "dd" command is quicker for viewing memory).
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();
DebugBreak(); // set the entry point for the application and save it assemblyBuilder.SetEntryPoint(methodbuilder, PEFileKinds.ConsoleApplication); assemblyBuilder.Save("HelloWorld.exe"); }}
Save the program as example.cs, compile and run "cdb -g example.exe"
When you reach the breakpoint, load psscor and run "!eeheap -gc". It lists the heap segments that objects are stored in:
0:000> !eeheap -gcNumber of GC Heaps: 1generation 0 starts at 0x00aa1b78generation 1 starts at 0x00a5100cgeneration 2 starts at 0x00a51000ephemeral segment allocation context: none segment begin allocated size00a50000 00a51000 00ae4000 0x00093000(602112)Large object heap starts at 0x01a51000 segment begin allocated size01a50000 01a51000 01a54060 0x00003060(12384)Total Size 0x96060(614496)------------------------------GC Heap Size 0x96060(614496)
This is a small gc heap, with only one normal object segment, and one large object segment (for objects over 80K). It's fine for our purposes. Normal-sized objects start at address 00a51000, and end at 00ae4000. In general we have this simple pattern:
|---------| segment.begin = 00a51000 |object 1 | |_________| |object 2 | |_________| | ... | |_________| |object N | |_________| segment.allocated = 00ae4000
How large is each object? You can run !dumpobj to find out. The interesting thing is that each object has a 4 byte header, and the size of the header for object 2 is included in the size of object 1. Another point is that a special kind of object called a "Free" object lives in the heap. This is used to plug holes between valid objects. These Free objects are temporary, in that if a compacting gc occurs they'll disappear. Yun wrote a great article about how the heap could be unable to compact in the face of heavy pinning , and be filled with Free objects (http://blogs.msdn.com/yunjin/archive/2004/01/27/63642.aspx).
Let's start walking. (My heap may look different because it's a Whidbey debug build)
0:000> !dumpobj 00a51000Free ObjectSize 12(0xc) bytes0:000> !dumpobj 00a51000+cFree ObjectSize 12(0xc) bytes0:000> !dumpobj 00a51000+c+cFree ObjectSize 12(0xc) bytes0:000> !dumpobj 00a51000+c+c+cName: System.OutOfMemoryExceptionMethodTable: 03077e9cEEClass: 03064050Size: 68(0x44) bytes (C:\WINDOWS\Microsoft.NET\Framework\v2.0.x86dbg\mscorlib.dll)Fields: MT Field Offset Type Attr Value Name03076b7c 40000a5 4 CLASS instance 00000000 _className03076b7c 40000a6 8 CLASS instance 00000000 _exceptionMethod03076b7c 40000a7 c CLASS instance 00000000 _exceptionMethodString03076b7c 40000a8 10 CLASS instance 00000000 _message03076b7c 40000a9 14 CLASS instance 00000000 _data03076b7c 40000aa 18 CLASS instance 00000000 _innerException03076b7c 40000ab 1c CLASS instance 00000000 _helpURL03076b7c 40000ac 20 CLASS instance 00000000 _stackTrace03076b7c 40000ad 24 CLASS instance 00000000 _stackTraceString03076b7c 40000ae 28 CLASS instance 00000000 _remoteStackTraceString03076b7c 40000af 30 System.Int32 instance 0 _remoteStackIndex03076b7c 40000b0 34 System.Int32 instance -2147024882 _HResult03076b7c 40000b1 2c CLASS instance 00000000 _source03076b7c 40000b2 38 System.IntPtr instance 0 _xptrs03076b7c 40000b3 3c System.Int32 instance -532459699 _xcode
Wow, it took some time to get to something interesting. You could continue like this until you get a buffer overflow due to all the "+c+44+68+12+..." You can also let !DumpHeap do this for you. It gives a rather sparse printout of the object pointers. Let's limit the output to the segment we care about (and note that Size is in decimal):
0:000> !dumpheap 00a51000 00ae4000 Address MT Size00a51000 0015c260 12 Free00a5100c 0015c260 12 Free00a51018 0015c260 12 Free00a51024 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...
How do we know the size of each object? Just look at the MethodTable, the first DWORD of the object. You can run !dumpmt on it:
0:000> !dumpmt 03077e9cEEClass: 03064050Module: 0016b118Name: System.OutOfMemoryExceptionmdToken: 02000038 (C:\WINDOWS\Microsoft.NET\Framework\v2.0.x86dbg\mscorlib.dll)BaseSize: 44Number of IFaces in IFaceMap: 2Slots in VTable: 21
BaseSize is in hex here. (We have a hard time deciding how we like to see these things!) How about arrays, how do we know their size? Let's list all the arrays in the segment to figure it out:
0:000> !dumpheap -type [] 00a51000 00ae4000 Address MT Size00a511f0 030592e0 8000a5129c 03115b68 5600a51348 03135ca0 7600a513a8 030592e0 1600a51434 0313b1c0 14400a51634 0313c234 10000a51698 0313c620 5600a51cc4 030592e0 1600a51e8c 0313b1c0 14400a52008 0313b1c0 14400a52244 0313b1c0 14400a52308 0313b1c0 14400a523cc 0313b1c0 14400a52620 0313b1c0 14400a526e4 0313b1c0 14400a52a14 031e23f8 3600a52b7c 0313b1c0 14400a52c0c 0315778c 108400a53048 0315778c 162800a536a4 0315778c 824...
Picking one at random:
0:000> !dumpobj 00a52c0cName: System.Int32[]MethodTable: 0315778cEEClass: 03157708Size: 1084(0x43c) bytesArray: Rank 1, Type Int32Element Type: System.Int32Fields:None
The formula for determining array size is:
MethodTable.BaseSize + (MethodTable.ComponentSize * Object.Components)
!dumpmt will tell you the first two:
0:000> !dumpmt 315778cEEClass: 03157708Module: 0016b118Name: System.Int32[]mdToken: 02000000 (C:\WINDOWS\Microsoft.NET\Framework\v2.0.x86dbg\mscorlib.dll)BaseSize: 0xcComponentSize: 0x4Number of IFaces in IFaceMap: 4Slots in VTable: 25
and you can find the number of items in the array with:
0:000> dd 00a52c0c+4 l100a52c10 0000010C0:000>
[I'm sure Josh Williams will come along and chide me for forgetting that on 64-bit pointers are 8 bytes, so I'd have to add 8 instead of 4 above. :p]. 0xc + (0x10C*0x4) = 0x43c, so our size is correct.
So we understand object sizes, and how they are arranged. There is one thing missing though, and this is the presence of zero-filled regions throughout the heap called Allocation Contexts. For efficiency, each managed thread can be given such a region to direct new allocations to. This allows multithreaded apps to allocate without expensive locking operations. There is also an Allocation Context for the heap segment that contains generations 0 and 1 (also called the Ephemeral Segment). The !dumpheap command is aware of these regions, and steps lightly over them. You can get the thread Allocation Context addresses with the !threads command:
0:000> !threadsThreadCount: 2UnstartedThread: 0BackgroundThread: 1PendingThread: 0DeadThread: 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)
Thread 0 (the main thread) has an allocation context, from 00ae2e1c to 00ae3ff4. If we look at that memory, we'll see all zeros:
0:000> dd 00ae2e1c00ae2e1c 00000000 00000000 00000000 0000000000ae2e2c 00000000 00000000 00000000 0000000000ae2e3c 00000000 00000000 00000000 0000000000ae2e4c ...
As for the Ephemeral Segment Allocation Context, we don't have one. Recalling !eeheap -gc output:
0:000> !eeheap -gcNumber of GC Heaps: 1generation 0 starts at 0x00aa1b78generation 1 starts at 0x00a5100cgeneration 2 starts at 0x00a51000ephemeral segment allocation context: none segment begin allocated size00a50000 00a51000 00ae4000 0x00093000(602112)...
You might end up with a buffer overflow someday, and obliterate the MethodTable of an object right after your array of StrongBad fan club members. The next time a GC occurs, your program will crash. Let's simulate that dreadful occurrance and see how !dumpheap responds:
0:000> ed adf7f8 00650033 (I'm overwriting the MethodTable of the array we've been enjoying)0:000> !dumpheap 00a51000 00ae4000...00adf7ac 03135ca0 76object 00adf7f8: does not have valid MTcurr_object : 00adf7f8Last good object: 00adf7ac
This allows you to become suspicious of the last good object, 00adf7ac. Of course we know he's alright, he's not responsible for what happened. But in the real world, an aggressive response is required! [imagine WWII air-raid siren here]
What is that last good object anyway?
0:000> !dumpobj adf7acName: System.Byte[]MethodTable: 03135ca0EEClass: 03135c1cSize: 76(0x4c) bytesArray: Rank 1, Type ByteElement Type: System.ByteFields:None
Who cares about him? If I can find a root to this object on a stack, I may be close to code that would overwrite the next object:
0:000> !gcroot adf7acNote: Roots found on stacks may be false positives. Run "!help gcroot" formore info.Scan Thread 0 OSTHread 16acESP:12ea9c:Root:00ad4914(System.Reflection.Emit.MethodBuilder)->00ad4448(System.Reflection.Emit.TypeBuilder)->00adf52c(System.Reflection.Emit.MethodBuilder)->00adf74c(System.Reflection.Emit.ILGenerator)->00adf7ac(System.Byte[])Scan Thread 2 OSTHread 169c
Thread 0, eh? He's employed by an ILGenerator, eh? What kind of nefarious operations are going on in their shop! Okay I'll stop. But it's true, often the last good object is somehow responsible, and a PInvoke overrun is the reason why.
I've ignore the Large Object Heap Segment, but it is crawled in the same way. It has no pesky Allocation Contexts to muddy the water. Large Object segments are never compacted, it would take to long to move such objects around, as they are over 80K in size.
Have fun with PSSCOR.DLL.