Skip to content

Exploring Locking and Hash codes with WinDBG and SOS

Evgeny Belov edited this page Mar 22, 2020 · 5 revisions

About

Today we're going to find out how Object Header holds auxillary information about a managed object's instance including:

  • synchronization flags
  • hash code when GetHasCode() method is not overridden

We are going to use debug a simple C# application with WinDBG + SOS debugger extension

using System;

namespace TestProfiler
{
    class SyncClass
    {
    }

    class Program
    {
        static void Main(string[] args)
        {
            var x = new SyncClass();
            // Console.WriteLine(x.GetHashCode());

            lock (x)
            {
                Console.WriteLine("Locked");
               // var p = x.GetHashCode();
               // Console.WriteLine(p);
            }

            Console.WriteLine("Unlocked");
        }
    }
}

Let's get started.

Abbreviations

MT - Method Table Pointer

OH - Object Header

Object Layout

Every reference type in .net has the following layout on the managed heap

Layout

Object Reference aka Type Handle is a pointer to the MT. Before MT, with negative 4 bytes offset, there's a data structure called Object Header.

Synchronization

Let’s start with setting a breakpoint inside the Main method on line 15, right after an instance of SyncClass has been created.

0:000> !bpmd Program.cs:15
0:000> g

To find more about the created instance, we dump the heap and trying to locate the address of the instance

0:000> !DumpHeap -type SyncClass
Address     MT          Size
02f62440    012c4dc8    12
Total 1 objects

The output shows that a single instance of SyncClass exists in the heap. Address column is a pointer to the type's MT. Having found out the address of the MT, we can dump both OH and MT with the following command

0:000> dd 02f62440 - 4 L2
       00000000 012c4dc8

We subtracted 4 from the MT's address because OH has a -4 bytes offset relative to MT address

  • the first 8 bytes represent the OH
  • the second 8 bytes are a pointer to the MT

As we see, the OH is zeroed which means that neither the object was for synchronization nor the GetHashCode() method was called.

Now we can proceed and try using the object for synchronization, so we’re setting a breakpoint on line 21 inside the lock statement

0:000> !bpmd Program.cs:21
0:000> g

and dump OH together with MT again

0:000> dd 02f62440 - 4 L2
       00000001 012c4dc8

Now the OH has a bit set to 1 indicating that the object is being used for synchronization.

When the latter happens, the CLR can either creates a thin lock or a sync block. Thin lock is an optimization and it’s used when, for example, there’s no other thread accessing the locked resource.

We can check existing sync blocks by the command

0:000> !SyncBlk
Total           0

which shows that no sync blocks were created.

Now let's check for thin locks

0:000> !DumpHeap -thinlock
Address         MT             Size
02fe2440        012c4dc8       12       ThinLock owner 1 (010fa288) Recursive 0
Found 1 objects.

As we see, a thin lock was created by the CLR.

Next, we leave the lock statement and examine OH once again

0:000> !bpmd Program.cs:23
0:000> g
0:000> dd 02f62440 - 4 L2
       00000000 012c4dc8

ANow the OH is zeroed again which means the object is no longer used for synchronization.

Hash code

Let’s modify the program and call the GetHashCode() method twice: outside and inside the lock statement.

using System;

namespace TestProfiler
{
    class SyncClass
    {
    }

    class Program
    {
        static void Main(string[] args)
        {
            var x = new SyncClass();
            Console.WriteLine(x.GetHashCode());

            lock (x)
            {
                Console.WriteLine("Locked");
                var p = x.GetHashCode();
                Console.WriteLine(p);
            }

            Console.WriteLine("Unlocked");
        }
    }
}

We set a breakpoint after GetHashCode() method was called for the first time.

0:000> !bpmd Program.cs:16
0:000> g

In the console, we see that the object's hash code is equal to 46104728. We'll need the binary and hex representation of this value so let's convert it

.formats 0n46104728
Evaluate expression:
  Hex:     02bf8098
  Binary:  00000010 10111111 10000000 10011000

Repeat the same steps we did in part 1 to dump the object's OH and MT.

0:000> !DumpHeap -type SyncClass
Address        MT              Size
02852440        00a04dc8        12
0:000> dd 02852440 - 4 L2
       0ebf8098 00a04dc8

The first 8 bytes aren't zero and have a value, let's find out what it is but first convert the value into binary representation

.formats 0ebf8098
Evaluate expression:
  Hex:     0ebf8098
  Binary:  00001110 10111111 10000000 10011000

Let's compare binary representations of the value we see in the console and the value we dumped from the OH

000000 10 10111111 10000000 10011000 - Console

000011 10 10111111 10000000 10011000 - Header

We can clearly see that the hash code presents in the OH

It's time to move on to the most interesting part. What happens when we call the GetHashCode() method inside the lock statement?

GetHashCode() and Locking

Let's set a breakpoint on line 19 inside the lock and dump OH and MT again

0:000> !bpmd Program.cs:19
0:000> g
0:000> dd 02852440-4 L2
       08000001 00a04dc8
0:000> .formats 08000001
Evaluate expression:
Binary:  00001000 00000000 00000000 00000001

Interesting, the hash code is gone and new flags have been set. As we already remember, last bit set to 1 indicates that the object is being used for synchronization. Keeping in mind the two types of synchronization mechanisms (thin locks and sync blocks) we can find out what the other bits mean. Let's check for a thin lock first

0:000>  !DumpHeap -thinlock
 Address       MT     Size
Found 0 objects.

Nothing was found, so maybe there's a sync block?

0:000> !SyncBlk
Index SyncBlock MonitorHeld Recursion Owning    Thread  Info    SyncBlock   Owner
    1 00a8e9bc            1         1 00a6a2b8  2688    0       02852440    TestProfiler.SyncClass
-----------------------------
Total           1
Free            0

A sync block has been created. But why? The only difference with the first example is that we called GetHashCode() method prior to acquiring the lock.

Now let's call the GetHashCode() inside the lock statement and examine the OH

0:000> !bpmd Program.cs:21
0:000> g
0:000> dd 02852440-4 L2
       08000001 00a04dc8

The OH is intact, hash code value is displayed in the console though. So where is it stored? Maybe the value was moved to the sync block? Let's check it by dumping the sync block's memory.

0:000> dd 00a8e9bc
00a8e9bc  00000001 00000001 00a6a2b8 00000000
00a8e9cc  80000001 ffffffff 00000000 00000000
00a8e9dc  00000000 00000000 00000000 02bf8098

The last 4 bytes hold the object's hashcode values (remember the hex value we converted earlier?). This means that when a sync block is created, the hashcode value is transferred and stored in its memory.

Clone this wiki locally