Skip to content
Sasha Goldshtein edited this page Jul 8, 2015 · 27 revisions

About msos

msos was born out of a need to debug .NET process dumps without the SOS.dll extension available. It quickly turned into more than that with a variety of commands and options that, in many cases, exceed what SOS.dll has to offer.

You should use msos when you don't have access to SOS.dll (e.g., when debugging Windows Phone dumps), when you need some of msos's advanced functionality, or simply when you don't want to deal with the overhead of the full-blown WinDbg debugger. At this time, msos offers commands (such as !hq) that are extremely difficult to implement using the SOS.dll extension.

Getting Started

msos is a standalone command-line debugger. It can attach to a live process (in non-invasive mode) or open a dump file. You will need the x86 version of msos to debug x86 targets, and the x64 version to debug x64 targets. This is a limitation of the .NET debugging API.

msos is designed to be self-documenting. For example, try running msos --help from the command line to see the first-level command-line options. When inside msos, type help to get a list of commands or help cmdname to get information about a specific command's switches.

The command-line switches are case-sensitive, but the command verbs are not. For example, you can use !clrstack, !CLRStack, or !CLRSTACK with the same effect.

To open a dump file, use the -z switch:

msos -z myapp.dmp

To attach to a running process, use either the --pn or the --pid switches. If there is more than one process with the given name, you will be asked to specify the process id.

msos --pid 1008
msos --pn devenv

Automation

You can run msos and ask it to execute a set of initial commands, separated by semicolons. This is useful for automatic analysis, or when you always begin a debugging session by displaying basic information. For example, if you always begin a session by looking at the list of modules followed by the current thread's stack, use this command line:

msos -z myapp.dmp -c "!lm; !clrstack"

You can include the q command so that the debugger exits without waiting for further input. This is especially useful when analyzing multiple dump files in a row, automatically, as part of a script.

You can also load initial commands from a file. Create a file with each command on a separate line, and pass that file to the -i switch:

msos -z myapp.dmp -i commands.txt

Finally, you can pipe msos's output to a file (instead of the console) by using the -o switch:

msos -z myapp.dmp -i commands.txt -o output.txt

Basic Crash Analysis

When you give msos a crash dump, it will automatically try to switch to the crashing thread and display the exception information along with its stack trace. The !pe (or !PrintException) command can be used to display the current exception again. If you have the address of a .NET exception object, pass it to !pe as a parameter.

Inspecting Threads and Stacks

To display a list of all managed threads in the target, run !Threads. When available, additional information is displayed in the Special and Exception columns. The Lock# column indicates the number of locks the thread currently owns.

1> !Threads
2 CLR threads, 0 CLR thread pool threads, 1 background threads
MgdId  OSId   Lock#  Apt    Special              Exception
1      5980   1      MTA
2      8952   0      MTA    Finalizer

msos attempts to set the current thread to either the thread that encountered an exception (useful with crash dumps), or the first managed thread it can find. You can switch threads by using the ~ command and the managed thread id:

~ 7

To display the current thread's managed stack, run !CLRStack. If the -a switch is specified, this command displays some of the local variables for each frame. Only local variables pointing to reference types (on the heap) are currently supported, and only variable addresses (not names) are available.

1> !clrstack
SP                   IP                   Function
000000000020F180     0000000000000000     InlinedCallFrame
000000000020F17C     0000000073C8B7BF     DomainNeutralILStubClass.IL_STUB_PInvoke(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr)
000000000020F180     0000000000000000     InlinedCallFrame
000000000020F1E4     00000000743DCD64     System.IO.__ConsoleStream.ReadFileNative(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte[], Int32, Int32, Boolean, Boolean, Int32 ByRef)
000000000020F218     00000000743DCC6B     System.IO.__ConsoleStream.Read(Byte[], Int32, Int32)
000000000020F238     0000000073C74EC8     System.IO.StreamReader.ReadBuffer()
000000000020F24C     0000000073BDE90F     System.IO.StreamReader.ReadLine()
000000000020F268     00000000743E2E5D     System.IO.TextReader+SyncTextReader.ReadLine()
000000000020F278     00000000742EA842     System.Console.ReadLine()
000000000020F280     00000000003D00C9     VSDebugging.Program.Main(System.String[])
000000000020F410     0000000000000000     GCFrame

The !ThreadPool command displays general information about the CLR thread pool.

