用sos查看.NET對象內存布局

前面我們圖解了.NET里各種對象的內存布局,我們再來從調試器和clr源碼的角度來看一下對象的內存布局。我寫了一個測試程序來加深對.net對象內存布局的了解:

using System;
using System.Runtime.InteropServices;

// 實際上是一個C語言里的聯(lián)合體
[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("數(shù)組的長度:{0}", olArr.Length);

        object[] objArr = new object[2];
        objArr[0] = o;
        Console.WriteLine("數(shù)組的長度:{0}", objArr.Length);

        string[] strArr = new string[2];
        strArr[0] = s;
        strArr[1] = s + "!";
        Console.WriteLine("數(shù)組的長度:{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數(shù)組的長度:{0}", intArr.Length);

        TestStruct[] tsArr = new TestStruct[2];
        tsArr[0] = ts;
        Console.WriteLine("TestStruct數(shù)組的長度:{0}", tsArr.Length);
    }
}

使用命令編譯一個調試版本的objectlayout.exe程序: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函數(shù)的最后一行設置斷點;
    在VS里,打開“立即”窗口,菜單命令是:“調試(Debug)” -> “窗口(Windows)” -> “立即(Immediate)”;
  • 在“立即”窗口里,執(zhí)行命令將sos加載到VS中:!load C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\sos.dll

這個時候就可以在vs里使用sos插件里面的命令了,如下圖所示:

這里對sos命令不做過多的解釋,有興趣的網(wǎng)友可以參看我的《Windows調試技術》 視頻來了解sos的用法,下面我用類似bash注釋的方式解釋查看過程:

#
#使用 !clrstack 命令查看當前被調試進程的堆棧,而 -l 參數(shù)則告訴sos同時
# 顯示堆棧上每個函數(shù)的局部變量。
#
!clrstack -l
OS Thread Id: 0xba4 (2980)
ESP       EIP     
0012f408 00e10341 ObjectLayout.Main()

#
# 下面打印了Main函數(shù)里的所有局部變量,如果執(zhí)行過GC,有些變量可能不可見
# 局部變量左邊是局部變量的內存地址,而右邊則是局部變量的值,因為大部分
# 局部變量都是引用類型,所有值大部分都是指針,除了少數(shù)幾個,如第二個變量
# 就是一個值類型,因此直接保存了它的值: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:

!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)”。在命令窗口里執(zhí)行下面的命令(熟悉windbg的同學應該知道這是windbg里的命令):

#
# 你可以直接給出變量名,vs會自動將變量名解析為內存地址
# 可以看到,對象的第一個指針就是說明自己類型的MethodTable
# 指針 -> 790f9c18,
#
>dd o
0x012B1BD8  790f9c18 00000000 00000000 00000000 
#
# 當然也可以直接給dd命令內存地址
#
>dd 0x012b1bd8
0x012B1BD8  790f9c18 00000000 00000000 00000000 

前文我們已經(jīng)提到clr將對象的指針做了一些處理,對托管代碼隱藏了objheader信息,這個信息其中一個作用就是處理線程同步信息,要看看syncvalue是怎么工作的話,可以重新啟動被調試程序,并將程序中斷在代碼的第39行即lock語句那里 - 在其執(zhí)行之前中斷程序,如下圖所示:

然后我們在“命令窗口”查看對象的內存布局:

#
#我用的是虛擬機上安裝的32位xp系統(tǒng),因此我們將地址提前
#一個指針,看看當前objheader的synvalue的值,目前因為
#沒有線程需要同步訪問這個對象,所以其值為0
#
>dd 0x012b1bd4
0x012B1BD4  00000000 790f9c18 00000000 00000000 
 
#
#單步執(zhí)行l(wèi)ock語句一次,以便開始線程同步,再看相同地址的值
#現(xiàn)在會發(fā)現(xiàn)lockvalue已經(jīng)更新了,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指針,
#接下來第二個指針就是如果將字符串當作數(shù)組看待的話,它的長度,
#這個長度會包括最后的’\0’,這里它的值是 0x11,也就是17。
#第三個指針就是字符串的長度,即 0x10,也就是16個字符。
#在字符串長度后面就是實際的字符串WCHAR數(shù)組了。
#
>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........

接下來我們再來看引用類型的數(shù)組對象在內存里的布局,下面是 ObjectLayout[] 類型的對象olArr的結果,可以看到在clr里,對象的類型實際上 System.Object[],而不是 ObjectLayout[]。

!do 0x012c9520
Name: System.Object[]
#
#MethodTable的值是 79124228,跟后面 object[] 類型對象的 objArr 的
#MethodTable是一樣的
#
MethodTable: 79124228
EEClass: 7912479c
Size: 56(0x38) bytes
#
#對于數(shù)組對象,sos會打印出數(shù)組的維度和大小信心
#
Array: Rank 1, Number of elements 10, Type CLASS
#
#數(shù)組元素的類型
#
Element Type: ObjectLayout
Fields:
None

#
#使用 da 命令可以打印出數(shù)組的詳細內容
#
!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,也就是對象類型的指針;
  • 第二個指針是數(shù)組的大小0xa,也就是10;
  • 第三個指針是數(shù)組里元素的類型指針;
  • 第四個指針開始則是各個對象的引用。
>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的內存布局,可以看到,與前面的引用類型數(shù)組不同,第三個指針就是數(shù)組的第一個元素(值為0)了,而引用類型數(shù)組的第三個指針是元素的類型指針。

>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直接將結構體的所有內容保存在數(shù)組元素的內存空間里,與 int[] 類型的對象一樣,結構體數(shù)組也不保存元素的類型信息。

>dd tsArr
0x012C98B8  00933200 00000002 012c977c 012b1bd8  
0x012C98C8  00000064 44454000 00000000 00000000  
0x012C98D8  00000000 00000000 00000000 00000000  
0x012C98E8  00000000 00000000 00000000 00000000  
#
#使用df命令,打印出內存里的浮點數(shù),可以看到在結構體的最后一個指針
#就是浮點數(shù)的值
#
>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命令查看數(shù)組第一個元素的第一個指針,是結構體的字符串成員變量
#通過這個例子也可以看到,在實際的內存布局里,如果不顯示指定成員變量的
#內存布局,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  ….e@.y........

通過前面的分析,可以看到,實際上所有的引用類型數(shù)組的類型都是一樣的,即 object[] 類型,但是值類型數(shù)組的類型卻各不相同,這個差異在jit的時候就已經(jīng)決定了,也就是說,雖然在 IL 代碼里,創(chuàng)建數(shù)組的指令都是 newarr 指令,但是在jit編譯生成代碼后,傳給 newarr 指令的類型參數(shù)就已經(jīng)不一樣了。我們在后面解讀 jit 源碼的時候會繼續(xù)提到這一點。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,242評論 25 708
  • *面試心聲:其實這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個offer,總結起來就是把...
    Dove_iOS閱讀 27,210評論 30 471
  • 黑夜里的曇花,竭盡的開放,只為留下屬于自己的片刻美好,不圖眾生為之動容,只要你回眸一眼。 引:緣起緣滅緣終盡、花開...
    孜然味的玉米閱讀 292評論 0 1
  • 十多年來我一直研究和傳播一個叫NLP的東西。 我在高中的時候我特別喜歡人性的優(yōu)點,人性的弱點這兩本書。我發(fā)現(xiàn)西...
    陽光心靈成長工作室閱讀 158評論 0 0
  • 喜歡一個人到底要不要和他說? 反正我是喜歡了一個人四年才和他說的,而且說的一點都不正式,就是高三畢業(yè)以后...
    木春曉閱讀 264評論 1 2