前面咱們圖解了.NET裏各類對象的內存佈局,咱們再來從調試器和clr源碼的角度來看一下對象的內存佈局。我寫了一個測試程序來加深對.net對象內存佈局的瞭解:數組
using System; using System.Runtime.InteropServices; // 其實是一個C語言裏的聯合體 [StructLayout(LayoutKind.Explicit)] public struct InnerStruct { [FieldOffset(0)] public float FloatValue; [FieldOffset(0)] public double DoubleValue; } public struct TestStruct { public int IntValue; public string StringValue; public object ObjectValue; public InnerStruct InnerStructValue; } public class ObjectLayout { private int _IntValue = 456; public int IntValue { get { return _IntValue; } set { _IntValue = value; } } public static void Main() { Object o = new Object(); lock (o) { Console.WriteLine("Object實例: {0}", o.ToString()); } int i = 123; Console.WriteLine("int值: {0}", i); string s = "This is a string"; Console.WriteLine("字符串:{0}", s); ObjectLayout[] olArr = new ObjectLayout[10]; olArr[0] = new ObjectLayout(); olArr[0].IntValue = 2222; Console.WriteLine("數組的長度:{0}", olArr.Length); object[] objArr = new object[2]; objArr[0] = o; Console.WriteLine("數組的長度:{0}", objArr.Length); string[] strArr = new string[2]; strArr[0] = s; strArr[1] = s + "!"; Console.WriteLine("數組的長度:{0}", strArr.Length); TestStruct ts = new TestStruct(); ts.IntValue = 100; ts.StringValue = s + "!"; ts.ObjectValue = o; ts.InnerStructValue.FloatValue = 789.0f; int[] intArr = new int[10]; for (int j = 0; j < intArr.Length; ++j) { intArr[j] = j; } Console.WriteLine("int數組的長度:{0}", intArr.Length); TestStruct[] tsArr = new TestStruct[2]; tsArr[0] = ts; Console.WriteLine("TestStruct數組的長度:{0}", tsArr.Length); } }
使用命令編譯一個調試版本的objectlayout.exe程序:bash
csc /debug objectlayout.cs函數
用sos瀏覽對象內存佈局工具
咱們用sos這個工具加深對.net對象的理解,sos能夠在Visual Studio裏使用:佈局
這個時候就能夠在vs裏使用sos插件裏面的命令了,以下圖所示:測試
這裏對sos命令不作過多的解釋,有興趣的網友能夠參看個人《Windows調試技術》視頻來了解sos的用法,下面我用相似bash註釋的方式解釋查看過程:spa
# #使用 !clrstack 命令查看當前被調試進程的堆棧,而 -l 參數則告訴sos同時 # 顯示堆棧上每一個函數的局部變量。 # !clrstack -l OS Thread Id: 0xba4 (2980) ESP EIP 0012f408 00e10341 ObjectLayout.Main() # # 下面打印了Main函數裏的全部局部變量,若是執行過GC,有些變量可能不可見 # 局部變量左邊是局部變量的內存地址,而右邊則是局部變量的值,由於大部分 # 局部變量都是引用類型,全部值大部分都是指針,除了少數幾個,如第二個變量 # 就是一個值類型,所以直接保存了它的值:0x0000007b # LOCALS: 0x0012f444 = 0x012b1bd8 0x0012f440 = 0x0000007b <CLR reg> = 0x012b1af4 0x0012f438 = 0x012c9520 0x0012f434 = 0x012c966c 0x0012f430 = 0x012c9788 0x0012f41c = 0x012c98d8 0x0012f418 = 0x012c990c 0x0012f414 = 0x0000000a 0x0012f410 = 0x012c9a5c 0x0012f40c = 0x00000000 0012f69c 79e88f63 [GCFrame: 0012f69c]
接下來,咱們一個個分析這些對象的內存佈局,首先是第一個對象 - object類型的o:.net
!do 0x012b1bd8 Name: System.Object #指明瞭類型,這個類型由保存在對象裏的MethodTable獲取 MethodTable: 790f9c18 # MethodTable地址,直接保存在對象裏 EEClass: 790f9bb4 #經過MethodTable解析到 Size: 12(0xc) bytes (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll) Object Fields: None
打開VS的「內存(Memory)」窗口,或者「命令(Command)」窗口,查看0x012b1bd8地址處的內存,這裏爲了寫文章方便,我用的是「命令」窗口,在VS裏依次點擊菜單「視圖(View)」 -> 「其餘窗口(Other Windows)」 -> 「命令窗口(Command Window)」。在命令窗口裏執行下面的命令(熟悉windbg的同窗應該知道這是windbg裏的命令):插件
# # 你能夠直接給出變量名,vs會自動將變量名解析爲內存地址 # 能夠看到,對象的第一個指針就是說明本身類型的MethodTable # 指針 -> 790f9c18, # >dd o 0x012B1BD8 790f9c18 00000000 00000000 00000000 # # 固然也能夠直接給dd命令內存地址 # >dd 0x012b1bd8 0x012B1BD8 790f9c18 00000000 00000000 00000000
前文咱們已經提到clr將對象的指針作了一些處理,對託管代碼隱藏了objheader信息,這個信息其中一個做用就是處理線程同步信息,要看看syncvalue是怎麼工做的話,能夠從新啓動被調試程序,並將程序中斷在代碼的第39行即lock語句那裏 - 在其執行以前中斷程序,以下圖所示:命令行
而後咱們在「命令窗口」查看對象的內存佈局:
# #我用的是虛擬機上安裝的32位xp系統,所以咱們將地址提早 #一個指針,看看當前objheader的synvalue的值,目前由於 #沒有線程須要同步訪問這個對象,因此其值爲0 # >dd 0x012b1bd4 0x012B1BD4 00000000 790f9c18 00000000 00000000 # #單步執行lock語句一次,以便開始線程同步,再看相同地址的值 #如今會發現lockvalue已經更新了,lockvalue的做用在後文說明 #這裏就不詳細說明它了。 # >dd 0x012b1bd4 0x012B1BD4 00000001 790f9c18 00000000 00000000
咱們再看第三個局部變量,string類型的s,使用sos命令查看的結果以下:
!do 0x012b1af4 Name: System.String MethodTable: 790fa3e0 EEClass: 790fa340 Size: 50(0x32) bytes (C:\WINDOWS\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll) # #對於字符串對象,!do命令足夠聰明,能夠直接將字符串的內容打印出來 # String: This is a string # #顯示該對象實例的每個成員變量的值 # Fields: MT Field Offset Type VT Attr Value Name 790fed1c 4000096 4 System.Int32 0 instance 17 m_arrayLength 790fed1c 4000097 8 System.Int32 0 instance 16 m_stringLength 790fbefc 4000098 c System.Char 0 instance 54 m_firstChar 790fa3e0 4000099 10 System.String 0 shared static Empty >> Domain:Value 0014c558:790d6584 << 79124670 400009a 14 System.Char[] 0 shared static WhitespaceChars >> Domain:Value 0014c558:012b1670 <<
咱們再在命令行裏用dd查看它的內存佈局,由於是字符串,因此這裏咱們用dc命令,除了用16進制顯示內存之外,還儘可能使用字符的形式打印內存的每一個指針:
# #第一個指針保存的仍然是對象的類型信息 - MethodTable指針, #接下來第二個指針就是若是將字符串看成數組看待的話,它的長度, #這個長度會包括最後的’\0’,這裏它的值是 0x11,也就是17。 #第三個指針就是字符串的長度,即 0x10,也就是16個字符。 #在字符串長度後面就是實際的字符串WCHAR數組了。 # >dc s 0x012B1AE8 790fa3e0 00000011 00000010 00680054 à£.y........T.h. 0x012B1AF8 00730069 00690020 00200073 00200061 i.s. .i.s. .a. . 0x012B1B08 00740073 00690072 0067006e 00000000 s.t.r.i.n.g..... 0x012B1B18 00000000 790fa3e0 00000008 00000007 ....à£.y........
接下來咱們再來看引用類型的數組對象在內存裏的佈局,下面是 ObjectLayout[] 類型的對象olArr的結果,能夠看到在clr裏,對象的類型實際上 System.Object[],而不是 ObjectLayout[]。
!do 0x012c9520 Name: System.Object[] # #MethodTable的值是 79124228,跟後面 object[] 類型對象的 objArr 的 #MethodTable是同樣的 # MethodTable: 79124228 EEClass: 7912479c Size: 56(0x38) bytes # #對於數組對象,sos會打印出數組的維度和大小信心 # Array: Rank 1, Number of elements 10, Type CLASS # #數組元素的類型 # Element Type: ObjectLayout Fields: None # #使用 da 命令能夠打印出數組的詳細內容 # !da 0x012c9520 Name: ObjectLayout[] MethodTable: 79124228 EEClass: 7912479c Size: 56(0x38) bytes Array: Rank 1, Number of elements 10, Type CLASS Element Methodtable: 00933018 [0] 012c9558 [1] null [2] null [3] null [4] null [5] null [6] null [7] null [8] null [9] null
把olArr對象的內存打印出來,能夠看到:
>dd olArr 0x012C9438 79124228 0000000a 00933018 012c9558 0x012C9448 00000000 00000000 00000000 00000000 # #打印 object[] 類型的 objArr 對象的信息 # !do 0x012c966c Name: System.Object[] # #MethodTable指針與前面的ObjectLayout[]對象的MethodTable徹底同樣 # MethodTable: 79124228 EEClass: 7912479c Size: 24(0x18) bytes Array: Rank 1, Number of elements 2, Type CLASS Element Type: System.Object Fields: None !da 0x012c966c Name: System.Object[] MethodTable: 79124228 EEClass: 7912479c Size: 24(0x18) bytes Array: Rank 1, Number of elements 2, Type CLASS Element Methodtable: 790f9c18 [0] 012b1c3c [1] null # #打印 string[] 類型的 strArr 對象的信息 # !do 0x012c9788 Name: System.Object[] # #MethodTable指針與前面的ObjectLayout[]和object[]對象的MethodTable徹底同樣 # MethodTable: 79124228 EEClass: 7912479c Size: 24(0x18) bytes Array: Rank 1, Number of elements 2, Type CLASS Element Type: System.String Fields: None !da 0x012c9788 Name: System.String[] MethodTable: 79124228 EEClass: 7912479c Size: 24(0x18) bytes Array: Rank 1, Number of elements 2, Type CLASS Element Methodtable: 790fa3e0 [0] 012b1af4 [1] 012c97a0 # #打印 int[] 類型的 intArr 對象的信息 # !da 0x012c990c Name: System.Int32[] # #注意:MethodTable 也就是類型指針跟前面引用類型的MethodTable不一樣 # MethodTable: 791240f0 EEClass: 791241a8 Size: 52(0x34) bytes Array: Rank 1, Number of elements 10, Type Int32 Element Methodtable: 790fed1c [0] null [1] 00000001 [2] 00000002 [3] 00000003 [4] 00000004 [5] 00000005 [6] 00000006 [7] 00000007 [8] 00000008 [9] 00000009
查看intArr的內存佈局,能夠看到,與前面的引用類型數組不一樣,第三個指針就是數組的第一個元素(值爲0)了,而引用類型數組的第三個指針是元素的類型指針。
>dd intArr 0x012C97B0 791240f0 0000000a 00000000 00000001 0x012C97C0 00000002 00000003 00000004 00000005 0x012C97D0 00000006 00000007 00000008 00000009 # #打印自定義結構體 TestStruct[] 類型的 tsArr對象的信息 # !da 0x012c9a5c Name: TestStruct[] # #注意:MethodTable 指針不只跟前面引用類型的MethodTable不一樣, #並且跟intArr的類型也不同 # MethodTable: 00933200 EEClass: 00933180 Size: 52(0x34) bytes Array: Rank 1, Number of elements 2, Type VALUETYPE Element Methodtable: 0093313c [0] 012c9a64 [1] 012c9a78
查看tsArr的內存佈局,也能夠看到,clr直接將結構體的全部內容保存在數組元素的內存空間裏,與 int[] 類型的對象同樣,結構體數組也不保存元素的類型信息。
>dd tsArr 0x012C98B8 00933200 00000002 012c977c 012b1bd8 0x012C98C8 00000064 44454000 00000000 00000000 0x012C98D8 00000000 00000000 00000000 00000000 0x012C98E8 00000000 00000000 00000000 00000000 # #使用df命令,打印出內存裏的浮點數,能夠看到在結構體的最後一個指針 #就是浮點數的值 # >df tsArr 0x012C98B8 1.3517755e-038 2.803e-045#DEN 3.1700095e-038 3.1427717e-038 0x012C98C8 1.401e-043#DEN 789.00000 0.00000000 0.00000000 0x012C98D8 0.00000000 0.00000000 0.00000000 0.00000000 0x012C98E8 0.00000000 0.00000000 0.00000000 0.00000000 # #使用dc命令查看數組第一個元素的第一個指針,是結構體的字符串成員變量 #經過這個例子也能夠看到,在實際的內存佈局裏,若是不顯示指定成員變量的 #內存佈局,clr裏對象的成員變量的佈局順序跟源碼的順序有多是不同的 # >dc 0x012c977c 0x012C977C 790fa3e0 00000012 00000011 00680054 à£.y........T.h. 0x012C978C 00730069 00690020 00200073 00200061 i.s. .i.s. .a. . 0x012C979C 00740073 00690072 0067006e 00000021 s.t.r.i.n.g.!... 0x012C97AC 00000000 791240f0 0000000a 00000000 ….ð@.y........
經過前面的分析,能夠看到,實際上全部的引用類型數組的類型都是同樣的,即 object[] 類型,可是值類型數組的類型卻各不相同,這個差別在jit的時候就已經決定了,也就是說,雖然在 IL 代碼裏,建立數組的指令都是 newarr 指令,可是在jit編譯生成代碼後,傳給 newarr 指令的類型參數就已經不同了。咱們在後面解讀 jit 源碼的時候會繼續提到這一點。