The !SyncBlk command displays all synchronization objects. These include Monitors (the C# lock statement acquires and releases a Monitor), Reader-Writer Locks, and other synchronization mechanisms. When available, ownership information is also displayed along with a list of threads waiting for the synchronization object. For example, in the following example, thread #1 owns the first monitor:

1> !syncblk
Address              Type       Locked   Owner(s)             Waiter(s)
0000000001fc3f34     Monitor    1        1
0000000001fc21f0     None       0

Memory and Objects

Use the !dumpheap --stat command to get an initial reading of what's going on with your managed heap. It displays statistics for each type indicating the number of objects from that type and their total size. Generally, object sizes do not include the sizes of any children. For example, the size of an array of strings doesn't include the size of the strings themselves. The !ObjSize command displays the size of the entire object graph rooted at a specific object.

The !dumpheap command accepts multiple advanced options to filter the output and to display individual objects. The --type switch is most flexible, and takes a regular expression that filters the types of objects to be displayed. For example (omit the --stat switch to get a list of objects, and not just statistics):

1> !dumpheap --type String --stat
Statistics:
MT                   Count      TotalSize  Class Name
0000000073cf4cf8     1          12         System.Collections.Generic.GenericEqualityComparer<System.String>
00000000738ec734     1          48         System.Collections.Generic.Dictionary<System.String,System.Globalization.CultureData>
0000000073cf4b04     2          56         System.Text.StringBuilder
0000000073cf4e04     1          60         System.Collections.Generic.Dictionary+Entry<System.String,System.Globalization.CultureData>[]
0000000073cb5738     18         680        System.String[]
0000000073cf21b4     165        5236       System.String
Total 188 objects

Other heap filtering options include --min, --max and --mt.

To display an individual object, use the !do (or !DumpObj) command. It displays the object's fields (unless you pass the --nofields switch), including any static and thread-static fields. Nested value types are displayed unless you pass the --norecurse switch. Nested reference types are never displayed.

1> !do 0000000001fc3984
Name:     System.Globalization.NumberFormatInfo
MT:       0000000073cf4e7c
Size:     132(0x84) bytes
Assembly: C:\WINDOWS\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.
dll
Value:    1942965884
Fields:
Offset   Type                 VT  Attr       Value                Name
0        System.Int32[]       0   instance   33303612             numberGroupSizes
8        System.Int32[]       0   instance   33303628             currencyGroupSizes
10       System.Int32[]       0   instance   33303612             percentGroupSizes
18       System.String        0   instance   +                    positiveSign
20       System.String        0   instance   -                    negativeSign
28       System.String        0   instance   .                    numberDecimalSeparator
30       System.String        0   instance   ,                    numberGroupSeparator
38       System.String        0   instance   ,                    currencyGroupSeparator
40       System.String        0   instance   .                    currencyDecimalSeparator
48       System.String        0   instance   ☼                    currencySymbol
50       System.String        0   instance   <null>               ansiCurrencySymbol
58       System.String        0   instance   NaN                  nanSymbol
60       System.String        0   instance   Infinity             positiveInfinitySymbol
68       System.String        0   instance   -Infinity            negativeInfinitySymbol
70       System.String        0   instance   .                    percentDecimalSeparator
78       System.String        0   instance   ,                    percentGroupSeparator
80       System.String        0   instance   %                    percentSymbol
88       System.String        0   instance   %                    perMilleSymbol
90       System.Object[]      0   instance   33303556             nativeDigits
98       System.Int32         1   instance   0                    m_dataItem
a0       System.Int32         1   instance   2                    numberDecimalDigits
a8       System.Int32         1   instance   2                    currencyDecimalDigits
b0       System.Int32         1   instance   0                    currencyPositivePattern
b8       System.Int32         1   instance   0                    currencyNegativePattern
c0       System.Int32         1   instance   1                    numberNegativePattern
c8       System.Int32         1   instance   0                    percentPositivePattern
d0       System.Int32         1   instance   0                    percentNegativePattern
d8       System.Int32         1   instance   2                    percentDecimalDigits
e0       System.Int32         1   instance   1                    digitSubstitution
e8       System.Boolean       1   instance   True                 isReadOnly
ea       System.Boolean       1   instance   False                m_useUserOverride
ec       System.Boolean       1   instance   True                 m_isInvariant
ee       System.Boolean       1   instance   True                 validForParseAsNumber
f0       System.Boolean       1   instance   True                 validForParseAsCurrency
528      ...NumberFormatInfo  0   shared     static               invariantInfo
   >> Domain:Value  0000000000604ac8:<null> <<

To display collections, use the !DumpCollection command. It supports arrays, lists, and dictionaries. Other collections can be displayed using the !hq command (discussed below).

1> !dumpcollection 0000000001fc1bb0
Type:   System.Collections.Generic.Dictionary<System.Type,System.Security.Policy.EvidenceTypeDescriptor>
Size:   10
Key                                      Value
[0000000001fc1d3c System.RuntimeType]    <null>
[0000000001fc1d58 System.RuntimeType]    <null>
[0000000001fc1d74 System.RuntimeType]    <null>
[0000000001fc1d90 System.RuntimeType]    <null>
[0000000001fc1dac System.RuntimeType]    <null>
[0000000001fc1dc8 System.RuntimeType]    <null>
[0000000001fc1de4 System.RuntimeType]    <null>
[0000000001fc1e00 System.RuntimeType]    <null>
[0000000001fc1e1c System.RuntimeType]    <null>
[0000000001fc1e38 System.RuntimeType]    <null>
Time: 3 ms, Memory start: 568.008kb, Memory end: 568.008kb, Memory delta: -0b

Use the !ObjSize command to display the size of the object graph rooted at a specific object. This includes all referenced objects.

1> !objsize 0000000001fc3984
0000000001fc3984 graph size is 24 objects, 574 bytes

Memory Leaks

The !EEHeap command provides basic information about the CLR's memory usage in the target. The sizes of each heap and each generation (including the Large Object Heap) are displayed. Non-GC regions (such as the loader heap) are also shown.

1> !EEHeap
GC regions:
Address              Size         Type         Heap#   Commit/Reserve
0000000001fc1000     68.000kb     Ephemeral    0       Committed
0000000001fd2000     15.930mb     Ephemeral    0       Reserved
0000000002fc1000     68.000kb     LargeObject  0       Committed
0000000002fd2000     15.930mb     LargeObject  0       Reserved
Gen    Size
0      11.965kb
1      12b
2      12b
LOH    16.898kb
Total  28.887kb
Other regions:
...snipped...

After identifying objects that you suspect should have been collected, use the !GCRoot command to understand why they are being retained. Some duplicates might be displayed. In the following output, there is a static variable s_InvariantCultureInfo that references a CultureInfo object, which in turn references the NumberFormatInfo object (at address 0000000001fc3984) that was the target of our query.

1> !gcroot 0000000001fc3984
0000000002FC1430 -> 0000000001FC2594 static var System.Globalization.CultureInfo.s_InvariantCultureInfo
        -> 0000000001FC2594 System.Globalization.CultureInfo
        -> 0000000001FC3984 System.Globalization.NumberFormatInfo

If you are having trouble with finalization, e.g. objects are not being finalized quickly enough, you can review the contents of the f-reachable queue (objects ready for finalization) by using the !frq command:

1> !frq
Address              Size       Class Name
0000000001fc1b54     20         Microsoft.Win32.SafeHandles.SafePEFileHandle
0000000001fc21f0     44         System.Threading.ReaderWriterLock
0000000001fc235c     20         Microsoft.Win32.SafeHandles.SafeFileHandle
0000000001fc3b14     20         Microsoft.Win32.SafeHandles.SafeViewOfFileHandle
0000000001fc3b28     20         Microsoft.Win32.SafeHandles.SafeFileMappingHandle
Total 5 objects ready for finalization

Heap Queries

Heap queries are probably the most powerful feature msos currently has to offer. They have no parallel in SOS.dll, and offer a lot of room for customization and creativity. The !hq command takes an arbitrary C# expression and executes it in a context that gives you access to the current target's heap. You can locate specific objects, access their properties, aggregate statistics, filter, group, and sort the results, and so on.

The following utility methods are available:

  • AllObjects() returns a collection of all heap objects. Each object has special __Type and __Size properties. You can cast the returned objects to ulong to obtain their address. In addition, any fields of the original object are accessible directly. For example, !hq (from o in AllObjects() where o.__Type == "System.String" select (int)o.m_stringLength).Sum() displays the total length of all the strings found on the heap. Auto-generated properties can be accessed directly using the property name -- you don't (and can't) use the backing field (<PropName>k__BackingField) for this.

  • ObjectsOfType("...") is a convenience method that returns a collection of heap objects that have the specified type. The type is matched exactly, e.g. ObjectsOfType("System.String").

  • AllClasses() returns a collection of objects representing each type on the heap. These objects have special __Name, __Fields, __StaticFields, and __ThreadStaticFields properties.

  • Class("...") returns an object representing a specific type. You can access static fields by using their name, but you currently need to provide an app domain object to get the actual value. This will be addressed in the future.

  • Object(...) returns a dynamic object based on address. This is useful when you discovered the object's address using some other command (e.g. !gcroot) but want to write code against it dynamically using C# expressions. Note that most commands display object addresses in hex, so prefix the object address with 0x when passing it to the Object method.

