Visual Studio + SOS 小實驗
咋一看標題,覺得有些奇怪,同步塊索引和HashCode有啥關系呢。從名字上來看離著十萬八千裡。在 不知道細節之前,我也是這樣想的,知道細節之後,才發現這兩兄弟如此親密。我們還是先來用Visual Studio + SOS,看一個東西,下面是作為小白兔的示例代碼:
1: using System;
2: public class Program
3: {
4: static void Main()
5: {
6: Foo f = new Foo();
7: Console.WriteLine(f.GetHashCode());
8:
9: Console.ReadLine();
10: }
11: }
12: //就這麼一個簡單的類
13: public class Foo
14: {
15:
16: }
(使用Visual Studio + SOS調試的時候,請先在項目的屬性,調試欄裡設置“允許非托管代碼調試” )
我們分別在第7行,第9行設置斷點,F5運行,當程序停在第一個斷點處時(此時f.GetHashCode()還沒 有執行),我們在Visual Studio的立即窗口裡輸入:
1: .load sos.dll
2: extension C:\Windows\Microsoft.NET\Framework\v2.0.50727\sos.dll loaded
3: !dso
4: PDB symbol for mscorwks.dll not loaded
5: OS Thread Id: 0x1730 (5936)
6: ESP/REG Object Name
7: 0013ed78 01b72d58 Foo
8: 0013ed7c 01b72d58 Foo
9: 0013efc0 01b72d58 Foo
10: 0013efc4 01b72d58 Foo
使用.load sos.dll加載sos模塊,然後使用!dso,我們找到了Foo類型的f對象的內存地址:01b72d58 ,然後使用Visual Studio調試菜單下的查看內存的窗口,查看f對象頭部的內容:
陰影遮住的00 00 00 00就是同步塊索引所在的地方了,可以看得出來,此時同步塊索引的值還是0( 後面會對這個做解釋),然後繼續F5,程序運行到下一個斷點處,這個時候f.GetHashCode()也已調用了 ,細心的你就會發現,原來對象同步塊索引所在的地方的值變了:
Visual Studio這個內存查看器有個很好的功能,對內存變化的以紅色標出。我們看到,原來是00 00 00 00變成了現在的4a 73 78 0f。嗯,看來HashCode的獲取和同步塊索引還是有一些關系的,不然調用 GetHashCode方法為什麼同步塊索引的值會變化呢。再來看看Console.WriteLine(f.GetHashCode())的輸 出:
不知道著兩個值有沒有什麼關系,我們先把它們都換算成二進制吧。注意,這裡的4a 73 78 0f是低位 在左,高位在右,下面的十進制是高位再左,低位在右,那4a 73 78 0f實際上就是0x0f78734a了。
0x0f78734a:00001111011110000111001101001010
58225482:00000011011110000111001101001010
Rotor源代碼
我們先用0補齊32位,突然發現這兩者低26位居然是一模一樣的(紅色標出的部分),這是巧合還是必 然?為了一探究竟只好搬出Rotor的源代碼,從源代碼裡看看是否能發現什麼東西。還是遵循老路子,我 們先從托管代碼開始:
1: public virtual int GetHashCode()
2: {
3: return InternalGetHashCode(this);
4: }
5: [MethodImpl (MethodImplOptions.InternalCall)]
6: internal static extern int InternalGetHashCode(object obj);
在本系列的第一篇文章已經提到過,標記有[MethodImpl(MethodImplOptions.InternalCall)]特性的 方法是使用Native Code的方式實現的,在Rotor中,這些代碼位於sscli20\clr\src\vm\ecall.cpp文件中 :
1: FCFuncElement("InternalGetHashCode", ObjectNative::GetHashCode)
2: FCIMPL1(INT32, ObjectNative::GetHashCode, Object* obj) {
3: DWORD idx = 0;
4: OBJECTREF objRef(obj);
5: idx = GetHashCodeEx (OBJECTREFToObject(objRef));
6: return idx;
7: }
8: FCIMPLEND
9: INT32 ObjectNative::GetHashCodeEx(Object *objRef)
10: {
11: // This loop exists because we're inspecting the header dword of the object
12: // and it may change under us because of races with other threads.
13: // On top of that, it may have the spin lock bit set, in which case we're
14: // not supposed to change it.
15: // In all of these case, we need to retry the operation.
16: DWORD iter = 0;
17: while (true)
18: {
19: DWORD bits = objRef- >GetHeader()->GetBits();
20:
21: if (bits & BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX)
22: {
23: if (bits & BIT_SBLK_IS_HASHCODE)
24: {
25: // Common case: the object already has a hash code
26: return bits & MASK_HASHCODE;
27: }
28: else
29: {
30: // We have a sync block index. This means if we already have a hash code,
31: // it is in the sync block, otherwise we generate a new one and store it there
32: SyncBlock *psb = objRef->GetSyncBlock();
33: DWORD hashCode = psb->GetHashCode();
34: if (hashCode != 0)
35: return hashCode;
36:
37: hashCode = Object::ComputeHashCode();
38:
39: return psb- >SetHashCode(hashCode);
40: }
41: }
42: else
43: {
44: // If a thread is holding the thin lock or an appdomain index is set, we need a syncblock
45: if ((bits & (SBLK_MASK_LOCK_THREADID | (SBLK_MASK_APPDOMAININDEX << SBLK_APPDOMAIN_SHIFT))) != 0)
46: {
47: objRef- >GetSyncBlock();
48: // No need to replicate the above code dealing with sync blocks
49: // here - in the next iteration of the loop, we'll realize
50: // we have a syncblock, and we'll do the right thing.
51: }
52: else
53: {
54: DWORD hashCode = Object::ComputeHashCode();
55:
56: DWORD newBits = bits | BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX | BIT_SBLK_IS_HASHCODE | hashCode;
57:
58: if (objRef->GetHeader()->SetBits(newBits, bits) == bits)
59: return hashCode;
60: // Header changed under us - let's restart this whole thing.
61: }
62: }
63: }
64: }
代碼很多,不過大部分操作都是在做與、或、移位等。而操作的對象就是這行代碼獲取的:objRef- >GetHeader()->GetBits(),實際上就是獲取同步塊索引。
想想,在第一個斷點命中的時候,同步塊索引的值還是0x00000000,那應該是下面這塊代碼執行:
1: DWORD hashCode = Object::ComputeHashCode();
2: DWORD newBits = bits | BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX | BIT_SBLK_IS_HASHCODE | hashCode;
3: if (objRef->GetHeader()->SetBits(newBits, bits) == bits)
4: return hashCode;
通過Object的ComputeHashCode方法算出一個哈希值來(由於本文不是關注哈希算法的,所以這裡不討 論這個ComputeHashCode方法的實現)。然後進行幾個或操作(這裡還要與原先的bits或操作是為了保留 原來的值,說明這個同步塊索引還起了別的作用,比如上篇文章的lock),然後將同步塊索引中老的位換 掉。從這裡我們還看不出來什麼。不過,如果我們再次對這個對象調用GetHashCode()方法呢?那同步塊 索引不再為0x00000000,而是0x0f78734a,在來看看幾個定義的常量的值:
1: #define BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX 0x08000000
2: #define BIT_SBLK_IS_HASHCODE 0x04000000
3: #define HASHCODE_BITS 26
4: #define MASK_HASHCODE ((1<<HASHCODE_BITS)-1)
從剛才設置hashcode的地方可以看到:DWORD newBits = bits | BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX | BIT_SBLK_IS_HASHCODE | hashCode;
所以開頭的兩個if都可以通過了,返回的hashcode就是bits & MASK_HASHCODE。
這個MASK_HASHCODE是將1向左移26位=100000000000000000000000000,然後減 1=00000011111111111111111111111111(低26位全部為1,高6位為0),然後與同步塊索引相與,其實這 裡的作用不就是為了取出同步塊索引的低26位的值麼。再回想一下本文開頭的那個試驗,原來不是巧合啊 。
連上上一篇,我們可以看到同步塊索引不僅僅起到lock的作用,有時還承擔著存儲HashCode的責任。 實際上同步塊索引是這樣的一個結構:總共32位,高6位作為控制位,後26的具體含義隨著高6位的不同而 變化,高6位就像很多小開關,有的打開(1),有的關閉(0),不同位的打開和關閉有著不同的意義,程序 也就知道低26位到底是干啥的了。這裡的設計真是巧妙,不斷占用內存很緊湊,程序也可以靈活處理,靈 活擴展。
後記
本篇和上一篇一樣,都是單獨將獨立的內容拿出來,這樣可以更簡單的來闡述。比如在本文中,我只 設想同步塊索引做hashcode的存儲,這個時候,同步塊索引就干干淨淨(本文前面的試驗中先得到的同步 塊索引就是一個0),但實際中同步塊索引可能擔任更多的職責,比如既lock,又要獲取HashCode,這個 時候情況就更復雜,這個在後面一篇文章會綜合各種情況更詳細的說明。