使用sos查看.NET對象內存佈局

前面咱們圖解了.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,依次點擊菜單裏的「文件(File)」 -> 「打開(Open)」 -> 「工程或解決方案(Project/Solution)」,而後選擇剛剛編譯的objectlayout.exe程序,開始調試這個程序;
  • 對於託管程序,VS支持多種調試模式,若是要使用sos插件的話,須要採用「混合(Mixed)」調試模式調試程序,具體作法是在「解決方案管理器(Solution Explorer)」裏右鍵單擊objectlayout.exe程序,而後點擊「屬性(Properties)」打開屬性窗口,將裏面的「調試器類型(Debugger Type)」改爲「混合(Mixed)」模式;

  

  • 在VS裏打開程序的源碼objectlayout.cs,並在Main函數的最後一行設置斷點;
  • 在VS裏,打開「當即」窗口,菜單命令是:「調試(Debug)」 -> 「窗口(Windows)」 -> 「當即(Immediate)」;
  • 在「當即」窗口裏,執行命令將sos加載到VS中:!load C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\sos.dll

這個時候就能夠在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對象的內存打印出來,能夠看到:

  • 第一個指針跟其餘對象同樣,是MethodTable,也就是對象類型的指針;
  • 第二個指針是數組的大小0xa,也就是10;
  • 第三個指針是數組裏元素的類型指針;
  • 第四個指針開始則是各個對象的引用。
>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 源碼的時候會繼續提到這一點。

相關文章
相關標籤/搜索