Example query:

1> !hq (from obj in AllObjects()
        group obj by obj.__Type into g
        let size = g.Sum(o => (long)o.__Size)
        let count = g.Count()
        orderby size descending
        select new { Type = g.Key, Count = count, Size = size }
       ).Take(10)
Type                               Count                              Size
System.Object[]                    6                                  17308
System.String                      165                                5236
System.RuntimeType                 26                                 728
System.Char[]                      4                                  718
System.String[]                    18                                 680
System.Globalization.CultureDa...  2                                  616
System.Int32[]                     11                                 564
System.Collections.Generic.Dic...  3                                  468
System.Byte[]                      2                                  280
System.Threading.ThreadAbortEx...  2                                  168
Rows: 10

To work your way through collections as part of a query, you can use the GetDictionaryItems() method on dictionaries and the GetItems() method on arrays and lists. They both return a collection of dynamic objects that you can work with. Occasionally, if you try to use LINQ to process the returned collections, you might hit a compiler error (e.g., CS1943) because the collection object is dynamic. You can work around this by inserting a cast to a concrete type (not dynamic). For example:

1> !hq from o in AllObjects()
       where o.__Type.Contains("List<VSDebugging.Person")
       from item in o._items.GetItems()
       select item.Name
Query compilation failed with 1 errors:
c:\Users\Sasha\AppData\Local\Temp\n3wsitxl.0.cs(48,104) : error CS1943: An expression of type 'dynamic' is not allowed in a subsequent from clause in a query expression with source type 'System.Collections.Generic.IEnumerable<dynamic>'.  Type inference failed in the call to 'SelectMany'.

