前面我們圖解了.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ù)提到這一點。