前面我们图解了.NET里各种对象的内存布局,我们再来从调试器和clr源码的角度来看一下对象的内存布局。我写了一个测试程序来加深对.net对象内存布局的了解:

using System;
using System.Runtime.InteropServices; // 实际上是一个C语言里的联合体
[StructLayout(LayoutKind.Explicit)]
public struct InnerStruct
{
[FieldOffset()]
public float FloatValue; [FieldOffset()]
public double DoubleValue;
} public struct TestStruct
{
public int IntValue; public string StringValue; public object ObjectValue; public InnerStruct InnerStructValue;
} public class ObjectLayout
{
private int _IntValue = ;
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 = ;
Console.WriteLine("int值: {0}", i); string s = "This is a string";
Console.WriteLine("字符串:{0}", s); ObjectLayout[] olArr = new ObjectLayout[];
olArr[] = new ObjectLayout();
olArr[].IntValue = ;
Console.WriteLine("数组的长度:{0}", olArr.Length); object[] objArr = new object[];
objArr[] = o;
Console.WriteLine("数组的长度:{0}", objArr.Length); string[] strArr = new string[];
strArr[] = s;
strArr[] = s + "!";
Console.WriteLine("数组的长度:{0}", strArr.Length); TestStruct ts = new TestStruct();
ts.IntValue = ;
ts.StringValue = s + "!";
ts.ObjectValue = o;
ts.InnerStructValue.FloatValue = 789.0f; int[] intArr = new int[];
for (int j = ; j < intArr.Length; ++j)
{
intArr[j] = j;
}
Console.WriteLine("int数组的长度:{0}", intArr.Length); TestStruct[] tsArr = new TestStruct[];
tsArr[] = ts;
Console.WriteLine("TestStruct数组的长度:{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函数的最后一行设置断点;
  • 在VS里,打开“立即”窗口,菜单命令是:“调试(Debug)” -> “窗口(Windows)” -> “立即(Immediate)”;
  • 在“立即”窗口里,执行命令将sos加载到VS中:!load C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\sos.dll

这个时候就可以在vs里使用sos插件里面的命令了,如下图所示:

这里对sos命令不做过多的解释,有兴趣的网友可以参看我的《Windows调试技术》视频来了解sos的用法,下面我用类似bash注释的方式解释查看过程:


#使用 !clrstack 命令查看当前被调试进程的堆栈,而 -l 参数则告诉sos同时
# 显示堆栈上每个函数的局部变量。

!clrstack -l
OS Thread Id: 0xba4 ()
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:

!do 0x012b1bd8
Name: System.Object #指明了类型,这个类型由保存在对象里的MethodTable获取
MethodTable: 790f9c18 # MethodTable地址,直接保存在对象里
EEClass: 790f9bb4 #通过MethodTable解析到
Size: (0xc) bytes
(C:\WINDOWS\assembly\GAC_32\mscorlib\2.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
#
# 当然也可以直接给dd命令内存地址
#
>dd 0x012b1bd8
0x012B1BD8 790f9c18

前文我们已经提到clr将对象的指针做了一些处理,对托管代码隐藏了objheader信息,这个信息其中一个作用就是处理线程同步信息,要看看syncvalue是怎么工作的话,可以重新启动被调试程序,并将程序中断在代码的第39行即lock语句那里 - 在其执行之前中断程序,如下图所示:

然后我们在“命令窗口”查看对象的内存布局:


#我用的是虚拟机上安装的32位xp系统,因此我们将地址提前
#一个指针,看看当前objheader的synvalue的值,目前因为
#没有线程需要同步访问这个对象,所以其值为0

>dd 0x012b1bd4
0x012B1BD4 790f9c18 #
#单步执行lock语句一次,以便开始线程同步,再看相同地址的值
#现在会发现lockvalue已经更新了,lockvalue的作用在后文说明
#这里就不详细说明它了。

>dd 0x012b1bd4
0x012B1BD4 790f9c18

我们再看第三个局部变量,string类型的s,使用sos命令查看的结果如下:

!do 0x012b1af4
Name: System.String
MethodTable: 790fa3e0
EEClass: 790fa340
Size: (0x32) bytes
(C:\WINDOWS\assembly\GAC_32\mscorlib\2.0..0__b77a5c561934e089\mscorlib.dll)

#对于字符串对象,!do命令足够聪明,可以直接将字符串的内容打印出来

String: This is a string

#显示该对象实例的每一个成员变量的值

Fields:
MT Field Offset Type VT Attr Value Name
790fed1c System.Int32 instance m_arrayLength
790fed1c System.Int32 instance m_stringLength
790fbefc c System.Char instance m_firstChar
790fa3e0 System.String shared static Empty
>> Domain:Value 0014c558:790d6584 <<
400009a System.Char[] shared static WhitespaceChars
>> Domain:Value 0014c558:012b1670 <<

我们再在命令行里用dd查看它的内存布局,因为是字符串,所以这里我们用dc命令,除了用16进制显示内存以外,还尽量使用字符的形式打印内存的每个指针:


#第一个指针保存的仍然是对象的类型信息 - MethodTable指针,
#接下来第二个指针就是如果将字符串当作数组看待的话,它的长度,
#这个长度会包括最后的’\’,这里它的值是 0x11,也就是17。
#第三个指针就是字符串的长度,即 0x10,也就是16个字符。
#在字符串长度后面就是实际的字符串WCHAR数组了。

>dc s
0x012B1AE8 790fa3e0 à£.y........T.h.
0x012B1AF8 i.s. .i.s. .a. .
0x012B1B08 0067006e s.t.r.i.n.g.....
0x012B1B18 790fa3e0 ....à£.y........

接下来我们再来看引用类型的数组对象在内存里的布局,下面是 ObjectLayout[] 类型的对象olArr的结果,可以看到在clr里,对象的类型实际上 System.Object[],而不是 ObjectLayout[]。

!do 0x012c9520
Name: System.Object[]

#MethodTable的值是 ,跟后面 object[] 类型对象的 objArr 的
#MethodTable是一样的

MethodTable:
EEClass: 7912479c
Size: (0x38) bytes

#对于数组对象,sos会打印出数组的维度和大小信心

Array: Rank , Number of elements , Type CLASS

#数组元素的类型

Element Type: ObjectLayout
Fields:
None #
#使用 da 命令可以打印出数组的详细内容

!da 0x012c9520
Name: ObjectLayout[]
MethodTable:
EEClass: 7912479c
Size: (0x38) bytes
Array: Rank , Number of elements , Type CLASS
Element Methodtable:
[] 012c9558
[] null
[] null
[] null
[] null
[] null
[] null
[] null
[] null
[] null

把olArr对象的内存打印出来,可以看到:

  • 第一个指针跟其他对象一样,是MethodTable,也就是对象类型的指针;
  • 第二个指针是数组的大小0xa,也就是10;
  • 第三个指针是数组里元素的类型指针;
  • 第四个指针开始则是各个对象的引用。
>dd olArr
0x012C9438 0000000a 012c9558
0x012C9448 #
#打印 object[] 类型的 objArr 对象的信息

!do 0x012c966c
Name: System.Object[]

#MethodTable指针与前面的ObjectLayout[]对象的MethodTable完全一样

MethodTable:
EEClass: 7912479c
Size: (0x18) bytes
Array: Rank , Number of elements , Type CLASS
Element Type: System.Object
Fields:
None !da 0x012c966c
Name: System.Object[]
MethodTable:
EEClass: 7912479c
Size: (0x18) bytes
Array: Rank , Number of elements , Type CLASS
Element Methodtable: 790f9c18
[] 012b1c3c
[] null #
#打印 string[] 类型的 strArr 对象的信息

!do 0x012c9788
Name: System.Object[]

#MethodTable指针与前面的ObjectLayout[]和object[]对象的MethodTable完全一样

MethodTable:
EEClass: 7912479c
Size: (0x18) bytes
Array: Rank , Number of elements , Type CLASS
Element Type: System.String
Fields:
None !da 0x012c9788
Name: System.String[]
MethodTable:
EEClass: 7912479c
Size: (0x18) bytes
Array: Rank , Number of elements , Type CLASS
Element Methodtable: 790fa3e0
[] 012b1af4
[] 012c97a0 #
#打印 int[] 类型的 intArr 对象的信息

!da 0x012c990c
Name: System.Int32[]

#注意:MethodTable 也就是类型指针跟前面引用类型的MethodTable不同

MethodTable: 791240f0
EEClass: 791241a8
Size: (0x34) bytes
Array: Rank , Number of elements , Type Int32
Element Methodtable: 790fed1c
[] null
[]
[]
[]
[]
[]
[]
[]
[]
[]

查看intArr的内存布局,可以看到,与前面的引用类型数组不同,第三个指针就是数组的第一个元素(值为0)了,而引用类型数组的第三个指针是元素的类型指针。

>dd intArr
0x012C97B0 791240f0 0000000a
0x012C97C0
0x012C97D0 #
#打印自定义结构体 TestStruct[] 类型的 tsArr对象的信息

!da 0x012c9a5c
Name: TestStruct[]

#注意:MethodTable 指针不仅跟前面引用类型的MethodTable不同,
#而且跟intArr的类型也不一样

MethodTable:
EEClass:
Size: (0x34) bytes
Array: Rank , Number of elements , Type VALUETYPE
Element Methodtable: 0093313c
[] 012c9a64
[] 012c9a78

查看tsArr的内存布局,也可以看到,clr直接将结构体的所有内容保存在数组元素的内存空间里,与 int[] 类型的对象一样,结构体数组也不保存元素的类型信息。

>dd tsArr
0x012C98B8 012c977c 012b1bd8
0x012C98C8
0x012C98D8
0x012C98E8

#使用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 à£.y........T.h.
0x012C978C i.s. .i.s. .a. .
0x012C979C 0067006e s.t.r.i.n.g.!...
0x012C97AC 791240f0 0000000a ….ð@.y........

通过前面的分析,可以看到,实际上所有的引用类型数组的类型都是一样的,即 object[] 类型,但是值类型数组的类型却各不相同,这个差异在jit的时候就已经决定了,也就是说,虽然在 IL 代码里,创建数组的指令都是 newarr 指令,但是在jit编译生成代码后,传给 newarr 指令的类型参数就已经不一样了。我们在后面解读 jit 源码的时候会继续提到这一点。

使用sos查看.NET对象内存布局的更多相关文章

  1. 图说C++对象模型:对象内存布局详解

    0.前言 文章较长,而且内容相对来说比较枯燥,希望对C++对象的内存布局.虚表指针.虚基类指针等有深入了解的朋友可以慢慢看. 本文的结论都在VS2013上得到验证.不同的编译器在内存布局的细节上可能有 ...

  2. c/c++ 对象内存布局

    一.对象内存查看工具 VS 编译器 CL 的一个编译选项可以查看 C++ 类的内存布局,非常有用.使用如下,从开始程序菜单找到 Visual Stdio 2012. 选择 VS 的命令行工具,按如下格 ...

  3. c++ 对象内存布局详解

    今天看了的,感觉需要了解对象内存的问题.参考:http://blog.jobbole.com/101583/ 1.何为C++对象模型? 引用<深度探索C++对象模型>这本书中的话: 有两个 ...

  4. c++对象内存布局

    这篇文章我要简单地讲解下c++对象的内存布局,虽然已经有很多很好的文章,不过通过实现发现有些地方不同的编译器还是会有差别的,希望和大家交流. 在没有用到虚函数的时候,C++的对象内存布局和c语言的st ...

  5. c++对象内存布局的理解

    我对c++对象内存布局的理解   引言 结合网上的一些资料,通过自己的一番摸索,得出了一点个人见解.现在写下来,希望与各位同学共同探讨,共同进步. 以下所有代码均是在VS2012下测试. 一个普通的基 ...

  6. 好文章系列C/C++——图说C++对象模型:对象内存布局详解

    注:收藏好文章,得出自己的笔记,以查漏补缺!     ------>原文链接:http://blog.jobbole.com/101583/ 前言 本文可加深对C++对象的内存布局.虚表指针.虚 ...

  7. 【转载】图说C++对象模型:对象内存布局详解

    原文: 图说C++对象模型:对象内存布局详解 正文 回到顶部 0.前言 文章较长,而且内容相对来说比较枯燥,希望对C++对象的内存布局.虚表指针.虚基类指针等有深入了解的朋友可以慢慢看.本文的结论都在 ...

  8. JVM-对象及对象内存布局

    目录 前言 类与对象 对象类二分模型 对象 对象内存布局 JOL工具 对象头 Mark Word 类型句柄 对象头与锁膨胀 无锁 偏向锁 轻量级锁 重量级锁 重量级锁降级 实例数据 填充 对象生命周期 ...

  9. JVM之对象创建、对象内存布局、对象访问定位

    对象创建 类加载过后可以直接确定一个对象的大小 对象栈上分配是通过逃逸分析判定.标量替换实现的,即把不存在逃逸的对象拆散,将成员变量恢复到基本类型,直接在栈上创建若干个成员变量 选择哪种分配方式由Ja ...

随机推荐

  1. JDBC远程连接数据库

    使用jdbc远程连接数据库(非本地数据库)出现的问题可用的解决方法: 1.修改mysql配置文件 配置文件中注释掉 bind-address=127.0.0.1 2.修改数据库 登入mysql后,更改 ...

  2. NBU 还原LINUX ORACLE数据库(CRM)

    [root@oraclea ~]# su - oracle [oracle@oraclea ~]$ bplist -C oracle-db1 -s // -e // -k oracle_oracle- ...

  3. Spark之 Spark Streaming整合kafka(Java实现版本)

    pom依赖 <properties> <scala.version>2.11.8</scala.version> <hadoop.version>2.7 ...

  4. Windows phone自定义控件(无外观控件)——FlipPanel

    编码前 无外观自定义控件的定义在上一篇中已经有了,至于这一篇的自定义控件,比之前多加入了状态的变化,就像默认的Button具有Pressed.Normal等状态.在状态转变的同时可以加上一些动画,可以 ...

  5. 识别名人 · Find the Celebrity

    [抄题]: 假设你和 n 个人在一个聚会中(标记为 0 到 n - 1),其中可能存在一个名人.名人的定义是所有其他 n - 1 人都认识他/她,但他/她不知道任何一个.现在你想要找出这个名人是谁或者 ...

  6. Swift4 - 动态计算UITableView中tableHeaderView的高度 - 获取子控件高度和宽度

    核心 : /// 获取 子控件高度 func sizeHeaderToFit(view:UIView) { view.setNeedsLayout() view.layoutIfNeeded() le ...

  7. [leetcode]113. Path Sum II路径和(返回路径)

    Given a binary tree and a sum, find all root-to-leaf paths where each path's sum equals the given su ...

  8. 解决Pycharm添加虚拟解释器的报错问题

    问题背景: 新添加一个virtualenv环境时,需要安装指定的django==1.9.8,但是在添加解释器时,总报一个fuck egg的问题!! 解决方式如下: 1. 2. 3.搞定

  9. [Meteor] meteor project structure

  10. 13.11.20 jquery 核心 siblings() 获得同类(不包含自己)循环所有,

    jquery 核心1.选择器,2. 创建dom 元素 3. jquery 执行时 4. 延迟执行 5. 循环 6. 计算长度.7.8 获得选择器和所在节点 9. 获得下标 10. 元素存放数据  11 ...