1> !hq from o in AllObjects()
       where o.__Type.Contains("List<VSDebugging.Person")
       from item in (IEnumerable<dynamic>)o._items.GetItems()
       select item.Name
Mike
Rows: 1

Miscellaneous Commands

Include the --diag switch when launching msos to get diagnostic information after each command is executed. Currently this information includes the time it took to execute each command, and the memory usage before and after the command's execution. This helps detect memory leaks.

1> !hq (from i in Enumerable.Range(0, 100000000) select i).Count()
100000000
Rows: 1
Time: 1692 ms, Memory start: 89.262kb, Memory end: 116.945kb, Memory delta: +27.684kb

The !lm command lists the managed assemblies loaded into the target. Symbol load status is also displayed. If symbols aren't loaded, it doesn't necessarily mean they cannot be found -- the debugger loads symbols on demand.

1> !lm
start                size       symloaded  filename
00000000738e0000     01095000   False      C:\WINDOWS\Microsoft.Net\assembly\GAC_32\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dll
0000000000080000     00008000   False      C:\Temp\VSDebugging\bin\Debug\VSDebugging.exe

Use the db command to display a raw region of memory as hex bytes and ASCII characters. The -l and -c switches control how many bytes to display and how many columns to put in each row, respectively.

1> db 0000000001fc3984 -l 20 -c 10
0000000001fc3984  7c 4e cf 73 3c 2c fc 01 4c 2c   |N.s<,..L,
0000000001fc398e  fc 01 3c 2c fc 01 bc 26 fc 01   ..<,...&..

Use the q command to quit the debugger.

Symbol Loading

Currently, msos loads symbols from the location specified by the _NT_SYMBOL_PATH environment variable. Point it to the location of your symbols and to the Microsoft symbol server for best results. For example:

set _NT_SYMBOL_PATH=d:\mysymbols;\\myserver\moresymbols;srv*C:\symbols*http://msdl.microsoft.com/download/symbols
msos -z myapp.dmp

msos will complain if it needs but cannot find symbols, and will report when symbols are downloaded from the symbol server.

Tutorial

TODO